diff --git a/Fixtures/PIFBuilder/UnknownPlatforms/Package.swift b/Fixtures/PIFBuilder/UnknownPlatforms/Package.swift new file mode 100644 index 00000000000..3aedee44976 --- /dev/null +++ b/Fixtures/PIFBuilder/UnknownPlatforms/Package.swift @@ -0,0 +1,17 @@ +// swift-tools-version: 6.2 + +import PackageDescription + +let package = Package( + name: "UnknownPlatforms", + targets: [ + .executableTarget( + name: "UnknownPlatforms", + swiftSettings: [ + .define("FOO", .when(platforms: [.custom("DoesNotExist")])), + .define("BAR", .when(platforms: [.linux])), + .define("BAZ", .when(platforms: [.macOS])), + ], + ), + ] +) diff --git a/Fixtures/PIFBuilder/UnknownPlatforms/Sources/UnknownPlatforms/UnknownPlatforms.swift b/Fixtures/PIFBuilder/UnknownPlatforms/Sources/UnknownPlatforms/UnknownPlatforms.swift new file mode 100644 index 00000000000..7ab2b4f88fa --- /dev/null +++ b/Fixtures/PIFBuilder/UnknownPlatforms/Sources/UnknownPlatforms/UnknownPlatforms.swift @@ -0,0 +1,9 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +@main +struct UnknownPlatforms { + static func main() { + print("Hello, world!") + } +} diff --git a/Package.swift b/Package.swift index e8292a38f0f..0b565527562 100644 --- a/Package.swift +++ b/Package.swift @@ -985,6 +985,10 @@ let package = Package( "_InternalTestSupport", ] ), + .testTarget( + name: "SwiftBuildSupportTests", + dependencies: ["SwiftBuildSupport", "_InternalTestSupport", "_InternalBuildTestSupport"] + ), // Examples (These are built to ensure they stay up to date with the API.) .executableTarget( name: "package-info", diff --git a/Sources/SwiftBuildSupport/PIFBuilder.swift b/Sources/SwiftBuildSupport/PIFBuilder.swift index dc91af90b1f..9b86b7ac79c 100644 --- a/Sources/SwiftBuildSupport/PIFBuilder.swift +++ b/Sources/SwiftBuildSupport/PIFBuilder.swift @@ -83,9 +83,7 @@ extension ModulesGraph { } /// The parameters required by `PIFBuilder`. -struct PIFBuilderParameters { - let triple: Basics.Triple - +package struct PIFBuilderParameters { /// Whether the toolchain supports `-package-name` option. let isPackageAccessModifierSupported: Bool @@ -101,9 +99,6 @@ struct PIFBuilderParameters { /// An array of paths to search for pkg-config `.pc` files. let pkgConfigDirectories: [AbsolutePath] - /// The toolchain's SDK root path. - let sdkRootPath: AbsolutePath? - /// The Swift language versions supported by the SwiftBuild being used for the build. let supportedSwiftVersions: [SwiftLanguageVersion] @@ -118,6 +113,19 @@ struct PIFBuilderParameters { /// Additional rules for including a source or resource file in a target let additionalFileRules: [FileRuleDescription] + + package init(isPackageAccessModifierSupported: Bool, enableTestability: Bool, shouldCreateDylibForDynamicProducts: Bool, toolchainLibDir: AbsolutePath, pkgConfigDirectories: [AbsolutePath], supportedSwiftVersions: [SwiftLanguageVersion], pluginScriptRunner: PluginScriptRunner, disableSandbox: Bool, pluginWorkingDirectory: AbsolutePath, additionalFileRules: [FileRuleDescription]) { + self.isPackageAccessModifierSupported = isPackageAccessModifierSupported + self.enableTestability = enableTestability + self.shouldCreateDylibForDynamicProducts = shouldCreateDylibForDynamicProducts + self.toolchainLibDir = toolchainLibDir + self.pkgConfigDirectories = pkgConfigDirectories + self.supportedSwiftVersions = supportedSwiftVersions + self.pluginScriptRunner = pluginScriptRunner + self.disableSandbox = disableSandbox + self.pluginWorkingDirectory = pluginWorkingDirectory + self.additionalFileRules = additionalFileRules + } } /// PIF object builder for a package graph. @@ -146,7 +154,7 @@ public final class PIFBuilder { /// - parameters: The parameters used to configure the PIF. /// - fileSystem: The file system to read from. /// - observabilityScope: The ObservabilityScope to emit diagnostics to. - init( + package init( graph: ModulesGraph, parameters: PIFBuilderParameters, fileSystem: FileSystem, @@ -163,7 +171,7 @@ public final class PIFBuilder { /// - prettyPrint: Whether to return a formatted JSON. /// - preservePIFModelStructure: Whether to preserve model structure. /// - Returns: The package graph in the JSON PIF format. - func generatePIF( + package func generatePIF( prettyPrint: Bool = true, preservePIFModelStructure: Bool = false, printPIFManifestGraphviz: Bool = false, @@ -227,7 +235,7 @@ public final class PIFBuilder { } /// Constructs a `PIF.TopLevelObject` representing the package graph. - private func constructPIF(buildParameters: BuildParameters) async throws -> PIF.TopLevelObject { + package func constructPIF(buildParameters: BuildParameters) async throws -> PIF.TopLevelObject { let pluginScriptRunner = self.parameters.pluginScriptRunner let outputDir = self.parameters.pluginWorkingDirectory.appending("outputs") @@ -727,13 +735,11 @@ extension PIFBuilderParameters { additionalFileRules: [FileRuleDescription] ) { self.init( - triple: buildParameters.triple, isPackageAccessModifierSupported: buildParameters.driverParameters.isPackageAccessModifierSupported, enableTestability: buildParameters.enableTestability, shouldCreateDylibForDynamicProducts: buildParameters.shouldCreateDylibForDynamicProducts, toolchainLibDir: (try? buildParameters.toolchain.toolchainLibDir) ?? .root, pkgConfigDirectories: buildParameters.pkgConfigDirectories, - sdkRootPath: buildParameters.toolchain.sdkRootPath, supportedSwiftVersions: supportedSwiftVersions, pluginScriptRunner: pluginScriptRunner, disableSandbox: disableSandbox, diff --git a/Sources/SwiftBuildSupport/PackagePIFBuilder+Helpers.swift b/Sources/SwiftBuildSupport/PackagePIFBuilder+Helpers.swift index d0bcc4eff7a..fcd0453308b 100644 --- a/Sources/SwiftBuildSupport/PackagePIFBuilder+Helpers.swift +++ b/Sources/SwiftBuildSupport/PackagePIFBuilder+Helpers.swift @@ -234,7 +234,7 @@ extension Sequence { } var pifPlatformsForCondition: [ProjectModel.BuildSettings.Platform] = platforms - .map { ProjectModel.BuildSettings.Platform(from: $0) } + .compactMap { try? ProjectModel.BuildSettings.Platform(from: $0) } // Treat catalyst like macOS for backwards compatibility with older tools versions. if pifPlatformsForCondition.contains(.macOS), toolsVersion < ToolsVersion.v5_5 { @@ -537,7 +537,7 @@ extension PackageGraph.ResolvedModule { /// Collect the build settings defined in the package manifest. /// Some of them apply *only* to the target itself, while others are also imparted to clients. /// Note that the platform is *optional*; unconditional settings have no platform condition. - var allBuildSettings: AllBuildSettings { + func computeAllBuildSettings(observabilityScope: ObservabilityScope) -> AllBuildSettings { var allSettings = AllBuildSettings() for (declaration, settingsAssigments) in self.underlying.buildSettings.assignments { @@ -565,7 +565,16 @@ extension PackageGraph.ResolvedModule { let (platforms, configurations, _) = settingAssignment.conditions.splitIntoConcreteConditions for platform in platforms { - let pifPlatform = platform.map { ProjectModel.BuildSettings.Platform(from: $0) } + let pifPlatform: ProjectModel.BuildSettings.Platform? + if let platform { + guard let computedPifPlatform = try? ProjectModel.BuildSettings.Platform(from: platform) else { + observabilityScope.logPIF(.warning, "Ignoring settings assignments for unknown platform '\(platform.name)'") + continue + } + pifPlatform = computedPifPlatform + } else { + pifPlatform = nil + } if pifDeclaration == .OTHER_LDFLAGS { var settingsByDeclaration: [ProjectModel.BuildSettings.Declaration: [String]] @@ -962,7 +971,11 @@ extension ProjectModel.BuildSettings.MultipleValueSetting { } extension ProjectModel.BuildSettings.Platform { - init(from platform: PackageModel.Platform) { + enum Error: Swift.Error { + case unknownPlatform(String) + } + + init(from platform: PackageModel.Platform) throws { self = switch platform { case .macOS: .macOS case .macCatalyst: .macCatalyst @@ -977,7 +990,7 @@ extension ProjectModel.BuildSettings.Platform { case .wasi: .wasi case .openbsd: .openbsd case .freebsd: .freebsd - default: preconditionFailure("Unexpected platform: \(platform.name)") + default: throw Error.unknownPlatform(platform.name) } } } diff --git a/Sources/SwiftBuildSupport/PackagePIFBuilder.swift b/Sources/SwiftBuildSupport/PackagePIFBuilder.swift index 510be26ce39..8964cb87072 100644 --- a/Sources/SwiftBuildSupport/PackagePIFBuilder.swift +++ b/Sources/SwiftBuildSupport/PackagePIFBuilder.swift @@ -562,7 +562,10 @@ public final class PackagePIFBuilder { self.delegate.configureProjectBuildSettings(&settings) for (platform, platformOptions) in self.package.sdkOptions(delegate: self.delegate) { - let pifPlatform = ProjectModel.BuildSettings.Platform(from: platform) + guard let pifPlatform = try? ProjectModel.BuildSettings.Platform(from: platform) else { + log(.warning, "Ignoring options '\(platformOptions.joined(separator: " "))' specified for unknown platform \(platform.name)") + continue + } settings.platformSpecificSettings[pifPlatform]![.SPECIALIZATION_SDK_OPTIONS]! .append(contentsOf: platformOptions) } @@ -584,11 +587,11 @@ public final class PackagePIFBuilder { let arm64ePlatforms: [PackageModel.Platform] = [.iOS, .macOS, .visionOS] for arm64ePlatform in arm64ePlatforms { if self.delegate.shouldPackagesBuildForARM64e(platform: arm64ePlatform) { - let pifPlatform: ProjectModel.BuildSettings.Platform = switch arm64ePlatform { - case .iOS: - ._iOSDevice - default: - .init(from: arm64ePlatform) + let pifPlatform: ProjectModel.BuildSettings.Platform + do { + pifPlatform = try .init(from: arm64ePlatform) + } catch { + preconditionFailure("Unhandled arm64e platform: \(error)") } settings.platformSpecificSettings[pifPlatform]![.ARCHS, default: []].append(contentsOf: ["arm64e"]) } diff --git a/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Modules.swift b/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Modules.swift index 81a9c42ff60..d490e37fa3b 100644 --- a/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Modules.swift +++ b/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Modules.swift @@ -738,7 +738,7 @@ extension PackagePIFProjectBuilder { var debugSettings = settings var releaseSettings = settings - let allBuildSettings = sourceModule.allBuildSettings + let allBuildSettings = sourceModule.computeAllBuildSettings(observabilityScope: pifBuilder.observabilityScope) // Apply target-specific build settings defined in the manifest. for (buildConfig, declarationsByPlatform) in allBuildSettings.targetSettings { @@ -756,7 +756,7 @@ extension PackagePIFProjectBuilder { } // Impart the linker flags. - for (platform, settingsByDeclaration) in sourceModule.allBuildSettings.impartedSettings { + for (platform, settingsByDeclaration) in sourceModule.computeAllBuildSettings(observabilityScope: pifBuilder.observabilityScope).impartedSettings { // Note: A `nil` platform means that the declaration applies to *all* platforms. for (declaration, stringValues) in settingsByDeclaration { impartedSettings.append(values: stringValues, to: declaration, platform: platform) diff --git a/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Products.swift b/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Products.swift index 90d47bd2830..5a889cae186 100644 --- a/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Products.swift +++ b/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Products.swift @@ -470,7 +470,7 @@ extension PackagePIFProjectBuilder { var releaseSettings: ProjectModel.BuildSettings = settings // Apply target-specific build settings defined in the manifest. - for (buildConfig, declarationsByPlatform) in mainModule.allBuildSettings.targetSettings { + for (buildConfig, declarationsByPlatform) in mainModule.computeAllBuildSettings(observabilityScope: pifBuilder.observabilityScope).targetSettings { for (platform, declarations) in declarationsByPlatform { // A `nil` platform means that the declaration applies to *all* platforms. for (declaration, stringValues) in declarations { diff --git a/Tests/SwiftBuildSupportTests/PIFBuilderTests.swift b/Tests/SwiftBuildSupportTests/PIFBuilderTests.swift new file mode 100644 index 00000000000..0d1fa537072 --- /dev/null +++ b/Tests/SwiftBuildSupportTests/PIFBuilderTests.swift @@ -0,0 +1,138 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Basics +import Testing +import PackageGraph +import PackageLoading +import PackageModel +import SPMBuildCore +import SwiftBuild +import SwiftBuildSupport +import _InternalTestSupport +import Workspace + +extension PIFBuilderParameters { + fileprivate static func constructDefaultParametersForTesting(temporaryDirectory: Basics.AbsolutePath) throws -> Self { + self.init( + isPackageAccessModifierSupported: true, + enableTestability: false, + shouldCreateDylibForDynamicProducts: false, + toolchainLibDir: temporaryDirectory.appending(component: "toolchain-lib-dir"), + pkgConfigDirectories: [], + supportedSwiftVersions: [.v4, .v4_2, .v5, .v6], + pluginScriptRunner: DefaultPluginScriptRunner( + fileSystem: localFileSystem, + cacheDir: temporaryDirectory.appending(component: "plugin-cache-dir"), + toolchain: try UserToolchain.default + ), + disableSandbox: false, + pluginWorkingDirectory: temporaryDirectory.appending(component: "plugin-working-dir"), + additionalFileRules: [] + ) + } +} + +fileprivate func withGeneratedPIF(fromFixture fixtureName: String, do doIt: (SwiftBuildSupport.PIF.TopLevelObject, TestingObservability) async throws -> ()) async throws { + try await fixture(name: fixtureName) { fixturePath in + let observabilitySystem = ObservabilitySystem.makeForTesting() + let workspace = try Workspace( + fileSystem: localFileSystem, + forRootPackage: fixturePath, + customManifestLoader: ManifestLoader(toolchain: UserToolchain.default), + delegate: MockWorkspaceDelegate() + ) + let rootInput = PackageGraphRootInput(packages: [fixturePath], dependencies: []) + let graph = try await workspace.loadPackageGraph( + rootInput: rootInput, + observabilityScope: observabilitySystem.topScope + ) + let builder = PIFBuilder( + graph: graph, + parameters: try PIFBuilderParameters.constructDefaultParametersForTesting(temporaryDirectory: fixturePath), + fileSystem: localFileSystem, + observabilityScope: observabilitySystem.topScope + ) + let pif = try await builder.constructPIF( + buildParameters: mockBuildParameters(destination: .host) + ) + try await doIt(pif, observabilitySystem) + } +} + +extension SwiftBuildSupport.PIF.Workspace { + fileprivate func project(named name: String) throws -> SwiftBuildSupport.PIF.Project { + let matchingProjects = projects.filter { + $0.underlying.name == name + } + if matchingProjects.isEmpty { + throw StringError("No project named \(name) in PIF workspace") + } else if matchingProjects.count > 1 { + throw StringError("Multiple projects named \(name) in PIF workspace") + } else { + return matchingProjects[0] + } + } +} + +extension SwiftBuildSupport.PIF.Project { + fileprivate func target(named name: String) throws -> ProjectModel.BaseTarget { + let matchingTargets = underlying.targets.filter { + $0.common.name == name + } + if matchingTargets.isEmpty { + throw StringError("No target named \(name) in PIF project") + } else if matchingTargets.count > 1 { + throw StringError("Multiple target named \(name) in PIF project") + } else { + return matchingTargets[0] + } + } +} + +extension SwiftBuild.ProjectModel.BaseTarget { + fileprivate func buildConfig(named name: String) throws -> SwiftBuild.ProjectModel.BuildConfig { + let matchingConfigs = common.buildConfigs.filter { + $0.name == name + } + if matchingConfigs.isEmpty { + throw StringError("No config named \(name) in PIF target") + } else if matchingConfigs.count > 1 { + throw StringError("Multiple configs named \(name) in PIF target") + } else { + return matchingConfigs[0] + } + } +} + +@Suite +struct PIFBuilderTests { + @Test func platformConditionBasics() async throws { + try await withGeneratedPIF(fromFixture: "PIFBuilder/UnknownPlatforms") { pif, observabilitySystem in + // We should emit a warning to the PIF log about the unknown platform + #expect(observabilitySystem.diagnostics.filter { + $0.severity == .warning && $0.message.contains("Ignoring settings assignments for unknown platform 'DoesNotExist'") + }.count > 0) + + let releaseConfig = try pif.workspace + .project(named: "UnknownPlatforms") + .target(named: "UnknownPlatforms") + .buildConfig(named: "Release") + + // The platforms with conditional settings should have those propagated to the PIF. + #expect(releaseConfig.settings.platformSpecificSettings[.linux]?[.SWIFT_ACTIVE_COMPILATION_CONDITIONS] == ["$(inherited)", "BAR"]) + #expect(releaseConfig.settings.platformSpecificSettings[.macOS]?[.SWIFT_ACTIVE_COMPILATION_CONDITIONS] == ["$(inherited)", "BAZ"]) + // Platforms without conditional settings should get the default. + #expect(releaseConfig.settings.platformSpecificSettings[.windows]?[.SWIFT_ACTIVE_COMPILATION_CONDITIONS] == ["$(inherited)"]) + } + } +}