From be97e9ec24a690a8cb3deafc4c909e3b88bae24d Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 14 Jun 2026 23:11:04 +0100 Subject: [PATCH 1/7] Support prebuilt macro plugins via binary artifact bundles Add a 'macro' artifact type to the artifact-bundle format so a compiler plugin that implements a Swift macro can be shipped as a prebuilt, host-keyed binary alongside (X)CFrameworks, instead of always being built from a source .macro target. A regular Swift target that depends on such a binary target now has the matching host variant resolved and passed to the compiler via -load-plugin-executable, exactly like a source-built macro. Verified end-to-end on macOS (arm64) and Linux (aarch64) with the native build system. --- .../SwiftModuleBuildDescription.swift | 28 +++++++++++++++++++ .../ArtifactsArchiveMetadata.swift | 4 +++ .../PackageModel/Module/BinaryModule.swift | 12 ++++++++ .../BinaryTarget+Extensions.swift | 27 ++++++++++++++++++ 4 files changed, 71 insertions(+) diff --git a/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift b/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift index 85ef4edd15e..c5e47d5d7a4 100644 --- a/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift +++ b/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift @@ -246,6 +246,18 @@ public final class SwiftModuleBuildDescription { } } + /// Binary dependencies that vend prebuilt macro plugin executables (an artifact bundle + /// containing a `macro`-typed artifact), as opposed to `.macro` targets built from source. + public var requiredBinaryMacros: [BinaryModule] { + get throws { + try self.target.recursiveModuleDependencies().compactMap { + guard $0.type == .binary, let binary = $0.underlying as? BinaryModule, + binary.containsMacro else { return nil } + return binary + } + } + } + /// ObservabilityScope with which to emit diagnostics private let observabilityScope: ObservabilityScope @@ -470,6 +482,22 @@ public final class SwiftModuleBuildDescription { } #endif + // Prebuilt macro plugins shipped as binary artifact bundles. These are host tools, so + // their variants are selected against the host (macro build) triple. The artifact key is + // used as the plugin module name, matching the `module:` of the `#externalMacro` decl. + try self.requiredBinaryMacros.forEach { binaryMacro in + let macros = try binaryMacro.parseMacroArtifactArchives( + for: macroBuildParameters.triple, + fileSystem: self.fileSystem + ) + for macro in macros where !macro.supportedTriples.isEmpty { + args += [ + "-Xfrontend", "-load-plugin-executable", + "-Xfrontend", "\(macro.executablePath.pathString)#\(macro.name)", + ] + } + } + if self.shouldDisableSandbox { let toolchainSupportsDisablingSandbox = DriverSupport.checkSupportedFrontendFlags( flags: ["-disable-sandbox"], diff --git a/Sources/PackageModel/ArtifactsArchiveMetadata.swift b/Sources/PackageModel/ArtifactsArchiveMetadata.swift index 4527640f00b..bbc68400c22 100644 --- a/Sources/PackageModel/ArtifactsArchiveMetadata.swift +++ b/Sources/PackageModel/ArtifactsArchiveMetadata.swift @@ -44,6 +44,10 @@ public struct ArtifactsArchiveMetadata: Equatable { // 3D models along with associated textures, or fonts, etc. public enum ArtifactType: String, RawRepresentable, Decodable { case executable + /// A compiler plugin executable that implements a Swift macro, to be loaded by the + /// compiler via `-load-plugin-executable`. Like `executable`, variants are keyed by + /// the host triple (the platform performing the compilation), not the build target. + case macro case staticLibrary case swiftSDK // Experimental support for Windows DLLs diff --git a/Sources/PackageModel/Module/BinaryModule.swift b/Sources/PackageModel/Module/BinaryModule.swift index 31d47fbb948..6d47dcbc568 100644 --- a/Sources/PackageModel/Module/BinaryModule.swift +++ b/Sources/PackageModel/Module/BinaryModule.swift @@ -97,6 +97,18 @@ public final class BinaryModule: Module { } } + /// Whether this binary artifact provides one or more prebuilt macro plugin executables. + public var containsMacro: Bool { + switch self.kind { + case .xcframework: + return false + case .artifactsArchive(let types): + return types.contains(.macro) + case .unknown: + return false + } + } + public enum Origin: Equatable { /// Represents an artifact that was downloaded from a remote URL. diff --git a/Sources/SPMBuildCore/BinaryTarget+Extensions.swift b/Sources/SPMBuildCore/BinaryTarget+Extensions.swift index 2b43741b21b..7c31b2aed3f 100644 --- a/Sources/SPMBuildCore/BinaryTarget+Extensions.swift +++ b/Sources/SPMBuildCore/BinaryTarget+Extensions.swift @@ -97,6 +97,33 @@ extension BinaryModule { } } + /// Parses the prebuilt macro plugin executables in this artifact bundle, selecting the + /// variant(s) matching the given host `triple`. Mirrors `parseExecutableArtifactArchives` + /// but filters for the `.macro` artifact type. The returned `ExecutableInfo.name` is the + /// artifact key, which doubles as the macro plugin module name passed to the compiler. + public func parseMacroArtifactArchives(for triple: Triple, fileSystem: any FileSystem) throws -> [ExecutableInfo] { + // The host triple might contain a version which we don't want to take into account here. + let versionLessTriple = try triple.withoutVersion() + let metadata = try ArtifactsArchiveMetadata.parse(fileSystem: fileSystem, rootPath: self.artifactPath) + // Filter out everything except macro plugins. + let macros = metadata.artifacts.filter { $0.value.type == .macro } + // Construct an ExecutableInfo for each matching variant. + return try macros.flatMap { entry in + try entry.value.variants.map { + guard let supportedTriples = $0.supportedTriples else { + throw StringError("No \"supportedTriples\" found in the artifact metadata for \(entry.key) in \(self.artifactPath)") + } + let filteredSupportedTriples = try supportedTriples + .filter { try $0.withoutVersion() == versionLessTriple } + return ExecutableInfo( + name: entry.key, + executablePath: self.artifactPath.appending($0.path), + supportedTriples: filteredSupportedTriples + ) + } + } + } + public func parseWindowsDLLArtifactArchives(for triple: Triple, fileSystem: any FileSystem) throws -> [WindowsDLLInfo] { // The host triple might contain a version which we don't want to take into account here. let versionLessTriple = try triple.withoutVersion() From 38c93ab331114947f5d180ba964b4fb27996b58d Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 15 Jun 2026 09:54:43 +0100 Subject: [PATCH 2/7] SwiftBuild: load prebuilt binary macros via SWIFT_LOAD_BINARY_MACROS Teach the SwiftBuild PIF bridge to recognize a binary artifact-bundle dependency that vends a prebuilt macro plugin (ArtifactType .macro): select the host variant and set SWIFT_LOAD_BINARY_MACROS on the consuming target (both the library path in buildSourceModule and the executable path in makeMainModuleProduct), and skip registering the bundle as a linkable file so the engine's artifact parser never sees the macro type. Requires the companion one-line change in the swift-build package adding SWIFT_LOAD_BINARY_MACROS to ProjectModel.BuildSettings.MultipleValueSetting (otherwise the setting is dropped during PIF decoding). Verified on macOS (arm64) and Linux (aarch64) with the default SwiftBuild engine and the native engine. --- Sources/SwiftBuildSupport/PIFBuilder.swift | 1 + .../SwiftBuildSupport/PackagePIFBuilder.swift | 9 +++++ .../PackagePIFProjectBuilder+Modules.swift | 30 ++++++++++++++++ .../PackagePIFProjectBuilder+Products.swift | 34 +++++++++++++++++++ 4 files changed, 74 insertions(+) diff --git a/Sources/SwiftBuildSupport/PIFBuilder.swift b/Sources/SwiftBuildSupport/PIFBuilder.swift index 83ee8c29e64..62e0f10225b 100644 --- a/Sources/SwiftBuildSupport/PIFBuilder.swift +++ b/Sources/SwiftBuildSupport/PIFBuilder.swift @@ -468,6 +468,7 @@ public final class PIFBuilder { addLocalRpaths: self.parameters.addLocalRpaths, packageDisplayVersion: package.manifest.displayName, pkgConfigDirectories: self.parameters.pkgConfigDirectories, + hostTriple: try self.parameters.pluginScriptRunner.hostTriple, fileSystem: self.fileSystem, observabilityScope: self.observabilityScope, ) diff --git a/Sources/SwiftBuildSupport/PackagePIFBuilder.swift b/Sources/SwiftBuildSupport/PackagePIFBuilder.swift index 0496993df0c..1593e161e5d 100644 --- a/Sources/SwiftBuildSupport/PackagePIFBuilder.swift +++ b/Sources/SwiftBuildSupport/PackagePIFBuilder.swift @@ -16,6 +16,7 @@ import protocol TSCBasic.FileSystem import struct Basics.AbsolutePath import struct Basics.SourceControlURL +import struct Basics.Triple import struct Basics.Diagnostic import struct Basics.ObservabilityMetadata import class Basics.ObservabilityScope @@ -50,6 +51,10 @@ public final class PackagePIFBuilder { let modulesGraph: ModulesGraph private let package: ResolvedPackage + /// The triple of the host performing the build. Used to select the right variant of a + /// prebuilt macro plugin shipped as a binary artifact bundle (macros are host tools). + let hostTriple: Basics.Triple + /// Contains the package declarative specification. let packageManifest: PackageModel.Manifest // FIXME: Can't we just use `package.manifest` instead? —— Paulo @@ -236,6 +241,7 @@ public final class PackagePIFBuilder { addLocalRpaths: AddLocalRpaths = .always, packageDisplayVersion: String?, pkgConfigDirectories: [AbsolutePath], + hostTriple: Basics.Triple, fileSystem: FileSystem, observabilityScope: ObservabilityScope, ) { @@ -249,6 +255,7 @@ public final class PackagePIFBuilder { self.createDynamicVariantsForLibraryProducts = createDynamicVariantsForLibraryProducts self.packageDisplayVersion = packageDisplayVersion self.pkgConfigDirectories = pkgConfigDirectories + self.hostTriple = hostTriple self.fileSystem = fileSystem self.observabilityScope = observabilityScope self.addLocalRpaths = addLocalRpaths @@ -266,6 +273,7 @@ public final class PackagePIFBuilder { addLocalRpaths: AddLocalRpaths = .always, packageDisplayVersion: String?, pkgConfigDirectories: [AbsolutePath], + hostTriple: Basics.Triple, fileSystem: FileSystem, observabilityScope: ObservabilityScope, ) { @@ -280,6 +288,7 @@ public final class PackagePIFBuilder { self.addLocalRpaths = addLocalRpaths self.packageDisplayVersion = packageDisplayVersion self.pkgConfigDirectories = pkgConfigDirectories + self.hostTriple = hostTriple self.fileSystem = fileSystem self.observabilityScope = observabilityScope } diff --git a/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Modules.swift b/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Modules.swift index b78d36efe71..ec61dd22606 100644 --- a/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Modules.swift +++ b/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Modules.swift @@ -31,6 +31,8 @@ import struct PackageGraph.ResolvedPackage import struct PackageLoading.GeneratedFiles +import SPMBuildCore + import enum SwiftBuild.ProjectModel /// Extension to create PIF **modules** for a given package. @@ -722,6 +724,26 @@ extension PackagePIFProjectBuilder { settings[.SKIP_BUILDING_DOCUMENTATION] = "YES" } + // A target that (transitively) depends on a binary artifact bundle vending a prebuilt + // macro plugin loads it via SWIFT_LOAD_BINARY_MACROS. Macros are host tools, so we select + // the variant matching the build host. The SwiftBuild engine turns each "path#Module" + // entry into `-load-plugin-executable`, exactly like a source-built macro. + for dependency in try sourceModule.recursiveModuleDependencies() { + guard dependency.type == .binary, + let binaryModule = dependency.underlying as? BinaryModule, + binaryModule.containsMacro else { continue } + let macros = try binaryModule.parseMacroArtifactArchives( + for: pifBuilder.hostTriple, + fileSystem: pifBuilder.fileSystem + ) + for macro in macros where !macro.supportedTriples.isEmpty { + let entry = "\(macro.executablePath.pathString)#\(macro.name)" + var loadBinaryMacros = settings[.SWIFT_LOAD_BINARY_MACROS] ?? [] + loadBinaryMacros.append(entry) + settings[.SWIFT_LOAD_BINARY_MACROS] = loadBinaryMacros + } + } + sourceModule.addParseAsLibrarySettings(to: &settings, toolsVersion: package.manifest.toolsVersion, fileSystem: pifBuilder.fileSystem) // Handle the target's dependencies (but only link against them if needed). @@ -763,6 +785,14 @@ extension PackagePIFProjectBuilder { log(.error, "'\(moduleDependency.name)' is a binary dependency, but its underlying module was not") break } + if binaryModule.containsMacro { + // Prebuilt macro plugin: loaded via SWIFT_LOAD_BINARY_MACROS (set above), not + // linked or embedded. Don't add it as a build file so the SwiftBuild engine does + // not treat the artifact bundle as a linkable/resource artifact (its info.json + // 'macro' type would otherwise be rejected by the engine's artifact parser). + log(.debug, indent: 1, "Loading prebuilt binary macro '\(moduleDependency.name)'") + break + } let binaryReference = self.binaryGroup.addFileReference { id in return Self.createBinaryModuleFileReference(binaryModule, id: id) } diff --git a/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Products.swift b/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Products.swift index f2107a43127..8beb1a7cdd1 100644 --- a/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Products.swift +++ b/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Products.swift @@ -19,6 +19,7 @@ import class Basics.ObservabilitySystem import struct Basics.SourceControlURL import PackageLoading +import SPMBuildCore import class PackageModel.BinaryModule import class PackageModel.Manifest @@ -203,6 +204,23 @@ extension PackagePIFProjectBuilder { settings[.CLANG_CXX_LANGUAGE_STANDARD] = mainModule.cxxLanguageStandard settings[.SWIFT_ENABLE_BARE_SLASH_REGEX] = "NO" + // Load any prebuilt macro plugins that this product's main module (transitively) depends + // on, shipped as binary artifact bundles. See the matching logic in `buildSourceModule`. + for dependency in try mainModule.recursiveModuleDependencies() { + guard dependency.type == .binary, + let binaryModule = dependency.underlying as? BinaryModule, + binaryModule.containsMacro else { continue } + let macros = try binaryModule.parseMacroArtifactArchives( + for: pifBuilder.hostTriple, + fileSystem: pifBuilder.fileSystem + ) + for macro in macros where !macro.supportedTriples.isEmpty { + var loadBinaryMacros = settings[.SWIFT_LOAD_BINARY_MACROS] ?? [] + loadBinaryMacros.append("\(macro.executablePath.pathString)#\(macro.name)") + settings[.SWIFT_LOAD_BINARY_MACROS] = loadBinaryMacros + } + } + // Create a group for the source files of the main module // For now we use an absolute path for it, but we should really make it // container-relative, since it's always inside the package directory. @@ -383,6 +401,12 @@ extension PackagePIFProjectBuilder { log(.error, "'\(moduleDependency.name)' is a binary dependency, but its underlying module was not") break } + if binaryModule.containsMacro { + // Prebuilt macro plugin: loaded via SWIFT_LOAD_BINARY_MACROS on the source + // module, not linked/embedded in the product. Skip adding it as a build file. + log(.debug, indent: 1, "Loading prebuilt binary macro '\(moduleDependency.name)'") + break + } let binaryFileRef = self.binaryGroup.addFileReference { id in Self.createBinaryModuleFileReference(binaryModule, id: id) } @@ -676,6 +700,11 @@ extension PackagePIFProjectBuilder { for module in product.modules { // Binary targets are special in that they are just linked, not built. if let binaryTarget = module.underlying as? BinaryModule { + if binaryTarget.containsMacro { + // Prebuilt macro plugin: loaded via SWIFT_LOAD_BINARY_MACROS, not linked. + log(.debug, indent: 1, "Loading prebuilt binary macro '\(binaryTarget.name)'") + continue + } let binaryFileRef = self.binaryGroup.addFileReference { id in FileReference(id: id, path: binaryTarget.artifactPath.pathString) } @@ -793,6 +822,11 @@ extension PackagePIFProjectBuilder { } if let binaryTarget = moduleDependency.underlying as? BinaryModule { + if binaryTarget.containsMacro { + // Prebuilt macro plugin: loaded via SWIFT_LOAD_BINARY_MACROS, not linked. + log(.debug, indent: 1, "Loading prebuilt binary macro '\(binaryTarget.name)'") + return + } let binaryFileRef = self.binaryGroup.addFileReference { id in FileReference(id: id, path: binaryTarget.artifactPath.pathString) } From ebe513cfc805f034343fd0cd3459b11002e24c32 Mon Sep 17 00:00:00 2001 From: Tim <0xtimc@gmail.com> Date: Tue, 16 Jun 2026 19:19:11 +0100 Subject: [PATCH 3/7] Update ignore for edited packages --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9c0fc9c6b3a..16e74f15cae 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ Package.resolved /stage/ Utilities/InstalledSwiftPMConfiguration/config.json Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/build +Packages/ From 560f73540424035bb2b7277808c37e75d187e567 Mon Sep 17 00:00:00 2001 From: Tim <0xtimc@gmail.com> Date: Tue, 16 Jun 2026 19:37:30 +0100 Subject: [PATCH 4/7] Tidy ups --- .../BuildDescription/SwiftModuleBuildDescription.swift | 9 +++------ Sources/PackageModel/ArtifactsArchiveMetadata.swift | 7 +++---- Sources/PackageModel/Module/BinaryModule.swift | 3 +-- Sources/SPMBuildCore/BinaryTarget+Extensions.swift | 5 +---- Sources/SwiftBuildSupport/PackagePIFBuilder.swift | 2 -- .../PackagePIFProjectBuilder+Modules.swift | 10 ++-------- .../PackagePIFProjectBuilder+Products.swift | 6 ++---- 7 files changed, 12 insertions(+), 30 deletions(-) diff --git a/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift b/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift index c5e47d5d7a4..94972ae749b 100644 --- a/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift +++ b/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift @@ -483,18 +483,15 @@ public final class SwiftModuleBuildDescription { #endif // Prebuilt macro plugins shipped as binary artifact bundles. These are host tools, so - // their variants are selected against the host (macro build) triple. The artifact key is - // used as the plugin module name, matching the `module:` of the `#externalMacro` decl. + // their variants are selected against the host triple. The artifact key is + // used as the plugin module name, matching the `module:` of the `#externalMacro` declaration try self.requiredBinaryMacros.forEach { binaryMacro in let macros = try binaryMacro.parseMacroArtifactArchives( for: macroBuildParameters.triple, fileSystem: self.fileSystem ) for macro in macros where !macro.supportedTriples.isEmpty { - args += [ - "-Xfrontend", "-load-plugin-executable", - "-Xfrontend", "\(macro.executablePath.pathString)#\(macro.name)", - ] + args += ["-Xfrontend", "-load-plugin-executable", "-Xfrontend", "\(macro.executablePath.pathString)#\(macro.name)"] } } diff --git a/Sources/PackageModel/ArtifactsArchiveMetadata.swift b/Sources/PackageModel/ArtifactsArchiveMetadata.swift index bbc68400c22..56028a0350f 100644 --- a/Sources/PackageModel/ArtifactsArchiveMetadata.swift +++ b/Sources/PackageModel/ArtifactsArchiveMetadata.swift @@ -44,10 +44,6 @@ public struct ArtifactsArchiveMetadata: Equatable { // 3D models along with associated textures, or fonts, etc. public enum ArtifactType: String, RawRepresentable, Decodable { case executable - /// A compiler plugin executable that implements a Swift macro, to be loaded by the - /// compiler via `-load-plugin-executable`. Like `executable`, variants are keyed by - /// the host triple (the platform performing the compilation), not the build target. - case macro case staticLibrary case swiftSDK // Experimental support for Windows DLLs @@ -55,6 +51,9 @@ public struct ArtifactsArchiveMetadata: Equatable { // Can't be marked as formally deprecated as we still need to use this value for warning users. case crossCompilationDestination + + // Plugin executable that implements a macro, keyed to the host triple, not build target + case macro } public struct Variant: Equatable { diff --git a/Sources/PackageModel/Module/BinaryModule.swift b/Sources/PackageModel/Module/BinaryModule.swift index 6d47dcbc568..14d425aa4c8 100644 --- a/Sources/PackageModel/Module/BinaryModule.swift +++ b/Sources/PackageModel/Module/BinaryModule.swift @@ -21,7 +21,7 @@ public final class BinaryModule: Module { /// The kind of binary artifact. public let kind: Kind - + /// The original source of the binary artifact. public let origin: Origin @@ -97,7 +97,6 @@ public final class BinaryModule: Module { } } - /// Whether this binary artifact provides one or more prebuilt macro plugin executables. public var containsMacro: Bool { switch self.kind { case .xcframework: diff --git a/Sources/SPMBuildCore/BinaryTarget+Extensions.swift b/Sources/SPMBuildCore/BinaryTarget+Extensions.swift index 7c31b2aed3f..cf0ef336e93 100644 --- a/Sources/SPMBuildCore/BinaryTarget+Extensions.swift +++ b/Sources/SPMBuildCore/BinaryTarget+Extensions.swift @@ -97,10 +97,7 @@ extension BinaryModule { } } - /// Parses the prebuilt macro plugin executables in this artifact bundle, selecting the - /// variant(s) matching the given host `triple`. Mirrors `parseExecutableArtifactArchives` - /// but filters for the `.macro` artifact type. The returned `ExecutableInfo.name` is the - /// artifact key, which doubles as the macro plugin module name passed to the compiler. + /// Parses the prebuilt macro plugin executables in this artifact bundle, matching the given host `triple`. public func parseMacroArtifactArchives(for triple: Triple, fileSystem: any FileSystem) throws -> [ExecutableInfo] { // The host triple might contain a version which we don't want to take into account here. let versionLessTriple = try triple.withoutVersion() diff --git a/Sources/SwiftBuildSupport/PackagePIFBuilder.swift b/Sources/SwiftBuildSupport/PackagePIFBuilder.swift index 1593e161e5d..8e92f46ef69 100644 --- a/Sources/SwiftBuildSupport/PackagePIFBuilder.swift +++ b/Sources/SwiftBuildSupport/PackagePIFBuilder.swift @@ -51,8 +51,6 @@ public final class PackagePIFBuilder { let modulesGraph: ModulesGraph private let package: ResolvedPackage - /// The triple of the host performing the build. Used to select the right variant of a - /// prebuilt macro plugin shipped as a binary artifact bundle (macros are host tools). let hostTriple: Basics.Triple /// Contains the package declarative specification. diff --git a/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Modules.swift b/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Modules.swift index ec61dd22606..69490b57e72 100644 --- a/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Modules.swift +++ b/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Modules.swift @@ -724,10 +724,6 @@ extension PackagePIFProjectBuilder { settings[.SKIP_BUILDING_DOCUMENTATION] = "YES" } - // A target that (transitively) depends on a binary artifact bundle vending a prebuilt - // macro plugin loads it via SWIFT_LOAD_BINARY_MACROS. Macros are host tools, so we select - // the variant matching the build host. The SwiftBuild engine turns each "path#Module" - // entry into `-load-plugin-executable`, exactly like a source-built macro. for dependency in try sourceModule.recursiveModuleDependencies() { guard dependency.type == .binary, let binaryModule = dependency.underlying as? BinaryModule, @@ -786,10 +782,8 @@ extension PackagePIFProjectBuilder { break } if binaryModule.containsMacro { - // Prebuilt macro plugin: loaded via SWIFT_LOAD_BINARY_MACROS (set above), not - // linked or embedded. Don't add it as a build file so the SwiftBuild engine does - // not treat the artifact bundle as a linkable/resource artifact (its info.json - // 'macro' type would otherwise be rejected by the engine's artifact parser). + // Don't add it as a build file so the SwiftBuild engine does + // not treat the artifact bundle as a linkable/resource artifact log(.debug, indent: 1, "Loading prebuilt binary macro '\(moduleDependency.name)'") break } diff --git a/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Products.swift b/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Products.swift index 8beb1a7cdd1..450e546b82f 100644 --- a/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Products.swift +++ b/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Products.swift @@ -204,8 +204,7 @@ extension PackagePIFProjectBuilder { settings[.CLANG_CXX_LANGUAGE_STANDARD] = mainModule.cxxLanguageStandard settings[.SWIFT_ENABLE_BARE_SLASH_REGEX] = "NO" - // Load any prebuilt macro plugins that this product's main module (transitively) depends - // on, shipped as binary artifact bundles. See the matching logic in `buildSourceModule`. + // Load any prebuilt macro plugins for dependency in try mainModule.recursiveModuleDependencies() { guard dependency.type == .binary, let binaryModule = dependency.underlying as? BinaryModule, @@ -402,8 +401,7 @@ extension PackagePIFProjectBuilder { break } if binaryModule.containsMacro { - // Prebuilt macro plugin: loaded via SWIFT_LOAD_BINARY_MACROS on the source - // module, not linked/embedded in the product. Skip adding it as a build file. + // Skip adding prebuilt macro plugin as a build file log(.debug, indent: 1, "Loading prebuilt binary macro '\(moduleDependency.name)'") break } From 3994914f8788ccada3ee377761e5c9dc55fd4335 Mon Sep 17 00:00:00 2001 From: Tim <0xtimc@gmail.com> Date: Wed, 17 Jun 2026 11:52:45 +0100 Subject: [PATCH 5/7] Add some tests --- .../ArtifactsArchiveMetadataTests.swift | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/Tests/SPMBuildCoreTests/ArtifactsArchiveMetadataTests.swift b/Tests/SPMBuildCoreTests/ArtifactsArchiveMetadataTests.swift index 7b930563a25..eaafa648ee0 100644 --- a/Tests/SPMBuildCoreTests/ArtifactsArchiveMetadataTests.swift +++ b/Tests/SPMBuildCoreTests/ArtifactsArchiveMetadataTests.swift @@ -12,6 +12,7 @@ import Basics import PackageModel +import SPMBuildCore import Testing struct ArtifactsArchiveMetadataTests { @@ -125,4 +126,98 @@ struct ArtifactsArchiveMetadataTests { ) } } + + @Test + func parseMacroMetadata() throws { + let fileSystem = InMemoryFileSystem() + try fileSystem.writeFileContents( + "/info.json", + string: """ + { + "schemaVersion": "1.0", + "artifacts": { + "MyMacros": { + "type": "macro", + "version": "1.0.0", + "variants": [ + { + "path": "arm64-apple-macosx/MyMacros", + "supportedTriples": ["arm64-apple-macosx"] + }, + { + "path": "aarch64-unknown-linux-gnu/MyMacros", + "supportedTriples": ["aarch64-unknown-linux-gnu"] + } + ] + } + } + } + """ + ) + + let metadata = try ArtifactsArchiveMetadata.parse(fileSystem: fileSystem, rootPath: .root) + let expected = try ArtifactsArchiveMetadata( + schemaVersion: "1.0", + artifacts: [ + "MyMacros": ArtifactsArchiveMetadata.Artifact( + type: .macro, + version: "1.0.0", + variants: [ + ArtifactsArchiveMetadata.Variant( + path: "arm64-apple-macosx/MyMacros", + supportedTriples: [Triple("arm64-apple-macosx")] + ), + ArtifactsArchiveMetadata.Variant( + path: "aarch64-unknown-linux-gnu/MyMacros", + supportedTriples: [Triple("aarch64-unknown-linux-gnu")] + ), + ] + ), + ] + ) + #expect(metadata == expected, "Actual is not as expected") + } + + @Test + func parseMacroArtifactArchivesSelectsHostVariant() throws { + let fileSystem = InMemoryFileSystem() + try fileSystem.writeFileContents( + "/info.json", + string: """ + { + "schemaVersion": "1.0", + "artifacts": { + "MyMacros": { + "type": "macro", + "version": "1.0.0", + "variants": [ + { + "path": "arm64-apple-macosx/MyMacros", + "supportedTriples": ["arm64-apple-macosx"] + }, + { + "path": "aarch64-unknown-linux-gnu/MyMacros", + "supportedTriples": ["aarch64-unknown-linux-gnu"] + } + ] + } + } + } + """ + ) + + let hostTriple = try Triple("arm64-apple-macosx") + let binaryTarget = BinaryModule( + name: "MyMacros", kind: .artifactsArchive(types: [.macro]), path: .root, origin: .local + ) + let macros = try binaryTarget.parseMacroArtifactArchives(for: hostTriple, fileSystem: fileSystem) + + // One entry per variant; only the variant matching the host triple keeps a non-empty + // `supportedTriples`, which is how the build plan picks the plugin to load. + let hostMacros = macros.filter { !$0.supportedTriples.isEmpty } + #expect(hostMacros.count == 1) + #expect(hostMacros.first?.name == "MyMacros") + #expect(hostMacros.first?.executablePath.pathString == "/arm64-apple-macosx/MyMacros") + #expect(hostMacros.first?.supportedTriples == [hostTriple]) + } } From 71a1133f8d92d660609c7f93f04fd4a84b6d70af Mon Sep 17 00:00:00 2001 From: Tim <0xtimc@gmail.com> Date: Wed, 17 Jun 2026 13:27:49 +0100 Subject: [PATCH 6/7] Add some more tests --- Tests/BuildTests/BuildPlanTests.swift | 74 +++++++++++++++++ .../PrebuiltsPIFTests.swift | 83 +++++++++++++++++++ 2 files changed, 157 insertions(+) diff --git a/Tests/BuildTests/BuildPlanTests.swift b/Tests/BuildTests/BuildPlanTests.swift index bead082765c..9a93b7100fe 100644 --- a/Tests/BuildTests/BuildPlanTests.swift +++ b/Tests/BuildTests/BuildPlanTests.swift @@ -6814,6 +6814,80 @@ class BuildPlanTestCase: BuildSystemProviderTestCase { try await self.sanitizerTest(.address, expectedName: "address") } + func testBinaryMacroLoadsPluginExecutable() async throws { + // A Swift module that depends on a `macro`-typed artifact bundle should be compiled + // with `-load-plugin-executable #`, where the artifact + // name matches the `module:` of the consumer's `#externalMacro` declaration. This is + // the prebuilt counterpart to building a `.macro` target from source. + let fs = InMemoryFileSystem(emptyFiles: "/Pkg/Sources/MyLib/MyLib.swift") + + let toolPath = AbsolutePath("/Pkg/MyMacros.artifactbundle") + try fs.createDirectory(toolPath, recursive: true) + try fs.writeFileContents( + toolPath.appending("info.json"), + string: """ + { + "schemaVersion": "1.0", + "artifacts": { + "MyMacros": { + "type": "macro", + "version": "1.0.0", + "variants": [ + { + "path": "x86_64-apple-macosx/MyMacros", + "supportedTriples": ["x86_64-apple-macosx"] + }, + { + "path": "x86_64-unknown-linux-gnu/MyMacros", + "supportedTriples": ["x86_64-unknown-linux-gnu"] + } + ] + } + } + } + """ + ) + + let observability = ObservabilitySystem.makeForTesting() + let graph = try loadModulesGraph( + fileSystem: fs, + manifests: [ + Manifest.createRootManifest( + displayName: "Pkg", + path: "/Pkg", + targets: [ + TargetDescription(name: "MyLib", dependencies: ["MyMacros"]), + TargetDescription(name: "MyMacros", path: "MyMacros.artifactbundle", type: .binary), + ] + ), + ], + binaryArtifacts: [ + .plain("pkg"): [ + "MyMacros": .init(kind: .artifactsArchive(types: [.macro]), originURL: nil, path: toolPath), + ], + ], + observabilityScope: observability.topScope + ) + XCTAssertNoDiagnostics(observability.diagnostics) + + let result = try await BuildPlanResult(plan: mockBuildPlan( + triple: .x86_64MacOS, + graph: graph, + fileSystem: fs, + observabilityScope: observability.topScope + )) + XCTAssertNoDiagnostics(observability.diagnostics) + + let lib = try result.moduleBuildDescription(for: "MyLib").swift() + let args = try lib.compileArguments().joined(separator: " ") + + // Only the host-matching variant (x86_64-apple-macosx) should be loaded, keyed by the + // artifact name; the foreign Linux variant must not be passed to the host compiler. + XCTAssertMatch(args, .contains("-load-plugin-executable")) + XCTAssertMatch(args, .contains("x86_64-apple-macosx/MyMacros#MyMacros")) + XCTAssertNoMatch(args, .contains("x86_64-unknown-linux-gnu/MyMacros")) + } + func testThreadSanitizer() async throws { try await self.sanitizerTest(.thread, expectedName: "thread") } diff --git a/Tests/SwiftBuildSupportTests/PrebuiltsPIFTests.swift b/Tests/SwiftBuildSupportTests/PrebuiltsPIFTests.swift index b4cfcdeee8e..8c893c31745 100644 --- a/Tests/SwiftBuildSupportTests/PrebuiltsPIFTests.swift +++ b/Tests/SwiftBuildSupportTests/PrebuiltsPIFTests.swift @@ -450,4 +450,87 @@ struct PrebuiltsPIFTests { #expect(isHost == false, "\(target.common.name)") } } + + // A module that depends on a `macro`-typed artifact bundle should have the host-matching + // plugin recorded in `SWIFT_LOAD_BINARY_MACROS`, which the build engine turns into + // `-load-plugin-executable #`. This is the PIF counterpart of the native + // `testBinaryMacroLoadsPluginExecutable` build-plan test. + @Test func testBinaryMacroSetsLoadBinaryMacrosSetting() async throws { + let observability = ObservabilitySystem.makeForTesting() + + let fs = InMemoryFileSystem( + emptyFiles: [ + "/MyPackage/Sources/MyLib/MyLib.swift", + "/MyPackage/Sources/MyApp/MyApp.swift", + ] + ) + + let bundlePath = AbsolutePath("/MyPackage/MyMacros.artifactbundle") + try fs.createDirectory(bundlePath, recursive: true) + // Cover every triple the test host might run on so a host variant is always selected. + try fs.writeFileContents( + bundlePath.appending("info.json"), + string: """ + { + "schemaVersion": "1.0", + "artifacts": { + "MyMacros": { + "type": "macro", + "version": "1.0.0", + "variants": [ + { "path": "arm64-apple-macosx/MyMacros", "supportedTriples": ["arm64-apple-macosx"] }, + { "path": "x86_64-apple-macosx/MyMacros", "supportedTriples": ["x86_64-apple-macosx"] }, + { "path": "aarch64-unknown-linux-gnu/MyMacros", "supportedTriples": ["aarch64-unknown-linux-gnu"] }, + { "path": "x86_64-unknown-linux-gnu/MyMacros", "supportedTriples": ["x86_64-unknown-linux-gnu"] } + ] + } + } + } + """ + ) + + let graph = try loadModulesGraph( + fileSystem: fs, + manifests: [ + Manifest.createRootManifest( + displayName: "MyPackage", + path: "/MyPackage", + products: [ + ProductDescription(name: "MyApp", type: .executable, targets: ["MyApp"]), + ], + targets: [ + TargetDescription(name: "MyApp", dependencies: ["MyLib"], type: .executable), + TargetDescription(name: "MyLib", dependencies: ["MyMacros"]), + TargetDescription(name: "MyMacros", path: "MyMacros.artifactbundle", type: .binary), + ] + ), + ], + binaryArtifacts: [ + .plain("mypackage"): [ + "MyMacros": .init(kind: .artifactsArchive(types: [.macro]), originURL: nil, path: bundlePath), + ], + ], + observabilityScope: observability.topScope + ) + + let pifBuilder: PIFBuilder = PIFBuilder( + graph: graph, + parameters: try PIFBuilderParameters.constructDefaultParametersForTesting( + temporaryDirectory: AbsolutePath.root, addLocalRpaths: .always), + fileSystem: fs, + observabilityScope: observability.topScope + ) + let (pif, _) = try await pifBuilder.constructPIF( + buildParameters: mockBuildParameters(destination: .host, buildSystemKind: .swiftbuild) + ) + + let targets = pif.workspace.projects.flatMap { $0.underlying.targets } + let myLib = try #require(targets.first { $0.common.name == "MyLib" }) + + let loadBinaryMacros = myLib.common.buildConfigs + .compactMap { $0.settings[.SWIFT_LOAD_BINARY_MACROS] } + .flatMap { $0 } + #expect(!loadBinaryMacros.isEmpty, "MyLib should load the prebuilt macro plugin") + #expect(loadBinaryMacros.contains { $0.hasSuffix("#MyMacros") }) + } } From f8e1f9f0ed2bdbd2e4218b97701ebf1f7af49386 Mon Sep 17 00:00:00 2001 From: Tim <0xtimc@gmail.com> Date: Thu, 18 Jun 2026 17:07:12 +0100 Subject: [PATCH 7/7] Remove unnecessary test --- .../ArtifactsArchiveMetadataTests.swift | 51 ------------------- 1 file changed, 51 deletions(-) diff --git a/Tests/SPMBuildCoreTests/ArtifactsArchiveMetadataTests.swift b/Tests/SPMBuildCoreTests/ArtifactsArchiveMetadataTests.swift index eaafa648ee0..b7ca7f9aa62 100644 --- a/Tests/SPMBuildCoreTests/ArtifactsArchiveMetadataTests.swift +++ b/Tests/SPMBuildCoreTests/ArtifactsArchiveMetadataTests.swift @@ -127,57 +127,6 @@ struct ArtifactsArchiveMetadataTests { } } - @Test - func parseMacroMetadata() throws { - let fileSystem = InMemoryFileSystem() - try fileSystem.writeFileContents( - "/info.json", - string: """ - { - "schemaVersion": "1.0", - "artifacts": { - "MyMacros": { - "type": "macro", - "version": "1.0.0", - "variants": [ - { - "path": "arm64-apple-macosx/MyMacros", - "supportedTriples": ["arm64-apple-macosx"] - }, - { - "path": "aarch64-unknown-linux-gnu/MyMacros", - "supportedTriples": ["aarch64-unknown-linux-gnu"] - } - ] - } - } - } - """ - ) - - let metadata = try ArtifactsArchiveMetadata.parse(fileSystem: fileSystem, rootPath: .root) - let expected = try ArtifactsArchiveMetadata( - schemaVersion: "1.0", - artifacts: [ - "MyMacros": ArtifactsArchiveMetadata.Artifact( - type: .macro, - version: "1.0.0", - variants: [ - ArtifactsArchiveMetadata.Variant( - path: "arm64-apple-macosx/MyMacros", - supportedTriples: [Triple("arm64-apple-macosx")] - ), - ArtifactsArchiveMetadata.Variant( - path: "aarch64-unknown-linux-gnu/MyMacros", - supportedTriples: [Triple("aarch64-unknown-linux-gnu")] - ), - ] - ), - ] - ) - #expect(metadata == expected, "Actual is not as expected") - } - @Test func parseMacroArtifactArchivesSelectsHostVariant() throws { let fileSystem = InMemoryFileSystem()