Hive
swift-syntax prebuilts are not used when a macro package declares a test target
GitHub issue · Open
What happened?
Tuist builds swift-syntax from source even though SwiftPM downloaded the swift-syntax MacroSupport prebuilt and recorded it in workspace-state.json. This happens whenever the package that declares the macro also has a test target that reaches the macro through its runtime library. That’s the usual shape for macro packages, for example swift-composable-architecture, swift-dependencies, swift-case-paths and swift-perception.
The test target alone is enough, even though Tuist never generates test targets for remote packages.
Expected: the macro target links the prebuilt (-lMacroSupport) and no swift-syntax targets are compiled.
Actual: the macro target depends on the swift-syntax source targets and 17 of them are compiled. Removing the test target from the package manifest restores the prebuilt.
Cause
PackageInfoMapper.prebuiltEligibleTargets(packageInfo:packageType:) (source) adds every test target whose dependency closure reaches a macro to hostRoots:
let hostRoots = macroTargets.union(macroTestTargets)
let hostReachableTargets = dependencyClosure(from: hostRoots, includeHostOnlyTargets: true)
let nonHostReachableTargets = dependencyClosure(
from: allTargetNames.subtracting(hostRoots),
includeHostOnlyTargets: false
)
guard hostReachableTargets.isDisjoint(with: nonHostReachableTargets) else {
return []
}
A test like MacroDependencyTests → MacroDependency → MacroDependencyMacros pulls the runtime library MacroDependency into hostReachableTargets. MacroDependency is also a non-host root, so the sets overlap and prebuilts are disabled for the whole package.
Two things contribute:
- Tests that are never generated are still considered.
map(target:…)drops.testtargets unlesspackageType.includesTestTargets(includeLocalPackageTestTargets:)is true (source).prebuiltEligibleTargetsdoesn’t apply the same check. - The overlap check is broader than it needs to be. The overlapping runtime library doesn’t depend on swift-syntax, so it can’t conflict with the prebuilt.
Since an ineligible macro target keeps its swift-syntax source dependencies, one affected package anywhere in the graph means swift-syntax is compiled from source.
Possible fix
Only add macroTestTargets to hostRoots when packageType.includesTestTargets(includeLocalPackageTestTargets:) is true. With that change, the reproduction links -lMacroSupport, compiles no swift-syntax targets, and builds and runs. A narrower overlap check (only failing on overlapping targets that themselves depend on a prebuilt product) would also cover packages whose tests are generated.
The generated_app_with_swiftpm_prebuilt_macro_dependency fixture has no test target, which is why BuildAcceptanceTestSwiftPMPrebuiltMacro doesn’t catch this.
Disclosure: AI was used to investigate this problem and work up the example and write the report, though I have reviewed it thoroughly and am creating this issue by hand.
How do we reproduce it?
Reproduction project attached: tuist-prebuilt-repro.zip
It’s the generated_app_with_swiftpm_prebuilt_macro_dependency fixture plus one test target in MacroDependency/Package.swift:
.testTarget(
name: "MacroDependencyTests",
dependencies: ["MacroDependency"]
),
Tested with Tuist 4.212.0, Xcode 27.1 (Swift 6.4), Apple silicon.
unzip tuist-prebuilt-repro.zip && cd tuist-prebuilt-repro
mise trust && mise install # pins tuist 4.212.0 and sets TUIST_USE_SWIFTERPM=0
tuist install
tuist generate --no-open
xcodebuild build -workspace App.xcworkspace -scheme App CODE_SIGNING_ALLOWED=NO
Tuist/.build/workspace-state.jsonlists the swift-syntaxMacroSupportprebuilt.grep -rl --include=project.pbxproj -e -lMacroSupport .finds nothing.- The build log shows targets from the
swift-syntaxproject being compiled. - Remove the
.testTarget, then reruntuist generateandxcodebuild.-lMacroSupportis now linked and no swift-syntax targets are compiled.
TUIST_USE_SWIFTERPM=0 makes the prebuilt show up in workspace-state.json every time. With SwifterPM and a warm cache, the prebuilts array can be empty, which hides this issue. That’s a separate problem.
Error log
There’s no error. Tuist falls back to building swift-syntax from source without any message.
Tuist/.build/workspace-state.json (the prebuilt is available):
{
"identity": "swift-syntax",
"version": "602.0.0",
"libraryName": "MacroSupport",
"path": ".../Tuist/.build/prebuilts/swift-syntax/602.0.0/swiftlang-6.4.0.34.1-macosx27.0-MacroSupport"
}
Targets built by xcodebuild with the test target present:
(in target 'App' from project 'App')
(in target 'MacroDependency' from project 'MacroDependency')
(in target 'MacroDependencyMacros' from project 'MacroDependency')
(in target '_SwiftSyntaxCShims' from project 'swift-syntax')
(in target 'SwiftBasicFormat' from project 'swift-syntax')
(in target 'SwiftCompilerPlugin' from project 'swift-syntax')
(in target 'SwiftCompilerPluginMessageHandling' from project 'swift-syntax')
(in target 'SwiftDiagnostics' from project 'swift-syntax')
(in target 'SwiftOperators' from project 'swift-syntax')
(in target 'SwiftParser' from project 'swift-syntax')
(in target 'SwiftParserDiagnostics' from project 'swift-syntax')
(in target 'SwiftSyntax' from project 'swift-syntax')
(in target 'SwiftSyntax509' from project 'swift-syntax')
(in target 'SwiftSyntax510' from project 'swift-syntax')
(in target 'SwiftSyntax600' from project 'swift-syntax')
(in target 'SwiftSyntax601' from project 'swift-syntax')
(in target 'SwiftSyntax602' from project 'swift-syntax')
(in target 'SwiftSyntaxBuilder' from project 'swift-syntax')
(in target 'SwiftSyntaxMacroExpansion' from project 'swift-syntax')
(in target 'SwiftSyntaxMacros' from project 'swift-syntax')
Targets built with the test target removed:
(in target 'App' from project 'App')
(in target 'MacroDependency' from project 'MacroDependency')
(in target 'MacroDependencyMacros' from project 'MacroDependency')
macOS version
26.7.1
Tuist version
4.212.0
Xcode version
27.1
Investigate, reproduce, or fix this item in an isolated repository. Each Flight preserves its outcome and agent session.
Start a Flight and preserve its objective, outcome, and session here.
No GitHub comments yet.