diff --git a/Sources/PackageGraph/Resolution/ResolvedModule.swift b/Sources/PackageGraph/Resolution/ResolvedModule.swift index c7b535bcf49..c37e82c26a6 100644 --- a/Sources/PackageGraph/Resolution/ResolvedModule.swift +++ b/Sources/PackageGraph/Resolution/ResolvedModule.swift @@ -151,7 +151,7 @@ public struct ResolvedModule { self.underlying.sources } - let packageIdentity: PackageIdentity + package let packageIdentity: PackageIdentity /// The underlying module represented in this resolved module. public let underlying: Module diff --git a/Sources/SwiftBuildSupport/BuildSystem.swift b/Sources/SwiftBuildSupport/BuildSystem.swift index 28e7b02bca7..0418c5d3ce9 100644 --- a/Sources/SwiftBuildSupport/BuildSystem.swift +++ b/Sources/SwiftBuildSupport/BuildSystem.swift @@ -14,7 +14,7 @@ extension BuildSubset { var pifTargetName: String { switch self { case .product(let name, _): - PackagePIFProjectBuilder.targetName(for: name) + _PackagePIFProjectBuilder.targetName(for: name) case .target(let name, _): name case .allExcludingTests: diff --git a/Sources/SwiftBuildSupport/CMakeLists.txt b/Sources/SwiftBuildSupport/CMakeLists.txt index fdd58d7a503..1a7b9eca3ad 100644 --- a/Sources/SwiftBuildSupport/CMakeLists.txt +++ b/Sources/SwiftBuildSupport/CMakeLists.txt @@ -7,9 +7,15 @@ # See http://swift.org/CONTRIBUTORS.txt for Swift project authors add_library(SwiftBuildSupport STATIC + BuildSystem.swift + PackagePIFBuilder.swift + PackagePIFBuilder+Helpers.swift + PackagePIFBuilder+Plugins.swift + PackagePIFProjectBuilder.swift + PackagePIFProjectBuilder+Modules.swift + PackagePIFProjectBuilder+Products.swift PIF.swift PIFBuilder.swift - BuildSystem.swift SwiftBuildSystem.swift) target_link_libraries(SwiftBuildSupport PUBLIC Build diff --git a/Sources/SwiftBuildSupport/PIFBuilder.swift b/Sources/SwiftBuildSupport/PIFBuilder.swift index f40e5c7a033..c658d0bb4b6 100644 --- a/Sources/SwiftBuildSupport/PIFBuilder.swift +++ b/Sources/SwiftBuildSupport/PIFBuilder.swift @@ -120,7 +120,7 @@ public final class PIFBuilder { 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( + try _PackagePIFProjectBuilder( package: package, parameters: self.parameters, fileSystem: self.fileSystem, @@ -247,7 +247,7 @@ class PIFProjectBuilder { } } -final class PackagePIFProjectBuilder: PIFProjectBuilder { +final class _PackagePIFProjectBuilder: PIFProjectBuilder { private let package: ResolvedPackage private let parameters: PIFBuilderParameters private let fileSystem: FileSystem @@ -1074,7 +1074,7 @@ final class AggregatePIFProjectBuilder: PIFProjectBuilder { allIncludingTestsTarget.addBuildConfiguration(name: "Debug") allIncludingTestsTarget.addBuildConfiguration(name: "Release") - for case let project as PackagePIFProjectBuilder in projects where project.isRootPackage { + 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( @@ -1555,7 +1555,7 @@ extension ResolvedProduct { var pifTargetGUID: PIF.GUID { "PACKAGE-PRODUCT:\(name)" } var mainTarget: ResolvedModule { - modules.first { $0.type == underlying.type.targetType }! + modules.first { $0.type == underlying.type._targetType }! } /// Returns the recursive dependencies, limited to the target's package, which satisfy the input build environment, @@ -1612,7 +1612,7 @@ extension Module { } extension ProductType { - var targetType: Module.Kind { + var _targetType: Module.Kind { switch self { case .executable: .executable diff --git a/Sources/SwiftBuildSupport/PackagePIFBuilder+Helpers.swift b/Sources/SwiftBuildSupport/PackagePIFBuilder+Helpers.swift new file mode 100644 index 00000000000..0b2f782b4c2 --- /dev/null +++ b/Sources/SwiftBuildSupport/PackagePIFBuilder+Helpers.swift @@ -0,0 +1,1111 @@ +//===----------------------------------------------------------------------===// +// +// 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 Foundation + +import struct TSCUtility.Version + +import struct Basics.AbsolutePath +import struct Basics.Diagnostic +import let Basics.localFileSystem +import struct Basics.ObservabilityMetadata +import class Basics.ObservabilityScope +import class Basics.ObservabilitySystem +import struct Basics.RelativePath +import struct Basics.SourceControlURL +import class Basics.ThreadSafeArrayStore + +import enum PackageModel.BuildConfiguration +import enum PackageModel.BuildSettings +import class PackageModel.ClangModule +import struct PackageModel.ConfigurationCondition +import class PackageModel.Manifest +import class PackageModel.Module +import enum PackageModel.ModuleMapType +import class PackageModel.Package +import enum PackageModel.PackageCondition +import struct PackageModel.PackageIdentity +import struct PackageModel.Platform +import struct PackageModel.PlatformDescription +import struct PackageModel.PlatformRegistry +import struct PackageModel.PlatformsCondition +import class PackageModel.PluginModule +import class PackageModel.Product +import enum PackageModel.ProductType +import struct PackageModel.Resource +import struct PackageModel.SupportedPlatform +import struct PackageModel.SwiftLanguageVersion +import class PackageModel.SwiftModule +import class PackageModel.SystemLibraryModule +import struct PackageModel.ToolsVersion +import struct PackageModel.TraitCondition + +import struct PackageGraph.ResolvedModule +import struct PackageGraph.ResolvedPackage +import struct PackageGraph.ResolvedProduct + +import func PackageLoading.pkgConfigArgs + +#if canImport(SwiftBuild) +import enum SwiftBuild.PIF + +// MARK: - PIF GUID Helpers + +enum TargetGUIDSuffix: String { + case testable, dynamic +} + +extension TargetGUIDSuffix? { + func description(forName name: String) -> String { + switch self { + case .some(let suffix): + "-\(String(name.hash, radix: 16, uppercase: true))-\(suffix.rawValue)" + case .none: + "" + } + } +} + +extension PackageModel.Module { + func pifTargetGUID(suffix: TargetGUIDSuffix? = nil) -> String { + PIFPackageBuilder.targetGUID(forModuleName: self.name, suffix: suffix) + } +} + +extension PackageGraph.ResolvedModule { + func pifTargetGUID(suffix: TargetGUIDSuffix? = nil) -> String { + self.underlying.pifTargetGUID(suffix: suffix) + } +} + +extension PackageModel.Product { + func pifTargetGUID(suffix: TargetGUIDSuffix? = nil) -> String { + PIFPackageBuilder.targetGUID(forProductName: self.name, suffix: suffix) + } +} + +extension PackageGraph.ResolvedProduct { + func pifTargetGUID(suffix: TargetGUIDSuffix? = nil) -> String { + self.underlying.pifTargetGUID(suffix: suffix) + } + + /// Helper function to consistently generate a target name string for a product in a package. + /// This format helps make sure that targets and products with the same name (as they often have) have different + /// target names in the PIF. + func targetNameForProduct(suffix: String = "") -> String { + "\(name)\(suffix) product" + } +} + +extension PIFPackageBuilder { + /// Helper function to consistently generate a PIF target identifier string for a module 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). + static func targetGUID(forModuleName name: String, suffix: TargetGUIDSuffix? = nil) -> String { + let suffixDescription = suffix.description(forName: name) + return "PACKAGE-TARGET:\(name)\(suffixDescription)" + } + + /// Helper function to consistently generate a PIF target identifier string for a product 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). + static func targetGUID(forProductName name: String, suffix: TargetGUIDSuffix? = nil) -> String { + let suffixDescription = suffix.description(forName: name) + return "PACKAGE-PRODUCT:\(name)\(suffixDescription)" + } +} + +// MARK: - SwiftPM PackageModel Helpers + +extension PackageModel.PackageIdentity { + var c99name: String { + self.description.spm_mangledToC99ExtendedIdentifier() + } +} + +extension PackageModel.Package { + /// Package name as defined in the manifest. + var name: String { + self.manifest.displayName + } + + var packageBaseBuildSettings: SwiftBuild.PIF.BuildSettings { + var settings = SwiftBuild.PIF.BuildSettings() + settings.SDKROOT = "auto" + settings.SDK_VARIANT = "auto" + + if self.manifest.toolsVersion >= ToolsVersion.v6_0 { + if let version = manifest.version, !version.isPrerelease && !version.hasBuildMetadata { + settings.SWIFT_USER_MODULE_VERSION = version.stringRepresentation + } + } + return settings + } +} + +extension PackageModel.Module { + var isExecutable: Bool { + switch self.type { + case .executable, .snippet: + true + case .library, .test, .macro, .systemModule, .plugin, .binary: + false + } + } + + var isBinary: Bool { + switch self.type { + case .binary: + true + case .library, .executable, .snippet, .test, .plugin, .macro, .systemModule: + false + } + } + + /// Is this a source module? i.e., one that's compiled into a module from source code. + var isSourceModule: Bool { + switch self.type { + case .library, .executable, .snippet, .test, .macro: + true + case .systemModule, .plugin, .binary: + false + } + } +} + +extension PackageModel.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 + } + } +} + +extension PackageModel.Platform { + static var knownPlatforms: Set { + Set(PlatformRegistry.default.knownPlatforms) + } +} + +extension Sequence { + func toPlatformFilter(toolsVersion: ToolsVersion) -> Set { + let pifPlatforms = self.flatMap { packageCondition -> [SwiftBuild.PIF.BuildSettings.Platform] in + guard let platforms = packageCondition.platformsCondition?.platforms else { + return [] + } + + var pifPlatformsForCondition: [SwiftBuild.PIF.BuildSettings.Platform] = platforms + .map { SwiftBuild.PIF.BuildSettings.Platform(from: $0) } + + // Treat catalyst like macOS for backwards compatibility with older tools versions. + if pifPlatformsForCondition.contains(.macOS), toolsVersion < ToolsVersion.v5_5 { + pifPlatformsForCondition.append(.macCatalyst) + } + return pifPlatformsForCondition + } + return pifPlatforms.toPlatformFilter() + } + + var splitIntoConcreteConditions: ( + [PackageModel.Platform?], + [PackageModel.BuildConfiguration], + [PackageModel.TraitCondition] + ) { + var platformConditions: [PackageModel.PlatformsCondition] = [] + var configurationConditions: [PackageModel.ConfigurationCondition] = [] + var traitConditions: [PackageModel.TraitCondition] = [] + + for packageCondition in self { + switch packageCondition { + case .platforms(let condition): platformConditions.append(condition) + case .configuration(let condition): configurationConditions.append(condition) + case .traits(let condition): traitConditions.append(condition) + } + } + + // Determine the *platform* conditions, if any. + // An empty set means that there are no platform restrictions. + let platforms: [PackageModel.Platform?] = if platformConditions.isEmpty { + [nil] + } else { + platformConditions.flatMap(\.platforms) + } + + // Determine the *configuration* conditions, if any. + // If there are none, we apply the setting to both debug and release builds (ie, `allCases`). + let configurations: [BuildConfiguration] = if configurationConditions.isEmpty { + BuildConfiguration.allCases + } else { + configurationConditions.map(\.configuration) + } + + return (platforms, configurations, traitConditions) + } +} + +extension PackageModel.BuildSettings.Declaration { + var allowsMultipleValues: Bool { + switch self { + // Swift. + case .SWIFT_ACTIVE_COMPILATION_CONDITIONS, .OTHER_SWIFT_FLAGS: + true + + case .SWIFT_VERSION: + false + + // C family. + case .GCC_PREPROCESSOR_DEFINITIONS, .HEADER_SEARCH_PATHS, .OTHER_CFLAGS, .OTHER_CPLUSPLUSFLAGS: + true + + // Linker. + case .OTHER_LDFLAGS, .LINK_LIBRARIES, .LINK_FRAMEWORKS: + true + + default: + true + } + } +} + +// MARK: - SwiftPM PackageGraph Helpers + +extension PackageGraph.ResolvedPackage { + var name: String { + self.underlying.name + } + + /// The options declared per platform. + func sdkOptions(delegate: PIFPackageBuilder.BuildDelegate) -> [PackageModel.Platform: [String]] { + let platformDescriptionsByName: [String: PlatformDescription] = Dictionary( + uniqueKeysWithValues: self.manifest.platforms.map { platformDescription in + let key = platformDescription.platformName.lowercased() + let value = platformDescription + return (key, value) + } + ) + + var sdkOptions: [PackageModel.Platform: [String]] = [:] + for platform in Platform.knownPlatforms { + sdkOptions[platform] = platformDescriptionsByName[platform.name.lowercased()]?.options + + let customSDKOptions = delegate.customSDKOptions(forPlatform: platform) + if customSDKOptions.hasContent { + sdkOptions[platform, default: []].append(contentsOf: customSDKOptions) + } + } + return sdkOptions + } +} + +extension PackageGraph.ResolvedPackage { + public var packageBaseBuildSettings: SwiftBuild.PIF.BuildSettings { + self.underlying.packageBaseBuildSettings + } +} + +extension PackageGraph.ResolvedModule { + var isExecutable: Bool { self.underlying.isExecutable } + var isBinary: Bool { self.underlying.isBinary } + var isSourceModule: Bool { self.underlying.isSourceModule } + + /// The path of the module. + var path: AbsolutePath { self.underlying.path } + + /// The stable sorted list of resources in the module + var resources: [PackageModel.Resource] { + self.underlying.resources.sorted(on: \.path) + } + + /// The name of the group this module belongs to; by default, the package identity. + var packageName: String? { + self.packageAccess ? packageIdentity.c99name : nil + } + + /// Minimum deployment targets for particular platforms, as declared in the manifest. + func deploymentTargets(using delegate: PIFPackageBuilder.BuildDelegate) -> [PackageModel.Platform: String] { + let isUsingXCTest = (self.type == .test) + let derivedSupportedPlatforms: [SupportedPlatform] = Platform.knownPlatforms.map { + self.getSupportedPlatform(for: $0, usingXCTest: isUsingXCTest) + } + + var deploymentTargets: [PackageModel.Platform: String] = [:] + for derivedSupportedPlatform in derivedSupportedPlatforms { + deploymentTargets[derivedSupportedPlatform.platform] = derivedSupportedPlatform.version.versionString + + // If the version for this platform wasn't actually declared explicitly in the manifest, + // try to derive an aligned version from the iOS declaration, if there was one. + let targetPlatform = derivedSupportedPlatform.platform + let isPlatformMissing = !self.supportedPlatforms.map(\.platform).contains(targetPlatform) + guard isPlatformMissing else { continue } + + let iOSDeploymentTarget = self.getSupportedPlatform(for: .iOS, usingXCTest: isUsingXCTest).version + let mappedVersion = delegate.suggestAlignedPlatformVersionGiveniOSVersion( + platform: targetPlatform, + iOSVersion: iOSDeploymentTarget + ) + + if let mappedVersion { + deploymentTargets[targetPlatform] = mappedVersion + } + } + return deploymentTargets + } + + /// Platforms explicitly declared in the manifest for the purpose of customizing deployment targets. + /// + /// This does not include any custom platforms the user may have defined. + /// A package is still considered to be runnable for *all* platforms. + var declaredPlatforms: [PackageModel.Platform] { + let knownPlatforms = Platform.knownPlatforms + + let declaredPlatforms: [PackageModel.Platform] = self.supportedPlatforms.compactMap { + guard knownPlatforms.contains($0.platform) else { return nil } + return $0.platform + } + return declaredPlatforms + } + + /// Relative paths of each of the source files (relative to `target.sources.root`). + var sourceFileRelativePaths: [RelativePath] { + self.sources.relativePaths.map { try! RelativePath(validating: $0.pathString) } + } + + /// Absolute path of the top-level directory of the sources. + var sourceDirAbsolutePath: AbsolutePath { + try! AbsolutePath(validating: self.sources.root.pathString) + } + + /// Absolute paths to each of the header files (*only* applies to C-language modules). + var headerFileAbsolutePaths: [AbsolutePath] { + guard let clangTarget = self.underlying as? ClangModule else { return [] } + return clangTarget.headers + } + + /// Relative path of the `include` directory (*only* applies to C-language modules). + var includeDirRelativePath: RelativePath? { + guard let clangModule = self.underlying as? ClangModule else { return nil } + let relativePath = clangModule.includeDir.relative(to: self.sources.root).pathString + return try! RelativePath(validating: relativePath) + } + + /// Include directory as an *absolute* path. + var includeDirAbsolutePath: AbsolutePath? { + guard let includeDirRelativePath = self.includeDirRelativePath else { return nil } + return self.sourceDirAbsolutePath.appending(includeDirRelativePath) + } + + /// Relative path of the module-map file, if any (*only* applies to C-language modules). + var moduleMapFileRelativePath: RelativePath? { + guard let clangModule = self.underlying as? ClangModule else { return nil } + let moduleMapFileAbsolutePath = clangModule.moduleMapPath + + // Check whether there is actually a modulemap at the specified path. + // FIXME: Feels wrong to do file system access at this level —— instead, libSwiftPM's TargetBuilder should do that? + guard localFileSystem.isFile(moduleMapFileAbsolutePath) else { return nil } + + let moduleMapFileRelativePath = moduleMapFileAbsolutePath.relative(to: clangModule.sources.root) + return try! RelativePath(validating: moduleMapFileRelativePath.pathString) + } + + /// Module map type (*only* applies to C-language modules). + var moduleMapType: ModuleMapType? { + guard let clangModule = self.underlying as? ClangModule else { return nil } + return clangModule.moduleMapType + } + + /// The C language standard for which the module is configured (*only* applies to C-language modules). + var cLanguageStandard: String? { + guard let clangModule = self.underlying as? ClangModule else { return nil } + return clangModule.cLanguageStandard + } + + /// The C++ language standard for which the module is configured (*only* applies to C-language modules). + var cxxLanguageStandard: String? { + guard let clangTarget = self.underlying as? ClangModule else { return nil } + return clangTarget.cxxLanguageStandard + } + + /// Whether or not this module contains C++ sources (*only* applies to C-language modules). + var isCxx: Bool { + guard let clangTarget = self.underlying as? ClangModule else { return false } + return clangTarget.isCXX + } + + /// The list of swift versions declared by the manifest. + var declaredSwiftVersions: [SwiftLanguageVersion]? { + guard let swiftTarget = self.underlying as? SwiftModule else { return nil } + return swiftTarget.declaredSwiftVersions + } + + /// Is this a Swift module? + var usesSwift: Bool { + self.declaredSwiftVersions != nil + } + + /// Swift language version for which the module is configured. + func packageSwiftLanguageVersion(manifest: PackageModel.Manifest) -> String? { + guard let declaredSwiftVersions else { return nil } + + // Probably wrong at this point since we have *per* target versioning, + // but at the time the original code was written, the version aligned everywhere. + // See: rdar://147618136 (SwiftPM PIFBuilder — review how we compute the Swift version for a given target). + let packageSwiftLanguageVersion = declaredSwiftVersions.first ?? manifest.toolsVersion.swiftLanguageVersion + return packageSwiftLanguageVersion.rawValue + } + + var pluginsAppliedToModule: [PackageGraph.ResolvedModule] { + var pluginModules: [PackageGraph.ResolvedModule] = [] + + for dependency in self.dependencies { + switch dependency { + case .module(let moduleDependency, _): + if moduleDependency.type == .plugin { + pluginModules.append(moduleDependency) + } + case .product(let productDependency, _): + let productPlugins = productDependency.modules.filter { $0.type == .plugin } + pluginModules.append(contentsOf: productPlugins) + } + } + return pluginModules + } + + func productRepresentingDependencyOfBuildPlugin(in mainModuleProducts: [ResolvedProduct]) -> ResolvedProduct? { + mainModuleProducts.only { (mainModuleProduct: ResolvedProduct) -> Bool in + // NOTE: We can't use the 'id' here as we need to explicitly ignore the build triple because our build + // triple + // will be '.tools' while the target we want to depend on will have a build triple of '.destination'. + // See for more details: + // https://github.com/swiftlang/swift-package-manager/commit/b22168ec41061ddfa3438f314a08ac7a776bef7a. + return mainModuleProduct.mainModule!.packageIdentity == self.packageIdentity && + mainModuleProduct.mainModule!.name == self.name + // Intentionally ignore the build triple! + } + } + + struct AllBuildSettings { + typealias BuildSettingsByPlatform = [PackageModel.Platform?: [BuildSettings.Declaration: [String]]] + + /// Target-specific build settings declared in the manifest and that apply to the target itself. + var targetSettings: [BuildConfiguration: BuildSettingsByPlatform] = [:] + + /// Target-specific build settings that should be imparted to client targets (packages and projects). + var impartedSettings: BuildSettingsByPlatform = [:] + } + + /// Target-specific build settings declared in the manifest and that apply to the target itself. + /// + /// Collect the build settings defined in the package manifest. + /// Some of them apply *only* to the target itself, while others are also imparted to clients. + /// Note that the platform is *optional*; unconditional settings have no platform condition. + var allBuildSettings: AllBuildSettings { + var allSettings = AllBuildSettings() + + for (declaration, settingsAssigments) in self.underlying.buildSettings.assignments { + for settingAssignment in settingsAssigments { + // Create a build setting value; in some cases there isn't a direct mapping to Swift Build build + // settings. + let swbDeclaration: BuildSettings.Declaration + let values: [String] + switch declaration { + case .LINK_FRAMEWORKS: + swbDeclaration = .OTHER_LDFLAGS + values = settingAssignment.values.flatMap { ["-framework", $0] } + case .LINK_LIBRARIES: + swbDeclaration = .OTHER_LDFLAGS + values = settingAssignment.values.map { "-l\($0)" } + case .HEADER_SEARCH_PATHS: + swbDeclaration = .HEADER_SEARCH_PATHS + values = settingAssignment.values.map { self.sourceDirAbsolutePath.pathString + "/" + $0 } + default: + swbDeclaration = declaration + values = settingAssignment.values + } + + // TODO: We are currently ignoring package traits (see rdar://138149810). + let (platforms, configurations, _) = settingAssignment.conditions.splitIntoConcreteConditions + + for platform in platforms { + if swbDeclaration == .OTHER_LDFLAGS { + var settingsByDeclaration: [BuildSettings.Declaration: [String]] = allSettings + .impartedSettings[platform] ?? [:] + settingsByDeclaration[swbDeclaration, default: []].append(contentsOf: values) + + allSettings.impartedSettings[platform] = settingsByDeclaration + } + + for configuration in configurations { + var settingsByDeclaration: [BuildSettings.Declaration: [String]] = allSettings + .targetSettings[configuration]?[platform] ?? [:] + if swbDeclaration.allowsMultipleValues { + settingsByDeclaration[swbDeclaration, default: []].append(contentsOf: values) + } else { + settingsByDeclaration[swbDeclaration] = values.only.flatMap { [$0] } ?? [] + } + + allSettings.targetSettings[configuration, default: [:]][platform] = settingsByDeclaration + } + } + } + } + return allSettings + } +} + +/// Specialization of `Module` for "system module" targets, +/// i.e. those that just provide information about a library already on the system. +extension SystemLibraryModule { + /// Absolute path of the *module-map* file. + var modulemapFileAbsolutePath: String { + self.moduleMapPath.pathString + } + + /// Returns pkgConfig result for a system library target. + func pkgConfig( + package: PackageGraph.ResolvedPackage, + observabilityScope: ObservabilityScope + ) throws -> (cFlags: [String], libs: [String]) { + let diagnostics = ThreadSafeArrayStore() + defer { + for diagnostic in diagnostics.get() { + observabilityScope.emit(diagnostic) + } + } + + let pkgConfigParsingScope = ObservabilitySystem { _, diagnostic in + diagnostics.append(diagnostic) + }.topScope.makeChildScope(description: "PkgConfig") { + var packageMetadata = ObservabilityMetadata.packageMetadata( + identity: package.identity, + kind: package.manifest.packageKind + ) + packageMetadata.moduleName = self.name + return packageMetadata + } + + let brewPath = if FileManager.default.fileExists(atPath: "/opt/brew") { + "/opt/brew" // Legacy path for Homebrew. + } else if FileManager.default.fileExists(atPath: "/opt/homebrew") { + "/opt/homebrew" // Default path for Homebrew on Apple Silicon. + } else { + "/usr/local" // Fallback to default path for Homebrew. + } + + let emptyPkgConfig: (cFlags: [String], libs: [String]) = ([], []) + + let brewPrefix = try? AbsolutePath( + validating: UserDefaults.standard.string(forKey: "IDEHomebrewPrefixPath") ?? brewPath + ) + guard let brewPrefix else { return emptyPkgConfig } + + let pkgConfigResult = try? pkgConfigArgs( + for: self, + pkgConfigDirectories: [], + brewPrefix: brewPrefix, + fileSystem: localFileSystem, + observabilityScope: pkgConfigParsingScope + ) + guard let pkgConfigResult else { return emptyPkgConfig } + + let pkgConfig = ( + cFlags: pkgConfigResult.flatMap(\.cFlags), + libs: pkgConfigResult.flatMap(\.libs) + ) + return pkgConfig + } +} + +// MARK: - SwiftPM PackageGraph.ResolvedProduct Helpers + +extension PackageGraph.ResolvedProduct { + /// Returns the main module (aka, target) of this product, if any. + var mainModule: PackageGraph.ResolvedModule? { + self.modules.only { $0.type == self.type.targetType } + } + + /// Returns the other modules of this product. + var otherModules: [PackageGraph.ResolvedModule] { + modules.filter { $0.isSourceModule && $0.type != self.type.targetType } + } + + /// These are the kinds of products for whom one module is special + /// (e.g., executables have one executable module, test bundles have one test module, etc). + var isMainModuleProduct: Bool { + switch self.type { + case .executable, .snippet, .test: + true + case .library, .macro, .plugin: + false + } + } + + /// Is this a *system library* product? + var isSystemLibraryProduct: Bool { + if self.modules.only?.type == .systemModule { + true + } else { + false + } + } + + var isExecutable: Bool { + switch self.type { + case .executable, .snippet: + true + case .library, .test, .plugin, .macro: + false + } + } + + var isBinaryOnlyExecutableProduct: Bool { + self.isExecutable && !self.hasSourceTargets + } + + var hasSourceTargets: Bool { + self.modules.anySatisfy { !$0.isBinary } + } + + /// Returns the corresponding *system library* module, if this is a system library product. + var systemModule: SystemLibraryModule? { + guard self.isSystemLibraryProduct else { return nil } + return (self.modules.only?.underlying as! SystemLibraryModule) + } + + /// Returns the corresponding *plugin* module, if this is a plugin product. + var pluginModules: [PackageModel.PluginModule]? { + guard self.type == .plugin else { return nil } + return self.modules.compactMap { $0.underlying as? PackageModel.PluginModule } + } + + var c99name: String { + self.name.spm_mangledToC99ExtendedIdentifier() + } + + var libraryType: ProductType.LibraryType? { + switch self.type { + case .library(let libraryType): + libraryType + default: + nil + } + } + + /// Shoud we link this product dependency? + var isLinkable: Bool { + switch self.type { + case .library, .executable, .snippet, .test, .macro: + true + case .plugin: + false + } + } + + /// Is this product dependency automatic? + var isAutomatic: Bool { + self.type == .library(.automatic) + } + + var usesUnsafeFlags: Bool { + get throws { + try self.recursiveModuleDependencies().contains { $0.underlying.usesUnsafeFlags } + } + } +} + +extension PackageGraph.ResolvedModule { + func recursivelyTraverseDependencies(with block: (ResolvedModule.Dependency) -> Void) { + [self].recursivelyTraverseDependencies(with: block) + } +} + +extension Collection { + /// Recursively applies a block to each of the *dependencies* of the given module, in topological sort order. + /// Each module or product dependency is visited only once. + func recursivelyTraverseDependencies(with block: (ResolvedModule.Dependency) -> Void) { + var moduleNamesSeen: Set = [] + var productNamesSeen: Set = [] + + func visitDependency(_ dependency: ResolvedModule.Dependency) { + switch dependency { + case .module(let moduleDependency, _): + let (unseenModule, _) = moduleNamesSeen.insert(moduleDependency.name) + guard unseenModule else { return } + + if moduleDependency.underlying.type != .macro { + for dependency in moduleDependency.dependencies { + visitDependency(dependency) + } + } + block(dependency) + + case .product(let productDependency, let conditions): + let (unseenProduct, _) = productNamesSeen.insert(productDependency.name) + guard unseenProduct && !productDependency.isBinaryOnlyExecutableProduct else { return } + block(dependency) + + // We need to visit any binary modules to be able to add direct references to them to any client + // targets. + // This is needed so that XCFramework processing always happens *prior* to building any client targets. + for moduleDependency in productDependency.modules where moduleDependency.isBinary { + if moduleNamesSeen.contains(moduleDependency.name) { continue } + block(.module(moduleDependency, conditions: conditions)) + } + } + } + + for dependency in self.flatMap(\.dependencies) { + visitDependency(dependency) + } + } +} + +// MARK: - SwiftPM TSCUtility Helpers + +extension TSCUtility.Version { + var isPrerelease: Bool { + !self.prereleaseIdentifiers.isEmpty + } + + var hasBuildMetadata: Bool { + !self.buildMetadataIdentifiers.isEmpty + } + + var stringRepresentation: String { + self.description + } +} + +// MARK: - Swift Build PIF Helpers + +/// Helpers for building custom PIF targets by `PIFPackageBuilder` clients. +extension SwiftBuild.PIF.Project { + @discardableResult + public func addTarget( + packageProductName: String, + productType: SwiftBuild.PIF.Target.ProductType + ) throws -> SwiftBuild.PIF.Target { + let pifTarget = try self.addTargetThrowing( + id: PIFPackageBuilder.targetGUID(forProductName: packageProductName), + productType: productType, + name: packageProductName, + productName: packageProductName + ) + return pifTarget + } + + @discardableResult + public func addTarget( + packageModuleName: String, + productType: SwiftBuild.PIF.Target.ProductType + ) throws -> SwiftBuild.PIF.Target { + let pifTarget = try self.addTargetThrowing( + id: PIFPackageBuilder.targetGUID(forModuleName: packageModuleName), + productType: productType, + name: packageModuleName, + productName: packageModuleName + ) + return pifTarget + } +} + +extension SwiftBuild.PIF.BuildSettings { + /// Internal helper function that appends list of string values to a declaration. + /// If a platform is specified, then the values are appended to the `platformSpecificSettings`, + /// otherwise they are appended to the platform-neutral settings. + /// + /// Note that this restricts the settings that can be set by this function to those that can have platform-specific + /// values, + /// i.e. those in `PIF.Declaration`. If a platform is specified, it must be one of the known platforms in + /// `PIF.Platform`. + mutating func append(values: [String], to setting: Declaration, platform: Platform? = nil) { + // This dichotomy is quite unfortunate but that's currently the underlying model in `PIF.BuildSettings`. + if let platform { + // FIXME: The force unwraps here are pretty bad, + // but are the same as in the existing code before it was factored into this function. + // We should get rid of the force unwraps. And fix the PIF generation model. + // NOTE: Appending implies the setting is resilient to having ["$(inherited)"] + switch setting { + case .FRAMEWORK_SEARCH_PATHS: + self.platformSpecificSettings[platform]![setting]!.append(contentsOf: values) + case .GCC_PREPROCESSOR_DEFINITIONS: + self.platformSpecificSettings[platform]![setting]!.append(contentsOf: values) + case .HEADER_SEARCH_PATHS: + self.platformSpecificSettings[platform]![setting]!.append(contentsOf: values) + case .OTHER_CFLAGS: + self.platformSpecificSettings[platform]![setting]!.append(contentsOf: values) + case .OTHER_CPLUSPLUSFLAGS: + self.platformSpecificSettings[platform]![setting]!.append(contentsOf: values) + case .OTHER_LDFLAGS: + self.platformSpecificSettings[platform]![setting]!.append(contentsOf: values) + case .OTHER_SWIFT_FLAGS: + self.platformSpecificSettings[platform]![setting]!.append(contentsOf: values) + case .SWIFT_VERSION: + self.platformSpecificSettings[platform]![setting] = values // we are not resilient to $(inherited) + case .SWIFT_ACTIVE_COMPILATION_CONDITIONS: + self.platformSpecificSettings[platform]![setting]!.append(contentsOf: values) + default: + fatalError("Unsupported PIF.Declaration: \(setting)") + } + } else { + // FIXME: This is pretty ugly. + // The whole point of this helper function is to hide this ugliness from the rest of the logic. + // We need to fix the PIF generation model. + switch setting { + case .FRAMEWORK_SEARCH_PATHS: + self.FRAMEWORK_SEARCH_PATHS = (self.FRAMEWORK_SEARCH_PATHS ?? ["$(inherited)"]) + values + case .GCC_PREPROCESSOR_DEFINITIONS: + self.GCC_PREPROCESSOR_DEFINITIONS = (self.GCC_PREPROCESSOR_DEFINITIONS ?? ["$(inherited)"]) + values + case .HEADER_SEARCH_PATHS: + self.HEADER_SEARCH_PATHS = (self.HEADER_SEARCH_PATHS ?? ["$(inherited)"]) + values + case .OTHER_CFLAGS: + self.OTHER_CFLAGS = (self.OTHER_CFLAGS ?? ["$(inherited)"]) + values + case .OTHER_CPLUSPLUSFLAGS: + self.OTHER_CPLUSPLUSFLAGS = (self.OTHER_CPLUSPLUSFLAGS ?? ["$(inherited)"]) + values + case .OTHER_LDFLAGS: + self.OTHER_LDFLAGS = (self.OTHER_LDFLAGS ?? ["$(inherited)"]) + values + case .OTHER_SWIFT_FLAGS: + self.OTHER_SWIFT_FLAGS = (self.OTHER_SWIFT_FLAGS ?? ["$(inherited)"]) + values + case .SWIFT_VERSION: + self.SWIFT_VERSION = values.only.unwrap(orAssert: "Invalid values for 'SWIFT_VERSION': \(values)") + case .SWIFT_ACTIVE_COMPILATION_CONDITIONS: + self + .SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( + self + .SWIFT_ACTIVE_COMPILATION_CONDITIONS ?? ["$(inherited)"] + ) + values + default: + fatalError("Unsupported PIF.Declaration: \(setting)") + } + } + } +} + +extension SwiftBuild.PIF.BuildSettings.Platform { + init(from platform: PackageModel.Platform) { + self = switch platform { + case .macOS: .macOS + case .macCatalyst: .macCatalyst + case .iOS: .iOS + case .tvOS: .tvOS + case .watchOS: .watchOS + case .visionOS: .xrOS + case .driverKit: .driverKit + case .linux: .linux + case .android: .android + case .windows: .windows + case .wasi: .wasi + case .openbsd: .openbsd + default: preconditionFailure("Unexpected platform: \(platform.name)") + } + } +} + +extension SwiftBuild.PIF.BuildSettings { + /// Configure necessary settings for a dynamic library/framework. + mutating func configureDynamicSettings( + productName: String, + targetName: String, + executableName: String, + packageIdentity: PackageIdentity, + packageName: String?, + createDylibForDynamicProducts: Bool, + installPath: String, + delegate: PIFPackageBuilder.BuildDelegate + ) { + self.TARGET_NAME = targetName + self.PRODUCT_NAME = createDylibForDynamicProducts ? productName : executableName + self.PRODUCT_MODULE_NAME = productName + self.PRODUCT_BUNDLE_IDENTIFIER = "\(packageIdentity).\(productName)".spm_mangledToBundleIdentifier() + self.EXECUTABLE_NAME = executableName + self.CLANG_ENABLE_MODULES = "YES" + self.SWIFT_PACKAGE_NAME = packageName ?? nil + + if !createDylibForDynamicProducts { + self.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. + self.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. + self.MARKETING_VERSION = "1.0" // Version + self.CURRENT_PROJECT_VERSION = "1" // Build + } + + // Might set install path depending on build delegate. + if delegate.shouldSetInstallPathForDynamicLib(productName: productName) { + self.SKIP_INSTALL = "NO" + self.INSTALL_PATH = installPath + } + } +} + +extension SwiftBuild.PIF.BuildSettings.Declaration { + init(from declaration: PackageModel.BuildSettings.Declaration) { + self = switch declaration { + // Swift. + case .SWIFT_ACTIVE_COMPILATION_CONDITIONS: + .SWIFT_ACTIVE_COMPILATION_CONDITIONS + case .OTHER_SWIFT_FLAGS: + .OTHER_SWIFT_FLAGS + case .SWIFT_VERSION: + .SWIFT_VERSION + // C family. + case .GCC_PREPROCESSOR_DEFINITIONS: + .GCC_PREPROCESSOR_DEFINITIONS + case .HEADER_SEARCH_PATHS: + .HEADER_SEARCH_PATHS + case .OTHER_CFLAGS: + .OTHER_CFLAGS + case .OTHER_CPLUSPLUSFLAGS: + .OTHER_CPLUSPLUSFLAGS + // Linker. + case .OTHER_LDFLAGS: + .OTHER_LDFLAGS + case .LINK_LIBRARIES, .LINK_FRAMEWORKS: + preconditionFailure("Should not be reached") + default: + preconditionFailure("Unexpected BuildSettings.Declaration: \(declaration.name)") + } + } +} + +// MARK: - General Helpers + +extension SourceControlURL { + init(fileURLWithPath path: AbsolutePath) { + let fileURL = Foundation.URL(fileURLWithPath: path.pathString) + self.init(fileURL.description) + } +} + +extension String { + /// Returns the path extension from a `String`. + var pathExtension: String { + (self as NSString).pathExtension + } +} + +extension Optional { + func unwrap( + orAssert message: @autoclosure () -> String, + file: StaticString = #file, + line: UInt = #line + ) -> Wrapped { + if let unwrapped = self { + unwrapped + } else { + fatalError(message(), file: file, line: line) + } + } + + @discardableResult + mutating func lazilyInitialize( + _ initializer: () -> Wrapped + ) -> Wrapped { + if let result = self { + return result + } else { + let result = initializer() + self = .some(result) + return result + } + } + + @discardableResult + mutating func lazilyInitializeAndMutate( + initialValue initializer: @autoclosure () -> Wrapped, + mutator: (inout Wrapped) throws -> R + ) rethrows -> R { + if self == nil { + self = .some(initializer()) + } + return try mutator(&self!) + } +} + +extension Sequence { + /// Evaluates `predicate` on each element in the collection. + /// If exactly 1 element returns `true` return that element. + /// Returns the *only* element in the sequence satisfying the specified predicate. + /// + /// **Complexity**. O(n), where n is the length of the sequence. + func only(where predicate: (Element) throws -> Bool) rethrows -> Element? { + var match: Element? + for candidate in self { + if try predicate(candidate) { + if match == nil { + match = candidate + } else { + return nil + } + } + } + return match + } +} + +extension Collection { + /// Positive sense of `isEmpty`. + var hasContent: Bool { + !self.isEmpty + } + + var only: Element? { + (count == 1) ? first : nil + } + + func anySatisfy(_ predicate: (Element) throws -> Bool) rethrows -> Bool { + try contains(where: predicate) + } + + /// For example: `people.sorted(on: \.name)`. + func sorted(on projection: (Element) -> some Comparable) -> [Element] { + self.sorted(on: projection, by: <) + } + + /// For example: `people.sorted(on: \.name, comparator: >)`. + func sorted(on projection: (Element) -> T, by comparator: (T, T) -> Bool) -> [Element] { + self.sorted { lhs, rhs in + comparator(projection(lhs), projection(rhs)) + } + } +} + +extension Array { + func prepending(_ newElement: Element) -> [Element] { + [newElement] + self + } +} + +extension UserDefaults { + func bool(forKey key: String, defaultValue: Bool) -> Bool { + if self.object(forKey: key) != nil { + self.bool(forKey: key) + } else { + defaultValue + } + } +} + +#endif diff --git a/Sources/SwiftBuildSupport/PackagePIFBuilder+Plugins.swift b/Sources/SwiftBuildSupport/PackagePIFBuilder+Plugins.swift new file mode 100644 index 00000000000..693f15a5a21 --- /dev/null +++ b/Sources/SwiftBuildSupport/PackagePIFBuilder+Plugins.swift @@ -0,0 +1,137 @@ +//===----------------------------------------------------------------------===// +// +// 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 Foundation + +import struct Basics.AbsolutePath +import let Basics.localFileSystem +import enum Basics.Sandbox +import struct Basics.SourceControlURL + +#if canImport(SwiftBuild) +import enum SwiftBuild.PIF + +extension PIFPackageBuilder { + /// Contains all of the information resulting from applying a build tool plugin to a package target thats affect how + /// a target is built. + /// + /// This includes any commands that should be incorporated into the build graph and all derived source files that + /// should be compiled + /// (i.e., those from prebuild commands as well as from the build commands). + public struct BuildToolPluginInvocationResult: Equatable { + /// Absolute paths of output files of any prebuild commands. + public let prebuildCommandOutputPaths: [AbsolutePath] + + /// Build commands to incorporate into the dependency graph. + public let buildCommands: [CustomBuildCommand] + + /// Absolute paths of all derived source files that should be compiled as sources of the target. + /// This includes the outputs of any prebuild commands as well as all the outputs referenced in all the build + /// commands. + public var allDerivedOutputPaths: [AbsolutePath] { + self.prebuildCommandOutputPaths + self.buildCommands.flatMap(\.absoluteOutputPaths) + } + + public init( + prebuildCommandOutputPaths: [AbsolutePath], + buildCommands: [CustomBuildCommand] + ) { + self.prebuildCommandOutputPaths = prebuildCommandOutputPaths + self.buildCommands = buildCommands + } + } + + /// A command provided by a build tool plugin. + /// Build tool plugins are evaluated after package graph resolution (and subsequently, when conditions change). + /// + /// There are *two* basic kinds of build tool commands: prebuild commands and regular build commands. + public struct CustomBuildCommand: Equatable { + public var displayName: String? + public var executable: String + public var arguments: [String] + public var environment: [String: String] + public var workingDir: AbsolutePath? + public var inputPaths: [AbsolutePath] = [] + + /// Output paths can contain references with un-resolved paths (e.g. "$(DERIVED_FILE_DIR)/myOutput.txt") + public var outputPaths: [String] = [] + public var absoluteOutputPaths: [AbsolutePath] { + self.outputPaths.compactMap { try? AbsolutePath(validating: $0) } + } + + public var sandboxProfile: SandboxProfile? = nil + + public init( + displayName: String?, + executable: String, + arguments: [String], + environment: [String: String], + workingDir: AbsolutePath?, + inputPaths: [AbsolutePath], + outputPaths: [String], + sandboxProfile: SandboxProfile? + ) { + self.displayName = displayName + self.executable = executable + self.arguments = arguments + self.environment = environment + self.workingDir = workingDir + self.inputPaths = inputPaths + self.outputPaths = outputPaths + self.sandboxProfile = sandboxProfile + } + } + + /// Represents a libSwiftPM sandbox profile that can be applied to a given command line. + public struct SandboxProfile: Equatable { + public var strictness: Sandbox.Strictness + public var writableDirectories: [AbsolutePath] + public var readOnlyDirectories: [AbsolutePath] + + public init( + strictness: Sandbox.Strictness, + writableDirectories: [AbsolutePath], + readOnlyDirectories: [AbsolutePath] + ) { + self.strictness = strictness + self.writableDirectories = writableDirectories + self.readOnlyDirectories = readOnlyDirectories + } + + init(writableDirectories: [AbsolutePath] = [], readOnlyDirectories: [AbsolutePath] = []) { + self.strictness = .writableTemporaryDirectory + self.writableDirectories = writableDirectories + self.readOnlyDirectories = readOnlyDirectories + } + + public var writableDirectoryPathStrings: [String] { + self.writableDirectories.map(\.pathString) + } + + public var readOnlyDirectoryPathStrings: [String] { + self.readOnlyDirectories.map(\.pathString) + } + + /// Applies the sandbox profile to the given command line, and return the modified command line. + public func apply(to command: [String]) throws -> [String] { + try Sandbox.apply( + command: command, + fileSystem: localFileSystem, + strictness: self.strictness, + writableDirectories: self.writableDirectories, + readOnlyDirectories: self.readOnlyDirectories + ) + } + } +} + +#endif diff --git a/Sources/SwiftBuildSupport/PackagePIFBuilder.swift b/Sources/SwiftBuildSupport/PackagePIFBuilder.swift new file mode 100644 index 00000000000..5549ab693a6 --- /dev/null +++ b/Sources/SwiftBuildSupport/PackagePIFBuilder.swift @@ -0,0 +1,673 @@ +//===----------------------------------------------------------------------===// +// +// 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 Foundation + +import struct Basics.AbsolutePath +import struct Basics.SourceControlURL + +import class PackageModel.Manifest +import class PackageModel.Package +import struct PackageModel.Platform +import struct PackageModel.PlatformVersion +import class PackageModel.Product +import enum PackageModel.ProductType +import struct PackageModel.Resource + +import struct Basics.Diagnostic +import struct Basics.ObservabilityMetadata +import class Basics.ObservabilityScope +import struct PackageGraph.ModulesGraph +import struct PackageGraph.ResolvedModule +import struct PackageGraph.ResolvedPackage + +#if canImport(SwiftBuild) +import enum SwiftBuild.PIF + +/// A builder for generating the PIF object from a package. +public final class PIFPackageBuilder { + let modulesGraph: ModulesGraph + private let package: ResolvedPackage + + /// Contains the package declarative specification. + let packageManifest: PackageModel.Manifest // FIXME: Can't we just use `package.manifest` instead? —— Paulo + + /// The built PIF project object. + public var pifProject: SwiftBuild.PIF.Project { + assert(self._pifProject != nil, "Call build() method to build the PIF first") + return self._pifProject! + } + + private var _pifProject: SwiftBuild.PIF.Project? + + /// Scope for logging informational debug messages (intended for developers, not end users). + let observabilityScope: ObservabilityScope + + /// Logs an informational debug message (intended for developers, not end users). + func log( + _ severity: Diagnostic.Severity, + _ message: String, + sourceFile: StaticString = #fileID, + sourceLine: UInt = #line + ) { + var metadata = ObservabilityMetadata() + metadata.sourceLocation = SourceLocation(sourceFile, sourceLine) + + let diagnostic = Diagnostic(severity: severity, message: message, metadata: metadata) + self.observabilityScope.emit(diagnostic) + } + + unowned let delegate: BuildDelegate + + public protocol BuildDelegate: AnyObject { + /// Is this the root package? + var isRootPackage: Bool { get } + + // TODO: Maybe move these 3-4 properties to the `PIFBuilder.PIFBuilderParameters` struct. + + /// If a pure Swift package is open in the workspace. + var hostsOnlyPackages: Bool { get } + + /// Returns `true` if the package is managed by the user (i.e., the user is allowed to modify its sources, + /// package structure, etc). + var isUserManaged: Bool { get } + + /// Whether or not this package is required by *branch* or *revision*. + var isBranchOrRevisionBased: Bool { get } + + /// For executables — only executables for now — we check to see if there is a custom package product type + /// provider that can provide this information. + func customProductType(forExecutable product: PackageModel.Product) -> SwiftBuild.PIF.Target.ProductType? + + /// Returns all *device family* IDs for all SDK variants. + func deviceFamilyIDs() -> Set + + /// Have packages referenced by this workspace build for arm64e when building for iOS devices. + var shouldiOSPackagesBuildForARM64e: Bool { get } + + /// Is the sandbox disabled for plug-in execution? It should be `false` by default. + var isPluginExecutionSandboxingDisabled: Bool { get } + + /// Hook to customize the project-wide build settings. + func configureProjectBuildSettings(_ buildSettings: inout SwiftBuild.PIF.BuildSettings) + + /// Hook to customize source module build settings. + func configureSourceModuleBuildSettings( + sourceModule: PackageGraph.ResolvedModule, + settings: inout SwiftBuild.PIF.BuildSettings + ) + + /// Custom install path for the specified product, if any. + func customInstallPath(product: PackageModel.Product) -> String? + + /// Custom executable name for the specified product, if any. + func customExecutableName(product: PackageModel.Product) -> String? + + /// Custom library type for the specified product. + func customLibraryType(product: PackageModel.Product) -> PackageModel.ProductType.LibraryType? + + /// Custom option for the specified platform. + func customSDKOptions(forPlatform: PackageModel.Platform) -> [String] + + /// Create additional custom PIF targets after all targets have been built. + func addCustomTargets(pifProject: SwiftBuild.PIF.Project) throws -> [PIFPackageBuilder.ModuleOrProduct] + + /// Should we suppresses the specific product dependency, updating the provided build settings if necessary? + /// The specified product may be in the same package or a different one. + func shouldSuppressProductDependency( + product: PackageModel.Product, + buildSettings: inout SwiftBuild.PIF.BuildSettings + ) -> Bool + + /// Should we set the install path for a dynamic library/framework? + func shouldSetInstallPathForDynamicLib(productName: String) -> Bool + + /// Provides additional configuration and files for the specified library product. + func configureLibraryProduct( + product: PackageModel.Product, + pifTarget: SwiftBuild.PIF.Target, + additionalFiles: SwiftBuild.PIF.Group + ) + + /// The design intention behind this is to set a value for `watchOS`, `tvOS`, and `visionOS` + /// that "follows" the aligned iOS version if they are not explicitly set. + /// + /// Prior to this enhancement, it was common to find packages which worked perfectly fine on `watchOS` + /// aside from the one issue where developers failed to specify the correct deployment target. + /// + /// See: rdar://144661020 (SwiftPM PIFBuilder — compute unset deployment targets). + func suggestAlignedPlatformVersionGiveniOSVersion(platform: PackageModel.Platform, iOSVersion: PlatformVersion) + -> String? + + /// Validates the specified macro fingerprint. Each remote package has a fingerprint. + func validateMacroFingerprint(for macroModule: ResolvedModule) -> Bool + } + + /// Records the results of applying build tool plugins to modules in the package. + let buildToolPluginResultsByTargetName: [String: PIFPackageBuilder.BuildToolPluginInvocationResult] + + /// Whether to create dynamic libraries for dynamic products. + /// + /// This tracks removing this *user default* once clients stop relying on this implementation detail: + /// * Remove IDEPackageSupportCreateDylibsForDynamicProducts. + let createDylibForDynamicProducts: Bool + + /// Package display version, if any (i.e., it can be a version, branch or a git ref). + let packageDisplayVersion: String? + + /// Whether to suppress warnings from compilers, linkers, and other build tools for package dependencies. + private var suppressWarningsForPackageDependencies: Bool { + UserDefaults.standard.bool(forKey: "SuppressWarningsForPackageDependencies", defaultValue: true) + } + + /// Whether to skip running the static analyzer for package dependencies. + private var skipStaticAnalyzerForPackageDependencies: Bool { + UserDefaults.standard.bool(forKey: "SkipStaticAnalyzerForPackageDependencies", defaultValue: true) + } + + public static func computePackageProductFrameworkName(productName: String) -> String { + "\(productName)_\(String(productName.hash, radix: 16, uppercase: true))_PackageProduct" + } + + public init( + modulesGraph: ModulesGraph, + resolvedPackage: ResolvedPackage, + packageManifest: PackageModel.Manifest, + delegate: PIFPackageBuilder.BuildDelegate, + buildToolPluginResultsByTargetName: [String: BuildToolPluginInvocationResult], + createDylibForDynamicProducts: Bool = false, + packageDisplayVersion: String?, + observabilityScope: ObservabilityScope + ) { + self.package = resolvedPackage + self.packageManifest = packageManifest + self.modulesGraph = modulesGraph + self.delegate = delegate + self.buildToolPluginResultsByTargetName = buildToolPluginResultsByTargetName + self.createDylibForDynamicProducts = createDylibForDynamicProducts + self.packageDisplayVersion = packageDisplayVersion + self.observabilityScope = observabilityScope + } + + /// Build an empty PIF project. + public func buildEmptyPIF() { + self._pifProject = PIFPackageBuilder.buildEmptyPIF(package: self.package.underlying) + } + + /// Build an empty PIF project for the specified `Package`. + + public class func buildEmptyPIF(package: PackageModel.Package) -> SwiftBuild.PIF.Project { + self.buildEmptyPIF( + id: "PACKAGE:\(package.identity)", + path: package.manifest.path.pathString, + projectDir: package.path.pathString, + name: package.name, + developmentRegion: package.manifest.defaultLocalization + ) + } + + /// Build an empty PIF project. + public class func buildEmptyPIF( + id: String, + path: String, + projectDir: String, + name: String, + developmentRegion: String? = nil + ) -> SwiftBuild.PIF.Project { + let project = SwiftBuild.PIF.Project( + id: id, + path: path, + projectDir: projectDir, + name: name, + developmentRegion: developmentRegion + ) + let settings = SwiftBuild.PIF.BuildSettings() + + project.addBuildConfig(name: "Debug", settings: settings) + project.addBuildConfig(name: "Release", settings: settings) + + return project + } + + public func buildPlaceholderPIF(id: String, path: String, projectDir: String, name: String) -> ModuleOrProduct { + let project = SwiftBuild.PIF.Project( + id: id, + path: path, + projectDir: projectDir, + name: name + ) + let projectSettings = SwiftBuild.PIF.BuildSettings() + project.addBuildConfig(name: "Debug", settings: projectSettings) + project.addBuildConfig(name: "Release", settings: projectSettings) + + let target = project.addAggregateTarget(id: "PACKAGE-PLACEHOLDER:\(id)", name: id) + let targetSettings: SwiftBuild.PIF.BuildSettings = self.package.underlying.packageBaseBuildSettings + target.addBuildConfig(name: "Debug", settings: targetSettings) + target.addBuildConfig(name: "Release", settings: targetSettings) + + self._pifProject = project + + let placeholderModule = ModuleOrProduct( + type: .placeholder, + name: name, + moduleName: name, + pifTarget: target, + indexableFileURLs: [], + headerFiles: [], + linkedPackageBinaries: [], + swiftLanguageVersion: nil, + declaredPlatforms: nil, + deploymentTargets: nil + ) + return placeholderModule + } + + // FIXME: Maybe break this up in a `ArtifactMetadata` protocol and two value types —— Paulo + // Like `ProductMetadata` and also `ModuleMetadata`. + + /// Value type with information about a given PIF module or product. + public struct ModuleOrProduct { + public var type: ModuleOrProductType + public var name: String + public var moduleName: String? + public var isDynamicLibraryVariant: Bool = false + + public var pifTarget: SwiftBuild.PIF.BaseTarget? + + public var indexableFileURLs: [SourceControlURL] + public var headerFiles: Set + public var linkedPackageBinaries: [LinkedPackageBinary] + + public var swiftLanguageVersion: String? + + public var declaredPlatforms: [PackageModel.Platform]? + public var deploymentTargets: [PackageModel.Platform: String?]? + } + + public struct LinkedPackageBinary { + public let name: String + public let packageName: String + public let type: BinaryType + + @frozen + public enum BinaryType { + case product + case target + } + + public init(name: String, packageName: String, type: BinaryType) { + self.name = name + self.packageName = packageName + self.type = type + } + } + + public enum ModuleOrProductType: String, Sendable, CustomStringConvertible { + // Products. + case application + case staticArchive + case objectFile + case dynamicLibrary + case framework + case executable + case unitTest + case bundle + case resourceBundle + case packageProduct + case commandPlugin + case buildToolPlugin + + // Modules. + case module + case plugin + case macro + case placeholder + + public var description: String { rawValue } + + init(from pifProductType: SwiftBuild.PIF.Target.ProductType) { + self = switch pifProductType { + case .application: .application + case .staticArchive: .staticArchive + case .objectFile: .objectFile + case .dynamicLibrary: .dynamicLibrary + case .framework: .framework + case .executable: .executable + case .unitTest: .unitTest + case .bundle: .bundle + case .packageProduct: .packageProduct + case .hostBuildTool: fatalError("Unexpected hostBuildTool type") + @unknown default: + fatalError() + } + } + } + + /// Build the PIF. + @discardableResult + public func build() throws -> [ModuleOrProduct] { + self.log(.info, "building PIF for package \(self.package.identity)") + + var project = PackagePIFProjectBuilder(createForPackage: package, builder: self) + self.addProjectBuildSettings(project: project) + + self._pifProject = project.pif + + // + // Construct PIF *targets* (for modules, products, and test bundles) based on the contents of the parsed + // package. + // These PIF targets will be sent down to Swift Build. + // + // We also track all constructed objects as `ModuleOrProduct` value for easy introspection by clients. + // In SwiftPM a product is a codeless entity with a reference to the modules(s) that contains the + // implementation. + // In order to avoid creating two ModuleOrProducts for each product in the package, the logic below creates a + // single + // unified ModuleOrProduct from the combination of a product and the single target that contains its + // implementation. + // + // Products. SwiftPM considers unit tests to be products, so in this discussion, the term *product* + // refers to an *executable*, a *library*, or an *unit test*. + // + // Automatic libraries. The current implementation treats all automatic libraries as *static*; + // in the future, we will want to do more holistic analysis so that the decision about whether + // or not to build a separate dynamic library for a package library product takes into account + // the structure of the client(s). + // + + // For each of the **products** in the package we create a corresponding `PIFTarget` of the appropriate type. + for product in self.package.products { + switch product.type { + case .library(.static): + let libraryType = self.delegate.customLibraryType(product: product.underlying) ?? .static + try project.makeLibraryProduct(product, type: libraryType) + + case .library(.dynamic): + let libraryType = self.delegate.customLibraryType(product: product.underlying) ?? .dynamic + try project.makeLibraryProduct(product, type: libraryType) + + case .library(.automatic): + // Check if this is a system library product. + if product.isSystemLibraryProduct { + try project.makeSystemLibraryProduct(product) + } else { + // Otherwise, it is a regular library product. + let libraryType = self.delegate.customLibraryType(product: product.underlying) ?? .automatic + try project.makeLibraryProduct(product, type: libraryType) + } + + case .executable, .test: + try project.makeMainModuleProduct(product) + + case .plugin: + try project.makePluginProduct(product) + + case .snippet, .macro: + break // TODO: Double-check what's going on here as we skip snippet modules too (rdar://147705448) + } + } + + // For each of the **modules** in the package other than those that are the *main* module of a product + // —— which we've already dealt with above —— we create a corresponding `PIFTarget` of the appropriate type. + for module in self.package.modules { + switch module.type { + case .executable: + try project.makeTestableExecutableSourceModule(module) + + case .snippet: + // Already handled as a product. Note that snippets don't need testable modules. + break + + case .library: + try project.makeLibraryModule(module) + + case .systemModule: + try project.makeSystemLibraryModule(module) + + case .test: + // Skip test module targets. + // They will have been dealt with as part of the *products* to which they belong. + break + + case .binary: + // Skip binary module targets. + break + + case .plugin: + try project.makePluginModule(module) + + case .macro: + try project.makeMacroModule(module) + } + } + + let customModulesAndProducts = try delegate.addCustomTargets(pifProject: project.pif) + project.builtModulesAndProducts.append(contentsOf: customModulesAndProducts) + + return project.builtModulesAndProducts + } + + /// 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. + private func addProjectBuildSettings(project: PackagePIFProjectBuilder) { + var settings = SwiftBuild.PIF.BuildSettings() + settings.PRODUCT_NAME = "$(TARGET_NAME)" + settings.SUPPORTED_PLATFORMS = ["$(AVAILABLE_PLATFORMS)"] + settings.SKIP_INSTALL = "YES" + settings.MACOSX_DEPLOYMENT_TARGET = project.deploymentTargets[.macOS] ?? nil + settings.IPHONEOS_DEPLOYMENT_TARGET = project.deploymentTargets[.iOS] ?? nil + if let deploymentTarget_macCatalyst = project.deploymentTargets[.macCatalyst] ?? nil { + settings + .platformSpecificSettings[.macCatalyst]![.IPHONEOS_DEPLOYMENT_TARGET] = [deploymentTarget_macCatalyst] + } + settings.TVOS_DEPLOYMENT_TARGET = project.deploymentTargets[.tvOS] ?? nil + settings.WATCHOS_DEPLOYMENT_TARGET = project.deploymentTargets[.watchOS] ?? nil + settings.DRIVERKIT_DEPLOYMENT_TARGET = project.deploymentTargets[.driverKit] ?? nil + settings.XROS_DEPLOYMENT_TARGET = project.deploymentTargets[.visionOS] ?? nil + settings.DYLIB_INSTALL_NAME_BASE = "@rpath" + settings.USE_HEADERMAP = "NO" + settings.OTHER_SWIFT_FLAGS.lazilyInitializeAndMutate(initialValue: ["$(inherited)"]) { $0.append("-DXcode") } + + // TODO: Might be relevant to make customizable —— Paulo + // (If we want to be extra careful with differences to the existing PIF in the SwiftPM.) + settings.OTHER_CFLAGS = ["$(inherited)", "-DXcode"] + + if !self.delegate.isRootPackage { + if self.suppressWarningsForPackageDependencies { + settings.SUPPRESS_WARNINGS = "YES" + } + if self.skipStaticAnalyzerForPackageDependencies { + settings.SKIP_CLANG_STATIC_ANALYZER = "YES" + } + } + settings.SWIFT_ACTIVE_COMPILATION_CONDITIONS + .lazilyInitializeAndMutate(initialValue: ["$(inherited)"]) { $0.append("SWIFT_PACKAGE") } + settings.GCC_PREPROCESSOR_DEFINITIONS = ["$(inherited)", "SWIFT_PACKAGE"] + settings.CLANG_ENABLE_OBJC_ARC = "YES" + 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 = [] + + // Packages use the SwiftPM workspace's cache directory as a compiler working directory to maximize module + // sharing. + settings.COMPILER_WORKING_DIRECTORY = "$(WORKSPACE_DIR)" + + // Hook to customize the project-wide build settings. + self.delegate.configureProjectBuildSettings(&settings) + + for (platform, platformOptions) in self.package.sdkOptions(delegate: self.delegate) { + let pifPlatform = SwiftBuild.PIF.BuildSettings.Platform(from: platform) + settings.platformSpecificSettings[pifPlatform]![.SPECIALIZATION_SDK_OPTIONS]! + .append(contentsOf: platformOptions) + } + + let deviceFamilyIDs: Set = self.delegate.deviceFamilyIDs() + settings.TARGETED_DEVICE_FAMILY = deviceFamilyIDs.sorted().map { String($0) }.joined(separator: ",") + + // This will add the XCTest related search paths automatically, + // including the Swift overlays. + settings.ENABLE_TESTING_SEARCH_PATHS = "YES" + + // 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 = "" + + // If in a workspace that's set to build packages for arm64e, pass that along to Swift Build. + if self.delegate.shouldiOSPackagesBuildForARM64e { + settings.platformSpecificSettings[._iOSDevice]![.ARCHS] = ["arm64e"] + } + + // Add the build settings that are specific to debug builds, and set those as the "Debug" configuration. + 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 = (settings.SWIFT_ACTIVE_COMPILATION_CONDITIONS ?? []) + ["DEBUG"] + debugSettings + .GCC_PREPROCESSOR_DEFINITIONS = (settings.GCC_PREPROCESSOR_DEFINITIONS ?? ["$(inherited)"]) + ["DEBUG=1"] + project.pif.addBuildConfig(name: "Debug", settings: debugSettings) + + // Add the build settings that are specific to release builds, and set those as the "Release" configuration. + 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" + project.pif.addBuildConfig(name: "Release", settings: releaseSettings) + } + + private enum SourceModuleType { + case dynamicLibrary + case staticLibrary + case executable + case macro + } + + struct EmbedResourcesResult { + let bundleName: String? + let shouldGenerateBundleAccessor: Bool + let shouldGenerateEmbedInCodeAccessor: Bool + } + + struct Resource { + let path: String + let rule: PackageModel.Resource.Rule + + init(path: String, rule: PackageModel.Resource.Rule) { + self.path = path + self.rule = rule + } + + init(_ resource: PackageModel.Resource) { + self.path = resource.path.pathString + self.rule = resource.rule + } + } +} + +// MARK: - Helpers + +extension PIFPackageBuilder.ModuleOrProduct { + public init( + type moduleOrProductType: PIFPackageBuilder.ModuleOrProductType, + name: String, + moduleName: String?, + pifTarget: SwiftBuild.PIF.BaseTarget?, + indexableFileURLs: [SourceControlURL] = [], + headerFiles: Set = [], + linkedPackageBinaries: [PIFPackageBuilder.LinkedPackageBinary] = [], + swiftLanguageVersion: String? = nil, + declaredPlatforms: [PackageModel.Platform]? = [], + deploymentTargets: [PackageModel.Platform: String?]? = [:] + ) { + self.type = moduleOrProductType + self.name = name + self.moduleName = moduleName + self.pifTarget = pifTarget + self.indexableFileURLs = indexableFileURLs + self.headerFiles = headerFiles + self.linkedPackageBinaries = linkedPackageBinaries + self.swiftLanguageVersion = swiftLanguageVersion + self.declaredPlatforms = declaredPlatforms + self.deploymentTargets = deploymentTargets + } +} + +enum PIFBuildingError: Error { + case packageExtensionFeatureNotEnabled +} + +extension PIFPackageBuilder.LinkedPackageBinary { + init?(module: ResolvedModule, package: ResolvedPackage) { + let packageName = package.manifest.displayName + + switch module.type { + case .executable, .snippet, .test: + self.init(name: module.name, packageName: packageName, type: .product) + + case .library, .binary, .macro: + self.init(name: module.name, packageName: packageName, type: .target) + + case .systemModule, .plugin: + return nil + } + } + + init?(dependency: ResolvedModule.Dependency, package: ResolvedPackage) { + switch dependency { + case .product(let producutDependency, _): + guard producutDependency.hasSourceTargets else { return nil } + self.init(name: producutDependency.name, packageName: package.name, type: .product) + + case .module(let moduleDependency, _): + self.init(module: moduleDependency, package: package) + } + } +} + +extension ObservabilityMetadata { + public var sourceLocation: SourceLocation? { + get { + self[SourceLocationKey.self] + } + set { + self[SourceLocationKey.self] = newValue + } + } + + private enum SourceLocationKey: Key { + typealias Value = SourceLocation + } +} + +public struct SourceLocation: Sendable { + public let file: StaticString + public let line: UInt + + public init(_ file: StaticString, _ line: UInt) { + self.file = file + self.line = line + } +} + +#endif diff --git a/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Modules.swift b/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Modules.swift new file mode 100644 index 00000000000..0cdaa7617b5 --- /dev/null +++ b/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Modules.swift @@ -0,0 +1,818 @@ +//===----------------------------------------------------------------------===// +// +// 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 Foundation + +import struct Basics.AbsolutePath +import class Basics.ObservabilitySystem +import func Basics.resolveSymlinks +import struct Basics.SourceControlURL + +import class PackageModel.Manifest +import class PackageModel.Module +import class PackageModel.Product +import class PackageModel.SystemLibraryModule + +import struct PackageGraph.ResolvedModule +import struct PackageGraph.ResolvedPackage + +#if canImport(SwiftBuild) +import enum SwiftBuild.PIF + +/// Extension to create PIF **modules** for a given package. +extension PackagePIFProjectBuilder { + // MARK: - Plugin Modules + + mutating func makePluginModule(_ pluginModule: PackageGraph.ResolvedModule) throws { + precondition(pluginModule.type == .plugin) + + // Create an executable PIF target in order to get specialization. + let pluginPifTarget = try self.pif.addTargetThrowing( + id: pluginModule.pifTargetGUID(), + productType: .executable, + name: pluginModule.name, + productName: pluginModule.name + ) + log(.debug, "created \(type(of: pluginPifTarget)) '\(pluginPifTarget.id)' with name '\(pluginPifTarget.name)'") + + var buildSettings: SwiftBuild.PIF.BuildSettings = self.package.underlying.packageBaseBuildSettings + + // Add the dependencies. + pluginModule.recursivelyTraverseDependencies { dependency in + switch dependency { + case .module(let moduleDependency, let packageConditions): + // This assertion is temporarily disabled since we may see targets from + // _other_ packages, but this should be resolved; see rdar://95467710. + /* assert(moduleDependency.packageName == self.package.name) */ + + let dependencyPlatformFilters = packageConditions + .toPlatformFilter(toolsVersion: self.package.manifest.toolsVersion) + + switch moduleDependency.type { + case .executable, .snippet: + // For executable targets, add a build time dependency on the product. + // FIXME: Maybe we should we do this at the libSwiftPM level. + let moduleProducts = self.package.products.filter(\.isMainModuleProduct) + let productDependency = moduleDependency + .productRepresentingDependencyOfBuildPlugin(in: moduleProducts) + + if let productDependency { + pluginPifTarget.addDependency( + on: productDependency.pifTargetGUID(), + platformFilters: dependencyPlatformFilters + ) + log(.debug, ".. added dependency on product '\(productDependency.pifTargetGUID())'") + } else { + log( + .debug, + ".. could not find a build plugin product to depend on for target '\(moduleDependency.pifTargetGUID())'" + ) + } + + case .library, .systemModule, .test, .binary, .plugin, .macro: + let dependencyGUID = moduleDependency.pifTargetGUID() + pluginPifTarget.addDependency( + on: dependencyGUID, + platformFilters: dependencyPlatformFilters + ) + log(.debug, ".. added dependency on target '\(dependencyGUID)'") + } + + case .product(let productDependency, let packageConditions): + // Do not add a dependency for binary-only executable products since they are not part of the build. + if productDependency.isBinaryOnlyExecutableProduct { + break + } + + if !pifBuilder.delegate.shouldSuppressProductDependency( + product: productDependency.underlying, + buildSettings: &buildSettings + ) { + let dependencyGUID = productDependency.pifTargetGUID() + let dependencyPlatformFilters = packageConditions + .toPlatformFilter(toolsVersion: self.package.manifest.toolsVersion) + + pluginPifTarget.addDependency( + on: dependencyGUID, + platformFilters: dependencyPlatformFilters + ) + log(.debug, ".. added dependency on product '\(dependencyGUID)'") + } + } + } + + // Any dependencies of plugin targets need to be built for the host. + buildSettings.SUPPORTED_PLATFORMS = ["$(HOST_PLATFORM)"] + + pluginPifTarget.addBuildConfig(name: "Debug", settings: buildSettings) + pluginPifTarget.addBuildConfig(name: "Release", settings: buildSettings) + + let pluginModuleMetadata = PIFPackageBuilder.ModuleOrProduct( + type: .plugin, + name: pluginModule.name, + moduleName: pluginModule.name, + pifTarget: pluginPifTarget, + indexableFileURLs: [], + headerFiles: [], + linkedPackageBinaries: [], + swiftLanguageVersion: nil, + declaredPlatforms: self.declaredPlatforms, + deploymentTargets: self.deploymentTargets + ) + self.builtModulesAndProducts.append(pluginModuleMetadata) + } + + // MARK: - Macro Modules + + mutating func makeMacroModule(_ macroModule: PackageGraph.ResolvedModule) throws { + precondition(macroModule.type == .macro) + + let (builtMacroModule, _) = try buildSourceModule(macroModule, type: .macro) + self.builtModulesAndProducts.append(builtMacroModule) + + // We also create a testable version of the macro, similar to what we're doing for regular executable targets. + let (builtTestableMacroModule, _) = try buildSourceModule( + macroModule, + type: .executable, + targetSuffix: .testable + ) + self.builtModulesAndProducts.append(builtTestableMacroModule) + } + + // MARK: - Library Modules + + // Build a *static library* that can be linked together into other products. + mutating func makeLibraryModule(_ libraryModule: PackageGraph.ResolvedModule) throws { + precondition(libraryModule.type == .library) + + let (staticLibrary, resourceBundleName) = try buildSourceModule(libraryModule, type: .staticLibrary) + self.builtModulesAndProducts.append(staticLibrary) + + if self.shouldOfferDynamicTarget(libraryModule.name) { + var (dynamicLibraryVariant, _) = try buildSourceModule( + libraryModule, + type: .dynamicLibrary, + targetSuffix: .dynamic, + addBuildToolPluginCommands: false, + inputResourceBundleName: resourceBundleName + ) + dynamicLibraryVariant.isDynamicLibraryVariant = true + self.builtModulesAndProducts.append(dynamicLibraryVariant) + + let pifTarget = staticLibrary.pifTarget as? SwiftBuild.PIF.Target + let dynamicPifTarget = dynamicLibraryVariant.pifTarget as? SwiftBuild.PIF.Target + + guard let pifTarget, let dynamicPifTarget else { + fatalError("Could not assign dynamic PIF target") + } + pifTarget.dynamicTargetVariant = dynamicPifTarget + } + } + + // MARK: - Executable Source Modules + + /// If we're building an *executable* and the tools version is new enough, + /// we also construct a testable version of said executable. + mutating func makeTestableExecutableSourceModule(_ executableModule: PackageGraph.ResolvedModule) throws { + precondition(executableModule.type == .executable) + guard self.package.manifest.toolsVersion >= .v5_5 else { return } + + let inputResourceBundleName: String? = if mainModuleTargetNamesWithResources.contains(executableModule.name) { + resourceBundleName(forModuleName: executableModule.name) + } else { + nil + } + + let (testableExecutableModule, _) = try buildSourceModule( + executableModule, + type: .executable, + targetSuffix: .testable, + addBuildToolPluginCommands: false, + inputResourceBundleName: inputResourceBundleName + ) + self.builtModulesAndProducts.append(testableExecutableModule) + } + + // MARK: - Source Modules + + enum SourceModuleType { + case dynamicLibrary + case staticLibrary + case executable + case macro + } + + /// Constructs a *PIF target* for building a *module* target as a particular type. + /// An optional target identifier suffix is passed when building variants of a target. + @discardableResult + private mutating func buildSourceModule( + _ sourceModule: PackageGraph.ResolvedModule, + type desiredModuleType: SourceModuleType, + targetSuffix: TargetGUIDSuffix? = nil, + addBuildToolPluginCommands: Bool = true, + inputResourceBundleName: String? = nil + ) throws -> (PIFPackageBuilder.ModuleOrProduct, resourceBundleName: String?) { + precondition(sourceModule.isSourceModule) + + let pifTargetName: String + let executableName: String + let productType: SwiftBuild.PIF.Target.ProductType + + switch desiredModuleType { + case .dynamicLibrary: + if pifBuilder.createDylibForDynamicProducts { // We are re-using this default for dynamic targets as well. + pifTargetName = "lib\(sourceModule.name).dylib" + executableName = pifTargetName + productType = .dynamicLibrary + } else { + pifTargetName = sourceModule.name + ".framework" + executableName = sourceModule.name + productType = .framework + } + + case .staticLibrary, .executable: + pifTargetName = "\(sourceModule.name).o" + executableName = pifTargetName + productType = .objectFile + + case .macro: + pifTargetName = sourceModule.name + executableName = pifTargetName + productType = .hostBuildTool + } + + // Create a PIF target configured to build a single .o file. + // For now wrapped in a static archive, since Swift Build can *not* yet produce a single .o as an output. + + // Macros are currently the only target type that requires explicit approval by users. + let approvedByUser: Bool = if desiredModuleType == .macro { + // Look up the current approval status in the underlying fingerprint storage. + pifBuilder.delegate.validateMacroFingerprint(for: sourceModule) == true + } else { + true + } + + let sourceModulePifTarget = try self.pif.addTargetThrowing( + id: sourceModule.pifTargetGUID(suffix: targetSuffix), + productType: productType, + name: sourceModule.name, + productName: pifTargetName, + approvedByUser: approvedByUser + ) + log( + .debug, + "created \(type(of: sourceModulePifTarget)) '\(sourceModulePifTarget.id)' of type '\(sourceModulePifTarget.productType.asString)' with name '\(sourceModulePifTarget.name)' and product name '\(sourceModulePifTarget.productName)'" + ) + + // Deal with any generated source files or resource files. + let (generatedSourceFiles, generatedResourceFiles) = computePluginGeneratedFiles( + module: sourceModule, + pifTarget: sourceModulePifTarget, + addBuildToolPluginCommands: false + ) + + // Either create or reuse the resource bundle. + var resourceBundleName = inputResourceBundleName + let shouldGenerateBundleAccessor: Bool + let shouldGenerateEmbedInCodeAccessor: Bool + if resourceBundleName == nil && desiredModuleType != .executable && desiredModuleType != .macro { + let (result, resourceBundle) = try addResourceBundle( + for: sourceModule, + pifTarget: sourceModulePifTarget, + generatedResourceFiles: generatedResourceFiles + ) + if let resourceBundle { self.builtModulesAndProducts.append(resourceBundle) } + + resourceBundleName = result.bundleName + shouldGenerateBundleAccessor = result.shouldGenerateBundleAccessor + shouldGenerateEmbedInCodeAccessor = result.shouldGenerateEmbedInCodeAccessor + } else { + // Here we have to assume we need both types of accessors which will always bring in Foundation into the + // current target + // through the bundle accessor and will lead to Swift Build evaluating all resources, but neither should + // technically be a problem. + // Would still be nice to eventually make this accurate which would require storing these in addition to + // `inputResourceBundleName`. + shouldGenerateBundleAccessor = true + shouldGenerateEmbedInCodeAccessor = true + } + + // Find the PIF target for the resource bundle, if any. Otherwise fall back to the module. + let resourceBundlePifTarget = self + .resourceBundleTarget(forModuleName: sourceModule.name) ?? sourceModulePifTarget + + // Add build tool commands to the resource bundle target. + if desiredModuleType != .executable && desiredModuleType != .macro && addBuildToolPluginCommands { + addBuildToolCommands( + module: sourceModule, + sourceModulePifTarget: sourceModulePifTarget, + resourceBundlePifTarget: resourceBundlePifTarget, + sourceFilePaths: generatedSourceFiles, + resourceFilePaths: generatedResourceFiles + ) + } + + // Create a set of build settings that will be imparted to any target that depends on this one. + var impartedSettings = SwiftBuild.PIF.BuildSettings() + + // Configure the target-wide build settings. The details depend on the kind of product we're building. + var settings: SwiftBuild.PIF.BuildSettings = self.package.underlying.packageBaseBuildSettings + + if shouldGenerateBundleAccessor { + settings.GENERATE_RESOURCE_ACCESSORS = "YES" + } + if shouldGenerateEmbedInCodeAccessor { + settings.GENERATE_EMBED_IN_CODE_ACCESSORS = "YES" + } + + // Generate a module map file, if needed. + var moduleMapFileContents = "" + var moduleMapFile = "" + let generatedModuleMapDir = "$(GENERATED_MODULEMAP_DIR)" + + if sourceModule.usesSwift && desiredModuleType != .macro { + // Generate ObjC compatibility header for Swift library targets. + settings.SWIFT_OBJC_INTERFACE_HEADER_DIR = generatedModuleMapDir + settings.SWIFT_OBJC_INTERFACE_HEADER_NAME = "\(sourceModule.name)-Swift.h" + + moduleMapFileContents = """ + module \(sourceModule.c99name) { + header "\(sourceModule.name)-Swift.h" + export * + } + """ + moduleMapFile = "\(generatedModuleMapDir)/\(sourceModule.name).modulemap" + + // We only need to impart this to C clients. + impartedSettings.OTHER_CFLAGS = ["-fmodule-map-file=\(moduleMapFile)", "$(inherited)"] + } else if sourceModule.moduleMapFileRelativePath == nil { + // Otherwise, this is a C library module and we generate a modulemap if one is already not provided. + if case .umbrellaHeader(let path) = sourceModule.moduleMapType { + log(.debug, "\(package.name).\(sourceModule.name) generated umbrella header") + moduleMapFileContents = """ + module \(sourceModule.c99name) { + umbrella header "\(path)" + export * + } + """ + } else if case .umbrellaDirectory(let path) = sourceModule.moduleMapType { + log(.debug, "\(package.name).\(sourceModule.name) generated umbrella directory") + moduleMapFileContents = """ + module \(sourceModule.c99name) { + umbrella "\(path)" + export * + } + """ + } + if moduleMapFileContents.hasContent { + // Pass the path of the module map up to all direct and indirect clients. + moduleMapFile = "\(generatedModuleMapDir)/\(sourceModule.name).modulemap" + impartedSettings.OTHER_CFLAGS = ["-fmodule-map-file=\(moduleMapFile)", "$(inherited)"] + impartedSettings.OTHER_SWIFT_FLAGS = ["-Xcc", "-fmodule-map-file=\(moduleMapFile)", "$(inherited)"] + } + } + + if desiredModuleType == .dynamicLibrary { + settings.configureDynamicSettings( + productName: sourceModule.name, + targetName: sourceModule.name, + executableName: executableName, + packageIdentity: package.identity, + packageName: sourceModule.packageName, + createDylibForDynamicProducts: pifBuilder.createDylibForDynamicProducts, + installPath: "/usr/local/lib", + delegate: pifBuilder.delegate + ) + } else { + settings.TARGET_NAME = sourceModule.name + settings.PRODUCT_NAME = "$(TARGET_NAME)" + settings.PRODUCT_MODULE_NAME = sourceModule.c99name + settings.PRODUCT_BUNDLE_IDENTIFIER = "\(self.package.identity).\(sourceModule.name)" + .spm_mangledToBundleIdentifier() + settings.EXECUTABLE_NAME = executableName + settings.CLANG_ENABLE_MODULES = "YES" + settings.GENERATE_MASTER_OBJECT_FILE = "NO" + settings.STRIP_INSTALLED_PRODUCT = "NO" + + // Macros build as executables, so they need slightly different + // build settings from other module types which build a "*.o". + if desiredModuleType == .macro { + settings.MACH_O_TYPE = "mh_execute" + } else { + settings.MACH_O_TYPE = "mh_object" + // 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" + } + settings.SWIFT_PACKAGE_NAME = sourceModule.packageName + + if desiredModuleType == .executable { + // Tell the Swift compiler to produce an alternate entry point rather than the standard `_main` entry + // point`, + // so that we can link one or more testable executable modules together into a single test bundle. + // This allows the test bundle to treat the executable as if it were any regular library module, + // and will have access to all symbols except the main entry point its. + settings.OTHER_SWIFT_FLAGS.lazilyInitializeAndMutate(initialValue: ["$(inherited)"]) { + $0.append(contentsOf: ["-Xfrontend", "-entry-point-function-name"]) + $0.append(contentsOf: ["-Xfrontend", "\(sourceModule.c99name)_main"]) + } + + // We have to give each target a unique name. + settings.TARGET_NAME = sourceModule.name + targetSuffix.description(forName: sourceModule.name) + + // Redirect the built executable into a separate directory so it won't conflict with the real one. + settings.TARGET_BUILD_DIR = "$(TARGET_BUILD_DIR)/ExecutableModules" + + // Don't install the Swift module of the testable side-built artifact, lest it conflict with the regular + // one. + // The modules should have compatible contents in any case — only the entry point function name is + // different in the Swift module + // (the actual runtime artifact is of course very different, and that's why we're building a separate + // testable artifact). + settings.SWIFT_INSTALL_MODULE = "NO" + } + + if let aliases = sourceModule.moduleAliases { + // Format each entry as "original_name=alias" + let list = aliases.map { $0.0 + "=" + $0.1 } + settings.SWIFT_MODULE_ALIASES = list.isEmpty ? nil : list + } + + // We mark in the PIF that we are intentionally not offering a dynamic target here, + // so we can emit a diagnostic if it is being requested by Swift Build. + if !self.shouldOfferDynamicTarget(sourceModule.name) { + settings.PACKAGE_TARGET_NAME_CONFLICTS_WITH_PRODUCT_NAME = "YES" + } + + // We are setting this instead of `LD_DYLIB_INSTALL_NAME` because `mh_object` files + // don't actually have install names, so we should not pass an install name to the linker. + settings.TAPI_DYLIB_INSTALL_NAME = sourceModule.name + } + + settings.PACKAGE_RESOURCE_TARGET_KIND = "regular" + settings.MODULEMAP_FILE_CONTENTS = moduleMapFileContents + settings.MODULEMAP_PATH = moduleMapFile + settings.DEFINES_MODULE = "YES" + + // Settings for text-based API. + // Due to rdar://78331694 (Cannot use TAPI for packages in contexts where we need to code-sign (e.g. apps)) + // we are only enabling TAPI in `configureSourceModuleBuildSettings`, if desired. + settings.SUPPORTS_TEXT_BASED_API = "NO" + + // If the module includes C headers, we set up the HEADER_SEARCH_PATHS setting appropriately. + if let includeDirAbsPath = sourceModule.includeDirAbsolutePath { + // Let the target itself find its own headers. + settings.HEADER_SEARCH_PATHS = [includeDirAbsPath.pathString, "$(inherited)"] + log(.debug, ".. added '\(includeDirAbsPath)' to HEADER_SEARCH_PATHS") + + // Also propagate this search path to all direct and indirect clients. + impartedSettings.HEADER_SEARCH_PATHS = [includeDirAbsPath.pathString, "$(inherited)"] + log(.debug, ".. added '\(includeDirAbsPath)' to imparted HEADER_SEARCH_PATHS") + } + + // Additional settings for the linker. + let baselineOTHER_LDFLAGS: [String] + let enableDuplicateLinkageCulling = UserDefaults.standard.bool( + forKey: "IDESwiftPackagesEnableDuplicateLinkageCulling", + defaultValue: true + ) + if enableDuplicateLinkageCulling { + baselineOTHER_LDFLAGS = [ + "-Wl,-no_warn_duplicate_libraries", + "$(inherited)", + ] + } else { + baselineOTHER_LDFLAGS = ["$(inherited)"] + } + impartedSettings.OTHER_LDFLAGS = (sourceModule.isCxx ? ["-lc++"] : []) + baselineOTHER_LDFLAGS + impartedSettings.OTHER_LDRFLAGS = [] + log(.debug, ".. added '\(String(describing: impartedSettings.OTHER_LDFLAGS))' to imparted OTHER_LDFLAGS") + + // This should be only for dynamic targets, but that isn't possible today. + // Improvement is tracked by rdar://77403529 (Only impart `PackageFrameworks` search paths to clients of dynamic + // package targets and products). + impartedSettings.FRAMEWORK_SEARCH_PATHS = ["$(BUILT_PRODUCTS_DIR)/PackageFrameworks", "$(inherited)"] + log( + .debug, + ".. added '\(String(describing: impartedSettings.FRAMEWORK_SEARCH_PATHS))' to imparted FRAMEWORK_SEARCH_PATHS" + ) + + // Set the appropriate language versions. + settings.SWIFT_VERSION = sourceModule.packageSwiftLanguageVersion(manifest: packageManifest) + settings.GCC_C_LANGUAGE_STANDARD = sourceModule.cLanguageStandard + settings.CLANG_CXX_LANGUAGE_STANDARD = sourceModule.cxxLanguageStandard + settings.SWIFT_ENABLE_BARE_SLASH_REGEX = "NO" + + // 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. Resolve symbolic links otherwise there will + // be a mismatch between the paths that the index service is using for Swift Build queries, + // and what paths Swift Build uses in its build description; such a mismatch would result + // in the index service failing to get compiler arguments for source files of the target. + let targetSourceFileGroup = self.pif.mainGroup.addGroup( + path: try! resolveSymlinks(sourceModule.sourceDirAbsolutePath).pathString, + pathBase: .absolute + ) + log(.debug, ".. added source file group '\(targetSourceFileGroup.path)'") + + // Add a source file reference for each of the source files, + // and also an indexable-file URL for each one. + // + // Symlinks should be resolved externally. + var indexableFileURLs: [SourceControlURL] = [] + for sourcePath in sourceModule.sourceFileRelativePaths { + sourceModulePifTarget.addSourceFile( + ref: targetSourceFileGroup.addFileReference(path: sourcePath.pathString, pathBase: .groupDir) + ) + log(.debug, ".. .. added source file '\(sourcePath)'") + indexableFileURLs.append( + SourceControlURL(fileURLWithPath: sourceModule.sourceDirAbsolutePath.appending(sourcePath)) + ) + } + for resource in sourceModule.resources { + log(.debug, ".. .. added resource file '\(resource.path)'") + indexableFileURLs.append(SourceControlURL(fileURLWithPath: resource.path)) + } + + let headerFiles = try Set(sourceModule.headerFileAbsolutePaths) + + // Add any additional source files emitted by custom build commands. + for path in generatedSourceFiles { + sourceModulePifTarget.addSourceFile( + ref: targetSourceFileGroup.addFileReference(path: path.pathString, pathBase: .absolute) + ) + log(.debug, ".. .. added generated source file '\(path)'") + } + + if let resourceBundle = resourceBundleName { + impartedSettings.EMBED_PACKAGE_RESOURCE_BUNDLE_NAMES = ["$(inherited)", resourceBundle] + settings.PACKAGE_RESOURCE_BUNDLE_NAME = resourceBundle + settings.COREML_CODEGEN_LANGUAGE = sourceModule.usesSwift ? "Swift" : "Objective-C" + settings.COREML_COMPILER_CONTAINER = "swift-package" + } + + if desiredModuleType == .macro { + settings.SWIFT_IMPLEMENTS_MACROS_FOR_MODULE_NAMES = [sourceModule.c99name] + } + if sourceModule.type == .macro { + settings.SKIP_BUILDING_DOCUMENTATION = "YES" + } + + // Handle the target's dependencies (but only link against them if needed). + let shouldLinkProduct = (desiredModuleType == .dynamicLibrary) || (desiredModuleType == .macro) + sourceModule.recursivelyTraverseDependencies { dependency in + switch dependency { + case .module(let moduleDependency, let packageConditions): + // This assertion is temporarily disabled since we may see targets from + // _other_ packages, but this should be resolved; see rdar://95467710. + /* assert(moduleDependency.packageName == self.package.name) */ + + let dependencyPlatformFilters = packageConditions + .toPlatformFilter(toolsVersion: self.package.manifest.toolsVersion) + + switch moduleDependency.type { + case .executable, .snippet: + // Always depend on product of executable targets (if available). + // FIXME: Maybe we should we do this at the libSwiftPM level. + let moduleMainProducts = self.package.products.filter(\.isMainModuleProduct) + if let product = moduleDependency + .productRepresentingDependencyOfBuildPlugin(in: moduleMainProducts) + { + sourceModulePifTarget.addDependency( + on: product.pifTargetGUID(), + platformFilters: dependencyPlatformFilters, + linkProduct: false + ) + log(.debug, ".. added dependency on product '\(product.pifTargetGUID)'") + } else { + log( + .debug, + ".. could not find a build plugin product to depend on for target '\(moduleDependency.pifTargetGUID())'" + ) + } + + case .binary: + let binaryReference = self.binaryGroup.addFileReference(path: moduleDependency.path.pathString) + if shouldLinkProduct { + sourceModulePifTarget.addLibrary( + ref: binaryReference, + platformFilters: dependencyPlatformFilters, + codeSignOnCopy: true, + removeHeadersOnCopy: true + ) + } else { + // If we are producing a single ".o", don't link binaries since they + // could be static which would cause them to become part of the ".o". + sourceModulePifTarget.addResourceFile( + ref: binaryReference, + platformFilters: dependencyPlatformFilters + ) + } + log(.debug, ".. added use of binary library '\(moduleDependency.path)'") + + case .plugin: + let dependencyGUID = moduleDependency.pifTargetGUID() + sourceModulePifTarget.addDependency( + on: dependencyGUID, + platformFilters: dependencyPlatformFilters, + linkProduct: false + ) + log(.debug, ".. added use of plugin target '\(dependencyGUID)'") + + case .library, .test, .macro, .systemModule: + sourceModulePifTarget.addDependency( + on: moduleDependency.pifTargetGUID(), + platformFilters: dependencyPlatformFilters, + linkProduct: shouldLinkProduct + ) + log( + .debug, + ".. added \(shouldLinkProduct ? "linked " : "")dependency on target '\(moduleDependency.pifTargetGUID())'" + ) + } + + case .product(let productDependency, let packageConditions): + // Do not add a dependency for binary-only executable products since they are not part of the build. + if productDependency.isBinaryOnlyExecutableProduct { + return + } + + if !pifBuilder.delegate.shouldSuppressProductDependency( + product: productDependency.underlying, + buildSettings: &settings + ) { + let dependencyPlatformFilters = packageConditions + .toPlatformFilter(toolsVersion: self.package.manifest.toolsVersion) + let shouldLinkProduct = shouldLinkProduct && productDependency.isLinkable + + sourceModulePifTarget.addDependency( + on: productDependency.pifTargetGUID(), + platformFilters: dependencyPlatformFilters, + linkProduct: shouldLinkProduct + ) + log( + .debug, + ".. added \(shouldLinkProduct ? "linked " : "")dependency on product '\(productDependency.pifTargetGUID)'" + ) + } + } + } + + // Custom source module build settings, if any. + pifBuilder.delegate.configureSourceModuleBuildSettings(sourceModule: sourceModule, settings: &settings) + + // Until this point the build settings for the target have been the same between debug and release + // configurations. + // The custom manifest settings might cause them to diverge. + var debugSettings = settings + var releaseSettings = settings + + let allBuildSettings = sourceModule.allBuildSettings + + // Apply target-specific build settings defined in the manifest. + for (buildConfig, declarationsByPlatform) in allBuildSettings.targetSettings { + for (platform, settingsByDeclaration) in declarationsByPlatform { + // A `nil` platform means that the declaration applies to *all* platforms. + let pifPlatform = platform.map { SwiftBuild.PIF.BuildSettings.Platform(from: $0) } + + for (declaration, stringValues) in settingsByDeclaration { + let pifDeclaration = SwiftBuild.PIF.BuildSettings.Declaration(from: declaration) + switch buildConfig { + case .debug: + debugSettings.append(values: stringValues, to: pifDeclaration, platform: pifPlatform) + case .release: + releaseSettings.append(values: stringValues, to: pifDeclaration, platform: pifPlatform) + } + } + } + } + + // Impart the linker flags. + for (platform, settingsByDeclaration) in sourceModule.allBuildSettings.impartedSettings { + // A `nil` platform means that the declaration applies to *all* platforms. + let pifPlatform = platform.map { SwiftBuild.PIF.BuildSettings.Platform(from: $0) } + + for (declaration, stringValues) in settingsByDeclaration { + let pifDeclaration = SwiftBuild.PIF.BuildSettings.Declaration(from: declaration) + impartedSettings.append(values: stringValues, to: pifDeclaration, platform: pifPlatform) + } + } + + // Set the imparted settings, which are ones that clients (both direct and indirect ones) use. + var debugImpartedSettings = impartedSettings + debugImpartedSettings.LD_RUNPATH_SEARCH_PATHS = + ["$(BUILT_PRODUCTS_DIR)/PackageFrameworks"] + + (debugImpartedSettings.LD_RUNPATH_SEARCH_PATHS ?? ["$(inherited)"]) + + sourceModulePifTarget.addBuildConfig( + name: "Debug", + settings: debugSettings, + impartedBuildSettings: debugImpartedSettings + ) + sourceModulePifTarget.addBuildConfig( + name: "Release", + settings: releaseSettings, + impartedBuildSettings: impartedSettings + ) + + // Collect linked binaries. + let linkedPackageBinaries: [PIFPackageBuilder.LinkedPackageBinary] = sourceModule.dependencies.compactMap { + PIFPackageBuilder.LinkedPackageBinary(dependency: $0, package: self.package) + } + + let productOrModuleType: PIFPackageBuilder.ModuleOrProductType = if desiredModuleType == .dynamicLibrary { + pifBuilder.createDylibForDynamicProducts ? .dynamicLibrary : .framework + } else if desiredModuleType == .macro { + .macro + } else { + .module + } + + let moduleOrProduct = PIFPackageBuilder.ModuleOrProduct( + type: productOrModuleType, + name: sourceModule.name, + moduleName: sourceModule.c99name, + pifTarget: sourceModulePifTarget, + indexableFileURLs: indexableFileURLs, + headerFiles: headerFiles, + linkedPackageBinaries: linkedPackageBinaries, + swiftLanguageVersion: sourceModule.packageSwiftLanguageVersion(manifest: packageManifest), + declaredPlatforms: self.declaredPlatforms, + deploymentTargets: self.deploymentTargets + ) + + return (moduleOrProduct, resourceBundleName) + } + + // MARK: - System Library Targets + + mutating func makeSystemLibraryModule(_ resolvedSystemLibrary: PackageGraph.ResolvedModule) throws { + precondition(resolvedSystemLibrary.type == .systemModule) + + let systemLibrary = resolvedSystemLibrary.underlying as! SystemLibraryModule + + // Create an aggregate PIF target (which doesn't have an actual product). + let systemLibraryPifTarget = self.pif.addAggregateTarget( + id: resolvedSystemLibrary.pifTargetGUID(), + name: resolvedSystemLibrary.name + ) + log( + .debug, + "created \(type(of: systemLibraryPifTarget)) '\(systemLibraryPifTarget.id)' with name '\(systemLibraryPifTarget.name)'" + ) + + let settings: SwiftBuild.PIF.BuildSettings = self.package.underlying.packageBaseBuildSettings + let pkgConfig = try systemLibrary.pkgConfig( + package: self.package, + observabilityScope: pifBuilder.observabilityScope + ) + + // Impart the header search path to all direct and indirect clients. + var impartedSettings = SwiftBuild.PIF.BuildSettings() + impartedSettings.OTHER_CFLAGS = ["-fmodule-map-file=\(systemLibrary.modulemapFileAbsolutePath)"] + pkgConfig + .cFlags.prepending("$(inherited)") + impartedSettings.OTHER_LDFLAGS = pkgConfig.libs.prepending("$(inherited)") + impartedSettings.OTHER_LDRFLAGS = [] + impartedSettings.OTHER_SWIFT_FLAGS = ["-Xcc"] + impartedSettings.OTHER_CFLAGS! + log(.debug, ".. added '\(systemLibrary.path.pathString)' to imparted HEADER_SEARCH_PATHS") + + systemLibraryPifTarget.addBuildConfig( + name: "Debug", + settings: settings, + impartedBuildSettings: impartedSettings + ) + systemLibraryPifTarget.addBuildConfig( + name: "Release", + settings: settings, + impartedBuildSettings: impartedSettings + ) + + // FIXME: Should we also impart linkage? + + let systemModule = PIFPackageBuilder.ModuleOrProduct( + type: .module, + name: resolvedSystemLibrary.name, + moduleName: resolvedSystemLibrary.c99name, + pifTarget: systemLibraryPifTarget, + indexableFileURLs: [], + headerFiles: [], + linkedPackageBinaries: [], + swiftLanguageVersion: nil, + declaredPlatforms: self.declaredPlatforms, + deploymentTargets: self.deploymentTargets + ) + self.builtModulesAndProducts.append(systemModule) + } +} +#endif diff --git a/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Products.swift b/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Products.swift new file mode 100644 index 00000000000..50026203596 --- /dev/null +++ b/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Products.swift @@ -0,0 +1,901 @@ +//===----------------------------------------------------------------------===// +// +// 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 Foundation + +import struct Basics.AbsolutePath +import class Basics.ObservabilitySystem +import struct Basics.SourceControlURL + +import class PackageModel.BinaryModule +import class PackageModel.Manifest +import enum PackageModel.PackageCondition +import class PackageModel.Product +import enum PackageModel.ProductType +import struct PackageModel.RegistryReleaseMetadata + +import struct PackageGraph.ResolvedModule +import struct PackageGraph.ResolvedPackage +import struct PackageGraph.ResolvedProduct + +#if canImport(SwiftBuild) +import enum SwiftBuild.PIF + +/// Extension to create PIF **products** for a given package. +extension PackagePIFProjectBuilder { + // MARK: - Main Module Products + + mutating func makeMainModuleProduct(_ product: PackageGraph.ResolvedProduct) throws { + precondition(product.isMainModuleProduct) + + // We'll be infusing the product's main module into the one for the product itself. + guard let mainModule = product.mainModule, mainModule.isSourceModule else { + return + } + + // Skip test products from non-root packages. libSwiftPM will stop vending them after + // target-based dependency resolution anyway but this should be fine until then. + if !pifBuilder.delegate.isRootPackage && (mainModule.type == .test || mainModule.type == .binary) { + return + } + + // Determine the kind of PIF target *product type* to create for the package product. + let pifProductType: SwiftBuild.PIF.Target.ProductType + let moduleOrProductType: PIFPackageBuilder.ModuleOrProductType + let synthesizedResourceGeneratingPluginInvocationResults: [PIFPackageBuilder.BuildToolPluginInvocationResult] = + [] + + if product.type == .executable { + if let customPIFProductType = pifBuilder.delegate.customProductType(forExecutable: product.underlying) { + pifProductType = customPIFProductType + moduleOrProductType = PIFPackageBuilder.ModuleOrProductType(from: customPIFProductType) + } else { + // No custom type provider. Current behavior is to fall back on regular executable. + pifProductType = .executable + moduleOrProductType = .executable + } + } else { + // If it's not an executable product, it must currently be a test bundle. + assert(product.type == .test, "Unexpected product type: \(product.type)") + pifProductType = .unitTest + moduleOrProductType = .unitTest + } + + // It's not a library product, so create a regular PIF target of the appropriate product type. + let mainModulePifTarget = try self.pif.addTargetThrowing( + id: product.pifTargetGUID(), + productType: pifProductType, + name: product.name, + productName: product.name + ) + log( + .debug, + "created \(type(of: mainModulePifTarget)) '\(mainModulePifTarget.id)' of type '\(mainModulePifTarget.productType.asString)' with name '\(mainModulePifTarget.name)' and product name '\(mainModulePifTarget.productName)'" + ) + + // We're currently *not* handling other module targets (and SwiftPM should never return them) for + // a main-module product but, for diagnostic purposes, we warn about any that we do come across. + if product.otherModules.hasContent { + let otherModuleNames = product.otherModules.map(\.name).joined(separator: ",") + log(.debug, ".. warning: ignored unexpected other module targets \(otherModuleNames)") + } + + // Deal with any generated source files or resource files. + let (generatedSourceFiles, pluginGeneratedResourceFiles) = computePluginGeneratedFiles( + module: mainModule, + pifTarget: mainModulePifTarget, + addBuildToolPluginCommands: pifProductType == .application + ) + if mainModule.resources.hasContent || pluginGeneratedResourceFiles.hasContent { + mainModuleTargetNamesWithResources.insert(mainModule.name) + } + + // 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: SwiftBuild.PIF.BuildSettings = package.underlying.packageBaseBuildSettings + settings.TARGET_NAME = product.name + settings.PACKAGE_RESOURCE_TARGET_KIND = "regular" + settings.PRODUCT_NAME = "$(TARGET_NAME)" + settings.PRODUCT_MODULE_NAME = product.c99name + settings.PRODUCT_BUNDLE_IDENTIFIER = "\(self.package.identity).\(product.name)" + .spm_mangledToBundleIdentifier() + settings.EXECUTABLE_NAME = product.name + settings.CLANG_ENABLE_MODULES = "YES" + settings.SWIFT_PACKAGE_NAME = mainModule.packageName + + if mainModule.type == .test { + // FIXME: we shouldn't always include both the deep and shallow bundle paths here, but for that we'll need rdar://31867023 + settings.LD_RUNPATH_SEARCH_PATHS = ["@loader_path/Frameworks", "@loader_path/../Frameworks", "$(inherited)"] + settings.GENERATE_INFOPLIST_FILE = "YES" + settings.SKIP_INSTALL = "NO" + settings.SWIFT_ACTIVE_COMPILATION_CONDITIONS.lazilyInitialize { ["$(inherited)"] } + } else if mainModule.type == .executable { + // Setup install path for executables if it's in root of a pure Swift package. + if pifBuilder.delegate.hostsOnlyPackages && pifBuilder.delegate.isRootPackage { + settings.SKIP_INSTALL = "NO" + settings.INSTALL_PATH = "/usr/local/bin" + settings.LD_RUNPATH_SEARCH_PATHS = ["$(inherited)", "@executable_path/../lib"] + } + } + + let mainTargetDeploymentTargets = mainModule.deploymentTargets(using: pifBuilder.delegate) + + settings.MACOSX_DEPLOYMENT_TARGET = mainTargetDeploymentTargets[.macOS] ?? nil + settings.IPHONEOS_DEPLOYMENT_TARGET = mainTargetDeploymentTargets[.iOS] ?? nil + if let deploymentTarget_macCatalyst = mainTargetDeploymentTargets[.macCatalyst] { + settings + .platformSpecificSettings[.macCatalyst]![.IPHONEOS_DEPLOYMENT_TARGET] = [deploymentTarget_macCatalyst] + } + settings.TVOS_DEPLOYMENT_TARGET = mainTargetDeploymentTargets[.tvOS] ?? nil + settings.WATCHOS_DEPLOYMENT_TARGET = mainTargetDeploymentTargets[.watchOS] ?? nil + settings.DRIVERKIT_DEPLOYMENT_TARGET = mainTargetDeploymentTargets[.driverKit] ?? nil + settings.XROS_DEPLOYMENT_TARGET = mainTargetDeploymentTargets[.visionOS] ?? nil + + // If the main module includes C headers, then we need to set up the HEADER_SEARCH_PATHS setting appropriately. + if let includeDirAbsolutePath = mainModule.includeDirAbsolutePath { + // Let the main module itself find its own headers. + settings.HEADER_SEARCH_PATHS = [includeDirAbsolutePath.pathString, "$(inherited)"] + log(.debug, ".. added '\(includeDirAbsolutePath)' to HEADER_SEARCH_PATHS") + } + + // Set the appropriate language versions. + settings.SWIFT_VERSION = mainModule.packageSwiftLanguageVersion(manifest: packageManifest) + settings.GCC_C_LANGUAGE_STANDARD = mainModule.cLanguageStandard + settings.CLANG_CXX_LANGUAGE_STANDARD = mainModule.cxxLanguageStandard + settings.SWIFT_ENABLE_BARE_SLASH_REGEX = "NO" + + // Create a group for the source files of the main module + // For now we use an absolute path for it, but we should really make it + // container-relative, since it's always inside the package directory. + let mainTargetSourceFileGroup = self.pif.mainGroup.addGroup( + path: mainModule.sourceDirAbsolutePath.pathString, + pathBase: .absolute + ) + log(.debug, ".. added source file group '\(mainTargetSourceFileGroup.path)'") + + // Add a source file reference for each of the source files, and also an indexable-file URL for each one. + // Note that the indexer requires them to have any symbolic links resolved. + var indexableFileURLs: [SourceControlURL] = [] + for sourcePath in mainModule.sourceFileRelativePaths { + mainModulePifTarget.addSourceFile( + ref: mainTargetSourceFileGroup.addFileReference(path: sourcePath.pathString, pathBase: .groupDir) + ) + log(.debug, ".. .. added source file '\(sourcePath)'") + indexableFileURLs + .append(SourceControlURL(fileURLWithPath: mainModule.sourceDirAbsolutePath.appending(sourcePath))) + } + + let headerFiles = try Set(mainModule.headerFileAbsolutePaths) + + // Add any additional source files emitted by custom build commands. + for path in generatedSourceFiles { + mainModulePifTarget.addSourceFile( + ref: mainTargetSourceFileGroup.addFileReference(path: path.pathString, pathBase: .absolute) + ) + log(.debug, ".. .. added generated source file '\(path)'") + } + + // Add any additional resource files emitted by synthesized build commands + let generatedResourceFiles: [String] = { + var generatedResourceFiles = pluginGeneratedResourceFiles + generatedResourceFiles.append( + contentsOf: addBuildToolCommands( + from: synthesizedResourceGeneratingPluginInvocationResults, + pifTarget: mainModulePifTarget, + addBuildToolPluginCommands: pifProductType == .application + ) + ) + return generatedResourceFiles + }() + + // Create a separate target to build a resource bundle for any resources files in the product's main target. + // FIXME: We should extend this to other kinds of products, but the immediate need for Swift Playgrounds Projects is for applications. + if pifProductType == .application { + let result = processResources( + for: mainModule, + sourceModulePifTarget: mainModulePifTarget, + // For application products we embed the resources directly into the PIF target. + resourceBundlePifTarget: nil, + generatedResourceFiles: generatedResourceFiles + ) + + if result.shouldGenerateBundleAccessor { + settings.GENERATE_RESOURCE_ACCESSORS = "YES" + } + if result.shouldGenerateEmbedInCodeAccessor { + settings.GENERATE_EMBED_IN_CODE_ACCESSORS = "YES" + } + + // FIXME: We should also adjust the generated module bundle glue so that `Bundle.module` is a synonym for `Bundle.main` in this case. + } else { + let (result, resourceBundle) = try addResourceBundle( + for: mainModule, + pifTarget: mainModulePifTarget, + generatedResourceFiles: generatedResourceFiles + ) + if let resourceBundle { self.builtModulesAndProducts.append(resourceBundle) } + + if let resourceBundle = result.bundleName { + // Associate the resource bundle with the target. + settings.PACKAGE_RESOURCE_BUNDLE_NAME = resourceBundle + + if result.shouldGenerateBundleAccessor { + settings.GENERATE_RESOURCE_ACCESSORS = "YES" + } + if result.shouldGenerateEmbedInCodeAccessor { + settings.GENERATE_EMBED_IN_CODE_ACCESSORS = "YES" + } + + // If it's a kind of product that can contain resources, we also add a use of it. + let ref = self.pif.mainGroup + .addFileReference(path: "$(CONFIGURATION_BUILD_DIR)/\(resourceBundle).bundle") + if pifProductType == .bundle || pifProductType == .unitTest { + settings.COREML_CODEGEN_LANGUAGE = mainModule.usesSwift ? "Swift" : "Objective-C" + settings.COREML_COMPILER_CONTAINER = "swift-package" + + mainModulePifTarget.addResourceFile(ref: ref) + log(.debug, ".. added use of resource bundle '\(ref.path)'") + } else { + log( + .debug, + ".. ignored resource bundle '\(ref.path)' for main module of type \(type(of: mainModule))" + ) + } + + // Add build tool commands to the resource bundle target. + let resourceBundlePifTarget = self + .resourceBundleTarget(forModuleName: mainModule.name) ?? mainModulePifTarget + addBuildToolCommands( + module: mainModule, + sourceModulePifTarget: mainModulePifTarget, + resourceBundlePifTarget: resourceBundlePifTarget, + sourceFilePaths: generatedSourceFiles, + resourceFilePaths: generatedResourceFiles + ) + } else { + // Generated resources always trigger the creation of a bundle accessor. + settings.GENERATE_RESOURCE_ACCESSORS = "YES" + settings.GENERATE_EMBED_IN_CODE_ACCESSORS = "NO" + + // If we did not create a resource bundle target, we still need to add build tool commands for any + // generated files. + addBuildToolCommands( + module: mainModule, + sourceModulePifTarget: mainModulePifTarget, + resourceBundlePifTarget: mainModulePifTarget, + sourceFilePaths: generatedSourceFiles, + resourceFilePaths: generatedResourceFiles + ) + } + } + + // Handle the main target's dependencies (and link against them). + mainModule.recursivelyTraverseDependencies { dependency in + switch dependency { + case .module(let moduleDependency, let packageConditions): + // This assertion is temporarily disabled since we may see targets from + // _other_ packages, but this should be resolved; see rdar://95467710. + /* assert(moduleDependency.packageName == self.package.name) */ + + switch moduleDependency.type { + case .binary: + let binaryReference = self.binaryGroup.addFileReference(path: moduleDependency.path.pathString) + mainModulePifTarget.addLibrary( + ref: binaryReference, + platformFilters: packageConditions + .toPlatformFilter(toolsVersion: package.manifest.toolsVersion), + codeSignOnCopy: true, + removeHeadersOnCopy: true + ) + log(.debug, ".. added use of binary library '\(moduleDependency.path)'") + + case .plugin: + let dependencyId = moduleDependency.pifTargetGUID() + mainModulePifTarget.addDependency( + on: dependencyId, + platformFilters: packageConditions + .toPlatformFilter(toolsVersion: package.manifest.toolsVersion), + linkProduct: false + ) + log(.debug, ".. added use of plugin target '\(dependencyId)'") + + case .macro: + let dependencyId = moduleDependency.pifTargetGUID() + mainModulePifTarget.addDependency( + on: dependencyId, + platformFilters: packageConditions + .toPlatformFilter(toolsVersion: package.manifest.toolsVersion), + linkProduct: false + ) + log(.debug, ".. added dependency on product '\(dependencyId)'") + + // Link with a testable version of the macro if appropriate. + if product.type == .test { + mainModulePifTarget.addDependency( + on: moduleDependency.pifTargetGUID(suffix: .testable), + platformFilters: packageConditions + .toPlatformFilter(toolsVersion: package.manifest.toolsVersion), + linkProduct: true + ) + log( + .debug, + ".. added linked dependency on target '\(moduleDependency.pifTargetGUID(suffix: .testable))'" + ) + + // FIXME: Manually propagate product dependencies of macros but the build system should really handle this. + moduleDependency.recursivelyTraverseDependencies { dependency in + switch dependency { + case .product(let productDependency, let packageConditions): + let isLinkable = productDependency.isLinkable + self.handleProduct( + productDependency, + with: packageConditions, + isLinkable: isLinkable, + pifTarget: mainModulePifTarget, + settings: &settings + ) + case .module: + break + } + } + } + + case .executable, .snippet: + // For executable targets, we depend on the *product* instead + // (i.e., we infuse the product's main module target into the one for the product itself). + let productDependency = modulesGraph.allProducts.only { $0.name == moduleDependency.name } + if let productDependency { + let productDependencyGUID = productDependency.pifTargetGUID() + mainModulePifTarget.addDependency( + on: productDependencyGUID, + platformFilters: packageConditions + .toPlatformFilter(toolsVersion: package.manifest.toolsVersion), + linkProduct: false + ) + log(.debug, ".. added dependency on product '\(productDependencyGUID)'") + } + + // If we're linking against an executable and the tools version is new enough, + // we also link against a testable version of the executable. + if product.type == .test, self.package.manifest.toolsVersion >= .v5_5 { + let moduleDependencyGUID = moduleDependency.pifTargetGUID(suffix: .testable) + mainModulePifTarget.addDependency( + on: moduleDependencyGUID, + platformFilters: packageConditions + .toPlatformFilter(toolsVersion: package.manifest.toolsVersion), + linkProduct: true + ) + log(.debug, ".. added linked dependency on target '\(moduleDependencyGUID)'") + } + + case .library, .systemModule, .test: + let shouldLinkProduct = moduleDependency.type != .systemModule + let dependencyGUID = moduleDependency.pifTargetGUID() + mainModulePifTarget.addDependency( + on: dependencyGUID, + platformFilters: packageConditions + .toPlatformFilter(toolsVersion: package.manifest.toolsVersion), + linkProduct: shouldLinkProduct + ) + log( + .debug, + ".. added \(shouldLinkProduct ? "linked " : "")dependency on target '\(dependencyGUID)'" + ) + } + + case .product(let productDependency, let packageConditions): + let isLinkable = productDependency.isLinkable + self.handleProduct( + productDependency, + with: packageConditions, + isLinkable: isLinkable, + pifTarget: mainModulePifTarget, + settings: &settings + ) + } + } + + // Until this point the build settings for the target have been the same between debug and release + // configurations. + // The custom manifest settings might cause them to diverge. + var debugSettings: SwiftBuild.PIF.BuildSettings = settings + var releaseSettings: SwiftBuild.PIF.BuildSettings = settings + + // Apply target-specific build settings defined in the manifest. + for (buildConfig, declarationsByPlatform) in mainModule.allBuildSettings.targetSettings { + for (platform, declarations) in declarationsByPlatform { + // A `nil` platform means that the declaration applies to *all* platforms. + let pifPlatform = platform.map { SwiftBuild.PIF.BuildSettings.Platform(from: $0) } + for (declaration, stringValues) in declarations { + let pifDeclaration = SwiftBuild.PIF.BuildSettings.Declaration(from: declaration) + switch buildConfig { + case .debug: + debugSettings.append(values: stringValues, to: pifDeclaration, platform: pifPlatform) + case .release: + releaseSettings.append(values: stringValues, to: pifDeclaration, platform: pifPlatform) + } + } + } + } + mainModulePifTarget.addBuildConfig(name: "Debug", settings: debugSettings) + mainModulePifTarget.addBuildConfig(name: "Release", settings: releaseSettings) + + // Collect linked binaries. + let linkedPackageBinaries: [PIFPackageBuilder.LinkedPackageBinary] = mainModule.dependencies.compactMap { + PIFPackageBuilder.LinkedPackageBinary(dependency: $0, package: self.package) + } + + let moduleOrProduct = PIFPackageBuilder.ModuleOrProduct( + type: moduleOrProductType, + name: product.name, + moduleName: product.c99name, + pifTarget: mainModulePifTarget, + indexableFileURLs: indexableFileURLs, + headerFiles: headerFiles, + linkedPackageBinaries: linkedPackageBinaries, + swiftLanguageVersion: mainModule.packageSwiftLanguageVersion(manifest: packageManifest), + declaredPlatforms: self.declaredPlatforms, + deploymentTargets: self.deploymentTargets + ) + self.builtModulesAndProducts.append(moduleOrProduct) + } + + private func handleProduct( + _ product: PackageGraph.ResolvedProduct, + with packageConditions: [PackageModel.PackageCondition], + isLinkable: Bool, + pifTarget: SwiftBuild.PIF.Target, + settings: inout SwiftBuild.PIF.BuildSettings + ) { + // Do not add a dependency for binary-only executable products since they are not part of the build. + if product.isBinaryOnlyExecutableProduct { + return + } + + if !pifBuilder.delegate.shouldSuppressProductDependency(product: product.underlying, buildSettings: &settings) { + let shouldLinkProduct = isLinkable + pifTarget.addDependency( + on: product.pifTargetGUID(), + platformFilters: packageConditions.toPlatformFilter(toolsVersion: package.manifest.toolsVersion), + linkProduct: shouldLinkProduct + ) + log( + .debug, + ".. added \(shouldLinkProduct ? "linked " : "")dependency on product '\(product.pifTargetGUID()))'" + ) + } + } + + // MARK: - Library Products + + /// We treat library products specially, in that they are just collections of other targets. + mutating func makeLibraryProduct( + _ libraryProduct: PackageGraph.ResolvedProduct, + type libraryType: ProductType.LibraryType + ) throws { + precondition(libraryProduct.type.isLibrary) + + let library = try self.buildLibraryProduct( + libraryProduct, + type: libraryType, + embedResources: false + ) + self.builtModulesAndProducts.append(library) + + // Also create a dynamic product for use by development-time features such as Previews and Swift Playgrounds. + // If all targets this product is comprised of are binaries, we should *not* create a dynamic variant. + if libraryType == .automatic && libraryProduct.hasSourceTargets { + var dynamicLibraryVariant = try self.buildLibraryProduct( + libraryProduct, + type: .dynamic, + targetSuffix: .dynamic, + embedResources: true + ) + dynamicLibraryVariant.isDynamicLibraryVariant = true + self.builtModulesAndProducts.append(dynamicLibraryVariant) + + let pifTarget = library.pifTarget as? SwiftBuild.PIF.Target + let dynamicPifTarget = dynamicLibraryVariant.pifTarget as? SwiftBuild.PIF.Target + + if let pifTarget, let dynamicPifTarget { + pifTarget.dynamicTargetVariant = dynamicPifTarget + } else { + assertionFailure("Could not assign dynamic PIF target") + } + } + } + + /// Helper function to create a PIF target for a **library product**. + /// + /// In order to support development-time features such as Preview and Swift Playgrounds, + /// all SwiftPM library products are represented by two PIF targets: + /// one of the "native" manifestation that gets linked into the client, + /// and another for a dynamic framework specifically for use by the development-time features. + private func buildLibraryProduct( + _ product: PackageGraph.ResolvedProduct, + type desiredProductType: ProductType.LibraryType, + targetSuffix: TargetGUIDSuffix? = nil, + embedResources: Bool + ) throws -> PIFPackageBuilder.ModuleOrProduct { + precondition(product.type.isLibrary) + + // FIXME: Cleanup this mess with + + let pifTargetProductName: String + let executableName: String + let productType: SwiftBuild.PIF.Target.ProductType + + if desiredProductType == .dynamic { + if pifBuilder.createDylibForDynamicProducts { + pifTargetProductName = "lib\(product.name).dylib" + executableName = pifTargetProductName + productType = .dynamicLibrary + } else { + // If a product is explicitly declared dynamic, we preserve its name, otherwise we will compute an + // automatic one. + if product.libraryType == .dynamic { + if let customExecutableName = pifBuilder.delegate + .customExecutableName(product: product.underlying) + { + executableName = customExecutableName + } else { + executableName = product.name + } + } else { + executableName = PIFPackageBuilder.computePackageProductFrameworkName(productName: product.name) + } + pifTargetProductName = "\(executableName).framework" + productType = .framework + } + } else { + pifTargetProductName = "lib\(product.name).a" + executableName = pifTargetProductName + productType = .packageProduct + } + + // Create a special kind of 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 = try self.pif.addTargetThrowing( + id: product.pifTargetGUID(suffix: targetSuffix), + productType: productType, + name: product.name, + productName: pifTargetProductName + ) + log( + .debug, + "created \(type(of: pifTarget)) '\(pifTarget.id)' of type '\(pifTarget.productType.asString)' with name '\(pifTarget.name)' and product name '\(pifTarget.productName)'" + ) + + // Add linked dependencies on the *targets* that comprise the product. + for module in product.modules { + // Binary targets are special in that they are just linked, not built. + if let binaryTarget = module.underlying as? BinaryModule { + let binaryReference = self.binaryGroup.addFileReference(path: binaryTarget.artifactPath.pathString) + pifTarget.addLibrary(ref: binaryReference, codeSignOnCopy: true, removeHeadersOnCopy: true) + log(.debug, ".. added use of binary library '\(binaryTarget.artifactPath.pathString)'") + continue + } + // We add these as linked dependencies; because the product type is `.packageProduct`, + // SwiftBuild won't actually link them, but will instead impart linkage to any clients that + // link against the package product. + pifTarget.addDependency(on: module.pifTargetGUID(), platformFilters: [], linkProduct: true) + log(.debug, ".. added linked dependency on target '\(module.pifTargetGUID())'") + } + + for module in product.modules where module.underlying.isSourceModule && module.resources.hasContent { + // FIXME: Find a way to determine whether a module has generated resources here so that we can embed resources into dynamic targets. + pifTarget.addDependency(on: pifTargetIdForResourceBundle(module.name), platformFilters: []) + + let filreRef = self.pif.mainGroup + .addFileReference(path: "$(CONFIGURATION_BUILD_DIR)/\(package.name)_\(module.name).bundle") + if embedResources { + pifTarget.addResourceFile(ref: filreRef) + log(.debug, ".. added use of resource bundle '\(filreRef.path)'") + } else { + log(.debug, ".. ignored resource bundle '\(filreRef.path)' because resource embedding is disabled") + } + } + + var settings: SwiftBuild.PIF.BuildSettings = package.underlying.packageBaseBuildSettings + + // Add other build settings when we're building an actual dylib. + if desiredProductType == .dynamic { + settings.configureDynamicSettings( + productName: product.name, + targetName: product.targetNameForProduct(), + executableName: executableName, + packageIdentity: package.identity, + packageName: package.identity.c99name, + createDylibForDynamicProducts: pifBuilder.createDylibForDynamicProducts, + installPath: installPath(for: product.underlying), + delegate: pifBuilder.delegate + ) + + pifTarget.addSourcesBuildPhase() + } + + // Additional configuration and files for this library product. + pifBuilder.delegate.configureLibraryProduct( + product: product.underlying, + pifTarget: pifTarget, + additionalFiles: self.additionalFilesGroup + ) + + // If the given package is a root package or it is used via a branch/revision, we allow unsafe flags. + let implicitlyAllowAllUnsafeFlags = pifBuilder.delegate.isBranchOrRevisionBased || pifBuilder.delegate + .isUserManaged + let recordUsesUnsafeFlags = try !implicitlyAllowAllUnsafeFlags && product.usesUnsafeFlags + settings.USES_SWIFTPM_UNSAFE_FLAGS = recordUsesUnsafeFlags ? "YES" : "NO" + + // 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). + product.modules.recursivelyTraverseDependencies { dependency in + switch dependency { + case .module(let moduleDependency, let packageConditions): + // This assertion is temporarily disabled since we may see targets from + // _other_ packages, but this should be resolved; see rdar://95467710. + /* assert(moduleDependency.packageName == self.package.name) */ + + if moduleDependency.type == .systemModule { + log(.debug, ".. noted use of system module '\(moduleDependency.name)'") + return + } + + if let binaryTarget = moduleDependency.underlying as? BinaryModule { + let binaryReference = self.binaryGroup.addFileReference(path: binaryTarget.path.pathString) + pifTarget.addLibrary( + ref: binaryReference, + platformFilters: packageConditions + .toPlatformFilter(toolsVersion: package.manifest.toolsVersion), + codeSignOnCopy: true, + removeHeadersOnCopy: true + ) + log(.debug, ".. added use of binary library '\(binaryTarget.path)'") + return + } + + if moduleDependency.type == .plugin { + let dependencyId = moduleDependency.pifTargetGUID() + pifTarget.addDependency( + on: dependencyId, + platformFilters: packageConditions + .toPlatformFilter(toolsVersion: package.manifest.toolsVersion), + linkProduct: false + ) + log(.debug, ".. added use of plugin target '\(dependencyId)'") + return + } + + // If this dependency is already present in the product's module target then don't re-add it. + if product.modules.contains(where: { $0.name == moduleDependency.name }) { return } + + // For executable targets, add a build time dependency on the product. + // FIXME: Maybe we should we do this at the libSwiftPM level. + if moduleDependency.isExecutable { + let mainModuleProducts = package.products.filter(\.isMainModuleProduct) + + if let product = moduleDependency + .productRepresentingDependencyOfBuildPlugin(in: mainModuleProducts) + { + pifTarget.addDependency( + on: product.pifTargetGUID(), + platformFilters: packageConditions + .toPlatformFilter(toolsVersion: package.manifest.toolsVersion), + linkProduct: false + ) + log(.debug, ".. added dependency on product '\(product.pifTargetGUID())'") + return + } else { + log( + .debug, + ".. could not find a build plugin product to depend on for target '\(product.pifTargetGUID()))'" + ) + } + } + + pifTarget.addDependency( + on: moduleDependency.pifTargetGUID(), + platformFilters: packageConditions.toPlatformFilter(toolsVersion: package.manifest.toolsVersion), + linkProduct: true + ) + log(.debug, ".. added linked dependency on target '\(moduleDependency.pifTargetGUID()))'") + + case .product(let productDependency, let packageConditions): + // Do not add a dependency for binary-only executable products since they are not part of the build. + if productDependency.isBinaryOnlyExecutableProduct { + return + } + + if !pifBuilder.delegate.shouldSuppressProductDependency( + product: productDependency.underlying, + buildSettings: &settings + ) { + let shouldLinkProduct = productDependency.isLinkable + pifTarget.addDependency( + on: productDependency.pifTargetGUID(), + platformFilters: packageConditions + .toPlatformFilter(toolsVersion: package.manifest.toolsVersion), + linkProduct: shouldLinkProduct + ) + log( + .debug, + ".. added \(shouldLinkProduct ? "linked" : "") dependency on product '\(productDependency.pifTargetGUID()))'" + ) + } + } + } + + // For *registry* packages, vend any registry release metadata to the build system. + if let metadata = package.registryMetadata, + let signature = metadata.signature, + let version = pifBuilder.packageDisplayVersion, + case RegistryReleaseMetadata.Source.registry(let url) = metadata.source + { + let signatureData = PackageRegistrySignature( + packageIdentity: package.identity.description, + packageVersion: version, + signature: signature, + libraryName: product.name, + source: .registry(url: url) + ) + + let encoder = PropertyListEncoder() + encoder.outputFormat = .xml + let data = try encoder.encode(signatureData) + settings.PACKAGE_REGISTRY_SIGNATURE = String(data: data, encoding: .utf8) + } + + pifTarget.addBuildConfig(name: "Debug", settings: settings) + pifTarget.addBuildConfig(name: "Release", settings: settings) + + // Collect linked binaries. + let linkedPackageBinaries = product.modules.compactMap { + PIFPackageBuilder.LinkedPackageBinary(module: $0, package: self.package) + } + + let moduleOrProductType: PIFPackageBuilder.ModuleOrProductType = switch product.libraryType { + case .dynamic: + pifBuilder.createDylibForDynamicProducts ? .dynamicLibrary : .framework + default: + .staticArchive + } + + return PIFPackageBuilder.ModuleOrProduct( + type: moduleOrProductType, + name: product.name, + moduleName: product.c99name, + pifTarget: pifTarget, + indexableFileURLs: [], + headerFiles: [], + linkedPackageBinaries: linkedPackageBinaries, + swiftLanguageVersion: nil, + declaredPlatforms: self.declaredPlatforms, + deploymentTargets: self.deploymentTargets + ) + } + + // MARK: - System Library Products + + mutating func makeSystemLibraryProduct(_ product: PackageGraph.ResolvedProduct) throws { + precondition(product.type == .library(.automatic)) + + let pifTarget = try self.pif.addTargetThrowing( + id: product.pifTargetGUID(), + productType: .packageProduct, + name: product.name, + productName: product.name + ) + + log( + .debug, + "created \(type(of: pifTarget)) '\(pifTarget.id)' of type '\(pifTarget.productType.asString)' " + + "with name '\(pifTarget.name)' and product name '\(pifTarget.productName)'" + ) + + let buildSettings = self.package.underlying.packageBaseBuildSettings + pifTarget.addBuildConfig(name: "Debug", settings: buildSettings) + pifTarget.addBuildConfig(name: "Release", settings: buildSettings) + + pifTarget.addDependency( + on: product.systemModule!.pifTargetGUID(), + platformFilters: [], + linkProduct: false + ) + + let systemLibrary = PIFPackageBuilder.ModuleOrProduct( + type: .staticArchive, + name: product.name, + moduleName: product.c99name, + pifTarget: pifTarget, + indexableFileURLs: [], + headerFiles: [], + linkedPackageBinaries: [], + swiftLanguageVersion: nil, + declaredPlatforms: self.declaredPlatforms, + deploymentTargets: self.deploymentTargets + ) + self.builtModulesAndProducts.append(systemLibrary) + } + + // MARK: - Plugin Product + + mutating func makePluginProduct(_ pluginProduct: PackageGraph.ResolvedProduct) throws { + precondition(pluginProduct.type == .plugin) + + let pluginPifTarget = self.pif.addAggregateTarget( + id: pluginProduct.pifTargetGUID(), + name: pluginProduct.name + ) + log(.debug, "created \(type(of: pluginPifTarget)) '\(pluginPifTarget.id)' with name '\(pluginPifTarget.name)'") + + let buildSettings: SwiftBuild.PIF.BuildSettings = package.underlying.packageBaseBuildSettings + pluginPifTarget.addBuildConfig(name: "Debug", settings: buildSettings) + pluginPifTarget.addBuildConfig(name: "Release", settings: buildSettings) + + for pluginModule in pluginProduct.pluginModules! { + pluginPifTarget.addDependency( + on: pluginModule.pifTargetGUID(), + platformFilters: [] + ) + } + + let pluginType: PIFPackageBuilder.ModuleOrProductType = { + if let pluginTarget = pluginProduct.pluginModules!.only { + switch pluginTarget.capability { + case .buildTool: + return .buildToolPlugin + case .command: + return .commandPlugin + } + } else { + assertionFailure( + "This should never be reached since there is always exactly one plugin target in a product by definition" + ) + return .commandPlugin + } + }() + + let pluginProductMetadata = PIFPackageBuilder.ModuleOrProduct( + type: pluginType, + name: pluginProduct.name, + moduleName: pluginProduct.c99name, + pifTarget: pluginPifTarget, + indexableFileURLs: [], + headerFiles: [], + linkedPackageBinaries: [], + swiftLanguageVersion: nil, + declaredPlatforms: self.declaredPlatforms, + deploymentTargets: self.deploymentTargets + ) + self.builtModulesAndProducts.append(pluginProductMetadata) + } +} + +// MARK: - Helper Types + +private struct PackageRegistrySignature: Encodable { + enum Source: Encodable { + case registry(url: URL) + } + + let packageIdentity: String + let packageVersion: String + let signature: RegistryReleaseMetadata.RegistrySignature + let libraryName: String + let source: Source + let formatVersion = 2 +} + +#endif diff --git a/Sources/SwiftBuildSupport/PackagePIFProjectBuilder.swift b/Sources/SwiftBuildSupport/PackagePIFProjectBuilder.swift new file mode 100644 index 00000000000..e41074799ef --- /dev/null +++ b/Sources/SwiftBuildSupport/PackagePIFProjectBuilder.swift @@ -0,0 +1,482 @@ +//===----------------------------------------------------------------------===// +// +// 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 Foundation + +import struct Basics.AbsolutePath +import struct Basics.Diagnostic +import class Basics.ObservabilitySystem +import struct Basics.SourceControlURL + +import class PackageModel.Manifest +import struct PackageModel.Platform +import class PackageModel.Product +import struct PackageModel.Resource +import struct PackageModel.ToolsVersion + +import struct PackageGraph.ModulesGraph +import struct PackageGraph.ResolvedModule +import struct PackageGraph.ResolvedPackage + +import struct PackageLoading.FileRuleDescription +import struct PackageLoading.TargetSourcesBuilder + +#if canImport(SwiftBuild) +import enum SwiftBuild.PIF +import struct SwiftBuild.SwiftBuildFileType + +/// Helper type to create PIF **project** and **targets** for a given package. +struct PackagePIFProjectBuilder { + let pifBuilder: PIFPackageBuilder + let package: PackageGraph.ResolvedPackage + let packageManifest: PackageModel.Manifest + let modulesGraph: PackageGraph.ModulesGraph + + let pif: SwiftBuild.PIF.Project + let binaryGroup: SwiftBuild.PIF.Group + let additionalFilesGroup: SwiftBuild.PIF.Group + + let declaredPlatforms: [PackageModel.Platform]? + let deploymentTargets: [PackageModel.Platform: String?] + + /// Current set of names of any package products that are explicitly declared dynamic libraries. + private let dynamicLibraryProductNames: Set + + /// FIXME: We should eventually clean this up but right now we have to carry over this + /// bit of information from processing the *products* to processing the *targets*. + var mainModuleTargetNamesWithResources: Set = [] + + var builtModulesAndProducts: [PIFPackageBuilder.ModuleOrProduct] + + func log( + _ severity: Diagnostic.Severity, + _ message: String, + sourceFile: StaticString = #fileID, + sourceLine: UInt = #line + ) { + self.pifBuilder.log(severity, message, sourceFile: sourceFile, sourceLine: sourceLine) + } + + init(createForPackage package: PackageGraph.ResolvedPackage, builder: PIFPackageBuilder) { + // Create a PIF project using an identifier that's based on the normalized absolute path of the package. + // We use the package manifest path as the project path, and the package path as the project's base source + // directory. + // FIXME: The PIF creation should ideally be done on a background thread. + let pifProject = SwiftBuild.PIF.Project( + id: "PACKAGE:\(package.identity)", + path: package.manifest.path.pathString, + projectDir: package.path.pathString, + name: package.name, + developmentRegion: package.manifest.defaultLocalization + ) + + let additionalFilesGroup = pifProject.mainGroup.addGroup( + path: "/", + pathBase: .absolute, + name: "AdditionalFiles" + ) + let binaryGroup = pifProject.mainGroup.addGroup(path: "/", pathBase: .absolute, name: "Binaries") + + // Test modules have a higher minimum deployment target by default, + // so we favor non-test modules as representative for the package's deployment target. + let firstModule = package.modules.first { $0.type != .test } ?? package.modules.first + + let moduleDeploymentTargets = firstModule?.deploymentTargets(using: builder.delegate) + + // The deployment targets are passed through to the eventual `ModuleOrProduct` values, + // so that querying them yields reasonable results for those build settings. + var deploymentTargets: [PackageModel.Platform: String?] = [ + .macOS: moduleDeploymentTargets?[.macOS], + .macCatalyst: moduleDeploymentTargets?[.macCatalyst], + .iOS: moduleDeploymentTargets?[.iOS], + .tvOS: moduleDeploymentTargets?[.tvOS], + .watchOS: moduleDeploymentTargets?[.watchOS], + .driverKit: moduleDeploymentTargets?[.driverKit], + ] + deploymentTargets[.visionOS] = moduleDeploymentTargets?[.visionOS] + let declaredPlatforms = firstModule?.declaredPlatforms + + // Compute the names of all explicitly dynamic library products, we need to avoid + // name clashes with any package targets we could decide to build dynamically. + let allPackages = builder.modulesGraph.packages + let dynamicLibraryProductNames = Set( + allPackages + .flatMap(\.products) + .filter { $0.type == .library(.dynamic) } + .map(\.name) + ) + + self.pifBuilder = builder + self.package = package + self.packageManifest = self.pifBuilder.packageManifest + self.modulesGraph = self.pifBuilder.modulesGraph + self.pif = pifProject + self.binaryGroup = binaryGroup + self.additionalFilesGroup = additionalFilesGroup + self.declaredPlatforms = declaredPlatforms + self.deploymentTargets = deploymentTargets + self.dynamicLibraryProductNames = dynamicLibraryProductNames + self.builtModulesAndProducts = [] + } + + // MARK: - Handling Resources + + func addResourceBundle( + for module: PackageGraph.ResolvedModule, + pifTarget: SwiftBuild.PIF.Target, + generatedResourceFiles: [String] + ) throws -> (PIFPackageBuilder.EmbedResourcesResult, PIFPackageBuilder.ModuleOrProduct?) { + if module.resources.isEmpty && generatedResourceFiles.isEmpty { + return (PIFPackageBuilder.EmbedResourcesResult( + bundleName: nil, + shouldGenerateBundleAccessor: false, + shouldGenerateEmbedInCodeAccessor: false + ), nil) + } + + let bundleName = self.resourceBundleName(forModuleName: module.name) + let resourcesTarget = try self.pif.addTargetThrowing( + id: self.pifTargetIdForResourceBundle(module.name), + productType: .bundle, + name: bundleName, + productName: bundleName + ) + + pifTarget.addDependency(on: resourcesTarget.id, platformFilters: [], linkProduct: false) + self.log(.debug, ".. added dependency on resource target '\(resourcesTarget.id)'") + + for pluginModule in module.pluginsAppliedToModule { + resourcesTarget.addDependency(on: pluginModule.pifTargetGUID(), linkProduct: false) + } + + self.log( + .debug, + ".. created \(type(of: resourcesTarget)) '\(resourcesTarget.id)' of type '\(resourcesTarget.productType.asString)' with name '\(resourcesTarget.name)' and product name '\(resourcesTarget.productName)'" + ) + + var settings: SwiftBuild.PIF.BuildSettings = self.package.underlying.packageBaseBuildSettings + settings.TARGET_NAME = bundleName + settings.PRODUCT_NAME = "$(TARGET_NAME)" + settings.PRODUCT_MODULE_NAME = bundleName + settings.PRODUCT_BUNDLE_IDENTIFIER = "\(self.package.identity).\(module.name).resources" + .spm_mangledToBundleIdentifier() + settings.EXECUTABLE_NAME = "" + settings.GENERATE_INFOPLIST_FILE = "YES" + settings.PACKAGE_RESOURCE_TARGET_KIND = "resource" + + settings.COREML_COMPILER_CONTAINER = "swift-package" + settings.COREML_CODEGEN_LANGUAGE = "None" + + resourcesTarget.addBuildConfig(name: "Debug", settings: settings) + resourcesTarget.addBuildConfig(name: "Release", settings: settings) + + let result = self.processResources( + for: module, + sourceModulePifTarget: pifTarget, + resourceBundlePifTarget: resourcesTarget, + generatedResourceFiles: generatedResourceFiles + ) + + let resourceBundle = PIFPackageBuilder.ModuleOrProduct( + type: .resourceBundle, + name: bundleName, + moduleName: bundleName, + pifTarget: resourcesTarget, + indexableFileURLs: [], + headerFiles: [], + linkedPackageBinaries: [], + swiftLanguageVersion: nil, + declaredPlatforms: [], + deploymentTargets: [:] + ) + + return (result, resourceBundle) + } + + func processResources( + for module: PackageGraph.ResolvedModule, + sourceModulePifTarget: SwiftBuild.PIF.Target, + resourceBundlePifTarget: SwiftBuild.PIF.Target?, + generatedResourceFiles: [String] + ) -> PIFPackageBuilder.EmbedResourcesResult { + if module.resources.isEmpty && generatedResourceFiles.isEmpty { + return PIFPackageBuilder.EmbedResourcesResult( + bundleName: nil, + shouldGenerateBundleAccessor: false, + shouldGenerateEmbedInCodeAccessor: false + ) + } + // If `resourceBundlePifTarget` is nil, we add resources to the `sourceModulePifTarget`. + let pifTargetForResources = resourceBundlePifTarget ?? sourceModulePifTarget + + // Generated resources get a default treatment for rule and localization. + let generatedResources = generatedResourceFiles.compactMap { + PIFPackageBuilder.Resource(path: $0, rule: .process(localization: nil)) + } + + let resources = module.resources.map { PIFPackageBuilder.Resource($0) } + generatedResources + let shouldGenerateBundleAccessor = resources.anySatisfy { $0.rule != .embedInCode } + let shouldGenerateEmbedInCodeAccessor = resources.anySatisfy { $0.rule == .embedInCode } + + for resource in resources { + let resourcePath = resource.path + // Add a file reference for the resource. We use an absolute path, as for all the other files, + // but we should be able to optimize this later by making it group-relative. + let ref = self.pif.mainGroup.addFileReference( + path: resourcePath, pathBase: .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. + let isCoreDataFile = [SwiftBuild.SwiftBuildFileType.xcdatamodeld, .xcdatamodel] + .contains { $0.fileTypes.contains(resourcePath.pathExtension) } + + if isCoreDataFile { + sourceModulePifTarget.addSourceFile(ref: ref) + self.log(.debug, ".. .. added core data resource as source file '\(resourcePath)'") + } + + // Core ML files need to be included in the source module as well, because there is code generation. + let coreMLFileTypes: [SwiftBuild.SwiftBuildFileType] = [.mlmodel, .mlpackage] + let isCoreMLFile = coreMLFileTypes.contains { $0.fileTypes.contains(resourcePath.pathExtension) } + + if isCoreMLFile { + sourceModulePifTarget.addSourceFile(ref: ref, generatedCodeVisibility: .public) + self.log(.debug, ".. .. added coreml resource as source file '\(resourcePath)'") + } + + // Metal source code needs to be added to the source build phase. + let isMetalFile = SwiftBuild.SwiftBuildFileType.metal.fileTypes.contains(resourcePath.pathExtension) + + if isMetalFile { + pifTargetForResources.addSourceFile(ref: ref) + } else { + // FIXME: Handle additional rules here (e.g. `.copy`). + pifTargetForResources.addResourceFile( + ref: ref, + platformFilters: [], + resourceRule: resource.rule == .embedInCode ? .embedInCode : .process + ) + } + + // Asset Catalogs need to be included in the sources modules for generated asset symbols. + let isAssetCatalog = resourcePath.pathExtension == "xcassets" + if isAssetCatalog { + sourceModulePifTarget.addSourceFile(ref: ref) + self.log(.debug, ".. .. added asset catalog as source file '\(resourcePath)'") + } + + self.log(.debug, ".. .. added resource file '\(resourcePath)'") + } + + return PIFPackageBuilder.EmbedResourcesResult( + bundleName: resourceBundlePifTarget?.name, + shouldGenerateBundleAccessor: shouldGenerateBundleAccessor, + shouldGenerateEmbedInCodeAccessor: shouldGenerateEmbedInCodeAccessor + ) + } + + func resourceBundleTarget(forModuleName name: String) -> SwiftBuild.PIF.Target? { + let resourceBundleGUID = self.pifTargetIdForResourceBundle(name) + let target = self.pif.targets.only { $0.id == resourceBundleGUID } as? SwiftBuild.PIF.Target + return target + } + + func pifTargetIdForResourceBundle(_ name: String) -> String { + "PACKAGE-RESOURCE:\(name)" + } + + func resourceBundleName(forModuleName name: String) -> String { + "\(self.package.name)_\(name)" + } + + // MARK: - Plugin Helpers + + /// Helper function that compiles the plugin-generated files for a target, + /// optionally also adding the corresponding plugin-provided commands to the PIF target. + /// + /// The reason we might not add them is that some targets are derivatives of other targets — in such cases, + /// only the primary target adds the build tool commands to the PIF target. + func computePluginGeneratedFiles( + module: PackageGraph.ResolvedModule, + pifTarget: SwiftBuild.PIF.Target, + addBuildToolPluginCommands: Bool + ) -> (sourceFilePaths: [AbsolutePath], resourceFilePaths: [String]) { + guard let pluginResult = pifBuilder.buildToolPluginResultsByTargetName[module.name] else { + // We found no results for the target. + return (sourceFilePaths: [], resourceFilePaths: []) + } + + // Process the results of applying any build tool plugins on the target. + // If we've been asked to add build tool commands for the result, we do so now. + if addBuildToolPluginCommands { + for command in pluginResult.buildCommands { + self.addBuildToolCommand(command, to: pifTarget) + } + } + + // Process all the paths of derived output paths using the same rules as for source. + let result = self.process( + pluginGeneratedFilePaths: pluginResult.allDerivedOutputPaths, + forModule: module, + toolsVersion: self.package.manifest.toolsVersion + ) + return ( + sourceFilePaths: result.sourceFilePaths, + resourceFilePaths: result.resourceFilePaths.map(\.path.pathString) + ) + } + + /// Helper function for adding build tool commands to the right PIF target depending on whether they generate + /// sources or resources. + func addBuildToolCommands( + module: PackageGraph.ResolvedModule, + sourceModulePifTarget: SwiftBuild.PIF.Target, + resourceBundlePifTarget: SwiftBuild.PIF.Target, + sourceFilePaths: [AbsolutePath], + resourceFilePaths: [String] + ) { + guard let pluginResult = pifBuilder.buildToolPluginResultsByTargetName[module.name] else { + return + } + + for command in pluginResult.buildCommands { + let producesResources = Set(command.outputPaths).intersection(resourceFilePaths).hasContent + + if producesResources { + self.addBuildToolCommand(command, to: resourceBundlePifTarget) + } else { + self.addBuildToolCommand(command, to: sourceModulePifTarget) + } + } + } + + /// Adds build rules to `pifTarget` for any build tool commands from invocation results. + /// Returns the absolute paths of any generated source files that should be added to the sources build phase of the + /// PIF target. + func addBuildToolCommands( + from pluginInvocationResults: [PIFPackageBuilder.BuildToolPluginInvocationResult], + pifTarget: SwiftBuild.PIF.Target, + addBuildToolPluginCommands: Bool + ) -> [String] { + var generatedSourceFileAbsPaths: [String] = [] + for result in pluginInvocationResults { + // Create build rules for all the commands in the result. + if addBuildToolPluginCommands { + for command in result.buildCommands { + self.addBuildToolCommand(command, to: pifTarget) + } + } + // Add the paths of the generated source files, so that they can be added to the Sources build phase. + generatedSourceFileAbsPaths.append(contentsOf: result.allDerivedOutputPaths.map(\.pathString)) + } + return generatedSourceFileAbsPaths + } + + /// Adds a single plugin-created build command to a PIF target. + func addBuildToolCommand( + _ command: PIFPackageBuilder.CustomBuildCommand, + to pifTarget: SwiftBuild.PIF.Target + ) { + var commandLine = [command.executable] + command.arguments + if let sandbox = command.sandboxProfile, !pifBuilder.delegate.isPluginExecutionSandboxingDisabled { + commandLine = try! sandbox.apply(to: commandLine) + } + + pifTarget.customTasks.append( + SwiftBuild.PIF.CustomTask( + commandLine: commandLine, + environment: command.environment.map { ($0, $1) }.sorted(by: <), + workingDirectory: command.workingDir?.pathString, + executionDescription: command.displayName ?? "Performing build tool plugin command", + inputFilePaths: [command.executable] + command.inputPaths.map(\.pathString), + outputFilePaths: command.outputPaths, + enableSandboxing: false, + preparesForIndexing: true + ) + ) + } + + /// Processes the paths of plugin-generated files for a particular package target, + /// returning paths of those that should be treated as sources vs resources. + private func process( + pluginGeneratedFilePaths: [AbsolutePath], + forModule module: PackageGraph.ResolvedModule, + toolsVersion: PackageModel.ToolsVersion? + ) -> (sourceFilePaths: [AbsolutePath], resourceFilePaths: [Resource]) { + precondition(module.isSourceModule) + + // If we have no tools version, all files are treated as *source* files. + guard let toolsVersion else { + return (sourceFilePaths: pluginGeneratedFilePaths, resourceFilePaths: []) + } + + // FIXME: Will be fixed by (SwiftPM PIFBuilder — adopt ObservabilityScope as the logging API). + let observabilityScope = ObservabilitySystem.NOOP + + // Use the `TargetSourcesBuilder` from libSwiftPM to split the generated files into sources and resources. + let (generatedSourcePaths, generatedResourcePaths) = TargetSourcesBuilder.computeContents( + for: pluginGeneratedFilePaths, + toolsVersion: toolsVersion, + additionalFileRules: Self.additionalFileRules, + defaultLocalization: module.defaultLocalization, + targetName: module.name, + targetPath: module.path, + observabilityScope: observabilityScope + ) + + // FIXME: We are not handling resource rules here, but the same is true for non-generated resources. + // (Today, everything gets essentially treated as `.processResource` even if it may have been declared as + // `.copy` in the manifest.) + return (generatedSourcePaths, generatedResourcePaths) + } + + private static let additionalFileRules: [FileRuleDescription] = + FileRuleDescription.xcbuildFileTypes + [ + FileRuleDescription( + rule: .compile, + toolsVersion: .v5_5, + fileTypes: ["docc"] + ), + FileRuleDescription( + rule: .processResource(localization: .none), + toolsVersion: .v5_7, + fileTypes: ["mlmodel", "mlpackage"] + ), + FileRuleDescription( + rule: .processResource(localization: .none), + toolsVersion: .v5_7, + fileTypes: ["rkassets"] // visionOS + ), + ] + + // MARK: - General Helpers + + func installPath(for product: PackageModel.Product) -> String { + if let customInstallPath = pifBuilder.delegate.customInstallPath(product: product) { + customInstallPath + } else { + "/usr/local/lib" + } + } + + /// Always create a dynamic variant for targets, for automatic resolution of diamond problems, + /// unless there is a potential name clash with an explicitly *dynamic library* product. + /// + /// Swift Build will emit a diagnostic if such a package target is part of a diamond. + func shouldOfferDynamicTarget(_ targetName: String) -> Bool { + !self.dynamicLibraryProductNames.contains(targetName) + } +} + +#endif