Skip to content

Commit

Permalink
Fix integration of swift-testing (#6062)
Browse files Browse the repository at this point in the history
* Fix integration of swift-testing

* Move macro build phase before compile sources

* chore: add reference

Co-authored-by: Shai Mishali <freak4pc@gmail.com>

---------

Co-authored-by: Daniele Formichelli <df@bendingspoons.com>
Co-authored-by: Shai Mishali <freak4pc@gmail.com>
  • Loading branch information
3 people committed Mar 11, 2024
1 parent 878aa83 commit 87cdf10
Show file tree
Hide file tree
Showing 11 changed files with 154 additions and 135 deletions.
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 {
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)
- or, in some cases, they directly depend on the executable: Target -> 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", // https://github.com/apple/swift-testing
].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

0 comments on commit 87cdf10

Please sign in to comment.