diff --git a/Sources/SWBCore/ProjectModel/Target.swift b/Sources/SWBCore/ProjectModel/Target.swift index 1a79679a2..99800e928 100644 --- a/Sources/SWBCore/ProjectModel/Target.swift +++ b/Sources/SWBCore/ProjectModel/Target.swift @@ -36,6 +36,9 @@ public final class CustomTask: ProjectModelItem, Sendable { public let outputFilePaths: [MacroStringExpression] public let enableSandboxing: Bool public let preparesForIndexing: Bool + /// Platform filters used by `CustomTaskProducer` to gate this task per-`ConfiguredTarget`. + /// An empty set matches every platform. + public let platformFilters: Set init(_ model: SWBProtocol.CustomTask, _ pifLoader: PIFLoader) { self.commandLine = model.commandLine.map { pifLoader.userNamespace.parseString($0) } @@ -46,6 +49,7 @@ public final class CustomTask: ProjectModelItem, Sendable { self.outputFilePaths = model.outputFilePaths.map { pifLoader.userNamespace.parseString($0) } self.enableSandboxing = model.enableSandboxing self.preparesForIndexing = model.preparesForIndexing + self.platformFilters = Set(model.platformFilters.map { SWBCore.PlatformFilter($0, pifLoader) }) } init(fromDictionary pifDict: ProjectModelItemPIF, withPIFLoader pifLoader: PIFLoader) throws { @@ -74,6 +78,11 @@ public final class CustomTask: ProjectModelItem, Sendable { self.outputFilePaths = try Self.parseValueForKeyAsArrayOfStrings(PIFKey_CustomTask_outputFilePaths, pifDict: pifDict).map { pifLoader.userNamespace.parseString($0) } self.enableSandboxing = try Self.parseValueForKeyAsBool(PIFKey_CustomTask_enableSandboxing, pifDict: pifDict) self.preparesForIndexing = try Self.parseValueForKeyAsBool(PIFKey_CustomTask_preparesForIndexing, pifDict: pifDict) + // Optional for back-compat with PIFs emitted before the field existed; defaults to + // empty set, which matches every platform via PlatformFilter.matches([]). + self.platformFilters = try Set(Self.parseOptionalValueForKeyAsArrayOfProjectModelItems(PIFKey_platformFilters, pifDict: pifDict, pifLoader: pifLoader, construct: { + try PlatformFilter(fromDictionary: $0, withPIFLoader: pifLoader) + }) ?? []) } } diff --git a/Sources/SWBProtocol/PIFKeyConstants.swift b/Sources/SWBProtocol/PIFKeyConstants.swift index 2d4cbfd9f..4653db286 100644 --- a/Sources/SWBProtocol/PIFKeyConstants.swift +++ b/Sources/SWBProtocol/PIFKeyConstants.swift @@ -130,6 +130,8 @@ public let PIFKey_CustomTask_inputFilePaths = "inputFilePaths" public let PIFKey_CustomTask_outputFilePaths = "outputFilePaths" public let PIFKey_CustomTask_enableSandboxing = "enableSandboxing" public let PIFKey_CustomTask_preparesForIndexing = "preparesForIndexing" +// CustomTask reuses the shared PIFKey_platformFilters key (see line 24 above) +// for the same reason BuildFile and TargetDependency do — same JSON key, same shape. // Special value for PIFKey_BuildRule_fileTypeIdentifier public let PIFKey_BuildRule_fileTypeIdentifier_pattern_proxy = "pattern.proxy" diff --git a/Sources/SWBProtocol/ProjectModel/CustomTask.swift b/Sources/SWBProtocol/ProjectModel/CustomTask.swift index 683803033..11df18031 100644 --- a/Sources/SWBProtocol/ProjectModel/CustomTask.swift +++ b/Sources/SWBProtocol/ProjectModel/CustomTask.swift @@ -21,8 +21,11 @@ public struct CustomTask: SerializableCodable, Sendable { public let outputFilePaths: [MacroExpressionSource] public let enableSandboxing: Bool public let preparesForIndexing: Bool + /// Platform filters used to gate this task per-`ConfiguredTarget`. + /// An empty set matches every platform. + public let platformFilters: Set - public init(commandLine: [MacroExpressionSource], environment: [(MacroExpressionSource, MacroExpressionSource)], workingDirectory: MacroExpressionSource, executionDescription: MacroExpressionSource, inputFilePaths: [MacroExpressionSource], outputFilePaths: [MacroExpressionSource], enableSandboxing: Bool, preparesForIndexing: Bool) { + public init(commandLine: [MacroExpressionSource], environment: [(MacroExpressionSource, MacroExpressionSource)], workingDirectory: MacroExpressionSource, executionDescription: MacroExpressionSource, inputFilePaths: [MacroExpressionSource], outputFilePaths: [MacroExpressionSource], enableSandboxing: Bool, preparesForIndexing: Bool, platformFilters: Set = []) { self.commandLine = commandLine self.environment = environment self.workingDirectory = workingDirectory @@ -31,6 +34,7 @@ public struct CustomTask: SerializableCodable, Sendable { self.outputFilePaths = outputFilePaths self.enableSandboxing = enableSandboxing self.preparesForIndexing = preparesForIndexing + self.platformFilters = platformFilters } enum CodingKeys: CodingKey { @@ -43,6 +47,7 @@ public struct CustomTask: SerializableCodable, Sendable { case outputFilePaths case enableSandboxing case preparesForIndexing + case platformFilters } public func encode(to encoder: any Encoder) throws { @@ -56,6 +61,7 @@ public struct CustomTask: SerializableCodable, Sendable { try container.encode(outputFilePaths, forKey: .outputFilePaths) try container.encode(enableSandboxing, forKey: .enableSandboxing) try container.encode(preparesForIndexing, forKey: .preparesForIndexing) + try container.encode(platformFilters, forKey: .platformFilters) } public init(from decoder: any Decoder) throws { @@ -70,6 +76,8 @@ public struct CustomTask: SerializableCodable, Sendable { self.outputFilePaths = try container.decode([MacroExpressionSource].self, forKey: .outputFilePaths) self.enableSandboxing = try container.decode(Bool.self, forKey: .enableSandboxing) self.preparesForIndexing = try container.decode(Bool.self, forKey: .preparesForIndexing) + // Deliberate divergence from BuildFile precedent: existing PIFs in the wild lack this key. + self.platformFilters = try container.decodeIfPresent(Set.self, forKey: .platformFilters) ?? [] } } diff --git a/Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/CustomTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/CustomTaskProducer.swift index f5f474286..81ed2c9ea 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/CustomTaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/CustomTaskProducer.swift @@ -19,7 +19,18 @@ final class CustomTaskProducer: PhasedTaskProducer, TaskProducer { func generateTasks() async -> [any PlannedTask] { var tasks: [any PlannedTask] = [] + // The current ConfiguredTarget's PlatformFilter, derived from PLATFORM_NAME / SDK info + // in globalScope. Used below to gate per-platform CustomTasks. Mirrors the pattern + // used by 8+ task producers for BuildFile.platformFilters. + let currentPlatformFilter = PlatformFilter(context.settings.globalScope) + for customTask in context.configuredTarget?.target.customTasks ?? [] { + // Skip tasks whose platform filters don't match the current ConfiguredTarget. + // Empty platformFilters matches every platform (preserves today's behavior for + // callers that don't tag their custom tasks). + guard currentPlatformFilter.matches(customTask.platformFilters) else { + continue + } let commandLine = customTask.commandLine.map { context.settings.globalScope.evaluate($0) } var environmentAssignments = await computeScriptEnvironment(.shellScriptPhase, scope: context.settings.globalScope, settings: context.settings, workspaceContext: context.workspaceContext, allDeploymentTargetMacroNames: context.allDeploymentTargetMacroNames()) diff --git a/Sources/SwiftBuild/ProjectModel/CustomTask.swift b/Sources/SwiftBuild/ProjectModel/CustomTask.swift index f27aac54a..5b31c01f2 100644 --- a/Sources/SwiftBuild/ProjectModel/CustomTask.swift +++ b/Sources/SwiftBuild/ProjectModel/CustomTask.swift @@ -42,6 +42,12 @@ extension ProjectModel { public var outputFilePaths: [String] public var enableSandboxing: Bool public var preparesForIndexing: Bool + /// Platform filters used to gate this task per-`ConfiguredTarget`. + /// + /// An empty set matches every platform, preserving today's behavior for callers that + /// don't tag their custom tasks. `CustomTaskProducer` evaluates these against the + /// `ConfiguredTarget`'s scope to decide whether to emit a task for a given CT. + public var platformFilters: Set public init( commandLine: [String], @@ -51,7 +57,8 @@ extension ProjectModel { inputFilePaths: [String], outputFilePaths: [String], enableSandboxing: Bool, - preparesForIndexing: Bool + preparesForIndexing: Bool, + platformFilters: Set = [] ) { self.commandLine = commandLine self.environment = environment @@ -61,6 +68,7 @@ extension ProjectModel { self.outputFilePaths = outputFilePaths self.enableSandboxing = enableSandboxing self.preparesForIndexing = preparesForIndexing + self.platformFilters = platformFilters } } } @@ -82,6 +90,12 @@ extension ProjectModel.CustomTask: Codable { self.outputFilePaths = try container.decode([String].self, forKey: .outputFilePaths) self.enableSandboxing = try container.decode(String.self, forKey: .enableSandboxing) == "true" self.preparesForIndexing = try container.decode(String.self, forKey: .preparesForIndexing) == "true" + // Deliberate divergence from BuildFile.platformFilters: existing PIFs in the wild + // were emitted before this field existed, so missing-key must round-trip as empty. + self.platformFilters = try container.decodeIfPresent( + Set.self, + forKey: .platformFilters + ) ?? [] } public func encode(to encoder: any Encoder) throws { @@ -94,6 +108,7 @@ extension ProjectModel.CustomTask: Codable { try container.encode(self.outputFilePaths, forKey: .outputFilePaths) try container.encode(self.enableSandboxing ? "true" : "false", forKey: .enableSandboxing) try container.encode(self.preparesForIndexing ? "true" : "false", forKey: .preparesForIndexing) + try container.encode(self.platformFilters.sorted(), forKey: .platformFilters) } enum CodingKeys: String, CodingKey { @@ -105,5 +120,6 @@ extension ProjectModel.CustomTask: Codable { case outputFilePaths case enableSandboxing case preparesForIndexing + case platformFilters } }