Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix integration of swift-testing #6062

Merged
merged 4 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 6 additions & 1 deletion Sources/TuistCore/Graph/GraphTraverser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -457,9 +457,14 @@ public class GraphTraverser: GraphTraversing {
}

public func allSwiftMacroTargets(path: AbsolutePath, name: String) -> Set<GraphTarget> {
let dependencies = allTargetDependencies(path: path, name: name)
var dependencies = allTargetDependencies(path: path, name: name)
.filter { [.staticFramework, .framework, .dynamicLibrary, .staticLibrary].contains($0.target.product) }
.filter { self.directSwiftMacroExecutables(path: $0.path, name: $0.target.name).count != 0 }

if let target = target(path: path, name: name), !directSwiftMacroExecutables(path: path, name: name).isEmpty {
fortmarek marked this conversation as resolved.
Show resolved Hide resolved
dependencies.insert(target)
}

return Set(dependencies)
}

Expand Down
70 changes: 70 additions & 0 deletions Sources/TuistGenerator/Generator/BuildPhaseGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,22 @@ final class BuildPhaseGenerator: BuildPhaseGenerating {
)
}

/**
Targets that depend on a Swift Macro have the following dependency graph:

Target -> MyMacro (Static framework) -> MyMacro (Executable)

The executable is compiled transitively through the static library, and we place it inside the framework to make it available to the target depending on the framework
to point it with the `-load-plugin-executable $(BUILD_DIR)/$(CONFIGURATION)/ExecutableName\#ExecutableName` build setting.
*/
let directSwiftMacroExecutables = graphTraverser.directSwiftMacroExecutables(path: path, name: target.name).sorted()
try generateCopySwiftMacroExecutableScriptBuildPhase(
directSwiftMacroExecutables: directSwiftMacroExecutables,
target: target,
pbxTarget: pbxTarget,
pbxproj: pbxproj
)

if target.supportsSources {
try generateSourcesBuildPhase(
files: target.sources,
Expand Down Expand Up @@ -407,6 +423,60 @@ final class BuildPhaseGenerator: BuildPhaseGenerating {
}
}

private func generateCopySwiftMacroExecutableScriptBuildPhase(
directSwiftMacroExecutables: [GraphDependencyReference],
target: Target,
pbxTarget: PBXTarget,
pbxproj: PBXProj
) throws {
if directSwiftMacroExecutables.isEmpty { return }

let copySwiftMacrosBuildPhase = PBXShellScriptBuildPhase(name: "Copy Swift Macro executable into $BUILT_PRODUCT_DIR")

let executableNames = directSwiftMacroExecutables.compactMap {
switch $0 {
case let .product(_, productName, _):
return productName
default:
return nil
}
}

let copyLines = executableNames.map {
"""
if [[ -f "$BUILD_DIR/$CONFIGURATION/\($0)" && ! -f "$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME/\($0)" ]]; then
mkdir -p "$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME/"
cp "$BUILD_DIR/$CONFIGURATION/\($0)" "$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME/\($0)"
fi
"""
}
copySwiftMacrosBuildPhase.shellScript = """
# This build phase serves two purposes:
# - Force Xcode build system to compile the macOS executable transitively when compiling for non-macOS destinations
# - Place the artifacts in the "Debug" directory where the built artifacts for the active destination live. We default to "Debug" because otherwise the Xcode editor fails to resolve the macro references.
\(copyLines.joined(separator: "\n"))
"""

copySwiftMacrosBuildPhase.inputPaths = executableNames.map { "$BUILD_DIR/$CONFIGURATION/\($0)" }

copySwiftMacrosBuildPhase.outputPaths = target.supportedPlatforms
.filter { $0 != .macOS }
.flatMap { platform in
var sdks: [String] = []
sdks.append(platform.xcodeDeviceSDK)
if let simulatorSDK = platform.xcodeSimulatorSDK { sdks.append(simulatorSDK) }
return sdks
}
.flatMap { sdk in
executableNames.map { executable in
"$BUILD_DIR/Debug-\(sdk)/\(executable)"
}
}

pbxproj.add(object: copySwiftMacrosBuildPhase)
pbxTarget.buildPhases.append(copySwiftMacrosBuildPhase)
}

private func generateResourcesBuildFile(
target: Target,
files: [ResourceFileElement],
Expand Down
72 changes: 0 additions & 72 deletions Sources/TuistGenerator/Generator/LinkGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,23 +116,6 @@ final class LinkGenerator: LinkGenerating { // swiftlint:disable:this type_body_
fileElements: fileElements
)

/**
Targets that depend on a Swift Macro have the following dependency graph:

Target -> MyMacro (Static framework) -> MyMacro (Executable)

The executable is compiled transitively through the static library, and we place it inside the framework to make it available to the target depending on the framework
to point it with the `-load-plugin-executable $(BUILD_DIR)/$(CONFIGURATION)/ExecutableName\#ExecutableName` build setting.
*/
let directSwiftMacroExecutables = graphTraverser.directSwiftMacroExecutables(path: path, name: target.name).sorted()
try generateCopySwiftMacroExecutableScriptBuildPhase(
directSwiftMacroExecutables: directSwiftMacroExecutables,
target: target,
pbxTarget: pbxTarget,
pbxproj: pbxproj,
fileElements: fileElements
)

try generatePackages(
target: target,
pbxTarget: pbxTarget,
Expand Down Expand Up @@ -513,61 +496,6 @@ final class LinkGenerator: LinkGenerating { // swiftlint:disable:this type_body_
)
}

func generateCopySwiftMacroExecutableScriptBuildPhase(
directSwiftMacroExecutables: [GraphDependencyReference],
target: Target,
pbxTarget: PBXTarget,
pbxproj: PBXProj,
fileElements _: ProjectFileElements
) throws {
if directSwiftMacroExecutables.isEmpty { return }

let copySwiftMacrosBuildPhase = PBXShellScriptBuildPhase(name: "Copy Swift Macro executable into $BUILT_PRODUCT_DIR")

let executableNames = directSwiftMacroExecutables.compactMap {
switch $0 {
case let .product(_, productName, _):
return productName
default:
return nil
}
}

let copyLines = executableNames.map {
"""
if [[ -f "$BUILD_DIR/$CONFIGURATION/\($0)" && ! -f "$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME/\($0)" ]]; then
mkdir -p "$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME/"
cp "$BUILD_DIR/$CONFIGURATION/\($0)" "$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME/\($0)"
fi
"""
}
copySwiftMacrosBuildPhase.shellScript = """
# This build phase serves two purposes:
# - Force Xcode build system to compile the macOS executable transitively when compiling for non-macOS destinations
# - Place the artifacts in the "Debug" directory where the built artifacts for the active destination live. We default to "Debug" because otherwise the Xcode editor fails to resolve the macro references.
\(copyLines.joined(separator: "\n"))
"""

copySwiftMacrosBuildPhase.inputPaths = executableNames.map { "$BUILD_DIR/$CONFIGURATION/\($0)" }

copySwiftMacrosBuildPhase.outputPaths = target.supportedPlatforms
.filter { $0 != .macOS }
.flatMap { platform in
var sdks: [String] = []
sdks.append(platform.xcodeDeviceSDK)
if let simulatorSDK = platform.xcodeSimulatorSDK { sdks.append(simulatorSDK) }
return sdks
}
.flatMap { sdk in
executableNames.map { executable in
"$BUILD_DIR/Debug-\(sdk)/\(executable)"
}
}

pbxproj.add(object: copySwiftMacrosBuildPhase)
pbxTarget.buildPhases.append(copySwiftMacrosBuildPhase)
}

private func generateDependenciesBuildPhase(
dependencies: [GraphDependencyReference],
target: Target,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ public final class PackageInfoMapper: PackageInfoMapping {
"ViewInspector", // https://github.com/nalexn/ViewInspector
"XCTVapor", // https://github.com/vapor/vapor
"MockableTest", // https://github.com/Kolos65/Mockable.git
"Testing",
danieleformichelli marked this conversation as resolved.
Show resolved Hide resolved
].map {
($0, ["ENABLE_TESTING_SEARCH_PATHS": "YES"])
}
Expand Down
5 changes: 5 additions & 0 deletions Tests/TuistCoreTests/Graph/GraphTraverserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4720,12 +4720,17 @@ final class GraphTraverserTests: TuistUnitTestCase {

// When
let got = subject.allSwiftMacroTargets(path: project.path, name: app.name)
let gotDirectMacroFramework = subject.allSwiftMacroTargets(path: project.path, name: directMacroFramework.name)

// Then
XCTAssertEqual(got.sorted(), [
GraphTarget(path: project.path, target: directMacroFramework, project: project),
GraphTarget(path: project.path, target: transitiveMacroLibrary, project: project),
])
XCTAssertEqual(gotDirectMacroFramework.sorted(), [
GraphTarget(path: project.path, target: directMacroFramework, project: project),
GraphTarget(path: project.path, target: transitiveMacroLibrary, project: project),
])
}

func test_directTargetDependenciesWithConditions() throws {
Expand Down
58 changes: 58 additions & 0 deletions Tests/TuistGeneratorTests/Generator/BuildPhaseGeneratorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1568,6 +1568,64 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase {
])
}

func test_generateLinks_generatesAShellScriptBuildPhase_when_targetIsAMacroFramework() throws {
// Given
let app = Target.test(name: "app", platform: .iOS, product: .app)
let macroFramework = Target.test(name: "framework", platform: .iOS, product: .staticFramework)
let macroExecutable = Target.test(name: "macro", platform: .macOS, product: .macro)
let project = Project.test(targets: [app, macroFramework, macroExecutable])
let fileElements = createProductFileElements(for: [app, macroFramework, macroExecutable])
let pbxproj = PBXProj()
let pbxTarget = PBXNativeTarget(name: app.name)

let graph = Graph.test(path: project.path, projects: [project.path: project], targets: [
project.path: [
app.name: app,
macroFramework.name: macroFramework,
macroExecutable.name: macroExecutable,
],
], dependencies: [
.target(name: app.name, path: project.path): Set([.target(name: macroFramework.name, path: project.path)]),
.target(name: macroFramework.name, path: project.path): Set([.target(
name: macroExecutable.name,
path: project.path
)]),
.target(name: macroExecutable.name, path: project.path): Set([]),
])
let graphTraverser = GraphTraverser(graph: graph)

// When
try subject.generateBuildPhases(
path: "/Project",
target: macroFramework,
graphTraverser: graphTraverser,
pbxTarget: pbxTarget,
fileElements: fileElements,
pbxproj: pbxproj
)

// Then
let buildPhase = pbxTarget
.buildPhases
.compactMap { $0 as? PBXShellScriptBuildPhase }
.first(where: { $0.name() == "Copy Swift Macro executable into $BUILT_PRODUCT_DIR" })

XCTAssertNotNil(buildPhase)

let expectedScript =
"if [[ -f \"$BUILD_DIR/$CONFIGURATION/macro\" && ! -f \"$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME/macro\" ]]; then\n mkdir -p \"$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME/\"\n cp \"$BUILD_DIR/$CONFIGURATION/macro\" \"$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME/macro\"\nfi"
XCTAssertTrue(buildPhase?.shellScript?.contains(expectedScript) == true)
XCTAssertTrue(buildPhase?.inputPaths.contains("$BUILD_DIR/$CONFIGURATION/\(macroExecutable.productName)") == true)
XCTAssertTrue(
buildPhase?.outputPaths
.contains("$BUILD_DIR/Debug-iphonesimulator/\(macroExecutable.productName)") == true
)
XCTAssertTrue(
buildPhase?.outputPaths
.contains("$BUILD_DIR/Debug-iphoneos/\(macroExecutable.productName)") == true
)
}

// MARK: - Helpers

private func createProductFileElements(for targets: [Target]) -> ProjectFileElements {
Expand Down
59 changes: 0 additions & 59 deletions Tests/TuistGeneratorTests/Generator/LinkGeneratorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -973,65 +973,6 @@ final class LinkGeneratorTests: XCTestCase {
])
}

func test_generateLinks_generatesAShellScriptBuildPhase_when_targetIsAMacroFramework() throws {
// Given
let app = Target.test(name: "app", platform: .iOS, product: .app)
let macroFramework = Target.test(name: "framework", platform: .iOS, product: .staticFramework)
let macroExecutable = Target.test(name: "macro", platform: .macOS, product: .macro)
let project = Project.test(targets: [app, macroFramework, macroExecutable])

let graph = Graph.test(path: project.path, projects: [project.path: project], targets: [
project.path: [
app.name: app,
macroFramework.name: macroFramework,
macroExecutable.name: macroExecutable,
],
], dependencies: [
.target(name: app.name, path: project.path): Set([.target(name: macroFramework.name, path: project.path)]),
.target(name: macroFramework.name, path: project.path): Set([.target(
name: macroExecutable.name,
path: project.path
)]),
.target(name: macroExecutable.name, path: project.path): Set([]),
])
let graphTraverser = GraphTraverser(graph: graph)
let xcodeProjElements = createXcodeprojElements()
let fileElements = createProjectFileElements(for: [app, macroFramework, macroExecutable])

// When
try subject.generateLinks(
target: macroFramework,
pbxTarget: xcodeProjElements.pbxTarget,
pbxproj: xcodeProjElements.pbxproj,
fileElements: fileElements,
path: project.path,
sourceRootPath: project.path,
graphTraverser: graphTraverser
)

// Then
let buildPhase = xcodeProjElements
.pbxTarget
.buildPhases
.compactMap { $0 as? PBXShellScriptBuildPhase }
.first(where: { $0.name() == "Copy Swift Macro executable into $BUILT_PRODUCT_DIR" })

XCTAssertNotNil(buildPhase)

let expectedScript =
"if [[ -f \"$BUILD_DIR/$CONFIGURATION/macro\" && ! -f \"$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME/macro\" ]]; then\n mkdir -p \"$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME/\"\n cp \"$BUILD_DIR/$CONFIGURATION/macro\" \"$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME/macro\"\nfi"
XCTAssertTrue(buildPhase?.shellScript?.contains(expectedScript) == true)
XCTAssertTrue(buildPhase?.inputPaths.contains("$BUILD_DIR/$CONFIGURATION/\(macroExecutable.productName)") == true)
XCTAssertTrue(
buildPhase?.outputPaths
.contains("$BUILD_DIR/Debug-iphonesimulator/\(macroExecutable.productName)") == true
)
XCTAssertTrue(
buildPhase?.outputPaths
.contains("$BUILD_DIR/Debug-iphoneos/\(macroExecutable.productName)") == true
)
}

// MARK: - Helpers

struct XcodeprojElements {
Expand Down
1 change: 1 addition & 0 deletions fixtures/app_with_spm_dependencies/App/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ let project = Project(
dependencies: [
.target(name: "AppKit"),
.external(name: "Nimble"),
.external(name: "Testing"),
],
settings: .targetSettings
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import Nimble
import XCTest
import Testing

final class AppKitTests: XCTestCase {
func testExample() {
struct AppKitTestingTests {
@Test func example() {
expect(1).to(equal(1))
}
}
9 changes: 9 additions & 0 deletions fixtures/app_with_spm_dependencies/Tuist/Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,15 @@
"version" : "509.0.2"
}
},
{
"identity" : "swift-testing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-testing",
"state" : {
"revision" : "3ed2f90058afe4437369aee4880890e7fa65d1ec",
"version" : "0.5.1"
}
},
{
"identity" : "swiftlint",
"kind" : "remoteSourceControl",
Expand Down
1 change: 1 addition & 0 deletions fixtures/app_with_spm_dependencies/Tuist/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ let package = Package(
// Has an umbrella header where moduleName must be sanitized
.package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.2.0"),
.package(url: "https://github.com/googleads/swift-package-manager-google-mobile-ads", from: "11.1.0"),
.package(url: "https://github.com/apple/swift-testing", .upToNextMajor(from: "0.5.1")),
.package(path: "../LocalSwiftPackage"),
.package(path: "../StringifyMacro"),
]
Expand Down