Swift Macro Support in Tuist#
Tuist models Swift macros as a first-class product type and routes them through Xcode's native macro dependency mechanism rather than shell-copy workarounds. Three distinct concerns drive the implementation:
- Graph modeling β dedicated
.macronodes in the dependency graph - Build settings generation β native Xcode settings for source and precompiled macro targets
- Framework search path exclusion β macro references are not linkable products and must be kept out of
FRAMEWORK_SEARCH_PATHS
This design was established in PR #11825 (merged 2026-07-14), which replaced an earlier workaround that copied macro executables to a hardcoded Debug path and injected -load-plugin-executable flags into every consumer.
Graph Modeling#
Product and Dependency Types#
The Product enum in XcodeGraph/Models/Product.swift carries a .macro case. At the dependency-graph layer, two parallel types represent macros:
GraphDependency.macro(path:)β a runtime node for a macro executable on diskGraphDependencyReference.macro(path:)β the resolved reference type used downstream by generators
GraphDependencyReference.macro carries no PlatformCondition β macros are platform-independent at the reference level β and its precompiledPath is nil , which is the key property that keeps macros out of framework search path generation.
Traversal Helpers in GraphTraverser#
isDependencyPrecompiledMacro(_:) is the central boolean filter: it returns true only for .macro nodes. It is passed as a skip predicate throughout filterDependencies calls so macro nodes are never traversed into by the standard linking and embedding logic.
allPrecompiledSwiftMacroExecutables(path:name:) collects precompiled macro executables for a given target. It matches both direct .macro nodes and XCFrameworks whose inner .macro sub-dependency indicates a cached macro. Results are formatted as path#baseName strings β the format expected by SWIFT_LOAD_BINARY_MACROS β and memoized in precompiledSwiftMacroExecutablesCache.
Native Xcode Build Settings#
Both macro producer and consumer settings are emitted in ConfigGenerator.swift.
Source Macro Targets#
swiftMacroImplementationDerivedSettings(_:) fires when target.product == .macro and emits:
| Setting | Value | Purpose |
|---|---|---|
SWIFT_IMPLEMENTS_MACROS_FOR_MODULE_NAMES | $(PRODUCT_MODULE_NAME) | Declares this target as a macro implementation |
SUPPORTED_PLATFORMS | $(HOST_PLATFORM) | Builds for the build-host (macOS), not the target device |
SDKROOT | auto | Lets Xcode pick the host SDK |
EAGER_COMPILATION_DISABLE | YES | Prevents premature compilation before macro host is ready |
SWIFT_INSTALL_MODULE | NO | Suppresses module installation for macro executables |
SKIP_BUILDING_DOCUMENTATION | YES | Avoids doc-build overhead on host-only tools |
With these settings, Xcode derives the macro executable path from the target dependency graph and propagates the macro implementation transitively. No shell copy phase is needed.
Binary / Precompiled Macros#
precompiledSwiftMacrosDerivedSettings(_:graphTraverser:projectPath:) calls allPrecompiledSwiftMacroExecutables and, if the result is non-empty, sets:
SWIFT_LOAD_BINARY_MACROS = ["path/to/macro#MacroName", ...]
This is the Xcode-native path for cached or vendored macro binaries.
PackageInfoMapper: SPM Macro Targets#
PackageInfoMapper identifies SPM macro targets through isHostOnlyTarget() and macOSTargets(), then:
- Assigns them exclusively to the
.macdestination - Maps them to the
.macroproduct type - Ensures
PackageSettings.baseSettings(e.g.,MACOSX_DEPLOYMENT_TARGET) is merged into macro targets β a gap that had caused build failures before PR #11574
Exclusion from Framework Search Paths#
Macro executables are compile-time tools β they are never linked into a final binary. FrameworkSearchPathsGraphMapper must therefore exclude them when computing FRAMEWORK_SEARCH_PATHS, OTHER_CFLAGS, and related settings.
The exclusion is achieved at the graph traversal layer, before the mapper runs:
-
searchablePathDependencies(called at line 71 ofFrameworkSearchPathsGraphMapper) delegates tolinkableDependenciesand related helpers, all of which useisDependencyPrecompiledMacroas askippredicate. -
isDependencyPrecompiledMacroreturnstruefor.macronodes, causing them to be skipped in everyfilterDependenciestraversal. It is used as a skip filter in:embeddableFrameworksβ skips macro nodes when collecting dynamically-embedded frameworkstransitiveStaticDependenciesβ skips macro nodes when collecting static products to link
-
precompiledPathisnilfor.macro.FrameworkSearchPathsGraphMapperderives search paths by calling.compactMap(\.precompiledPath)on the dependency set , so.macroreferences automatically produce no path and contribute nothing toFRAMEWORK_SEARCH_PATHS.
The net result: macro references flow through only the SWIFT_LOAD_BINARY_MACROS / SWIFT_IMPLEMENTS_MACROS_FOR_MODULE_NAMES channel, not through any linker search path setting.
Key Files#
| File | Role |
|---|---|
XcodeGraph/Models/Product.swift | Product.macro enum case |
TuistCore/Graph/GraphDependencyReference.swift | GraphDependencyReference.macro with nil precompiledPath |
TuistCore/Graph/GraphTraverser.swift | allPrecompiledSwiftMacroExecutables, isDependencyPrecompiledMacro |
TuistGenerator/Generator/ConfigGenerator.swift | swiftMacroImplementationDerivedSettings, precompiledSwiftMacrosDerivedSettings |
TuistGenerator/Mappers/FrameworkSearchPathsGraphMapper.swift | Framework search path generation (macros excluded upstream) |
TuistLoader/SwiftPackageManager/PackageInfoMapper.swift | SPM macro target β .macro product type mapping |