Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Fixtures/PIFBuilder/UnknownPlatforms/Package.swift
Original file line number Diff line number Diff line change
@@ -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])),
],
),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// The Swift Programming Language
// https://docs.swift.org/swift-book

@main
struct UnknownPlatforms {
static func main() {
print("Hello, world!")
}
}
4 changes: 4 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
28 changes: 17 additions & 11 deletions Sources/SwiftBuildSupport/PIFBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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]

Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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,
Expand Down
23 changes: 18 additions & 5 deletions Sources/SwiftBuildSupport/PackagePIFBuilder+Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ extension Sequence<PackageModel.PackageCondition> {
}

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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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]]
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
}
}
Expand Down
15 changes: 9 additions & 6 deletions Sources/SwiftBuildSupport/PackagePIFBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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"])
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
138 changes: 138 additions & 0 deletions Tests/SwiftBuildSupportTests/PIFBuilderTests.swift
Original file line number Diff line number Diff line change
@@ -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)"])
}
}
}