diff --git a/Package.swift b/Package.swift index 81a12a2f..63989112 100644 --- a/Package.swift +++ b/Package.swift @@ -192,7 +192,11 @@ let package = Package( swiftSettings: swiftSettings(languageMode: .v6)), .target( name: "SWBTaskConstruction", - dependencies: ["SWBCore", "SWBUtil"], + dependencies: [ + "SWBCore", + "SWBUtil", + .product(name: "SwiftDriver", package: "swift-driver") + ], exclude: ["CMakeLists.txt"], swiftSettings: swiftSettings(languageMode: .v5)), .target( @@ -206,6 +210,7 @@ let package = Package( "SWBCSupport", "SWBLibc", .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "SwiftDriver", package: "swift-driver"), .product(name: "SystemPackage", package: "swift-system", condition: .when(platforms: [.linux, .openbsd, .android, .windows, .custom("freebsd")])), ], exclude: ["CMakeLists.txt"], diff --git a/Sources/SWBApplePlatform/AppIntentsMetadataCompiler.swift b/Sources/SWBApplePlatform/AppIntentsMetadataCompiler.swift index ec4eed43..76bc843c 100644 --- a/Sources/SWBApplePlatform/AppIntentsMetadataCompiler.swift +++ b/Sources/SWBApplePlatform/AppIntentsMetadataCompiler.swift @@ -122,7 +122,7 @@ final public class AppIntentsMetadataCompilerSpec: GenericCommandLineToolSpec, S for variant in buildVariants { let scope = cbc.scope.subscope(binding: BuiltinMacros.variantCondition, to: variant) for arch in archs { - let scope = scope.subscope(binding: BuiltinMacros.archCondition, to: arch) + let scope = scope.subscopeBindingArchAndTriple(arch: arch) let dependencyInfoFile = scope.evaluate(BuiltinMacros.LD_DEPENDENCY_INFO_FILE) let libtoolDependencyInfo = scope.evaluate(BuiltinMacros.LIBTOOL_DEPENDENCY_INFO_FILE) if !isStaticLibrary && !dependencyInfoFile.isEmpty { diff --git a/Sources/SWBCore/ArtifactBundleMetadata.swift b/Sources/SWBCore/ArtifactBundleMetadata.swift new file mode 100644 index 00000000..5a9616e9 --- /dev/null +++ b/Sources/SWBCore/ArtifactBundleMetadata.swift @@ -0,0 +1,86 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +package import SWBUtil +import Foundation + +package struct ArtifactBundleInfo: Hashable { + package let bundlePath: Path + package let metadata: ArtifactBundleMetadata + + package init(bundlePath: Path, metadata: ArtifactBundleMetadata) { + self.bundlePath = bundlePath + self.metadata = metadata + } +} + +package struct ArtifactBundleMetadata: Sendable, Hashable, Decodable { + package let schemaVersion: String + package let artifacts: [String: ArtifactMetadata] + + package struct ArtifactMetadata: Sendable, Hashable, Decodable { + package let type: ArtifactType + package let version: String + package let variants: [VariantMetadata] + } + + package enum ArtifactType: String, Sendable, Decodable { + case executable + case staticLibrary + case swiftSDK + case crossCompilationDestination + } + + package struct VariantMetadata: Sendable, Hashable, Decodable { + package let path: Path + package let supportedTriples: [String]? + package let staticLibraryMetadata: StaticLibraryMetadata? + } + + package struct StaticLibraryMetadata: Sendable, Hashable, Decodable { + package let headerPaths: [Path] + package let moduleMapPath: Path? + } + + package static func parse( + at bundlePath: Path, + fileSystem: any FSProxy + ) throws -> ArtifactBundleMetadata { + let infoPath = bundlePath.join("info.json") + + guard fileSystem.exists(infoPath) else { + throw StubError.error("artifact bundle info.json not found at '\(infoPath.str)'") + } + + do { + let bytes = try fileSystem.read(infoPath) + let data = Data(bytes.bytes) + let decoder = JSONDecoder() + let metadata = try decoder.decode(ArtifactBundleMetadata.self, from: data) + + guard let version = try? Version(metadata.schemaVersion) else { + throw StubError.error("invalid schema version '\(metadata.schemaVersion)' in '\(infoPath.str)'") + } + + switch version { + case Version(1, 2), Version(1, 1), Version(1, 0): + break + default: + throw StubError.error("invalid `schemaVersion` of bundle manifest at '\(infoPath)': \(version)") + } + + return metadata + } catch { + throw StubError.error("failed to parse ArtifactBundle info.json at '\(infoPath.str)': \(error.localizedDescription)") + } + } +} diff --git a/Sources/SWBCore/BuildRequestContext.swift b/Sources/SWBCore/BuildRequestContext.swift index b7e3a3a3..d971d45c 100644 --- a/Sources/SWBCore/BuildRequestContext.swift +++ b/Sources/SWBCore/BuildRequestContext.swift @@ -44,25 +44,25 @@ public final class BuildRequestContext: Sendable { } /// Get the cached settings for the given parameters, without considering the context of any project/target. - public func getCachedSettings(_ parameters: BuildParameters) -> Settings { + package func getCachedSettings(_ parameters: BuildParameters) -> Settings { workspaceContext.workspaceSettingsCache.getCachedSettings(parameters, buildRequestContext: self, purpose: .build, filesSignature: filesSignature(for:)) } /// Get the cached settings for the given parameters and project. - public func getCachedSettings(_ parameters: BuildParameters, project: Project, purpose: SettingsPurpose = .build, provisioningTaskInputs: ProvisioningTaskInputs? = nil, impartedBuildProperties: [ImpartedBuildProperties]? = nil) -> Settings { - getCachedSettings(parameters, project: project, target: nil, purpose: purpose, provisioningTaskInputs: provisioningTaskInputs, impartedBuildProperties: impartedBuildProperties) + package func getCachedSettings(_ parameters: BuildParameters, project: Project, purpose: SettingsPurpose = .build, provisioningTaskInputs: ProvisioningTaskInputs? = nil) -> Settings { + getCachedSettings(parameters, project: project, target: nil, purpose: purpose, provisioningTaskInputs: provisioningTaskInputs, impartedBuildProperties: nil, artifactBundleInfo: nil) } /// Get the cached settings for the given parameters and target. - public func getCachedSettings(_ parameters: BuildParameters, target: Target, purpose: SettingsPurpose = .build, provisioningTaskInputs: ProvisioningTaskInputs? = nil, impartedBuildProperties: [ImpartedBuildProperties]? = nil) -> Settings { - getCachedSettings(parameters, project: workspaceContext.workspace.project(for: target), target: target, purpose: purpose, provisioningTaskInputs: provisioningTaskInputs, impartedBuildProperties: impartedBuildProperties) + package func getCachedSettings(_ parameters: BuildParameters, target: Target, purpose: SettingsPurpose = .build, provisioningTaskInputs: ProvisioningTaskInputs? = nil, impartedBuildProperties: [ImpartedBuildProperties]? = nil, artifactBundleInfo: [ArtifactBundleInfo]? = nil) -> Settings { + getCachedSettings(parameters, project: workspaceContext.workspace.project(for: target), target: target, purpose: purpose, provisioningTaskInputs: provisioningTaskInputs, impartedBuildProperties: impartedBuildProperties, artifactBundleInfo: artifactBundleInfo) } /// Private method to get the cached settings for the given parameters, project, and target. /// /// - remark: This is private so that clients don't somehow call this with a project which doesn't match the target. There are public methods covering this one. - private func getCachedSettings(_ parameters: BuildParameters, project: Project, target: Target?, purpose: SettingsPurpose = .build, provisioningTaskInputs: ProvisioningTaskInputs?, impartedBuildProperties: [ImpartedBuildProperties]?) -> Settings { - workspaceContext.workspaceSettingsCache.getCachedSettings(parameters, project: project, target: target, purpose: purpose, provisioningTaskInputs: provisioningTaskInputs, impartedBuildProperties: impartedBuildProperties, buildRequestContext: self, filesSignature: filesSignature(for:)) + private func getCachedSettings(_ parameters: BuildParameters, project: Project, target: Target?, purpose: SettingsPurpose = .build, provisioningTaskInputs: ProvisioningTaskInputs?, impartedBuildProperties: [ImpartedBuildProperties]?, artifactBundleInfo: [ArtifactBundleInfo]?) -> Settings { + workspaceContext.workspaceSettingsCache.getCachedSettings(parameters, project: project, target: target, purpose: purpose, provisioningTaskInputs: provisioningTaskInputs, impartedBuildProperties: impartedBuildProperties, artifactBundleInfo: artifactBundleInfo, buildRequestContext: self, filesSignature: filesSignature(for:)) } @_spi(Testing) public func getCachedMacroConfigFile(_ path: Path, project: Project? = nil, context: MacroConfigLoadContext) -> MacroConfigInfo { diff --git a/Sources/SWBCore/CMakeLists.txt b/Sources/SWBCore/CMakeLists.txt index 34551392..2255f5dc 100644 --- a/Sources/SWBCore/CMakeLists.txt +++ b/Sources/SWBCore/CMakeLists.txt @@ -12,6 +12,7 @@ add_library(SWBCore ActivityReporting.swift Apple/DeviceFamily.swift Apple/InterfaceBuilderShared.swift + ArtifactBundleMetadata.swift BuildDependencyInfo.swift BuildFileFilteringContext.swift BuildFileResolution.swift diff --git a/Sources/SWBCore/MacroEvaluationExtensions.swift b/Sources/SWBCore/MacroEvaluationExtensions.swift index 2522d300..db4c6c09 100644 --- a/Sources/SWBCore/MacroEvaluationExtensions.swift +++ b/Sources/SWBCore/MacroEvaluationExtensions.swift @@ -126,4 +126,10 @@ extension MacroEvaluationScope { return ($0 == BuiltinMacros.TARGET_BUILD_SUBPATH) ? self.table.namespace.parseLiteralString("") : nil }) } + + public func subscopeBindingArchAndTriple(arch: String) -> MacroEvaluationScope { + let archScope = subscope(binding: BuiltinMacros.archCondition, to: arch) + let swiftTargetTriple = archScope.evaluate(BuiltinMacros.SWIFT_TARGET_TRIPLE) + return archScope.subscope(binding: BuiltinMacros.normalizedUnversionedTripleCondition, to: swiftTargetTriple) + } } diff --git a/Sources/SWBCore/Settings/BuiltinMacros.swift b/Sources/SWBCore/Settings/BuiltinMacros.swift index bbda11b6..fd2a3d84 100644 --- a/Sources/SWBCore/Settings/BuiltinMacros.swift +++ b/Sources/SWBCore/Settings/BuiltinMacros.swift @@ -25,8 +25,9 @@ public final class BuiltinMacros { public static let platformCondition = BuiltinMacros.declareConditionParameter("__platform_filter") public static let sdkBuildVersionCondition = BuiltinMacros.declareConditionParameter("_sdk_build_version") public static let targetNameCondition = BuiltinMacros.declareConditionParameter("target") + public static let normalizedUnversionedTripleCondition = BuiltinMacros.declareConditionParameter("__normalized_unversioned_triple") - private static let allBuiltinConditionParameters = [archCondition, sdkCondition, variantCondition, configurationCondition, platformCondition, sdkBuildVersionCondition, targetNameCondition] + private static let allBuiltinConditionParameters = [archCondition, sdkCondition, variantCondition, configurationCondition, platformCondition, sdkBuildVersionCondition, targetNameCondition, normalizedUnversionedTripleCondition] // MARK: Built-in Macro Definitions diff --git a/Sources/SWBCore/Settings/Settings.swift b/Sources/SWBCore/Settings/Settings.swift index 2739ddf1..2cf6e245 100644 --- a/Sources/SWBCore/Settings/Settings.swift +++ b/Sources/SWBCore/Settings/Settings.swift @@ -811,12 +811,12 @@ public final class Settings: PlatformBuildContext, Sendable { /// The information about the project model components from which these settings were constructed. public let constructionComponents: ConstructionComponents - public convenience init(workspaceContext: WorkspaceContext, buildRequestContext: BuildRequestContext, parameters: BuildParameters, project: Project, target: Target? = nil, purpose: SettingsPurpose = .build, provisioningTaskInputs: ProvisioningTaskInputs? = nil, impartedBuildProperties: [ImpartedBuildProperties]? = nil, includeExports: Bool = true, sdkRegistry: (any SDKRegistryLookup)? = nil) { - self.init(workspaceContext: workspaceContext, buildRequestContext: buildRequestContext, parameters: parameters, settingsContext: SettingsContext(purpose, project: project, target: target), purpose: purpose, provisioningTaskInputs: provisioningTaskInputs, impartedBuildProperties: impartedBuildProperties, includeExports: includeExports, sdkRegistry: sdkRegistry) + package convenience init(workspaceContext: WorkspaceContext, buildRequestContext: BuildRequestContext, parameters: BuildParameters, project: Project, target: Target? = nil, purpose: SettingsPurpose = .build, provisioningTaskInputs: ProvisioningTaskInputs? = nil, impartedBuildProperties: [ImpartedBuildProperties]? = nil, artifactBundleInfo: [ArtifactBundleInfo]? = nil, includeExports: Bool = true, sdkRegistry: (any SDKRegistryLookup)? = nil) { + self.init(workspaceContext: workspaceContext, buildRequestContext: buildRequestContext, parameters: parameters, settingsContext: SettingsContext(purpose, project: project, target: target), purpose: purpose, provisioningTaskInputs: provisioningTaskInputs, impartedBuildProperties: impartedBuildProperties, artifactBundleInfo: artifactBundleInfo, includeExports: includeExports, sdkRegistry: sdkRegistry) } /// Construct the settings for a project and optionally a target. - public init(workspaceContext: WorkspaceContext, buildRequestContext: BuildRequestContext, parameters: BuildParameters, settingsContext: SettingsContext, purpose: SettingsPurpose = .build, provisioningTaskInputs: ProvisioningTaskInputs? = nil, impartedBuildProperties: [ImpartedBuildProperties]? = nil, includeExports: Bool = true, sdkRegistry: (any SDKRegistryLookup)? = nil) { + package init(workspaceContext: WorkspaceContext, buildRequestContext: BuildRequestContext, parameters: BuildParameters, settingsContext: SettingsContext, purpose: SettingsPurpose = .build, provisioningTaskInputs: ProvisioningTaskInputs? = nil, impartedBuildProperties: [ImpartedBuildProperties]? = nil, artifactBundleInfo: [ArtifactBundleInfo]? = nil, includeExports: Bool = true, sdkRegistry: (any SDKRegistryLookup)? = nil) { if let target = settingsContext.target { precondition(workspaceContext.workspace.project(for: target) === settingsContext.project) } @@ -825,8 +825,8 @@ public final class Settings: PlatformBuildContext, Sendable { self.settingsContext = settingsContext // Construct the settings table. - let builder = SettingsBuilder(workspaceContext, buildRequestContext, parameters, settingsContext, provisioningTaskInputs, includeExports: includeExports, sdkRegistry) - let (boundProperties, boundDeploymentTarget) = MacroNamespace.withExpressionInterningEnabled{ builder.construct(impartedBuildProperties) } + let builder = SettingsBuilder(workspaceContext, buildRequestContext, parameters, settingsContext, provisioningTaskInputs, impartedBuildProperties, artifactBundleInfo, includeExports: includeExports, sdkRegistry) + let (boundProperties, boundDeploymentTarget) = MacroNamespace.withExpressionInterningEnabled{ builder.construct() } // Extract the constructed data. self.targetConfiguration = builder.targetConfiguration @@ -1241,6 +1241,8 @@ private class SettingsBuilder { } let provisioningTaskInputs: ProvisioningTaskInputs? + let impartedBuildProperties: [ImpartedBuildProperties]? + let artifactBundleInfo: [ArtifactBundleInfo]? /// Whether this builder was constructed specifically for binding properties (versus for general table construction). let forBindingProperties: Bool @@ -1350,13 +1352,15 @@ private class SettingsBuilder { ) } - init(_ workspaceContext: WorkspaceContext, _ buildRequestContext: BuildRequestContext, _ parameters: BuildParameters, _ settingsContext: SettingsContext, _ provisioningTaskInputs: ProvisioningTaskInputs? = nil, includeExports: Bool = true, forBindingProperties: Bool = false, _ sdkRegistry: (any SDKRegistryLookup)?) { + init(_ workspaceContext: WorkspaceContext, _ buildRequestContext: BuildRequestContext, _ parameters: BuildParameters, _ settingsContext: SettingsContext, _ provisioningTaskInputs: ProvisioningTaskInputs? = nil, _ impartedBuildProperties: [ImpartedBuildProperties]? = nil, _ artifactBundleInfo: [ArtifactBundleInfo]? = nil, includeExports: Bool = true, forBindingProperties: Bool = false, _ sdkRegistry: (any SDKRegistryLookup)?) { self.workspaceContext = workspaceContext self.buildRequestContext = buildRequestContext self.sdkRegistry = sdkRegistry ?? workspaceContext.sdkRegistry self.parameters = parameters self.settingsContext = settingsContext self.provisioningTaskInputs = provisioningTaskInputs + self.impartedBuildProperties = impartedBuildProperties + self.artifactBundleInfo = artifactBundleInfo // FIXME: We should almost certainly not be creating a namespace here, but instead should use an already bound one. self.userNamespace = MacroNamespace(parent: workspaceContext.workspace.userNamespace, debugDescription: "settings") self._table = MacroValueAssignmentTable(namespace: userNamespace) @@ -1368,7 +1372,7 @@ private class SettingsBuilder { // MARK: Settings construction /// Construct the settings data. - func construct(_ impartedBuildProperties: [ImpartedBuildProperties]? = nil) -> (BoundProperties, BoundDeploymentTarget) { + func construct() -> (BoundProperties, BoundDeploymentTarget) { precondition(!forBindingProperties) // Compute the effective configurations to build with. @@ -1576,6 +1580,52 @@ private class SettingsBuilder { } } + for artifactBundle in artifactBundleInfo ?? [] { + pushTable(.exported) { table in + for artifact in artifactBundle.metadata.artifacts.values { + switch artifact.type { + case .staticLibrary: + for variant in artifact.variants { + guard let staticLibraryMetadata = variant.staticLibraryMetadata else { + continue + } + var conditionSets: [MacroConditionSet?] = [] + if let triples = variant.supportedTriples { + for triple in triples { + conditionSets.append(.init(conditions: [.init(parameter: BuiltinMacros.normalizedUnversionedTripleCondition, valuePattern: triple)])) + } + } else { + conditionSets.append(nil) + } + for conditionSet in conditionSets { + if !staticLibraryMetadata.headerPaths.isEmpty { + table.push( + BuiltinMacros.HEADER_SEARCH_PATHS, + BuiltinMacros.namespace.parseStringList(["$(inherited)"] + staticLibraryMetadata.headerPaths.map { artifactBundle.bundlePath.join($0).str }), + conditions: conditionSet + ) + } + if let moduleMapPath = staticLibraryMetadata.moduleMapPath { + table.push( + BuiltinMacros.OTHER_CFLAGS, + BuiltinMacros.namespace.parseStringList(["$(inherited)", "-fmodule-map-file=\(artifactBundle.bundlePath.join(moduleMapPath).str)"]), + conditions: conditionSet + ) + table.push( + BuiltinMacros.OTHER_SWIFT_FLAGS, + BuiltinMacros.namespace.parseStringList(["$(inherited)", "-Xcc", "-fmodule-map-file=\(artifactBundle.bundlePath.join(moduleMapPath).str)"]), + conditions: conditionSet + ) + } + } + } + case .crossCompilationDestination, .swiftSDK, .executable: + break + } + } + } + } + if scope.evaluate(BuiltinMacros.ENABLE_PROJECT_OVERRIDE_SPECS), let projectOverrideSpec = core.specRegistry.findSpecs(ProjectOverridesSpec.self, domain: "").filter({ spec in spec.projectName == (scope.evaluate(BuiltinMacros.RC_ProjectName).nilIfEmpty ?? scope.evaluate(BuiltinMacros.SRCROOT).basename) }).only { @@ -4609,7 +4659,7 @@ private class SettingsBuilder { for variant in buildVariants { let scope = scope.subscope(binding: BuiltinMacros.variantCondition, to: variant) for arch in archs { - let scope = scope.subscope(binding: BuiltinMacros.archCondition, to: arch) + let scope = scope.subscopeBindingArchAndTriple(arch: arch) let originalValues = scope.evaluate(macro) processRemapping(scope, originalValues.map{ Path($0) }, variant: variant, arch: arch) } diff --git a/Sources/SWBCore/SpecImplementations/Tools/TAPISymbolExtractor.swift b/Sources/SWBCore/SpecImplementations/Tools/TAPISymbolExtractor.swift index dfbeb75e..6ed9acc8 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/TAPISymbolExtractor.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/TAPISymbolExtractor.swift @@ -451,7 +451,7 @@ final public class TAPISymbolExtractor: GenericCompilerSpec, GCCCompatibleCompil var paths: [Path] = [] let archSpecificSubScopes = cbc.scope.evaluate(BuiltinMacros.ARCHS).map { arch in - return cbc.scope.subscope(binding: BuiltinMacros.archCondition, to: arch) + cbc.scope.subscopeBindingArchAndTriple(arch: arch) } for subScope in archSpecificSubScopes { diff --git a/Sources/SWBCore/WorkspaceSettingsCache.swift b/Sources/SWBCore/WorkspaceSettingsCache.swift index 1761abba..67f54c31 100644 --- a/Sources/SWBCore/WorkspaceSettingsCache.swift +++ b/Sources/SWBCore/WorkspaceSettingsCache.swift @@ -49,6 +49,9 @@ final class WorkspaceSettingsCache: Sendable { /// Additional properties imparted by dependencies. let impartedBuildProperties: [ImpartedBuildProperties]? + // Information about consumed artifact bundles + let artifactBundleInfo: [ArtifactBundleInfo]? + // Using just this instead of all of `impartedBuildProperties` for equality should be fine, because we should only be seeing the same // `impartedBuildProperties` each time when looking up cached settings. private var impartedMacroDeclarations: [[MacroDeclaration]]? { @@ -56,7 +59,7 @@ final class WorkspaceSettingsCache: Sendable { } static func == (lhs: SettingsCacheKey, rhs: SettingsCacheKey) -> Bool { - return lhs.parameters == rhs.parameters && lhs.projectGUID == rhs.projectGUID && lhs.targetGUID == rhs.targetGUID && lhs.purpose == rhs.purpose && lhs.provisioningTaskInputs == rhs.provisioningTaskInputs && lhs.impartedMacroDeclarations == rhs.impartedMacroDeclarations + return lhs.parameters == rhs.parameters && lhs.projectGUID == rhs.projectGUID && lhs.targetGUID == rhs.targetGUID && lhs.purpose == rhs.purpose && lhs.provisioningTaskInputs == rhs.provisioningTaskInputs && lhs.impartedMacroDeclarations == rhs.impartedMacroDeclarations && lhs.artifactBundleInfo == rhs.artifactBundleInfo } func hash(into hasher: inout Hasher) { @@ -66,12 +69,13 @@ final class WorkspaceSettingsCache: Sendable { hasher.combine(provisioningTaskInputs) hasher.combine(purpose) hasher.combine(impartedMacroDeclarations) + hasher.combine(artifactBundleInfo) } } /// Get the cached settings for the given parameters, without considering the context of any project/target. public func getCachedSettings(_ parameters: BuildParameters, buildRequestContext: BuildRequestContext, purpose: SettingsPurpose, filesSignature: ([Path]) -> FilesSignature) -> Settings { - let key = SettingsCacheKey(parameters: parameters, projectGUID: nil, targetGUID: nil, purpose: purpose, provisioningTaskInputs: nil, impartedBuildProperties: nil) + let key = SettingsCacheKey(parameters: parameters, projectGUID: nil, targetGUID: nil, purpose: purpose, provisioningTaskInputs: nil, impartedBuildProperties: nil, artifactBundleInfo: nil) // Check if there were any changes in used xcconfigs return settingsCache.getOrInsert(key, isValid: { settings in filesSignature(settings.macroConfigPaths) == settings.macroConfigSignature }) { @@ -83,12 +87,12 @@ final class WorkspaceSettingsCache: Sendable { /// Private method to get the cached settings for the given parameters, project, and target. /// /// - remark: This is internal so that clients don't somehow call this with a project which doesn't match the target, except for `BuildRequestContext` which has a cover method for it. There are public methods covering this one. - internal func getCachedSettings(_ parameters: BuildParameters, project: Project, target: Target?, purpose: SettingsPurpose, provisioningTaskInputs: ProvisioningTaskInputs?, impartedBuildProperties: [ImpartedBuildProperties]?, buildRequestContext: BuildRequestContext, filesSignature: ([Path]) -> FilesSignature) -> Settings { - let key = SettingsCacheKey(parameters: parameters, projectGUID: project.guid, targetGUID: target?.guid, purpose: purpose, provisioningTaskInputs: provisioningTaskInputs, impartedBuildProperties: impartedBuildProperties) + internal func getCachedSettings(_ parameters: BuildParameters, project: Project, target: Target?, purpose: SettingsPurpose, provisioningTaskInputs: ProvisioningTaskInputs?, impartedBuildProperties: [ImpartedBuildProperties]?, artifactBundleInfo: [ArtifactBundleInfo]?, buildRequestContext: BuildRequestContext, filesSignature: ([Path]) -> FilesSignature) -> Settings { + let key = SettingsCacheKey(parameters: parameters, projectGUID: project.guid, targetGUID: target?.guid, purpose: purpose, provisioningTaskInputs: provisioningTaskInputs, impartedBuildProperties: impartedBuildProperties, artifactBundleInfo: artifactBundleInfo) // Check if there were any changes in used xcconfigs return settingsCache.getOrInsert(key, isValid: { settings in filesSignature(settings.macroConfigPaths) == settings.macroConfigSignature }) { - Settings(workspaceContext: workspaceContext, buildRequestContext: buildRequestContext, parameters: parameters, project: project, target: target, purpose: purpose, provisioningTaskInputs: provisioningTaskInputs, impartedBuildProperties: impartedBuildProperties) + Settings(workspaceContext: workspaceContext, buildRequestContext: buildRequestContext, parameters: parameters, project: project, target: target, purpose: purpose, provisioningTaskInputs: provisioningTaskInputs, impartedBuildProperties: impartedBuildProperties, artifactBundleInfo: artifactBundleInfo) } } diff --git a/Sources/SWBMacro/MacroCondition.swift b/Sources/SWBMacro/MacroCondition.swift index 9251b1c9..9d1d8bb7 100644 --- a/Sources/SWBMacro/MacroCondition.swift +++ b/Sources/SWBMacro/MacroCondition.swift @@ -12,6 +12,7 @@ import SWBLibc public import SWBUtil +import SwiftDriver /// A condition for whether or not to use a particular macro value assignment, consisting of a parameter and a `fnmatch()`-style pattern against which a candidate parameter value should be compared. The condition parameter is not a string, but rather a MacroConditionParameter, which is created using API in MacroNamespace. Multiple macro conditions can be associated with a given public final class MacroCondition: Serializable, Hashable, CustomStringConvertible, Sendable { @@ -39,6 +40,10 @@ public final class MacroCondition: Serializable, Hashable, CustomStringConvertib /// Evaluates the condition against an arbitrary parameter value, returning `true` if there’s a match and `false` if not. public func evaluate(_ value: String) -> Bool { + if parameter.name == "__normalized_unversioned_triple" { + return normalizedTriplesCompareDisregardingOSVersions(valuePattern, value) + } + do { return try fnmatch(pattern: valuePattern, input: value) } catch { @@ -50,6 +55,16 @@ public final class MacroCondition: Serializable, Hashable, CustomStringConvertib public func evaluate(_ paramValues: [MacroConditionParameter: [String]]) -> Bool { // Look up the values for the parameter. If it’s missing, we evaluate to true iff the pattern is `*` (the “match anything” pattern). guard let values = paramValues[parameter], values.count > 0 else { return valuePattern == "*" } + + if parameter.name == "__normalized_unversioned_triple" { + for value in values { + if normalizedTriplesCompareDisregardingOSVersions(valuePattern, value) { + return true + } + } + return false + } + // Iterate through the values. For each, we invoke fnmatch() until we find a match or we reach the end of the list. for value in values { do { diff --git a/Sources/SWBProjectModel/IDE/IDESwiftPackageExtensions.swift b/Sources/SWBProjectModel/IDE/IDESwiftPackageExtensions.swift index 2e8b1faa..1d4d4d26 100644 --- a/Sources/SWBProjectModel/IDE/IDESwiftPackageExtensions.swift +++ b/Sources/SWBProjectModel/IDE/IDESwiftPackageExtensions.swift @@ -108,6 +108,8 @@ extension PIF.FileReference : PIFRepresentable { case "xcframework": return "wrapper.xcframework" + case "artifactbundle": + return "wrapper.artifactbundle" case "docc": return "folder.documentationcatalog" diff --git a/Sources/SWBTaskConstruction/ProductPlanning/ProductPlan.swift b/Sources/SWBTaskConstruction/ProductPlanning/ProductPlan.swift index 925fe08e..db7e6c7e 100644 --- a/Sources/SWBTaskConstruction/ProductPlanning/ProductPlan.swift +++ b/Sources/SWBTaskConstruction/ProductPlanning/ProductPlan.swift @@ -62,6 +62,9 @@ package final class GlobalProductPlan: GlobalTargetInfoProvider /// The mapping of user-module info for targets. private let moduleInfo = Registry() + /// Cache of parsed artifact bundle metadata. + package let artifactBundleMetadataCache = Registry() + /// The information about XCFrameworks used throughout the task planning process. let xcframeworkContext: XCFrameworkContext @@ -91,6 +94,9 @@ package final class GlobalProductPlan: GlobalTargetInfoProvider /// The set of targets which need to build a swiftmodule during installAPI package private(set) var targetsWhichShouldBuildModulesDuringInstallAPI: Set? + /// Maps each target to its artifactbundles (direct and transitive). + package private(set) var artifactBundlesByTarget: [ConfiguredTarget: [ArtifactBundleInfo]] + /// All targets in the product plan. /// - remark: This property is preferred over the `TargetBuildGraph` in the `BuildPlanRequest` as it performs additional computations for Swift packages. package private(set) var allTargets: [ConfiguredTarget] = [] @@ -202,10 +208,19 @@ package final class GlobalProductPlan: GlobalTargetInfoProvider self.xcframeworkContext = XCFrameworkContext(workspaceContext: planRequest.workspaceContext, buildRequestContext: planRequest.buildRequestContext) self.buildDirectories = BuildDirectoryContext() + enum LinkageGraphCacheKey: Hashable { case only } + let linkageGraphCache = AsyncCache() + let getLinkageGraph: @Sendable () async throws -> TargetLinkageGraph = { + try await linkageGraphCache.value(forKey: LinkageGraphCacheKey.only) { + await TargetLinkageGraph(workspaceContext: planRequest.workspaceContext, buildRequest: planRequest.buildRequest, buildRequestContext: planRequest.buildRequestContext, delegate: WrappingDelegate(delegate: delegate)) + } + } + // Perform post-processing analysis of the build graph self.clientsOfBundlesByTarget = Self.computeBundleClients(buildGraph: planRequest.buildGraph, buildRequestContext: planRequest.buildRequestContext) + self.artifactBundlesByTarget = await Self.computeArtifactBundleInfo(buildGraph: planRequest.buildGraph, provisioningInputs: planRequest.provisioningInputs, buildRequest: planRequest.buildRequest, buildRequestContext: planRequest.buildRequestContext, workspaceContext: planRequest.workspaceContext, getLinkageGraph: getLinkageGraph, metadataCache: self.artifactBundleMetadataCache, delegate: delegate) let directlyLinkedDependenciesByTarget: [ConfiguredTarget: OrderedSet] - (self.impartedBuildPropertiesByTarget, directlyLinkedDependenciesByTarget) = await Self.computeImpartedBuildProperties(planRequest: planRequest, delegate: delegate) + (self.impartedBuildPropertiesByTarget, directlyLinkedDependenciesByTarget) = await Self.computeImpartedBuildProperties(planRequest: planRequest, getLinkageGraph: getLinkageGraph, delegate: delegate) self.mergeableTargetsToMergingTargets = Self.computeMergeableLibraries(buildGraph: planRequest.buildGraph, provisioningInputs: planRequest.provisioningInputs, buildRequestContext: planRequest.buildRequestContext) self.productPathsToProducingTargets = Self.computeProducingTargetsForProducts(buildGraph: planRequest.buildGraph, provisioningInputs: planRequest.provisioningInputs, buildRequestContext: planRequest.buildRequestContext) self.targetGateNodes = Self.constructTargetGateNodes(buildGraph: planRequest.buildGraph, provisioningInputs: planRequest.provisioningInputs, buildRequestContext: planRequest.buildRequestContext, impartedBuildPropertiesByTarget: impartedBuildPropertiesByTarget, enableIndexBuildArena: planRequest.buildRequest.enableIndexBuildArena, nodeCreationDelegate: nodeCreationDelegate) @@ -441,59 +456,121 @@ package final class GlobalProductPlan: GlobalTargetInfoProvider } } + /// Compute artifactbundle metadata for each target in the build graph. + private static func computeArtifactBundleInfo(buildGraph: TargetBuildGraph, provisioningInputs: [ConfiguredTarget: ProvisioningTaskInputs], buildRequest: BuildRequest, buildRequestContext: BuildRequestContext, workspaceContext: WorkspaceContext, getLinkageGraph: @Sendable () async throws -> TargetLinkageGraph, metadataCache: Registry, delegate: any GlobalProductPlanDelegate) async -> [ConfiguredTarget: [ArtifactBundleInfo]] { + var artifactBundlesInfoByTarget: [ConfiguredTarget: Set] = [:] + + // Process targets in topological order + for configuredTarget in buildGraph.allTargets { + guard let standardTarget = configuredTarget.target as? SWBCore.StandardTarget else { + continue + } + let settings = buildRequestContext.getCachedSettings(configuredTarget.parameters, target: configuredTarget.target, provisioningTaskInputs: provisioningInputs[configuredTarget]) + let scope = settings.globalScope + + // Parse artifact bundle info from any bundles this target directly depends upon. + // + // We consider both the frameworks phase and resources phase because when building a relocatable object, SwiftPM PIF generation + // adds binary targets to the resources phase so their static content isn't pulled into the object. This model + // is confusing and we should reconsider it. + for phase in [standardTarget.frameworksBuildPhase, standardTarget.resourcesBuildPhase].compactMap(\.self) { + for buildFile in phase.buildFiles { + let currentPlatformFilter = PlatformFilter(scope) + guard currentPlatformFilter.matches(buildFile.platformFilters) else { continue } + guard case .reference(let referenceGUID) = buildFile.buildableItem else { continue } + guard let reference = workspaceContext.workspace.lookupReference(for: referenceGUID) else { continue } + let resolvedPath = settings.filePathResolver.resolveAbsolutePath(reference) + if resolvedPath.fileExtension == "artifactbundle" { + do { + let metadata = try metadataCache.getOrInsert(resolvedPath) { + try ArtifactBundleMetadata.parse(at: resolvedPath, fileSystem: buildRequestContext.fs) + } + let info = ArtifactBundleInfo(bundlePath: resolvedPath, metadata: metadata) + artifactBundlesInfoByTarget[configuredTarget, default: []].insert(info) + } catch { + delegate.error("Failed to parse artifactbundle at '\(resolvedPath.str)': \(error.localizedDescription)") + } + } + } + } + + // Propagate artifact bundle info from dependencies to dependents. Because we're considering targets in topological order, the info recorded for direct dependencies is already complete. + if !artifactBundlesInfoByTarget.isEmpty { + do { + let linkageGraph = try await getLinkageGraph() + for dependency in linkageGraph.dependencies(of: configuredTarget) { + if let dependencyArtifactInfo = artifactBundlesInfoByTarget[dependency] { + artifactBundlesInfoByTarget[configuredTarget, default: []].formUnion(dependencyArtifactInfo) + } + } + } catch { + delegate.error("failed to determine linkage dependencies of '\(configuredTarget.target.name)' when computing artifact bundle usage: \(error.localizedDescription)") + } + } + } + + return artifactBundlesInfoByTarget.mapValues { + $0.sorted(by: \.bundlePath.str) + } + } + /// Compute the build properties imparted on each target in the graph. - private static func computeImpartedBuildProperties(planRequest: BuildPlanRequest, delegate: any GlobalProductPlanDelegate) async -> ([ConfiguredTarget:[SWBCore.ImpartedBuildProperties]], [ConfiguredTarget:OrderedSet]) { + private static func computeImpartedBuildProperties(planRequest: BuildPlanRequest, getLinkageGraph: @Sendable () async throws -> TargetLinkageGraph, delegate: any GlobalProductPlanDelegate) async -> ([ConfiguredTarget:[SWBCore.ImpartedBuildProperties]], [ConfiguredTarget:OrderedSet]) { var impartedBuildPropertiesByTarget = [ConfiguredTarget:[SWBCore.ImpartedBuildProperties]]() var directlyLinkedDependenciesByTarget = [ConfiguredTarget:OrderedSet]() // We can skip computing contributing properties entirely if no target declares any and if there are no package products in the graph. let targetsContributingProperties = planRequest.buildGraph.allTargets.filter { !$0.target.hasImpartedBuildProperties || $0.target.type == .packageProduct } if !targetsContributingProperties.isEmpty { - let linkageGraph = await TargetLinkageGraph(workspaceContext: planRequest.workspaceContext, buildRequest: planRequest.buildRequest, buildRequestContext: planRequest.buildRequestContext, delegate: WrappingDelegate(delegate: delegate)) - - let configuredTargets = planRequest.buildGraph.allTargets - let targetsByBundleLoader = Self.computeBundleLoaderDependencies( - planRequest: planRequest, - configuredTargets: AnyCollection(configuredTargets), - diagnosticDelegate: delegate - ) - var bundleLoaderByTarget = [ConfiguredTarget:ConfiguredTarget]() - for (bundleLoaderTarget, targetsUsingThatBundleLoader) in targetsByBundleLoader { - for targetUsingBundleLoader in targetsUsingThatBundleLoader { - bundleLoaderByTarget[targetUsingBundleLoader] = bundleLoaderTarget + do { + let linkageGraph = try await getLinkageGraph() + + let configuredTargets = planRequest.buildGraph.allTargets + let targetsByBundleLoader = Self.computeBundleLoaderDependencies( + planRequest: planRequest, + configuredTargets: AnyCollection(configuredTargets), + diagnosticDelegate: delegate + ) + var bundleLoaderByTarget = [ConfiguredTarget:ConfiguredTarget]() + for (bundleLoaderTarget, targetsUsingThatBundleLoader) in targetsByBundleLoader { + for targetUsingBundleLoader in targetsUsingThatBundleLoader { + bundleLoaderByTarget[targetUsingBundleLoader] = bundleLoaderTarget + } } - } - for configuredTarget in configuredTargets { - // Compute the transitive dependencies of the configured target. - let dependencies: OrderedSet - if UserDefaults.useTargetDependenciesForImpartedBuildSettings { - dependencies = transitiveClosure([configuredTarget], successors: planRequest.buildGraph.dependencies(of:)).0 - } else { - dependencies = transitiveClosure([configuredTarget], successors: linkageGraph.dependencies(of:)).0 - } + for configuredTarget in configuredTargets { + // Compute the transitive dependencies of the configured target. + let dependencies: OrderedSet + if UserDefaults.useTargetDependenciesForImpartedBuildSettings { + dependencies = transitiveClosure([configuredTarget], successors: planRequest.buildGraph.dependencies(of:)).0 + } else { + dependencies = transitiveClosure([configuredTarget], successors: linkageGraph.dependencies(of:)).0 + } - let linkedDependencies: [LinkedDependency] = linkageGraph.dependencies(of: configuredTarget).map { .direct($0) } - let transitiveStaticDependencies: [LinkedDependency] = linkedDependencies.flatMap { origin in - transitiveClosure([origin.target]) { - let settings = planRequest.buildRequestContext.getCachedSettings($0.parameters, target: $0.target) - guard !Self.dynamicMachOTypes.contains(settings.globalScope.evaluate(BuiltinMacros.MACH_O_TYPE)) else { - return [] - } - return linkageGraph.dependencies(of: $0) - }.0.map { .staticTransitive($0, origin: origin.target) } - } + let linkedDependencies: [LinkedDependency] = linkageGraph.dependencies(of: configuredTarget).map { .direct($0) } + let transitiveStaticDependencies: [LinkedDependency] = linkedDependencies.flatMap { origin in + transitiveClosure([origin.target]) { + let settings = planRequest.buildRequestContext.getCachedSettings($0.parameters, target: $0.target) + guard !Self.dynamicMachOTypes.contains(settings.globalScope.evaluate(BuiltinMacros.MACH_O_TYPE)) else { + return [] + } + return linkageGraph.dependencies(of: $0) + }.0.map { .staticTransitive($0, origin: origin.target) } + } - directlyLinkedDependenciesByTarget[configuredTarget] = OrderedSet(linkedDependencies + transitiveStaticDependencies + (bundleLoaderByTarget[configuredTarget].map { [.bundleLoader($0)] } ?? [])) - impartedBuildPropertiesByTarget[configuredTarget] = dependencies.compactMap { $0.getImpartedBuildProperties(using: planRequest) } - } + directlyLinkedDependenciesByTarget[configuredTarget] = OrderedSet(linkedDependencies + transitiveStaticDependencies + (bundleLoaderByTarget[configuredTarget].map { [.bundleLoader($0)] } ?? [])) + impartedBuildPropertiesByTarget[configuredTarget] = dependencies.compactMap { $0.getImpartedBuildProperties(using: planRequest) } + } - for (targetToImpart, bundleLoaderTargets) in targetsByBundleLoader { - for bundleLoaderTarget in bundleLoaderTargets { - // Bundle loader target gets all of the build settings that are imparted to the target it is referencing _and_ the build settings that are being imparted by that referenced target. - let currentImpartedBuildProperties = targetToImpart.getImpartedBuildProperties(using: planRequest).map { [$0] } ?? [] - impartedBuildPropertiesByTarget[bundleLoaderTarget, default: []] += currentImpartedBuildProperties + (impartedBuildPropertiesByTarget[targetToImpart] ?? []) + for (targetToImpart, bundleLoaderTargets) in targetsByBundleLoader { + for bundleLoaderTarget in bundleLoaderTargets { + // Bundle loader target gets all of the build settings that are imparted to the target it is referencing _and_ the build settings that are being imparted by that referenced target. + let currentImpartedBuildProperties = targetToImpart.getImpartedBuildProperties(using: planRequest).map { [$0] } ?? [] + impartedBuildPropertiesByTarget[bundleLoaderTarget, default: []] += currentImpartedBuildProperties + (impartedBuildPropertiesByTarget[targetToImpart] ?? []) + } } + } catch { + delegate.error("failed to compute imparted build properties: \(error.localizedDescription)") } } return (impartedBuildPropertiesByTarget, directlyLinkedDependenciesByTarget) @@ -923,7 +1000,7 @@ package final class GlobalProductPlan: GlobalTargetInfoProvider package func getTargetSettings(_ configuredTarget: ConfiguredTarget) -> Settings { // FIXME: Reevaluate whether or not we should cache all of the things we compute here in the workspace context, that may lead to more memory use than it is worth. let provisioningTaskInputs: ProvisioningTaskInputs? = planRequest.buildRequest.enableIndexBuildArena ? nil : planRequest.provisioningInputs(for: configuredTarget) - return planRequest.buildRequestContext.getCachedSettings(configuredTarget.parameters, target: configuredTarget.target, provisioningTaskInputs: provisioningTaskInputs, impartedBuildProperties: impartedBuildPropertiesByTarget[configuredTarget]) + return planRequest.buildRequestContext.getCachedSettings(configuredTarget.parameters, target: configuredTarget.target, provisioningTaskInputs: provisioningTaskInputs, impartedBuildProperties: impartedBuildPropertiesByTarget[configuredTarget], artifactBundleInfo: artifactBundlesByTarget[configuredTarget]) } /// Get the settings to use for an unconfigured target. diff --git a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift index c5722196..c8e6296f 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift @@ -15,6 +15,7 @@ package import SWBUtil import struct SWBProtocol.BuildOperationTaskEnded import Foundation package import SWBMacro +import SwiftDriver // Some things that should probably live in this task producer that might not be immediately obvious: // Emitting an error if PGO is turned on for a target containing Swift files. @@ -408,7 +409,7 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F continue } processedArchitectures.insert(originalArch) - let scope = settings.globalScope.subscope(binding: BuiltinMacros.archCondition, to: originalArch) + let scope = settings.globalScope.subscopeBindingArchAndTriple(arch: originalArch) // Binding the architecture may change how we evaluate $(ARCHS). Ensure we process any new architectures. for potentiallyNewArch in scope.evaluate(BuiltinMacros.ARCHS) { if !processedArchitectures.contains(potentiallyNewArch) { @@ -654,6 +655,46 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F xcframeworkSourcePath: xcframeworkPath, privacyFile: nil ) + } else if fileType.conformsTo(identifier: "wrapper.artifactbundle") { + let metadata: ArtifactBundleMetadata + do { + metadata = try context.globalProductPlan.artifactBundleMetadataCache.getOrInsert(absolutePath) { + try ArtifactBundleMetadata.parse(at: absolutePath, fileSystem: context.fs) + } + } catch { + context.error("failed to parse artifact bundle metadata for '\(absolutePath)': \(error.localizedDescription)") + return nil + } + for (name, artifact) in metadata.artifacts { + switch artifact.type { + case .crossCompilationDestination, .executable, .swiftSDK: + context.warning("ignoring artifact '\(name)' of type '\(artifact.type)' because it cannot be linked", location: .path(absolutePath)) + continue + case .staticLibrary: + var foundMatch = false + let currentTripleString = scope.evaluate(BuiltinMacros.SWIFT_TARGET_TRIPLE) + for variant in artifact.variants { + if variant.supportedTriples == nil || variant.supportedTriples?.contains(where: { + normalizedTriplesCompareDisregardingOSVersions($0, currentTripleString) + }) == true { + foundMatch = true + return LinkerSpec.LibrarySpecifier( + kind: .static, + path: absolutePath.join(variant.path), + mode: .normal, + useSearchPaths: false, + swiftModulePaths: [:], + swiftModuleAdditionalLinkerArgResponseFilePaths: [:], + privacyFile: privacyFile + ) + } + } + if !foundMatch { + context.warning("ignoring '\(name)' because the artifact bundle did not contain a matching variant", location: .path(absolutePath)) + } + } + } + return nil } else if fileType.conformsTo(identifier: "compiled.object-library") { return LinkerSpec.LibrarySpecifier( kind: .objectLibrary, @@ -825,7 +866,7 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F for arch in archs { // Enter the per-arch scope. - let scope = scope.subscope(binding: BuiltinMacros.archCondition, to: arch) + let scope = scope.subscopeBindingArchAndTriple(arch: arch) let currentArchSpec = context.getSpec(arch) as! ArchitectureSpec? // Reset the set of used tools. @@ -1169,7 +1210,7 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F // Generate tasks for the module-only architectures. for arch in moduleOnlyArchs { // Enter the per-arch scope. - let scope = scope.subscope(binding: BuiltinMacros.archCondition, to: arch) + let scope = scope.subscopeBindingArchAndTriple(arch: arch) let currentArchSpec = context.getSpec(arch) as! ArchitectureSpec? // Reset the set of used tools. diff --git a/Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/InfoPlistTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/InfoPlistTaskProducer.swift index dadc2fe2..4e2c8a49 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/InfoPlistTaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/InfoPlistTaskProducer.swift @@ -195,7 +195,7 @@ private extension BundleProductTypeSpec for variant in scope.evaluate(BuiltinMacros.BUILD_VARIANTS) { let scope = scope.subscope(binding: BuiltinMacros.variantCondition, to: variant) for arch in scope.evaluate(BuiltinMacros.ARCHS) { - let scope = scope.subscope(binding: BuiltinMacros.archCondition, to: arch) + let scope = scope.subscopeBindingArchAndTriple(arch: arch) // Preprocess the file, if requested. if let preprocessedPlistPath = await self.addInfoPlistPreprocessTaskIfNeeded(rawPlistPath, basename: infoplistPath.basename, producer, scope, &tasks) { @@ -255,7 +255,7 @@ private extension ToolProductTypeSpec let scope = scope.subscope(binding: BuiltinMacros.variantCondition, to: variant) for arch in scope.evaluate(BuiltinMacros.ARCHS) { - let scope = scope.subscope(binding: BuiltinMacros.archCondition, to: arch) + let scope = scope.subscopeBindingArchAndTriple(arch: arch) // Preprocess the file, if requested. let preprocessedPlistPath = await addInfoPlistPreprocessTaskIfNeeded(rawPlistPath, basename: infoplistPath.basename, producer, scope, &tasks) ?? rawPlistPath diff --git a/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift index f69701a4..b09cb4ae 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift @@ -735,7 +735,9 @@ public class TaskProducerContext: StaleFileRemovalContext, BuildFileResolution assert((scope.values(for: BuiltinMacros.archCondition) != nil) == forArch) if !forArch { - return scope.evaluate(BuiltinMacros.ARCHS).contains(where: { arch in willProduceBinary(scope.subscope(binding: BuiltinMacros.archCondition, to: arch), forArch: true) }) + return scope.evaluate(BuiltinMacros.ARCHS).contains(where: { arch in + willProduceBinary(scope.subscopeBindingArchAndTriple(arch: arch), forArch: true) + }) } // If we're copying a stub binary for this target, then we have a binary. @@ -1340,7 +1342,7 @@ extension TaskProducerContext: CommandProducer { return dependencyScope .subscope(binding: BuiltinMacros.variantCondition, to: variant) - .subscope(binding: BuiltinMacros.archCondition, to: arch) + .subscopeBindingArchAndTriple(arch: arch) } } @@ -1432,7 +1434,9 @@ extension TaskProducerContext { guard let swiftFileType = lookupFileType(identifier: "sourcecode.swift") else { return false } guard let applescriptFileType = lookupFileType(identifier: "sourcecode.applescript") else { return false } guard let doccFileType = lookupFileType(identifier: "folder.documentationcatalog") else { return false } - for archSpecificSubscope in scope.evaluate(BuiltinMacros.ARCHS).map({ arch in scope.subscope(binding: BuiltinMacros.archCondition, to: arch) }) { + for archSpecificSubscope in scope.evaluate(BuiltinMacros.ARCHS).map({ arch in + scope.subscopeBindingArchAndTriple(arch: arch) + }) { let context = BuildFilesProcessingContext(archSpecificSubscope) guard !sourcesBuildPhase.buildFiles.contains(where: { buildFile in guard let resolvedBuildFileInfo = try? resolveBuildFileReference(buildFile), diff --git a/Sources/SWBTestSupport/TestWorkspaces.swift b/Sources/SWBTestSupport/TestWorkspaces.swift index 219269b3..da442f11 100644 --- a/Sources/SWBTestSupport/TestWorkspaces.swift +++ b/Sources/SWBTestSupport/TestWorkspaces.swift @@ -301,6 +301,8 @@ package final class TestFile: TestInternalStructureItem, CustomStringConvertible return "text.plist.xcappextensionpoints" case ".xcframework": return "wrapper.xcframework" + case ".artifactbundle": + return "wrapper.artifactbundle" case ".xcspec": return "text.plist.xcspec" case ".xib": diff --git a/Sources/SWBUniversalPlatform/Specs/StandardFileTypes.xcspec b/Sources/SWBUniversalPlatform/Specs/StandardFileTypes.xcspec index 9e400f27..6afa5833 100644 --- a/Sources/SWBUniversalPlatform/Specs/StandardFileTypes.xcspec +++ b/Sources/SWBUniversalPlatform/Specs/StandardFileTypes.xcspec @@ -1211,6 +1211,13 @@ } ); }, + { + Type = FileType; + Identifier = wrapper.artifactbundle; + BasedOn = wrapper; + Extensions = (artifactbundle); + UTI = "org.swift.artifactbundle"; + }, { Type = FileType; Identifier = wrapper.plug-in; diff --git a/Sources/SWBUniversalPlatform/TestEntryPointTaskProducer.swift b/Sources/SWBUniversalPlatform/TestEntryPointTaskProducer.swift index 6eef151e..b72ba93b 100644 --- a/Sources/SWBUniversalPlatform/TestEntryPointTaskProducer.swift +++ b/Sources/SWBUniversalPlatform/TestEntryPointTaskProducer.swift @@ -54,7 +54,7 @@ class TestEntryPointTaskProducer: PhasedTaskProducer, TaskProducer { for arch in settings.globalScope.evaluate(BuiltinMacros.ARCHS) { for variant in settings.globalScope.evaluate(BuiltinMacros.BUILD_VARIANTS) { let innerScope = settings.globalScope - .subscope(binding: BuiltinMacros.archCondition, to: arch) + .subscopeBindingArchAndTriple(arch: arch) .subscope(binding: BuiltinMacros.variantCondition, to: variant) let linkerFileListPath = innerScope.evaluate(BuiltinMacros.__INPUT_FILE_LIST_PATH__) if !linkerFileListPath.isEmpty { diff --git a/Sources/SWBUtil/CMakeLists.txt b/Sources/SWBUtil/CMakeLists.txt index 8121b52d..e519dfbb 100644 --- a/Sources/SWBUtil/CMakeLists.txt +++ b/Sources/SWBUtil/CMakeLists.txt @@ -96,6 +96,7 @@ add_library(SWBUtil Statistics.swift String.swift SWBDispatch.swift + SwiftDriverTripleExtensions.swift TAPIFileList.swift UniqueSerialization.swift UnsafeSendableDelayedInitializationWrapper.swift @@ -115,6 +116,7 @@ target_link_libraries(SWBUtil PUBLIC SWBCSupport SWBLibc ArgumentParser + SwiftDriver $<$>:SwiftSystem::SystemPackage>) set_target_properties(SWBUtil PROPERTIES diff --git a/Sources/SWBUtil/SwiftDriverTripleExtensions.swift b/Sources/SWBUtil/SwiftDriverTripleExtensions.swift new file mode 100644 index 00000000..ef7af6ca --- /dev/null +++ b/Sources/SWBUtil/SwiftDriverTripleExtensions.swift @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftDriver + +/// Compares two triples for equality after normalization, ignoring OS versions. +package func normalizedTriplesCompareDisregardingOSVersions(_ firstTripleString: String, _ secondTripleString: String) -> Bool { + // Normalize both triples + let firstTriple = Triple(firstTripleString, normalizing: true) + let secondTriple = Triple(secondTripleString, normalizing: true) + + // Ignore OS versions in the comparison + return firstTriple.arch == secondTriple.arch && + firstTriple.subArch == secondTriple.subArch && + firstTriple.vendor == secondTriple.vendor && + firstTriple.os == secondTriple.os && + firstTriple.environment == secondTriple.environment && + firstTriple.objectFormat == secondTriple.objectFormat +} diff --git a/Sources/SwiftBuild/ProjectModel/References.swift b/Sources/SwiftBuild/ProjectModel/References.swift index 3de841e8..796ffebe 100644 --- a/Sources/SwiftBuild/ProjectModel/References.swift +++ b/Sources/SwiftBuild/ProjectModel/References.swift @@ -227,6 +227,8 @@ extension ProjectModel.FileReference: Codable { case "xcframework": return "wrapper.xcframework" + case "artifactbundle": + return "wrapper.artifactbundle" case "docc": return "folder.documentationcatalog" diff --git a/Tests/SWBCoreTests/SettingsTests.swift b/Tests/SWBCoreTests/SettingsTests.swift index 1c6b835a..333db96d 100644 --- a/Tests/SWBCoreTests/SettingsTests.swift +++ b/Tests/SWBCoreTests/SettingsTests.swift @@ -5630,5 +5630,27 @@ import SWBMacro let otherCflagsValues = BuiltinMacros.ifSet(BuiltinMacros.OTHER_CFLAGS, in: scope) { ["blah", $0] } #expect(otherCflagsValues == ["blah", "-Wall", "blah", "-O3"]) } + + @Test + func tripleCondition() async throws { + let namespace = try await getCore().specRegistry.internalMacroNamespace + var table = MacroValueAssignmentTable(namespace: namespace) + + table.push( + BuiltinMacros.OTHER_CFLAGS, + literal: ["-DARM64_APPLE_IOS"], + conditions: .init(conditions: [MacroCondition(parameter: BuiltinMacros.normalizedUnversionedTripleCondition, valuePattern: "arm64-apple-ios")]) + ) + + do { + let scope = MacroEvaluationScope(table: table).subscope(binding: BuiltinMacros.normalizedUnversionedTripleCondition, to: "arm64-apple-ios26.0") + #expect(scope.evaluate(BuiltinMacros.OTHER_CFLAGS) == ["-DARM64_APPLE_IOS"]) + } + + do { + let scope = MacroEvaluationScope(table: table).subscope(binding: BuiltinMacros.normalizedUnversionedTripleCondition, to: "arm64-apple-macos") + #expect(scope.evaluate(BuiltinMacros.OTHER_CFLAGS) == []) + } + } } } diff --git a/Tests/SWBMacroTests/MacroConditionTripleTests.swift b/Tests/SWBMacroTests/MacroConditionTripleTests.swift new file mode 100644 index 00000000..9455a131 --- /dev/null +++ b/Tests/SWBMacroTests/MacroConditionTripleTests.swift @@ -0,0 +1,93 @@ +//===----------------------------------------------------------------------===// +// +// 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 Testing +import SWBMacro +import SwiftDriver + +@Suite fileprivate struct MacroConditionTripleTests { + @Test + func normalizedUnversionedTripleConditionBasicMatching() throws { + let namespace = MacroNamespace(debugDescription: "test") + let tripleParam = namespace.declareConditionParameter("__normalized_unversioned_triple") + + // Basics + do { + let condition = MacroCondition( + parameter: tripleParam, + valuePattern: "arm64-apple-macos" + ) + + #expect(condition.evaluate("arm64-apple-macos")) + #expect(!condition.evaluate("x86_64-apple-ios")) + #expect(!condition.evaluate("arm64-apple-ios")) + #expect(!condition.evaluate("x86_64-unknown-linux-gnu")) + } + + // Versioning is ignored + do { + let condition = MacroCondition( + parameter: tripleParam, + valuePattern: "arm64-apple-tvos26.0" + ) + + #expect(condition.evaluate("arm64-apple-tvos")) + #expect(condition.evaluate("arm64-apple-tvos15.0")) + #expect(!condition.evaluate("arm64-apple-ios26.0")) + } + + // Versioning is ignored (Android) + do { + let condition = MacroCondition( + parameter: tripleParam, + valuePattern: "aarch64-unknown-linux-android" + ) + + #expect(condition.evaluate("aarch64-unknown-linux-android28")) + #expect(condition.evaluate("aarch64-unknown-linux-android")) + #expect(!condition.evaluate("aarch64-unknown-linux-foo")) + } + + // macosx/macos normalization + do { + let condition = MacroCondition( + parameter: tripleParam, + valuePattern: "arm64-apple-macosx" + ) + + #expect(condition.evaluate("arm64-apple-macosx")) + #expect(condition.evaluate("arm64-apple-macos")) + } + + // aarch64/arm64 normalization + do { + let condition = MacroCondition( + parameter: tripleParam, + valuePattern: "aarch64-unknown-linux-gnu" + ) + + #expect(condition.evaluate("aarch64-unknown-linux-gnu")) + #expect(condition.evaluate("arm64-unknown-linux-gnu")) + } + + do { + let condition = MacroCondition( + parameter: tripleParam, + valuePattern: "arm64-apple-ios" + ) + + #expect(condition.evaluate([tripleParam: ["x86_64-apple-macos", "arm64-apple-ios", "x86_64-unknown-linux-gnu"]])) + #expect(!condition.evaluate([tripleParam: ["x86_64-apple-macos", "arm64-apple-watchos", "x86_64-unknown-linux-gnu"]])) + } + } +} diff --git a/Tests/SWBTaskConstructionTests/ArtifactBundleTaskConstructionTests.swift b/Tests/SWBTaskConstructionTests/ArtifactBundleTaskConstructionTests.swift new file mode 100644 index 00000000..09f9f7f7 --- /dev/null +++ b/Tests/SWBTaskConstructionTests/ArtifactBundleTaskConstructionTests.swift @@ -0,0 +1,187 @@ +//===----------------------------------------------------------------------===// +// +// 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 Testing +import Foundation + +import SWBCore +import SWBProtocol +import SWBTestSupport +import SWBUtil +import SWBTaskConstruction + +@Suite +fileprivate struct ArtifactBundleTaskConstructionTests: CoreBasedTests { + @Test(.requireSDKs(.macOS)) + func buildSettingsTripleCondition() async throws { + try await withTemporaryDirectory { tmpDir in + let testProject = try await TestProject( + "aProject", + sourceRoot: tmpDir.join("srcroot"), + groupTree: TestGroup( + "SomeFiles", path: "Sources", + children: [ + TestFile("SourceFile.swift"), + ]), + buildConfigurations: [ + TestBuildConfiguration( + "Debug", + buildSettings: [ + "GENERATE_INFOPLIST_FILE": "YES", + "CODE_SIGN_IDENTITY": "", + "PRODUCT_NAME": "$(TARGET_NAME)", + "SWIFT_VERSION": swiftVersion, + "SWIFT_EXEC": swiftCompilerPath.str, + "LIBTOOL": libtoolPath.str, + "SWIFT_ACTIVE_COMPILATION_CONDITIONS[__normalized_unversioned_triple=arm64-apple-macos]": "CONDITION_ACTIVE", + ]), + ], + targets: [ + TestStandardTarget( + "target", + type: .staticLibrary, + buildPhases: [ + TestSourcesBuildPhase(["SourceFile.swift"]), + ] + ) + ]) + let tester = try await TaskConstructionTester(getCore(), testProject) + + await tester.checkBuild(runDestination: .macOSAppleSilicon) { results -> Void in + results.checkNoDiagnostics() + results.checkTask(.matchRuleType("SwiftDriver Compilation")) { task in + task.checkCommandLineContains(["-DCONDITION_ACTIVE"]) + } + } + + await tester.checkBuild(runDestination: .macOSIntel) { results -> Void in + results.checkNoDiagnostics() + results.checkTask(.matchRuleType("SwiftDriver Compilation")) { task in + task.checkCommandLineDoesNotContain("-DCONDITION_ACTIVE") + } + } + } + } + + @Test(.requireSDKs(.macOS)) + func artifactBundleWithStaticLibrary() async throws { + try await withTemporaryDirectory { (tmpDir: Path) in + let testProject = try await TestProject( + "aProject", + sourceRoot: tmpDir.join("srcroot"), + groupTree: TestGroup( + "SomeFiles", path: "Sources", + children: [ + TestFile("s.swift"), + TestFile("c.c"), + TestFile("MyLibrary.artifactbundle"), + ]), + buildConfigurations: [ + TestBuildConfiguration( + "Debug", + buildSettings: [ + "GENERATE_INFOPLIST_FILE": "YES", + "CODE_SIGN_IDENTITY": "", + "PRODUCT_NAME": "$(TARGET_NAME)", + "SWIFT_VERSION": swiftVersion, + "SWIFT_EXEC": swiftCompilerPath.str, + ]), + ], + targets: [ + TestStandardTarget( + "Framework", + type: .framework, + buildPhases: [ + TestSourcesBuildPhase(["s.swift", "c.c"]), + TestFrameworksBuildPhase(["MyLibrary.artifactbundle"]), + ] + ) + ]) + let tester = try await TaskConstructionTester(getCore(), testProject) + let SRCROOT = tester.workspace.projects[0].sourceRoot.str + + let fs = PseudoFS() + try fs.createDirectory(Path(SRCROOT).join("Sources"), recursive: true) + try fs.write(Path(SRCROOT).join("Sources/s.swift"), contents: "print(\"Hello\")") + try fs.write(Path(SRCROOT).join("Sources/c.c"), contents: "void f(void) {}") + + let artifactBundlePath = Path(SRCROOT).join("Sources/MyLibrary.artifactbundle") + try fs.createDirectory(artifactBundlePath, recursive: true) + let arm64VariantPath = artifactBundlePath.join("macos-arm64") + try fs.createDirectory(arm64VariantPath.join("include"), recursive: true) + try fs.write(arm64VariantPath.join("libMyLibrary.a"), contents: "") + try fs.write(arm64VariantPath.join("include/MyLibrary.h"), contents: "void bar(void);") + try fs.write(arm64VariantPath.join("include/module.modulemap"), contents: "module MyLibrary { header \"MyLibrary.h\" }") + let x86VariantPath = artifactBundlePath.join("macos-x86_64") + try fs.createDirectory(x86VariantPath.join("include"), recursive: true) + try fs.write(x86VariantPath.join("libMyLibrary.a"), contents: "") + try fs.write(x86VariantPath.join("include/MyLibrary.h"), contents: "void bar(void);") + try fs.write(x86VariantPath.join("include/module.modulemap"), contents: "module MyLibrary { header \"MyLibrary.h\" }") + let infoJSONContent = """ + { + "schemaVersion": "1.2", + "artifacts": { + "MyLibrary": { + "type": "staticLibrary", + "version": "1.0.0", + "variants": [ + { + "path": "macos-arm64/libMyLibrary.a", + "supportedTriples": ["arm64-apple-macos"], + "staticLibraryMetadata": { + "headerPaths": ["macos-arm64/include"], + "moduleMapPath": "macos-arm64/include/module.modulemap" + } + }, + { + "path": "macos-x86_64/libMyLibrary.a", + "supportedTriples": ["x86_64-apple-macos"], + "staticLibraryMetadata": { + "headerPaths": ["macos-x86_64/include"], + "moduleMapPath": "macos-x86_64/include/module.modulemap" + } + } + ] + } + } + } + """ + try fs.write(artifactBundlePath.join("info.json"), contents: ByteString(encodingAsUTF8: infoJSONContent)) + + await tester.checkBuild(runDestination: .macOSAppleSilicon, fs: fs) { results in + results.checkNoDiagnostics() + + results.checkTask(.matchRuleType("SwiftDriver Compilation")) { task in + task.checkCommandLineContains([ + "-Xcc", "-fmodule-map-file=\(SRCROOT)/Sources/MyLibrary.artifactbundle/macos-arm64/include/module.modulemap" + ]) + task.checkCommandLineContains([ + "-Xcc", "-I\(SRCROOT)/Sources/MyLibrary.artifactbundle/macos-arm64/include" + ]) + } + + results.checkTask(.matchRuleType("CompileC")) { task in + task.checkCommandLineContains([ + "-fmodule-map-file=\(SRCROOT)/Sources/MyLibrary.artifactbundle/macos-arm64/include/module.modulemap" + ]) + task.checkCommandLineContains([ + "-I\(SRCROOT)/Sources/MyLibrary.artifactbundle/macos-arm64/include" + ]) + } + + results.checkTask(.matchRuleType("Ld")) { task in + task.checkCommandLineContains(["\(SRCROOT)/Sources/MyLibrary.artifactbundle/macos-arm64/libMyLibrary.a"]) + } + } + } + } +}