diff --git a/Sources/SWBApplePlatform/AssetCatalogCompiler.swift b/Sources/SWBApplePlatform/AssetCatalogCompiler.swift index bf5b722a..a0e1117c 100644 --- a/Sources/SWBApplePlatform/AssetCatalogCompiler.swift +++ b/Sources/SWBApplePlatform/AssetCatalogCompiler.swift @@ -239,7 +239,12 @@ public final class ActoolCompilerSpec : GenericCompilerSpec, SpecIdentifierType, // which it currently receives a listing of via the assetcatalog_dependencies file produced by actool. let carFiles = [cbc.resourcesDir?.join("Assets.car")].compactMap { $0 }.map(delegate.createNode) - let outputs = evaluatedOutputsResult + (additionalEvaluatedOutputsResult.outputs ).map(delegate.createNode) + let outputs = evaluatedOutputsResult + (additionalEvaluatedOutputsResult.outputs).map { output in + if let fileTypeIdentifier = output.fileType, let fileType = cbc.producer.lookupFileType(identifier: fileTypeIdentifier) { + delegate.declareOutput(FileToBuild(absolutePath: output.path, fileType: fileType)) + } + return delegate.createNode(output.path) + } guard !outputs.isEmpty else { preconditionFailure("ActoolCompilerSpec.constructTasks() invoked with no outputs defined") } let assetSymbolInputs = cbc.inputs diff --git a/Sources/SWBApplePlatform/InterfaceBuilderCompiler.swift b/Sources/SWBApplePlatform/InterfaceBuilderCompiler.swift index d9b86d09..92ccb55c 100644 --- a/Sources/SWBApplePlatform/InterfaceBuilderCompiler.swift +++ b/Sources/SWBApplePlatform/InterfaceBuilderCompiler.swift @@ -86,7 +86,12 @@ public class IbtoolCompilerSpec : GenericCompilerSpec, IbtoolCompilerSupport, @u // Add the additional outputs defined by the spec. These are not declared as outputs but should be processed by the tool separately. let additionalEvaluatedOutputsResult = await additionalEvaluatedOutputs(cbc, delegate) - outputs += additionalEvaluatedOutputsResult.outputs.map(delegate.createNode) + outputs += additionalEvaluatedOutputsResult.outputs.map { output in + if let fileTypeIdentifier = output.fileType, let fileType = cbc.producer.lookupFileType(identifier: fileTypeIdentifier) { + delegate.declareOutput(FileToBuild(absolutePath: output.path, fileType: fileType)) + } + return delegate.createNode(output.path) + } if let infoPlistContent = additionalEvaluatedOutputsResult.generatedInfoPlistContent { delegate.declareGeneratedInfoPlistContent(infoPlistContent) diff --git a/Sources/SWBApplePlatform/Plugin.swift b/Sources/SWBApplePlatform/Plugin.swift index adca6f0e..ae11f5c7 100644 --- a/Sources/SWBApplePlatform/Plugin.swift +++ b/Sources/SWBApplePlatform/Plugin.swift @@ -37,7 +37,6 @@ struct AppleDeveloperDirectoryExtension: DeveloperDirectoryExtension { } struct TaskProducersExtension: TaskProducerExtension { - func createPreSetupTaskProducers(_ context: TaskProducerContext) -> [any TaskProducer] { [DevelopmentAssetsTaskProducer(context)] } @@ -61,6 +60,9 @@ struct TaskProducersExtension: TaskProducerExtension { var globalTaskProducers: [any GlobalTaskProducerFactory] { [StubBinaryTaskProducerFactory()] } + + func generateAdditionalTasks(_ tasks: inout [any SWBCore.PlannedTask], _ producer: any SWBTaskConstruction.TaskProducer) { + } } struct ExtensionPointExtractorTaskProducerFactory: TaskProducerFactory { diff --git a/Sources/SWBCore/Core.swift b/Sources/SWBCore/Core.swift index d42a07db..1f61258e 100644 --- a/Sources/SWBCore/Core.swift +++ b/Sources/SWBCore/Core.swift @@ -596,6 +596,7 @@ public final class Core: Sendable { let specs: [SpecDump] = specRegistry.domains.flatMap { domain -> [SpecDump] in let allSpecs = specRegistry.findSpecs(BuildSettingsSpec.self, domain: domain, includeInherited: false) + + specRegistry.findSpecs(BuildSettingsExtensionSpec.self, domain: domain, includeInherited: false) + specRegistry.findSpecs(BuildSystemSpec.self, domain: domain, includeInherited: false) + specRegistry.findSpecs(CommandLineToolSpec.self, domain: domain, includeInherited: false) return allSpecs.map { spec in diff --git a/Sources/SWBCore/Settings/Settings.swift b/Sources/SWBCore/Settings/Settings.swift index 06cb4af5..bf5923f6 100644 --- a/Sources/SWBCore/Settings/Settings.swift +++ b/Sources/SWBCore/Settings/Settings.swift @@ -83,7 +83,7 @@ fileprivate struct PreOverridesSettings { let ignoredMacros: [MacroDeclaration] = [BuiltinMacros.OutputFormat, BuiltinMacros.OutputPath] for spec in specs { - for option in spec.flattenedBuildOptions.values { + for option in core.specRegistry.effectiveFlattenedBuildOptions(spec).values { guard !ignoredMacros.contains(option.macro) else { continue } if let value = option.defaultValue { @@ -236,11 +236,11 @@ fileprivate struct PreOverridesSettings { // Add the defaults from all the registered tools in the given domain. // // FIXME: This is somewhat wasteful, as we end up duplicating values for super specifications. However, that lets us keep the condition set required to enable a particular compiler very simple. - let unionedDefaults = unionedToolDefaults(domain: domain) + let unionedDefaults = unionedToolDefaults(domain: domain).table let customizedDefaults = MacroValueAssignmentTable(namespace: core.specRegistry.internalMacroNamespace) for spec in core.specRegistry.findSpecs(CompilerSpec.self, domain: domain) { // Add all the necessary defaults. - for option in spec.flattenedBuildOptions.values { + for option in core.specRegistry.effectiveFlattenedBuildOptions(spec).values { if let defaultValue = option.defaultValue { // Only push the default value if it diverges from the existing default. // diff --git a/Sources/SWBCore/SpecImplementations/CommandLineToolSpec.swift b/Sources/SWBCore/SpecImplementations/CommandLineToolSpec.swift index b02c0c80..5b5016c4 100644 --- a/Sources/SWBCore/SpecImplementations/CommandLineToolSpec.swift +++ b/Sources/SWBCore/SpecImplementations/CommandLineToolSpec.swift @@ -776,7 +776,7 @@ open class CommandLineToolSpec : PropertyDomainSpec, SpecType, TaskTypeDescripti let scope = cbc.scope let inputFileType = cbc.inputs.first?.fileType let lookup = { self.lookup($0, cbc, delegate) } - for buildOption in self.flattenedOrderedBuildOptions { + for buildOption in cbc.producer.effectiveFlattenedOrderedBuildOptions(self) { guard let dependencyFormat = buildOption.dependencyFormat else { continue } @@ -916,7 +916,12 @@ open class CommandLineToolSpec : PropertyDomainSpec, SpecType, TaskTypeDescripti // Add the additional outputs defined by the spec. These are not declared as outputs but should be processed by the tool separately. let additionalEvaluatedOutputsResult = await additionalEvaluatedOutputs(cbc, delegate) - outputs.append(contentsOf: additionalEvaluatedOutputsResult.outputs.map({ delegate.createNode($0) })) + outputs.append(contentsOf: additionalEvaluatedOutputsResult.outputs.map { output in + if let fileTypeIdentifier = output.fileType, let fileType = cbc.producer.lookupFileType(identifier: fileTypeIdentifier) { + delegate.declareOutput(FileToBuild(absolutePath: output.path, fileType: fileType)) + } + return delegate.createNode(output.path) + }) if let infoPlistContent = additionalEvaluatedOutputsResult.generatedInfoPlistContent { delegate.declareGeneratedInfoPlistContent(infoPlistContent) @@ -978,7 +983,7 @@ open class CommandLineToolSpec : PropertyDomainSpec, SpecType, TaskTypeDescripti } public struct AdditionalEvaluatedOutputsResult { - public var outputs = [Path]() + public var outputs = [(path: Path, fileType: String?)]() public var generatedInfoPlistContent: Path? = nil } @@ -994,7 +999,7 @@ open class CommandLineToolSpec : PropertyDomainSpec, SpecType, TaskTypeDescripti // FIXME: In Xcode, this is also marked as an "auxiliary output", which we use in conjunction with the "MightNotEmitAllOutput" flag to determine whether or not the tool needs to rerun if the output is missing. - result.outputs.append(output) + result.outputs.append((output, nil)) } let producer = cbc.producer @@ -1002,12 +1007,17 @@ open class CommandLineToolSpec : PropertyDomainSpec, SpecType, TaskTypeDescripti let inputFileType = cbc.inputs.first?.fileType let lookup = { self.lookup($0, cbc, delegate) } let optionContext = await discoveredCommandLineToolSpecInfo(producer, scope, delegate) - result.outputs.append(contentsOf: self.flattenedOrderedBuildOptions.flatMap { buildOption -> [Path] in + result.outputs.append(contentsOf: cbc.producer.effectiveFlattenedOrderedBuildOptions(self, filter: .all).flatMap { buildOption -> [(Path, String?)] in // Check if the effective arguments for this build option were non-empty as a proxy for whether it got filtered out by architecture mismatch, etc. guard let outputDependencies = buildOption.outputDependencies, !buildOption.getArgumentsForCommand(producer, scope: scope, inputFileType: inputFileType, optionContext: optionContext, lookup: lookup).isEmpty else { return [] } - return outputDependencies.compactMap { Path(scope.evaluate($0, lookup: lookup)).nilIfEmpty?.normalize() } + return outputDependencies.compactMap { outputDependency in + guard let path = Path(scope.evaluate(outputDependency.path, lookup: lookup)).nilIfEmpty else { + return nil + } + return (path.normalize(), outputDependency.fileType.map { scope.evaluate($0, lookup: lookup).nilIfEmpty } ?? nil) + } }) return result @@ -1231,7 +1241,7 @@ open class CommandLineToolSpec : PropertyDomainSpec, SpecType, TaskTypeDescripti return cbc.scope.evaluate(value, lookup: lookup).map { .literal(ByteString(encodingAsUTF8: $0)) } case .options: - return self.commandLineFromOptions(cbc, delegate, optionContext: optionContext, lookup: lookup) + return self.commandLineFromOptions(cbc, delegate, optionContext: optionContext, buildOptionsFilter: .all, lookup: lookup) case .output: // We always resolve the Output via a recursive macro evaluation. See constructTasks() for more information. @@ -1260,22 +1270,22 @@ open class CommandLineToolSpec : PropertyDomainSpec, SpecType, TaskTypeDescripti /// Creates and returns the command line arguments generated by the options of the specification. /// /// - parameter lookup: An optional closure which functionally defined overriding values during build setting evaluation. - public func commandLineFromOptions(_ producer: any CommandProducer, scope: MacroEvaluationScope, inputFileType: FileTypeSpec?, optionContext: (any BuildOptionGenerationContext)?, lookup: ((MacroDeclaration) -> MacroExpression?)? = nil) -> [CommandLineArgument] { - return self.flattenedOrderedBuildOptions.flatMap { $0.getArgumentsForCommand(producer, scope: scope, inputFileType: inputFileType, optionContext: optionContext, lookup: lookup) } + public func commandLineFromOptions(_ producer: any CommandProducer, scope: MacroEvaluationScope, inputFileType: FileTypeSpec?, optionContext: (any BuildOptionGenerationContext)?, buildOptionsFilter: BuildOptionsFilter, lookup: ((MacroDeclaration) -> MacroExpression?)? = nil) -> [CommandLineArgument] { + return producer.effectiveFlattenedOrderedBuildOptions(self, filter: buildOptionsFilter).flatMap { $0.getArgumentsForCommand(producer, scope: scope, inputFileType: inputFileType, optionContext: optionContext, lookup: lookup) } } /// Creates and returns the command line arguments generated by the options of the specification. /// /// - parameter lookup: An optional closure which functionally defined overriding values during build setting evaluation. - public func commandLineFromOptions(_ cbc: CommandBuildContext, _ delegate: any DiagnosticProducingDelegate, optionContext: (any BuildOptionGenerationContext)?, lookup: ((MacroDeclaration) -> MacroExpression?)? = nil) -> [CommandLineArgument] { - return commandLineFromOptions(cbc.producer, scope: cbc.scope, inputFileType: cbc.inputs.first?.fileType, optionContext: optionContext, lookup: { self.lookup($0, cbc, delegate, lookup) }) + public func commandLineFromOptions(_ cbc: CommandBuildContext, _ delegate: any DiagnosticProducingDelegate, optionContext: (any BuildOptionGenerationContext)?, buildOptionsFilter: BuildOptionsFilter = .all, lookup: ((MacroDeclaration) -> MacroExpression?)? = nil) -> [CommandLineArgument] { + return commandLineFromOptions(cbc.producer, scope: cbc.scope, inputFileType: cbc.inputs.first?.fileType, optionContext: optionContext, buildOptionsFilter: buildOptionsFilter, lookup: { self.lookup($0, cbc, delegate, lookup) }) } /// Creates and returns the command line arguments generated by the specification's build setting corresponding to the given macro declaration. /// /// - parameter lookup: An optional closure which functionally defined overriding values during build setting evaluation. func commandLineFromMacroDeclaration(_ producer: any CommandProducer, optionContext: (any BuildOptionGenerationContext)?, scope: MacroEvaluationScope, macro: MacroDeclaration, inputFileType: FileTypeSpec?, lookup: ((MacroDeclaration) -> MacroExpression?)? = nil) -> [CommandLineArgument] { - return buildOptions.first { $0.name == macro.name }?.getArgumentsForCommand(producer, scope: scope, inputFileType: inputFileType, optionContext: optionContext, lookup: lookup) ?? [] + return producer.effectiveBuildOptions(self).first { $0.name == macro.name }?.getArgumentsForCommand(producer, scope: scope, inputFileType: inputFileType, optionContext: optionContext, lookup: lookup) ?? [] } /// Creates and returns the command line arguments generated by the specification's build setting corresponding to the given macro declaration. @@ -1291,7 +1301,7 @@ open class CommandLineToolSpec : PropertyDomainSpec, SpecType, TaskTypeDescripti let scope = cbc.scope let inputFileType = cbc.inputs.first?.fileType let lookup = { self.lookup($0, cbc, delegate, lookup) } - return self.flattenedOrderedBuildOptions.flatMap { buildOption -> [Path] in + return cbc.producer.effectiveFlattenedOrderedBuildOptions(self).flatMap { buildOption -> [Path] in // Check if the effective arguments for this build option were non-empty as a proxy for whether it got filtered out by architecture mismatch, etc. guard let inputInclusions = buildOption.inputInclusions, !buildOption.getArgumentsForCommand(producer, scope: scope, inputFileType: inputFileType, optionContext: optionContext, lookup: lookup).isEmpty else { return [] @@ -1303,7 +1313,7 @@ open class CommandLineToolSpec : PropertyDomainSpec, SpecType, TaskTypeDescripti /// Compute the list of additional linker arguments to use when this tool is used for building with the given scope. public func computeAdditionalLinkerArgs(_ producer: any CommandProducer, scope: MacroEvaluationScope, inputFileTypes: [FileTypeSpec], optionContext: (any BuildOptionGenerationContext)?, delegate: any TaskGenerationDelegate) async -> (args: [[String]], inputPaths: [Path]) { // FIXME: Optimize the list to search here. - return (args: self.flattenedOrderedBuildOptions.map { $0.getAdditionalLinkerArgs(producer, scope: scope, inputFileTypes: inputFileTypes) }, inputPaths: []) + return (args: producer.effectiveFlattenedOrderedBuildOptions(self).map { $0.getAdditionalLinkerArgs(producer, scope: scope, inputFileTypes: inputFileTypes) }, inputPaths: []) } // Creates and returns the environment from the specification. This includes both the 'EnvironmentVariables' property for this tool spec, and any build options which define that their value should be exported via their 'SetValueInEnvironmentVariable' property. @@ -1319,7 +1329,7 @@ open class CommandLineToolSpec : PropertyDomainSpec, SpecType, TaskTypeDescripti // Add environment variables from build options which specify they should be added via a 'SetValueInEnvironmentVariable' property. // FIXME: Optimize the list to search here. - for buildOption in self.flattenedOrderedBuildOptions { + for buildOption in cbc.producer.effectiveFlattenedOrderedBuildOptions(self) { if let assignment = buildOption.getEnvironmentAssignmentForCommand(cbc, lookup: wrappedLookup) { environment.append(assignment) } diff --git a/Sources/SWBCore/SpecImplementations/PropertyDomainSpec.swift b/Sources/SWBCore/SpecImplementations/PropertyDomainSpec.swift index eed3f5e2..fe284753 100644 --- a/Sources/SWBCore/SpecImplementations/PropertyDomainSpec.swift +++ b/Sources/SWBCore/SpecImplementations/PropertyDomainSpec.swift @@ -345,7 +345,7 @@ private let buildOptionTypes: [String: any BuildOptionType] = [ let inputInclusions: [MacroStringExpression]? /// Additional output dependencies to consider when this build option is active. - let outputDependencies: [MacroStringExpression]? + let outputDependencies: [(path: MacroStringExpression, fileType: MacroStringExpression?)]? /// Helper function for extract an individual value definition from a dictionary. private static func parseBuildOptionValue(_ parser: SpecParser, _ name: String, _ type: any BuildOptionType, _ data: [String: PropertyListItem]) -> (String, BuildOptionValue)? { @@ -806,7 +806,7 @@ private let buildOptionTypes: [String: any BuildOptionType] = [ var dependencyFormat: DependencyDataFormat? = nil var featureFlags: [String]? = nil var inputInclusions: [MacroStringExpression]? = nil - var outputDependencies: [MacroStringExpression]? = nil + var outputDependencies: [(path: MacroStringExpression, fileType: MacroStringExpression?)]? = nil for (key, valueData) in items { switch key { case "Name": @@ -1033,14 +1033,36 @@ private let buildOptionTypes: [String: any BuildOptionType] = [ case "OutputDependencies": switch valueData { case .plString(let value): - outputDependencies = [parser.delegate.internalMacroNamespace.parseString(value)] + outputDependencies = [(parser.delegate.internalMacroNamespace.parseString(value), nil)] case .plArray(let values): outputDependencies = values.compactMap({ data in - guard case .plString(let value) = data else { - error("expected all string values in array for '\(key)'") + switch data { + case let .plString(value): + return (parser.delegate.internalMacroNamespace.parseString(value), nil) + case let .plDict(value): + let path: MacroStringExpression + switch value["Path"] { + case let .plString(expr): + path = parser.delegate.internalMacroNamespace.parseString(expr) + default: + error("expected string value for subkey 'Path' in element of array in '\(key)'") + return nil + } + + let fileType: MacroStringExpression + switch value["FileType"] { + case let .plString(expr): + fileType = parser.delegate.internalMacroNamespace.parseString(expr) + default: + error("expected string value for subkey 'FileType' in element of array in '\(key)'") + return nil + } + + return (path, fileType) + default: + error("expected all string or dictionary values in array for '\(key)'") return nil } - return parser.delegate.internalMacroNamespace.parseString(value) }) default: error("expected string or array value for build option key '\(key)'") @@ -1722,11 +1744,11 @@ open class PropertyDomainSpec : Spec, @unchecked Sendable { /// Extensions to PropertyDomainSpec for performance testing. -public extension PropertyDomainSpec { +extension PropertyDomainSpec { /// Creates and returns a ``MacroValueAssignmentTable`` populated with the default values of the receiver's build options. /// The table's namespace is also returned so that the caller can add further settings to it if desired. - func macroTableForBuildOptionDefaults(_ core: Core) -> (MacroValueAssignmentTable, MacroNamespace) { + @_spi(Testing) public func macroTableForBuildOptionDefaults(_ core: Core) -> (MacroValueAssignmentTable, MacroNamespace) { var table = MacroValueAssignmentTable(namespace: core.specRegistry.internalMacroNamespace) for option in self.flattenedOrderedBuildOptions { guard let value = option.defaultValue else { continue } diff --git a/Sources/SWBCore/SpecImplementations/RegisterSpecs.swift b/Sources/SWBCore/SpecImplementations/RegisterSpecs.swift index db2699f4..9199c2ec 100644 --- a/Sources/SWBCore/SpecImplementations/RegisterSpecs.swift +++ b/Sources/SWBCore/SpecImplementations/RegisterSpecs.swift @@ -30,6 +30,7 @@ public struct BuiltinSpecsExtension: SpecificationsExtension { ArchitectureSpec.self, BuildPhaseSpec.self, BuildSettingsSpec.self, + BuildSettingsExtensionSpec.self, BuildSystemSpec.self, CommandLineToolSpec.self, CompilerSpec.self, diff --git a/Sources/SWBCore/SpecImplementations/Specs.swift b/Sources/SWBCore/SpecImplementations/Specs.swift index e2b12bdc..cb8cf844 100644 --- a/Sources/SWBCore/SpecImplementations/Specs.swift +++ b/Sources/SWBCore/SpecImplementations/Specs.swift @@ -457,6 +457,20 @@ public final class BuildSettingsSpec : PropertyDomainSpec, SpecType, @unchecked } } +public final class BuildSettingsExtensionSpec: PropertyDomainSpec, SpecType, @unchecked Sendable { + class public override var typeName: String { + return "BuildSettingsExtension" + } + + /// The spec identifier that specs must conform to in order for these build settings to be incorporated into them. + let extendsConformsTo: String + + public required init(_ parser: SpecParser, _ basedOnSpec: Spec?) { + extendsConformsTo = parser.parseRequiredString("ExtendsConformsTo") + super.init(parser, basedOnSpec) + } +} + public final class BuildSystemSpec : PropertyDomainSpec, SpecType, @unchecked Sendable { class public override var typeName: String { return "BuildSystem" diff --git a/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift b/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift index 460489d1..fcf16b8f 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift @@ -626,7 +626,7 @@ public class ClangCompilerSpec : CompilerSpec, SpecIdentifierType, GCCCompatible var commandLine = Array() // Add the arguments from the specification. - commandLine += self.commandLineFromOptions(producer, scope: scope, inputFileType: inputFileType, optionContext: optionContext,lookup: { declaration in + commandLine += self.commandLineFromOptions(producer, scope: scope, inputFileType: inputFileType, optionContext: optionContext, buildOptionsFilter: .specOnly, lookup: { declaration in if declaration.name == "CLANG_INDEX_STORE_ENABLE" && optionContext is DiscoveredClangToolSpecInfo { let clangToolInfo = optionContext as! DiscoveredClangToolSpecInfo if !clangToolInfo.isAppleClang { @@ -1119,6 +1119,18 @@ public class ClangCompilerSpec : CompilerSpec, SpecIdentifierType, GCCCompatible inputDeps.append(contentsOf: constantFlags.headerSearchPaths.inputPaths) #endif + let cbcWithOutput = cbc.outputs.isEmpty ? cbc.appendingOutputs([outputNode.path]) : cbc + commandLine += self.commandLineFromOptions(cbc.producer, scope: cbc.scope, inputFileType: resolvedInputFileType, optionContext: clangInfo, buildOptionsFilter: .extendedOnly, lookup: { + self.lookup($0, cbcWithOutput, delegate) + }).map(\.asString) + + let additionalOutputs = await self.additionalEvaluatedOutputs(cbcWithOutput, delegate) + for output in additionalOutputs.outputs { + if let fileTypeIdentifier = output.fileType, let fileType = cbc.producer.lookupFileType(identifier: fileTypeIdentifier) { + delegate.declareOutput(FileToBuild(absolutePath: output.path, fileType: fileType)) + } + } + // Add custom per-file flags. var perFileFlags = [String]() if let perFileArgs = input.additionalArgs { @@ -1370,7 +1382,7 @@ public class ClangCompilerSpec : CompilerSpec, SpecIdentifierType, GCCCompatible } // Finally, create the task. - delegate.createTask(type: self, dependencyData: dependencyData, payload: payload, ruleInfo: ruleInfo, additionalSignatureData: additionalSignatureData, commandLine: commandLine, additionalOutput: additionalOutput, environment: environmentBindings, workingDirectory: compilerWorkingDirectory(cbc), inputs: inputNodes + extraInputs, outputs: [outputNode] + extraOutputs, action: action ?? delegate.taskActionCreationDelegate.createDeferredExecutionTaskActionIfRequested(userPreferences: cbc.producer.userPreferences), execDescription: resolveExecutionDescription(cbc, delegate), enableSandboxing: enableSandboxing, additionalTaskOrderingOptions: [.compilationForIndexableSourceFile], usesExecutionInputs: usesExecutionInputs, showEnvironment: true, priority: .preferred) + delegate.createTask(type: self, dependencyData: dependencyData, payload: payload, ruleInfo: ruleInfo, additionalSignatureData: additionalSignatureData, commandLine: commandLine, additionalOutput: additionalOutput, environment: environmentBindings, workingDirectory: compilerWorkingDirectory(cbc), inputs: inputNodes + extraInputs, outputs: [outputNode] + extraOutputs + additionalOutputs.outputs.map { delegate.createNode($0.path) }, action: action ?? delegate.taskActionCreationDelegate.createDeferredExecutionTaskActionIfRequested(userPreferences: cbc.producer.userPreferences), execDescription: resolveExecutionDescription(cbc, delegate), enableSandboxing: enableSandboxing, additionalTaskOrderingOptions: [.compilationForIndexableSourceFile], usesExecutionInputs: usesExecutionInputs, showEnvironment: true, priority: .preferred) // If the object file verifier is enabled and we are building with explicit modules, also create a job to produce adjacent objects using implicit modules, then compare the results. if cbc.scope.evaluate(BuiltinMacros.CLANG_ENABLE_EXPLICIT_MODULES_OBJECT_FILE_VERIFIER) && action != nil { diff --git a/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift b/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift index 293d6dba..959232e5 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift @@ -419,7 +419,12 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec // Add the additional outputs defined by the spec. These are not declared as outputs but should be processed by the tool separately. let additionalEvaluatedOutputsResult = await additionalEvaluatedOutputs(cbc, delegate) - outputs.append(contentsOf: additionalEvaluatedOutputsResult.outputs.map({ delegate.createNode($0) })) + outputs.append(contentsOf: additionalEvaluatedOutputsResult.outputs.map { output in + if let fileTypeIdentifier = output.fileType, let fileType = cbc.producer.lookupFileType(identifier: fileTypeIdentifier) { + delegate.declareOutput(FileToBuild(absolutePath: output.path, fileType: fileType)) + } + return delegate.createNode(output.path) + }) if let infoPlistContent = additionalEvaluatedOutputsResult.generatedInfoPlistContent { delegate.declareGeneratedInfoPlistContent(infoPlistContent) diff --git a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift index 827eaac7..4b3d7e34 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift @@ -2983,7 +2983,7 @@ public final class SwiftCompilerSpec : CompilerSpec, SpecIdentifierType, SwiftDi var inputPaths: [Path] = [] if !forTAPI { // TAPI can't use all of the additional linker options, and its spec has all of the build setting/option arguments that it can use. - args = self.flattenedOrderedBuildOptions.map { $0.getAdditionalLinkerArgs(producer, scope: scope, inputFileTypes: inputFileTypes) }.filter { !$0.isEmpty } + args = producer.effectiveFlattenedOrderedBuildOptions(self).map { $0.getAdditionalLinkerArgs(producer, scope: scope, inputFileTypes: inputFileTypes) }.filter { !$0.isEmpty } } // Determine if we are forced to use the standard system location; this is currently only for OS adopters of Swift, not any client. diff --git a/Sources/SWBCore/SpecImplementations/Tools/TAPITools.swift b/Sources/SWBCore/SpecImplementations/Tools/TAPITools.swift index fa384143..b58dbb03 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/TAPITools.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/TAPITools.swift @@ -40,7 +40,7 @@ public final class TAPIToolSpec : GenericCommandLineToolSpec, GCCCompatibleCompi } // FIXME: Ideally this would just go in the spec itself - public override func commandLineFromOptions(_ cbc: CommandBuildContext, _ delegate: any DiagnosticProducingDelegate, optionContext: (any BuildOptionGenerationContext)?, lookup: ((MacroDeclaration) -> MacroExpression?)? = nil) -> [CommandLineArgument] { + public override func commandLineFromOptions(_ cbc: CommandBuildContext, _ delegate: any DiagnosticProducingDelegate, optionContext: (any BuildOptionGenerationContext)?, buildOptionsFilter: BuildOptionsFilter, lookup: ((MacroDeclaration) -> MacroExpression?)? = nil) -> [CommandLineArgument] { let innerLookup: ((MacroDeclaration) -> MacroExpression?) = { macro in switch macro { case BuiltinMacros.TAPI_HEADER_SEARCH_PATHS: @@ -67,7 +67,7 @@ public final class TAPIToolSpec : GenericCommandLineToolSpec, GCCCompatibleCompi let tapiFileListArgs: [CommandLineArgument] = ["-filelist", .path(Path(cbc.scope.evaluate(BuiltinMacros.TapiFileListPath, lookup: lookup)))] - return super.commandLineFromOptions(cbc, delegate, optionContext: optionContext, lookup: requiresSRCROOTSupport ? innerLookup : lookup) + tapiFileListArgs + return super.commandLineFromOptions(cbc, delegate, optionContext: optionContext, buildOptionsFilter: buildOptionsFilter, lookup: requiresSRCROOTSupport ? innerLookup : lookup) + tapiFileListArgs } /// Construct a new task to run the TAPI tool. diff --git a/Sources/SWBCore/TaskGeneration.swift b/Sources/SWBCore/TaskGeneration.swift index 744ea2ac..6bba61ec 100644 --- a/Sources/SWBCore/TaskGeneration.swift +++ b/Sources/SWBCore/TaskGeneration.swift @@ -311,12 +311,67 @@ extension CommandProducer { return cache as! T } + func effectiveBuildOptions(_ spec: PropertyDomainSpec) -> [BuildOption] { + specRegistry.effectiveBuildOptions(spec) + } + + func effectiveFlattenedOrderedBuildOptions(_ spec: PropertyDomainSpec, filter: BuildOptionsFilter = .all) -> [BuildOption] { + specRegistry.effectiveFlattenedOrderedBuildOptions(spec, filter: filter) + } + /// Compute the expanded search path list (i.e. with recursive entries expanded) for the given macro. func expandedSearchPaths(for macro: PathListMacroDeclaration, scope: MacroEvaluationScope) -> [String] { return expandedSearchPaths(for: scope.evaluate(macro), scope: scope) } } +/// Which build options to include when evaluating the command line of a spec. This primarily exists because the Clang spec wants to specially evaluate a bunch of options as "constant" (see `getStandardFlags`), which we can't guarantee to be the case for extended options. +public enum BuildOptionsFilter { + /// Include only build options declared on the spec and its supertypes. + case specOnly + + /// Include only extended build options declared via any `BuildSettingsExtension` specs. + case extendedOnly + + /// Include build options declared by the spec itself and its supertypes, as well as via any `BuildSettingsExtension` specs. + case all +} + +extension SpecRegistry { + func effectiveBuildOptions(_ spec: PropertyDomainSpec) -> [BuildOption] { + var options: [BuildOption] = [] + options.append(contentsOf: spec.buildOptions) + for extensionSpec in findSpecs(BuildSettingsExtensionSpec.self) where spec.conformsTo(identifier: extensionSpec.extendsConformsTo) { + options.append(contentsOf: extensionSpec.buildOptions) + } + return options + } + + func effectiveFlattenedBuildOptions(_ spec: PropertyDomainSpec) -> [String: BuildOption] { + var options = spec.flattenedBuildOptions + for extensionSpec in findSpecs(BuildSettingsExtensionSpec.self) where spec.conformsTo(identifier: extensionSpec.extendsConformsTo) { + options.merge(extensionSpec.flattenedBuildOptions, uniquingKeysWith: { _, new in + // Should duplicates be an error? + return new + }) + } + return options + } + + func effectiveFlattenedOrderedBuildOptions(_ spec: PropertyDomainSpec, filter: BuildOptionsFilter) -> [BuildOption] { + var options: [BuildOption] = [] + if filter == .all || filter == .specOnly { + options.append(contentsOf: spec.flattenedOrderedBuildOptions) + } + if filter == .all || filter == .extendedOnly { + for extensionSpec in findSpecs(BuildSettingsExtensionSpec.self) where spec.conformsTo(identifier: extensionSpec.extendsConformsTo) { + options.append(contentsOf: extensionSpec.flattenedOrderedBuildOptions) + } + } + return options + } +} + /// Describes the context in which an individual invocation of a command line spec is being built. public struct CommandBuildContext { /// The settings context the command is being evaluated in. diff --git a/Sources/SWBTaskConstruction/ProductPlanning/BuildPlan.swift b/Sources/SWBTaskConstruction/ProductPlanning/BuildPlan.swift index aabc3b6a..3dd1ab20 100644 --- a/Sources/SWBTaskConstruction/ProductPlanning/BuildPlan.swift +++ b/Sources/SWBTaskConstruction/ProductPlanning/BuildPlan.swift @@ -172,9 +172,12 @@ package final class BuildPlan: StaleFileRemovalContext { progressContinuation.yield(()) if delegate.cancelled { return } - // FIXME: Change this API to just return an array for now. - let tasks = await Array(producer.generateTasks()) - aggregationQueue.async { + var tasks = await producer.generateTasks() + for ext in await taskProducerExtensions(planRequest.workspaceContext) { + await ext.generateAdditionalTasks(&tasks, producer) + } + + aggregationQueue.async { [tasks] in productPlanResultContext.addPlannedTasks(tasks) } } diff --git a/Sources/SWBTaskConstruction/ProductPlanning/ProductPlanner.swift b/Sources/SWBTaskConstruction/ProductPlanning/ProductPlanner.swift index 7dd6ef5b..9f4da8dc 100644 --- a/Sources/SWBTaskConstruction/ProductPlanning/ProductPlanner.swift +++ b/Sources/SWBTaskConstruction/ProductPlanning/ProductPlanner.swift @@ -14,7 +14,7 @@ import SWBCore import SWBUtil import SWBMacro -@PluginExtensionSystemActor private func taskProducerExtensions(_ workspaceContext: WorkspaceContext) -> [any TaskProducerExtension] { +@PluginExtensionSystemActor internal func taskProducerExtensions(_ workspaceContext: WorkspaceContext) -> [any TaskProducerExtension] { let extensions = workspaceContext.core.pluginManager.extensions(of: TaskProducerExtensionPoint.self) // Sort the extensions using their type name so we always go through them in a stable order. diff --git a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/FilesBasedBuildPhaseTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/FilesBasedBuildPhaseTaskProducer.swift index 0707113e..d6cb900b 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/FilesBasedBuildPhaseTaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/FilesBasedBuildPhaseTaskProducer.swift @@ -322,7 +322,7 @@ extension FilesBasedBuildPhaseTaskProducer { } /// Base class for build phase tasks producers which are based around processing file references. -class FilesBasedBuildPhaseTaskProducerBase: PhasedTaskProducer { +package class FilesBasedBuildPhaseTaskProducerBase: PhasedTaskProducer { /// Captures the state used when expanding groups, to prevent recursion. struct GroupContext { /// The set of rules which have been used during this evaluation. Each individual rule is used at most once. @@ -859,7 +859,7 @@ class FilesBasedBuildPhaseTaskProducerBase: PhasedTaskProducer { } /// Construct the tasks for the given rule against the supplied context. - func constructTasksForRule(_ rule: any BuildRuleAction, _ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) async { + package func constructTasksForRule(_ rule: any BuildRuleAction, _ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) async { // Evaluate the rule. if let action = rule as? BuildRuleTaskAction { let spec = action.toolSpec.resolveConcreteSpec(cbc) diff --git a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift index 1c847b6a..82af5212 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift @@ -196,7 +196,7 @@ private final class SourcesPhaseBasedTaskGenerationDelegate: TaskGenerationDeleg } } -final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBasedBuildPhaseTaskProducer { +package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBasedBuildPhaseTaskProducer { typealias ManagedBuildPhase = SourcesBuildPhase /// A virtual node representing the (conservative) construction of all non-Swift-generated headers. @@ -312,7 +312,7 @@ final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBase /// The default `TaskOrderingOptions` are used for the compile tasks in the architecture-variant loop, as the default options are primarily concerned with compilation. Other tasks set up by this producer use the `nonCompilationTaskOrderingOptions` below. But be mindful of which options are being used when adding new tasks to this producer. /// /// The rule of thumb is that using the default options imposes _more_ ordering constraints on a task. - override var defaultTaskOrderingOptions: TaskOrderingOptions { + package override var defaultTaskOrderingOptions: TaskOrderingOptions { let scope = self.context.settings.globalScope var options = [TaskOrderingOptions]() @@ -340,7 +340,7 @@ final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBase /// /// We override this to auto-attach tasks to the generated headers completion ordering gate. @discardableResult - override func appendGeneratedTasks( _ tasks: inout [any PlannedTask], options: TaskOrderingOptions? = nil, body: (any TaskGenerationDelegate) async -> Void) async -> (tasks: [any PlannedTask], outputs: [FileToBuild]) { + package override func appendGeneratedTasks( _ tasks: inout [any PlannedTask], options: TaskOrderingOptions? = nil, body: (any TaskGenerationDelegate) async -> Void) async -> (tasks: [any PlannedTask], outputs: [FileToBuild]) { return await super.appendGeneratedTasks(&tasks, options: options) { delegate in await body(SourcesPhaseBasedTaskGenerationDelegate(producer: self, userPreferences: context.workspaceContext.userPreferences, delegate: delegate)) } @@ -703,7 +703,7 @@ final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBase prepareTargetForIndexInputsObjectSet.formUnion(newInputs.map{ ObjectIdentifier($0) }) } - func prepare() { + package func prepare() { // CodeSigning in a monster... really, it is. Further, we don't actually have a model where we can perform proper pre-planning to determine what inputs will cause downstream outputs to end up in a particular location. Every source file has the potential to contribute some type of output that ends up in the wrapper. // For example, every metal file creates a library that is embedded into the product wrapper. However, the build system doesn't actually *know* that, as the outputs aren't really tracked in a way that the CodeSign task itself can depend on it. // What the build system does _know_, is that _all_ source files run tools that can potentially end up invalidating the code signature. Until we have a proper pre-planning (e.g. or dry-run) model (or a model that allows us to have dependencies on task producers), we need to be more conservative in our tracking of inputs, even if they can result in additional codesign work. @@ -722,7 +722,7 @@ final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBase } /// Compute a flattened list of build files to use - func generateTasks() async -> [any PlannedTask] { + package func generateTasks() async -> [any PlannedTask] { // NOTE: The sources build phase uses its own generateTasks() to deal with the per-variant and per-arch nature. let scope = context.settings.globalScope diff --git a/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift index 2c481238..b24fbc1c 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift @@ -103,7 +103,7 @@ public class TaskProducerContext: StaleFileRemovalContext, BuildFileResolution let provisionalTasks: [String: ProvisionalTask] /// The build rule set for file references (includes system rules as well as any custom rules). - let buildRuleSet: any BuildRuleSet + package let buildRuleSet: any BuildRuleSet /// The default working directory path to use. /// diff --git a/Sources/SWBTaskConstruction/TaskProducers/TaskProducerExtensionPoint.swift b/Sources/SWBTaskConstruction/TaskProducers/TaskProducerExtensionPoint.swift index 5f09aa00..555f60f4 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/TaskProducerExtensionPoint.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/TaskProducerExtensionPoint.swift @@ -28,6 +28,8 @@ package protocol TaskProducerExtension: Sendable { var unorderedPostSetupTaskProducers: [any TaskProducerFactory] { get } var unorderedPostBuildPhasesTaskProducers: [any TaskProducerFactory] { get } var globalTaskProducers: [any GlobalTaskProducerFactory] { get } + + func generateAdditionalTasks(_ tasks: inout [any PlannedTask], _ producer: any TaskProducer) async } package protocol TaskProducerFactory: Sendable { diff --git a/Sources/SWBUniversalPlatform/Plugin.swift b/Sources/SWBUniversalPlatform/Plugin.swift index b50a687c..e6c44e50 100644 --- a/Sources/SWBUniversalPlatform/Plugin.swift +++ b/Sources/SWBUniversalPlatform/Plugin.swift @@ -74,6 +74,9 @@ struct UniversalPlatformTaskProducerExtension: TaskProducerExtension { var unorderedPostBuildPhasesTaskProducers: [any SWBTaskConstruction.TaskProducerFactory] { [] } var globalTaskProducers: [any SWBTaskConstruction.GlobalTaskProducerFactory] { [] } + + func generateAdditionalTasks(_ tasks: inout [any SWBCore.PlannedTask], _ producer: any SWBTaskConstruction.TaskProducer) { + } } struct UniversalPlatformTaskActionExtension: TaskActionExtension { diff --git a/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift b/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift index b7669be0..c3094eb8 100644 --- a/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift +++ b/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift @@ -13,7 +13,7 @@ import Testing import SWBTestSupport -import SWBCore +@_spi(Testing) import SWBCore import SWBUtil import enum SWBProtocol.ExternalToolResult import struct SWBProtocol.BuildOperationTaskEnded