From 92a64f2a65e6286cc3025d4f32be81c76ff6a020 Mon Sep 17 00:00:00 2001 From: Andreas Neusuess Date: Fri, 5 Dec 2025 09:44:26 -0800 Subject: [PATCH 1/2] Introduce BUILD_ONLY_KNOWN_LOCALIZATIONS and only build resources for languages added to Localizations in the project when set --- .../InterfaceBuilderCompiler.swift | 2 +- .../InterfaceBuilderShared.swift | 16 +- .../SWBApplePlatform/XCStringsCompiler.swift | 30 +- Sources/SWBCore/FileToBuild.swift | 57 +++ Sources/SWBCore/ProjectModel/Project.swift | 4 + Sources/SWBCore/Settings/BuiltinMacros.swift | 2 + Sources/SWBCore/Specs/CoreBuildSystem.xcspec | 7 + Sources/SWBCore/TaskGeneration.swift | 3 + .../IDE/IDESwiftPackageExtensions.swift | 4 + .../SWBProjectModel/PIFGenerationModel.swift | 1 + Sources/SWBProtocol/PIFKeyConstants.swift | 1 + .../SWBProtocol/ProjectModel/Project.swift | 10 +- .../ResourcesTaskProducer.swift | 10 + .../SourcesTaskProducer.swift | 11 + .../TaskProducers/TaskProducer.swift | 2 +- .../SWBTestSupport/DummyCommandProducer.swift | 3 + Sources/SWBTestSupport/TestWorkspaces.swift | 6 +- .../InstallLocTaskConstructionTests.swift | 133 +++++++ .../ResourceTaskConstructionTests.swift | 213 ++++++++++++ .../XCStringsTaskConstructionTests.swift | 325 +++++++++++++++++- 20 files changed, 827 insertions(+), 13 deletions(-) diff --git a/Sources/SWBApplePlatform/InterfaceBuilderCompiler.swift b/Sources/SWBApplePlatform/InterfaceBuilderCompiler.swift index b8a181bf..02f1c296 100644 --- a/Sources/SWBApplePlatform/InterfaceBuilderCompiler.swift +++ b/Sources/SWBApplePlatform/InterfaceBuilderCompiler.swift @@ -69,7 +69,7 @@ public class IbtoolCompilerSpec : GenericCompilerSpec, IbtoolCompilerSupport, @u specialArgs += minimumDeploymentTargetArguments(cbc, delegate) // Get the strings file paths and regions. - let stringsFiles = stringsFilesAndRegions(cbc) + let stringsFiles = stringsFilesAndRegions(cbc, delegate) // Define the inputs, including the strings files from any variant groups. let inputs = cbc.inputs.map({ $0.absolutePath }) + stringsFiles.map({ $0.stringsFile }) diff --git a/Sources/SWBApplePlatform/InterfaceBuilderShared.swift b/Sources/SWBApplePlatform/InterfaceBuilderShared.swift index 32d0522c..80bc1ffb 100644 --- a/Sources/SWBApplePlatform/InterfaceBuilderShared.swift +++ b/Sources/SWBApplePlatform/InterfaceBuilderShared.swift @@ -24,7 +24,7 @@ public protocol IbtoolCompilerSupport { func minimumDeploymentTargetArguments(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) -> [String] /// Get the string files paths and regions. - func stringsFilesAndRegions(_ cbc: CommandBuildContext) -> [(stringsFile: Path, region: String)] + func stringsFilesAndRegions(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) -> [(stringsFile: Path, region: String)] } extension IbtoolCompilerSupport { @@ -55,13 +55,25 @@ extension IbtoolCompilerSupport { return minimumDeploymentTargetArguments } - public func stringsFilesAndRegions(_ cbc: CommandBuildContext) -> [(stringsFile: Path, region: String)] { + public func stringsFilesAndRegions(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) -> [(stringsFile: Path, region: String)] { var result = [(Path, String)]() + + // Check if we should filter localizations based on the setting BUILD_ONLY_KNOWN_LOCALIZATIONS: + let shouldFilter = cbc.scope.evaluate(BuiltinMacros.BUILD_ONLY_KNOWN_LOCALIZATIONS) + let allowedLocalizations = shouldFilter ? cbc.producer.project?.knownLocalizations : nil + for ftb in cbc.inputs { if let buildFile = ftb.buildFile { if case .reference(let guid) = buildFile.buildableItem, case let variantGroup as VariantGroup = cbc.producer.lookupReference(for: guid) { for ref in variantGroup.children { if let fileRef = ref as? FileReference, let region = fileRef.regionVariantName { + // Filter by knownLocalizations if BUILD_ONLY_KNOWN_LOCALIZATIONS is enabled: + if let allowedLocalizations, !allowedLocalizations.contains(region) { + // Skip this .strings file. + delegate.note("Skipping .lproj directory '\(region).lproj' because '\(region)' is not in project's known localizations (BUILD_ONLY_KNOWN_LOCALIZATIONS is enabled)") + continue + } + if let fileType = cbc.producer.lookupFileType(reference: fileRef), let stringFileType = cbc.producer.lookupFileType(identifier: "text.plist.strings"), fileType.conformsTo(stringFileType) { let absolutePath = cbc.producer.filePathResolver.resolveAbsolutePath(fileRef) result.append((absolutePath, region)) diff --git a/Sources/SWBApplePlatform/XCStringsCompiler.swift b/Sources/SWBApplePlatform/XCStringsCompiler.swift index fd7b0d3d..8e511994 100644 --- a/Sources/SWBApplePlatform/XCStringsCompiler.swift +++ b/Sources/SWBApplePlatform/XCStringsCompiler.swift @@ -189,7 +189,35 @@ public final class XCStringsCompilerSpec: GenericCompilerSpec, SpecIdentifierTyp /// Generates a task for compiling the .xcstrings to .strings/dict files. private func constructCatalogCompilationTask(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) async { - let commandLine = await commandLineFromTemplate(cbc, delegate, optionContext: discoveredCommandLineToolSpecInfo(cbc.producer, cbc.scope, delegate)).map(\.asString) + let isRunningInstallloc = cbc.scope.evaluate(BuiltinMacros.BUILD_COMPONENTS).contains("installLoc") + + /// Custom lookup function to overwrite `XCSTRINGS_LANGUAGES_TO_COMPILE` + /// when `BUILD_ONLY_KNOWN_LOCALIZATIONS` is enabled for a regular build. + func lookup(_ macro: MacroDeclaration) -> MacroExpression? { + switch macro { + case BuiltinMacros.XCSTRINGS_LANGUAGES_TO_COMPILE: + guard !isRunningInstallloc else { + // No need to intercept anything for installloc, its + // language specification should always take precedence. + return nil + } + if cbc.scope.evaluate(BuiltinMacros.BUILD_ONLY_KNOWN_LOCALIZATIONS), var knownLocalizations = cbc.producer.project?.knownLocalizations { + + knownLocalizations.removeAll(where: { $0 == "Base" }) + if !knownLocalizations.isEmpty { + delegate.note("XCStrings will compile languages for known regions: \(knownLocalizations.joined(separator: ", "))") + } + + // Only build the languages specified by the project: + return cbc.scope.namespace.parseLiteralStringList(knownLocalizations) + } + default: + break + } + return nil + } + + let commandLine = await commandLineFromTemplate(cbc, delegate, optionContext: discoveredCommandLineToolSpecInfo(cbc.producer, cbc.scope, delegate), lookup: lookup).map(\.asString) // We can't know our precise outputs statically because we don't know what languages are in the xcstrings file, // nor do we know if any strings have variations (which would require one or more .stringsdict outputs). diff --git a/Sources/SWBCore/FileToBuild.swift b/Sources/SWBCore/FileToBuild.swift index 00e8222a..b9aa6237 100644 --- a/Sources/SWBCore/FileToBuild.swift +++ b/Sources/SWBCore/FileToBuild.swift @@ -179,4 +179,61 @@ public extension RegionVariable { let installLocLanguages = Set(scope.evaluate(BuiltinMacros.INSTALLLOC_LANGUAGE)) return installLocLanguages.isEmpty || installLocLanguages.contains(regionVariantName) } + + /// When the build setting `BUILD_ONLY_KNOWN_LOCALIZATIONS` is active, + /// this region must be present in the project's known localizations + /// or else the file should not be built. + /// + /// `delegate` is used to emit a note about skipping this file if the + /// method returns `false`. + /// + /// - Returns: `true` if this file can be built because + /// `BUILD_ONLY_KNOWN_LOCALIZATIONS` is disabled, or the file's region + /// is present in the project's supported localizations. + /// If this file should not be built, `false` is returned. + func buildSettingAllowsBuildingLocale(_ scope: MacroEvaluationScope, in project: Project?, _ delegate: (any DiagnosticProducingDelegate)?) -> Bool { + + guard let project else { + // We can't find the project. Allow the file to build: + return true + } + + // Don't apply the BUILD_ONLY_KNOWN_LOCALIZATIONS setting + // for installloc, because installloc's languages have priority: + let isInstallloc = scope.evaluate(BuiltinMacros.BUILD_COMPONENTS).contains("installLoc") + guard !isInstallloc else { + // We're in the installloc build phase, and we're not interested + // in changing its behavior. Allow the flow to continue: + return true + } + + guard scope.evaluate(BuiltinMacros.BUILD_ONLY_KNOWN_LOCALIZATIONS) else { + // Build setting is off, this should build: + return true + } + + guard let knownLocalizations = project.knownLocalizations, !knownLocalizations.isEmpty else { + // We can't read the supported localizations, allow it to build: + return true + } + + guard let regionVariantName else { + // Unable to get a region name, allow it to built: + return true + } + + if regionVariantName == "mul" { + // Allow mul.lproj (multi-lingual) which is used when xcstrings are paired with IB files: + return true + } + + if knownLocalizations.contains(regionVariantName) { + // This is a known locale, allow it to build: + return true + } + + // This region is not supported, so it shouldn't build. + delegate?.note("Skipping .lproj directory '\(regionVariantName).lproj' because '\(regionVariantName)' is not in project's known localizations (BUILD_ONLY_KNOWN_LOCALIZATIONS is enabled)") + return false + } } diff --git a/Sources/SWBCore/ProjectModel/Project.swift b/Sources/SWBCore/ProjectModel/Project.swift index 67c4b47f..fa78b706 100644 --- a/Sources/SWBCore/ProjectModel/Project.swift +++ b/Sources/SWBCore/ProjectModel/Project.swift @@ -68,6 +68,7 @@ public final class Project: ProjectModelItem, PIFObject, Hashable, Encodable public let buildConfigurations: [BuildConfiguration] public let defaultConfigurationName: String public let developmentRegion: String? + public let knownLocalizations: [String]? public let classPrefix: String public let appPreferencesBuildSettings: [String: PropertyListItem] public let isPackage: Bool @@ -100,6 +101,7 @@ public final class Project: ProjectModelItem, PIFObject, Hashable, Encodable self.buildConfigurations = model.buildConfigurations.map{ BuildConfiguration($0, pifLoader) } self.defaultConfigurationName = model.defaultConfigurationName self.developmentRegion = model.developmentRegion + self.knownLocalizations = model.knownLocalizations self.classPrefix = model.classPrefix self.appPreferencesBuildSettings = BuildConfiguration.convertMacroBindingSourceToPlistDictionary(model.appPreferencesBuildSettings) @@ -147,6 +149,8 @@ public final class Project: ProjectModelItem, PIFObject, Hashable, Encodable // The development region name is required. developmentRegion = try Self.parseOptionalValueForKeyAsString(PIFKey_Project_developmentRegion, pifDict: pifDict) + knownLocalizations = try Self.parseOptionalValueForKeyAsArrayOfStrings(PIFKey_Project_knownRegions, pifDict: pifDict) + classPrefix = try Self.parseOptionalValueForKeyAsString(PIFKey_Project_classPrefix, pifDict: pifDict) ?? "" // Get the application preferences build settings. diff --git a/Sources/SWBCore/Settings/BuiltinMacros.swift b/Sources/SWBCore/Settings/BuiltinMacros.swift index a150cc57..2e73a8d9 100644 --- a/Sources/SWBCore/Settings/BuiltinMacros.swift +++ b/Sources/SWBCore/Settings/BuiltinMacros.swift @@ -1000,6 +1000,7 @@ public final class BuiltinMacros { public static let SHARED_FRAMEWORKS_FOLDER_PATH = BuiltinMacros.declarePathMacro("SHARED_FRAMEWORKS_FOLDER_PATH") public static let SHARED_SUPPORT_FOLDER_PATH = BuiltinMacros.declarePathMacro("SHARED_SUPPORT_FOLDER_PATH") public static let STRING_CATALOG_GENERATE_SYMBOLS = BuiltinMacros.declareBooleanMacro("STRING_CATALOG_GENERATE_SYMBOLS") + public static let BUILD_ONLY_KNOWN_LOCALIZATIONS = BuiltinMacros.declareBooleanMacro("BUILD_ONLY_KNOWN_LOCALIZATIONS") public static let STRINGS_FILE_INPUT_ENCODING = BuiltinMacros.declareStringMacro("STRINGS_FILE_INPUT_ENCODING") public static let STRINGS_FILE_OUTPUT_ENCODING = BuiltinMacros.declareStringMacro("STRINGS_FILE_OUTPUT_ENCODING") public static let STRINGS_FILE_OUTPUT_FILENAME = BuiltinMacros.declareStringMacro("STRINGS_FILE_OUTPUT_FILENAME") @@ -2202,6 +2203,7 @@ public final class BuiltinMacros { SPECIALIZATION_SDK_OPTIONS, SRCROOT, STRING_CATALOG_GENERATE_SYMBOLS, + BUILD_ONLY_KNOWN_LOCALIZATIONS, STRINGSDATA_DIR, STRINGS_FILE_INPUT_ENCODING, STRINGS_FILE_OUTPUT_ENCODING, diff --git a/Sources/SWBCore/Specs/CoreBuildSystem.xcspec b/Sources/SWBCore/Specs/CoreBuildSystem.xcspec index d0e4fdb1..5b24bb3e 100644 --- a/Sources/SWBCore/Specs/CoreBuildSystem.xcspec +++ b/Sources/SWBCore/Specs/CoreBuildSystem.xcspec @@ -3696,6 +3696,13 @@ When this setting is enabled: Category = "Localization"; Description = "When enabled, symbols will be generated for manually-managed strings in String Catalogs."; }, + { Name = BUILD_ONLY_KNOWN_LOCALIZATIONS; + Type = Boolean; + DefaultValue = NO; + DisplayName = "Build Known Localizations Only"; + Category = "Localization"; + Description = "When enabled, only builds content for languages explicitly supported by the project."; + }, // rdar://108915072 (Move Siri Category and relevant configs out of CoreBuildSystem) // Siri Settings diff --git a/Sources/SWBCore/TaskGeneration.swift b/Sources/SWBCore/TaskGeneration.swift index af1347e8..1422f914 100644 --- a/Sources/SWBCore/TaskGeneration.swift +++ b/Sources/SWBCore/TaskGeneration.swift @@ -106,6 +106,9 @@ public protocol CommandProducer: PlatformBuildContext, SpecLookupContext, Refere /// The configured target the command is being produced for, if any. var configuredTarget: ConfiguredTarget? { get } + /// The project the command is being produced for, if any. + var project: Project? { get } + /// The product type being built. (Only `StandardTarget`s have product types.) var productType: ProductTypeSpec? { get } diff --git a/Sources/SWBProjectModel/IDE/IDESwiftPackageExtensions.swift b/Sources/SWBProjectModel/IDE/IDESwiftPackageExtensions.swift index 1d4d4d26..1ca02cdb 100644 --- a/Sources/SWBProjectModel/IDE/IDESwiftPackageExtensions.swift +++ b/Sources/SWBProjectModel/IDE/IDESwiftPackageExtensions.swift @@ -42,6 +42,10 @@ extension PIF.Project : PIFRepresentable { dict[PIFKey_Project_developmentRegion] = developmentRegion } + if let knownLocalizations { + dict[PIFKey_Project_knownRegions] = knownLocalizations + } + return dict } } diff --git a/Sources/SWBProjectModel/PIFGenerationModel.swift b/Sources/SWBProjectModel/PIFGenerationModel.swift index fdbe73a4..00e96dc0 100644 --- a/Sources/SWBProjectModel/PIFGenerationModel.swift +++ b/Sources/SWBProjectModel/PIFGenerationModel.swift @@ -34,6 +34,7 @@ public enum PIF { public let id: String public let name: String public var developmentRegion: String? + public var knownLocalizations: [String]? public var path: String public let mainGroup: Group public var buildConfigs: [BuildConfig] diff --git a/Sources/SWBProtocol/PIFKeyConstants.swift b/Sources/SWBProtocol/PIFKeyConstants.swift index 59292d3c..2d4cbfd9 100644 --- a/Sources/SWBProtocol/PIFKeyConstants.swift +++ b/Sources/SWBProtocol/PIFKeyConstants.swift @@ -34,6 +34,7 @@ public let PIFKey_Project_targets = "targets" public let PIFKey_Project_groupTree = "groupTree" public let PIFKey_Project_defaultConfigurationName = "defaultConfigurationName" public let PIFKey_Project_developmentRegion = "developmentRegion" +public let PIFKey_Project_knownRegions = "knownRegions" public let PIFKey_Project_classPrefix = "classPrefix" public let PIFKey_Project_appPreferencesBuildSettings = "appPreferencesBuildSettings" diff --git a/Sources/SWBProtocol/ProjectModel/Project.swift b/Sources/SWBProtocol/ProjectModel/Project.swift index cc4d5404..aba72c5c 100644 --- a/Sources/SWBProtocol/ProjectModel/Project.swift +++ b/Sources/SWBProtocol/ProjectModel/Project.swift @@ -22,10 +22,11 @@ public struct Project: Sendable { public let buildConfigurations: [BuildConfiguration] public let defaultConfigurationName: String public let developmentRegion: String? + public let knownLocalizations: [String]? public let classPrefix: String public let appPreferencesBuildSettings: [BuildConfiguration.MacroBindingSource] - public init(guid: String, isPackage: Bool, xcodeprojPath: Path, sourceRoot: Path, targetSignatures: [String], groupTree: FileGroup, buildConfigurations: [BuildConfiguration], defaultConfigurationName: String, developmentRegion: String?, classPrefix: String, appPreferencesBuildSettings: [BuildConfiguration.MacroBindingSource] = []) { + public init(guid: String, isPackage: Bool, xcodeprojPath: Path, sourceRoot: Path, targetSignatures: [String], groupTree: FileGroup, buildConfigurations: [BuildConfiguration], defaultConfigurationName: String, developmentRegion: String?, knownLocalizations: [String]?, classPrefix: String, appPreferencesBuildSettings: [BuildConfiguration.MacroBindingSource] = []) { self.guid = guid self.isPackage = isPackage self.xcodeprojPath = xcodeprojPath @@ -35,6 +36,7 @@ public struct Project: Sendable { self.buildConfigurations = buildConfigurations self.defaultConfigurationName = defaultConfigurationName self.developmentRegion = developmentRegion + self.knownLocalizations = knownLocalizations self.classPrefix = classPrefix self.appPreferencesBuildSettings = appPreferencesBuildSettings } @@ -44,7 +46,7 @@ public struct Project: Sendable { extension Project: Serializable { public func serialize(to serializer: T) { - serializer.serializeAggregate(11) { + serializer.serializeAggregate(12) { serializer.serialize(guid) serializer.serialize(isPackage) serializer.serialize(xcodeprojPath) @@ -54,13 +56,14 @@ extension Project: Serializable { serializer.serialize(buildConfigurations) serializer.serialize(defaultConfigurationName) serializer.serialize(developmentRegion) + serializer.serialize(knownLocalizations) serializer.serialize(classPrefix) serializer.serialize(appPreferencesBuildSettings) } } public init(from deserializer: any Deserializer) throws { - try deserializer.beginAggregate(11) + try deserializer.beginAggregate(12) self.guid = try deserializer.deserialize() self.isPackage = try deserializer.deserialize() self.xcodeprojPath = try deserializer.deserialize() @@ -70,6 +73,7 @@ extension Project: Serializable { self.buildConfigurations = try deserializer.deserialize() self.defaultConfigurationName = try deserializer.deserialize() self.developmentRegion = try deserializer.deserialize() + self.knownLocalizations = try deserializer.deserialize() self.classPrefix = try deserializer.deserialize() self.appPreferencesBuildSettings = try deserializer.deserialize() } diff --git a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/ResourcesTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/ResourcesTaskProducer.swift index aee06f4f..b1c91cd2 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/ResourcesTaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/ResourcesTaskProducer.swift @@ -99,6 +99,11 @@ final class ResourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBa guard isXCStrings || group.isValidLocalizedContent(scope) else { return } } + // Check if we should filter localizations based on BUILD_ONLY_KNOWN_LOCALIZATIONS: + guard group.buildSettingAllowsBuildingLocale(scope, in: context.project, delegate) else { + return + } + // Compute the path to the effective localized directories (.lproj) in the resources and temp resources directories to define the output file for the tool. let assetPackInfo = context.onDemandResourcesAssetPack(for: group) @@ -172,6 +177,11 @@ final class ResourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBa } await appendGeneratedTasks(&tasks) { delegate in + // Check if we should filter localizations based on BUILD_ONLY_KNOWN_LOCALIZATIONS: + guard ftb.buildSettingAllowsBuildingLocale(scope, in: context.project, delegate) else { + return + } + // Compute the output path, taking the region into account. let assetPackInfo = context.onDemandResourcesAssetPack(for: FileToBuildGroup(nil, files: [ftb], action: nil)) let outputDir = assetPackInfo?.path ?? buildFilesContext.resourcesDir diff --git a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift index c8e6296f..2ee61fe3 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift @@ -1612,6 +1612,13 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F if isForInstallLoc { // For installLoc, we really only care about valid localized content from the sources task producer tasks = tasks.filter { $0.inputs.contains(where: { $0.path.isValidLocalizedContent(scope) || $0.path.fileExtension == "xcstrings" }) } + } else if scope.evaluate(BuiltinMacros.BUILD_ONLY_KNOWN_LOCALIZATIONS) { + // For non-installLoc builds, filter based on BUILD_ONLY_KNOWN_LOCALIZATIONS: + tasks = tasks.filter { task in + task.inputs.allSatisfy { input in + input.path.buildSettingAllowsBuildingLocale(scope, in: context.project, nil) + } + } } // Create a task to validate dependencies if that feature is enabled. @@ -1706,6 +1713,10 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F guard isXCStrings || group.isValidLocalizedContent(scope) else { return } } + guard group.buildSettingAllowsBuildingLocale(scope, in: context.project, delegate) else { + return + } + // Compute the resources directory. let resourcesDir = buildFilesContext.resourcesDir.join(group.regionVariantPathComponent) diff --git a/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift index 115a5396..71d638c6 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift @@ -64,7 +64,7 @@ public class TaskProducerContext: StaleFileRemovalContext, BuildFileResolution var phase: TaskProducerPhase = .none /// The project this context is for. - let project: Project? + public let project: Project? /// The high-level global build information. package let globalProductPlan: GlobalProductPlan diff --git a/Sources/SWBTestSupport/DummyCommandProducer.swift b/Sources/SWBTestSupport/DummyCommandProducer.swift index def5fdee..6dfe5147 100644 --- a/Sources/SWBTestSupport/DummyCommandProducer.swift +++ b/Sources/SWBTestSupport/DummyCommandProducer.swift @@ -22,6 +22,9 @@ package struct MockCommandProducer: CommandProducer, Sendable { package let platform: Platform? package let sdk: SDK? package let sdkVariant: SDKVariant? + package var project: SWBCore.Project? { + return nil + } package var specRegistry: SpecRegistry { return core.specRegistry } diff --git a/Sources/SWBTestSupport/TestWorkspaces.swift b/Sources/SWBTestSupport/TestWorkspaces.swift index da442f11..9d550ea4 100644 --- a/Sources/SWBTestSupport/TestWorkspaces.swift +++ b/Sources/SWBTestSupport/TestWorkspaces.swift @@ -1288,6 +1288,7 @@ package class TestProject: TestInternalObjectItem, @unchecked Sendable { package let sourceRoot: Path? private let defaultConfigurationName: String private let developmentRegion: String + private let knownLocalizations: [String]? private let buildConfigurations: [TestBuildConfiguration] package let targets: [any TestTarget] fileprivate var _targets: [any TestInternalTarget] { @@ -1304,12 +1305,13 @@ package class TestProject: TestInternalObjectItem, @unchecked Sendable { return overriddenGuid ?? "\(type(of: self).guidCode)\(guidIdentifier)" } - package init(_ name: String, guid: String? = nil, sourceRoot: Path? = nil, defaultConfigurationName: String? = nil, groupTree: TestGroup, buildConfigurations: [TestBuildConfiguration]? = nil, targets: [any TestTarget] = [], developmentRegion: String? = nil, classPrefix: String = "", appPreferencesBuildSettings: [String: String] = [:]) { + package init(_ name: String, guid: String? = nil, sourceRoot: Path? = nil, defaultConfigurationName: String? = nil, groupTree: TestGroup, buildConfigurations: [TestBuildConfiguration]? = nil, targets: [any TestTarget] = [], developmentRegion: String? = nil, knownLocalizations: [String]? = nil, classPrefix: String = "", appPreferencesBuildSettings: [String: String] = [:]) { self.name = name self.overriddenGuid = guid self.sourceRoot = sourceRoot self.defaultConfigurationName = defaultConfigurationName ?? buildConfigurations?.first?.name ?? "Release" self.developmentRegion = developmentRegion ?? "English" + self.knownLocalizations = knownLocalizations self.buildConfigurations = buildConfigurations ?? [TestBuildConfiguration("Debug")] self.targets = targets self.groupTree = groupTree @@ -1334,7 +1336,7 @@ package class TestProject: TestInternalObjectItem, @unchecked Sendable { fileprivate func toProtocol(_ resolver: any Resolver) throws -> SWBProtocol.Project { let path = getPath(resolver) - return try SWBProtocol.Project(guid: guid, isPackage: isPackage, xcodeprojPath: path, sourceRoot: sourceRoot ?? path.dirname, targetSignatures: _targets.map{ $0.signature }, groupTree: groupTree.toProtocol(resolver, isRoot: true), buildConfigurations: buildConfigurations.map{ try $0.toProtocol(resolver) }, defaultConfigurationName: defaultConfigurationName, developmentRegion: developmentRegion, classPrefix: classPrefix, appPreferencesBuildSettings: appPreferencesBuildSettings.map{ .init(key: $0.0, value: .string($0.1)) }) + return try SWBProtocol.Project(guid: guid, isPackage: isPackage, xcodeprojPath: path, sourceRoot: sourceRoot ?? path.dirname, targetSignatures: _targets.map{ $0.signature }, groupTree: groupTree.toProtocol(resolver, isRoot: true), buildConfigurations: buildConfigurations.map{ try $0.toProtocol(resolver) }, defaultConfigurationName: defaultConfigurationName, developmentRegion: developmentRegion, knownLocalizations: knownLocalizations, classPrefix: classPrefix, appPreferencesBuildSettings: appPreferencesBuildSettings.map{ .init(key: $0.0, value: .string($0.1)) }) } } diff --git a/Tests/SWBTaskConstructionTests/InstallLocTaskConstructionTests.swift b/Tests/SWBTaskConstructionTests/InstallLocTaskConstructionTests.swift index 83432e46..0ce74281 100644 --- a/Tests/SWBTaskConstructionTests/InstallLocTaskConstructionTests.swift +++ b/Tests/SWBTaskConstructionTests/InstallLocTaskConstructionTests.swift @@ -150,6 +150,139 @@ fileprivate struct InstallLocTaskConstructionTests: CoreBasedTests { } } + // The same test as installLocBasic, but with knownLocalizations, which + // should be ignored in favor of installLoc settings. + @Test(.requireSDKs(.macOS)) + func installLocLanguageIgnoresKnownLocalizations() async throws { + let testProject = try await TestProject( + "aProject", + groupTree: TestGroup( + "SomeFiles", path: "Sources", + children: [ + TestVariantGroup("foo.xib", children: [ + TestFile("Base.lproj/foo.xib", regionVariantName: "Base"), + TestFile("en.lproj/foo.strings", regionVariantName: "en"), + TestFile("de.lproj/foo.strings", regionVariantName: "de"), + ]), + TestVariantGroup("Localizable.strings", children: [ + TestFile("en.lproj/Localizable.strings", regionVariantName: "en"), + TestFile("de.lproj/Localizable.strings", regionVariantName: "de"), + TestFile("ja.lproj/Localizable.strings", regionVariantName: "ja"), + ]), + TestVariantGroup("Main.storyboard", children: [ + TestFile("Base.lproj/Main.storyboard", regionVariantName: "Base"), + TestFile("en.lproj/Main.strings", regionVariantName: "en"), + TestFile("de.lproj/Main.strings", regionVariantName: "de"), + TestFile("ja.lproj/Main.strings", regionVariantName: "ja"), + ]), + TestVariantGroup("Intents.intentdefinition", children: [ + TestFile("Base.lproj/Intents.intentdefinition", regionVariantName: "Base"), + TestFile("de.lproj/Intents.strings", regionVariantName: "de"), + TestFile("ja.lproj/Intents.strings", regionVariantName: "ja"), + ]) + ]), + buildConfigurations: [ + TestBuildConfiguration( + "Debug", + buildSettings: [ + "IBC_EXEC": ibtoolPath.str, + "PRODUCT_NAME": "$(TARGET_NAME)", + "BUILD_ONLY_KNOWN_LOCALIZATIONS": "YES", + ]), + ], + targets: [ + TestStandardTarget( + "App", + type: .application, + buildPhases: [ + TestResourcesBuildPhase([ + "foo.xib", + "Localizable.strings", + "Main.storyboard" + ]), + TestSourcesBuildPhase([ + // Intent definition files appear in the "Compile Sources" phase + "Intents.intentdefinition" + ]), + ]) + ], + knownLocalizations: ["en", "de", "Base"]) + let tester = try await TaskConstructionTester(getCore(), testProject) + + await tester.checkBuild(BuildParameters(action: .installLoc, configuration: "Debug"), runDestination: .macOS) { results in + // Ignore all Gate tasks. + results.checkTasks(.matchRuleType("Gate")) { _ in } + // Ignore all build directory tasks + results.checkTasks(.matchRuleType("CreateBuildDirectory")) { _ in } + results.checkTasks(.matchRuleType("LinkStoryboards")) { _ in } + results.checkTasks(.matchRuleType("WriteAuxiliaryFile")) { _ in } + + results.checkTarget("App") { target in + results.checkTask(.matchTarget(target), .matchRule(["MkDir", "/tmp/aProject.dst/Applications/App.app"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["MkDir", "/tmp/aProject.dst/Applications/App.app/Contents"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["MkDir", "/tmp/aProject.dst/Applications/App.app/Contents/Resources"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["SymLink", "/tmp/Test/aProject/build/Debug/App.app", "../../../../aProject.dst/Applications/App.app"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["CompileXIB", "/tmp/Test/aProject/Sources/Base.lproj/foo.xib"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/aProject.dst/Applications/App.app/Contents/Resources/en.lproj/Localizable.strings", "/tmp/Test/aProject/Sources/en.lproj/Localizable.strings"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/aProject.dst/Applications/App.app/Contents/Resources/ja.lproj/Localizable.strings", "/tmp/Test/aProject/Sources/ja.lproj/Localizable.strings"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/aProject.dst/Applications/App.app/Contents/Resources/de.lproj/Localizable.strings", "/tmp/Test/aProject/Sources/de.lproj/Localizable.strings"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/aProject.dst/Applications/App.app/Contents/Resources/de.lproj/Intents.strings", "/tmp/Test/aProject/Sources/de.lproj/Intents.strings"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/aProject.dst/Applications/App.app/Contents/Resources/ja.lproj/Intents.strings", "/tmp/Test/aProject/Sources/ja.lproj/Intents.strings"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["CompileStoryboard", "/tmp/Test/aProject/Sources/Base.lproj/Main.storyboard"])) { _ in } + if SWBFeatureFlag.enableDefaultInfoPlistTemplateKeys.value { + results.checkTask(.matchTarget(target), .matchRule(["WriteAuxiliaryFile", "/tmp/Test/aProject/build/aProject.build/Debug/App.build/empty.plist"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["ProcessInfoPlistFile", "/tmp/aProject.dst/Applications/App.app/Contents/Info.plist", "/tmp/Test/aProject/build/aProject.build/Debug/App.build/empty.plist"])) { _ in } + } + } + results.checkNoTask() + results.checkNoDiagnostics() + } + + // INSTALLLOC_LANGUAGE set to "de" should only have de.lproj/foo.strings and not Base.lproj/foo.xib in the output. + await tester.checkBuild(BuildParameters(action: .installLoc, configuration: "Debug", overrides: ["INSTALLLOC_LANGUAGE": "de"]), runDestination: .macOS) { results in + // Ignore all Gate, build directory, MkDir, and SymLink tasks. + results.checkTasks(.matchRuleType("Gate")) { _ in } + results.checkTasks(.matchRuleType("CreateBuildDirectory")) { _ in } + results.checkTasks(.matchRuleType("MkDir")) { _ in } + results.checkTasks(.matchRuleType("SymLink")) { _ in } + results.checkTasks(.matchRuleType("LinkStoryboards")) { _ in } + results.checkTasks(.matchRuleType("WriteAuxiliaryFile")) { _ in } + + results.checkTarget("App") { target in + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/aProject.dst/Applications/App.app/Contents/Resources/de.lproj/foo.strings", "/tmp/Test/aProject/Sources/de.lproj/foo.strings"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/aProject.dst/Applications/App.app/Contents/Resources/de.lproj/Localizable.strings", "/tmp/Test/aProject/Sources/de.lproj/Localizable.strings"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/aProject.dst/Applications/App.app/Contents/Resources/de.lproj/Main.strings", "/tmp/Test/aProject/Sources/de.lproj/Main.strings"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/aProject.dst/Applications/App.app/Contents/Resources/de.lproj/Intents.strings", "/tmp/Test/aProject/Sources/de.lproj/Intents.strings"])) { _ in } + } + results.checkNoTask() + results.checkNoDiagnostics() + } + + // INSTALLLOC_LANGUAGE can also be set to a string list of language codes + // These would generally contain all languages other than English, but could differ. + await tester.checkBuild(BuildParameters(action: .installLoc, configuration: "Debug", overrides: ["INSTALLLOC_LANGUAGE": "de ja"]), runDestination: .macOS) { results in + // Ignore all Gate, build directory, MkDir, and SymLink tasks. + results.checkTasks(.matchRuleType("Gate")) { _ in } + results.checkTasks(.matchRuleType("CreateBuildDirectory")) { _ in } + results.checkTasks(.matchRuleType("MkDir")) { _ in } + results.checkTasks(.matchRuleType("SymLink")) { _ in } + results.checkTasks(.matchRuleType("LinkStoryboards")) { _ in } + results.checkTasks(.matchRuleType("WriteAuxiliaryFile")) { _ in } + + results.checkTarget("App") { target in + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/aProject.dst/Applications/App.app/Contents/Resources/de.lproj/foo.strings", "/tmp/Test/aProject/Sources/de.lproj/foo.strings"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/aProject.dst/Applications/App.app/Contents/Resources/de.lproj/Localizable.strings", "/tmp/Test/aProject/Sources/de.lproj/Localizable.strings"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/aProject.dst/Applications/App.app/Contents/Resources/ja.lproj/Localizable.strings", "/tmp/Test/aProject/Sources/ja.lproj/Localizable.strings"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/aProject.dst/Applications/App.app/Contents/Resources/de.lproj/Main.strings", "/tmp/Test/aProject/Sources/de.lproj/Main.strings"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/aProject.dst/Applications/App.app/Contents/Resources/ja.lproj/Main.strings", "/tmp/Test/aProject/Sources/ja.lproj/Main.strings"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/aProject.dst/Applications/App.app/Contents/Resources/de.lproj/Intents.strings", "/tmp/Test/aProject/Sources/de.lproj/Intents.strings"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/aProject.dst/Applications/App.app/Contents/Resources/ja.lproj/Intents.strings", "/tmp/Test/aProject/Sources/ja.lproj/Intents.strings"])) { _ in } + } + results.checkNoTask() + results.checkNoDiagnostics() + } + } + /// Check that we don't generate any tasks for any resources that don't exist in an lproj. @Test(.requireSDKs(.macOS)) func installLocIgnoreUnlocalizedResources() async throws { diff --git a/Tests/SWBTaskConstructionTests/ResourceTaskConstructionTests.swift b/Tests/SWBTaskConstructionTests/ResourceTaskConstructionTests.swift index 48e8e1de..10b51330 100644 --- a/Tests/SWBTaskConstructionTests/ResourceTaskConstructionTests.swift +++ b/Tests/SWBTaskConstructionTests/ResourceTaskConstructionTests.swift @@ -2872,6 +2872,219 @@ fileprivate struct ResourcesTaskConstructionTests: CoreBasedTests { } } + @Test(.requireSDKs(.macOS)) + func interfaceBuilderCompilers_LegacyLocalization_WithKnownLocalizationsFilter() async throws { + let testProject = try await TestProject( + "aProject", + groupTree: TestGroup( + "SomeFiles", path: "Sources", + children: [ + TestFile("Info.plist"), + TestFile("en.lproj/Foo.xib", regionVariantName: "en"), + TestFile("de.lproj/Foo.xib", regionVariantName: "de"), + TestFile("ja.lproj/Foo.xib", regionVariantName: "ja"), + TestFile("fr.lproj/Foo.xib", regionVariantName: "fr"), + ]), + buildConfigurations: [ + TestBuildConfiguration( + "Debug", + buildSettings: [ + "IBC_EXEC": ibtoolPath.str, + "PRODUCT_NAME": "$(TARGET_NAME)", + "BUILD_ONLY_KNOWN_LOCALIZATIONS": "YES", + ]), + ], + targets: [ + TestStandardTarget( + "App", + type: .application, + buildConfigurations: [ + TestBuildConfiguration("Debug", + buildSettings: [ + "SDKROOT": "macosx", + "INFOPLIST_FILE": "Sources/Info.plist", + ]), + ], + buildPhases: [ + TestResourcesBuildPhase([ + "en.lproj/Foo.xib", + "de.lproj/Foo.xib", + "ja.lproj/Foo.xib", + "fr.lproj/Foo.xib", + ]), + ]) + ], + developmentRegion: "en", + knownLocalizations: ["en", "de", "Base"] // only build for en and de + ) + let tester = try await TaskConstructionTester(getCore(), testProject) + let SRCROOT = tester.workspace.projects[0].sourceRoot.str + + let ibtoolPath = try await self.ibtoolPath + + await tester.checkBuild(runDestination: .macOS) { results in + results.checkTarget("App") { target in + // Check that en.lproj/Foo.xib is compiled: + results.checkTask(.matchTarget(target), .matchRule(["CompileXIB", "\(SRCROOT)/Sources/en.lproj/Foo.xib"])) { task in + task.checkCommandLineContains([ibtoolPath.str, "--compile", "\(SRCROOT)/build/Debug/App.app/Contents/Resources/en.lproj/Foo.nib", "\(SRCROOT)/Sources/en.lproj/Foo.xib"]) + } + + // Check that de.lproj/Foo.xib is compiled: + results.checkTask(.matchTarget(target), .matchRule(["CompileXIB", "\(SRCROOT)/Sources/de.lproj/Foo.xib"])) { task in + task.checkCommandLineContains([ibtoolPath.str, "--compile", "\(SRCROOT)/build/Debug/App.app/Contents/Resources/de.lproj/Foo.nib", "\(SRCROOT)/Sources/de.lproj/Foo.xib"]) + } + + // Check that ja.lproj and fr.lproj are NOT compiled: + results.checkNoTask(.matchTarget(target), .matchRule(["CompileXIB", "\(SRCROOT)/Sources/ja.lproj/Foo.xib"])) + results.checkNoTask(.matchTarget(target), .matchRule(["CompileXIB", "\(SRCROOT)/Sources/fr.lproj/Foo.xib"])) + } + + // Check for notes about skipped localizations: + results.checkNote(.contains("Skipping .lproj directory 'ja.lproj' because 'ja' is not in project's known localizations")) + results.checkNote(.contains("Skipping .lproj directory 'fr.lproj' because 'fr' is not in project's known localizations")) + results.checkNoDiagnostics() + } + } + + @Test(.requireSDKs(.macOS)) + func interfaceBuilderCompilers_FullLocalization_WithKnownLocalizationsFilter() async throws { + let testProject = try await TestProject( + "aProject", + groupTree: TestGroup( + "SomeFiles", path: "Sources", + children: [ + TestVariantGroup("foo.xib", + children: [ + TestFile("Base.lproj/foo.xib", regionVariantName: "Base"), + TestFile("fr.lproj/foo.xib", regionVariantName: "fr"), + TestFile("ja.lproj/foo.strings", regionVariantName: "ja"), + TestFile("en.lproj/foo.strings", regionVariantName: "en"), + TestFile("de.lproj/foo.strings", regionVariantName: "de"), + ] + ), + ]), + buildConfigurations: [ + TestBuildConfiguration( + "Debug", + buildSettings: [ + "GENERATE_INFOPLIST_FILE": "YES", + "CODE_SIGN_IDENTITY": "", + "PRODUCT_NAME": "$(TARGET_NAME)", + "BUILD_ONLY_KNOWN_LOCALIZATIONS": "YES", + ]), + ], + targets: [ + TestStandardTarget( + "App", + type: .application, + buildConfigurations: [ + TestBuildConfiguration("Debug", + buildSettings: [ + "SDKROOT": "macosx", + "IBC_EXEC": ibtoolPath.str, + ]), + ], + buildPhases: [ + TestResourcesBuildPhase([ + "foo.xib", + ]), + ]) + ], + developmentRegion: "en", + knownLocalizations: ["en", "de", "Base"] + ) + let core = try await getCore() + let tester = try TaskConstructionTester(core, testProject) + let SRCROOT = tester.workspace.projects[0].sourceRoot.str + + let ibtoolPath = try await self.ibtoolPath + + await tester.checkBuild(runDestination: .macOS) { results in + results.checkTarget("App") { target in + // Base.lproj/foo.xib should compile with only en and de companion strings: + results.checkTask(.matchTarget(target), .matchRuleType("CompileXIB"), .matchRuleItem("\(SRCROOT)/Sources/Base.lproj/foo.xib")) { task in + task.checkRuleInfo(["CompileXIB", "\(SRCROOT)/Sources/Base.lproj/foo.xib"]) + task.checkCommandLine([ibtoolPath.str, "--errors", "--warnings", "--notices", "--companion-strings-file", "en:\(SRCROOT)/Sources/en.lproj/foo.strings", "--companion-strings-file", "de:\(SRCROOT)/Sources/de.lproj/foo.strings", "--module", "App", "--output-partial-info-plist", "\(SRCROOT)/build/aProject.build/Debug/App.build/Base.lproj/foo-PartialInfo.plist", "--auto-activate-custom-fonts", "--target-device", "mac", "--minimum-deployment-target", "\(core.loadSDK(.macOS).defaultDeploymentTarget)", "--output-format", "human-readable-text", "--compile", "\(SRCROOT)/build/Debug/App.app/Contents/Resources/Base.lproj/foo.nib", "\(SRCROOT)/Sources/Base.lproj/foo.xib"]) + } + + // fr.lproj and ja.lproj should NOT be compiled: + results.checkNoTask(.matchTarget(target), .matchRuleType("CompileXIB"), .matchRuleItem("\(SRCROOT)/Sources/fr.lproj/foo.xib")) + results.checkNoTask(.matchTarget(target), .matchRuleType("CompileXIB"), .matchRuleItem("\(SRCROOT)/Sources/ja.lproj/foo.strings")) + } + + // Check for notes about skipped localizations: + results.checkNote(.contains("Skipping .lproj directory 'fr.lproj' because 'fr' is not in project's known localizations")) + results.checkNote(.contains("Skipping .lproj directory 'ja.lproj' because 'ja' is not in project's known localizations")) + results.checkNoDiagnostics() + } + } + + @Test(.requireSDKs(.macOS)) + func variantGroupResourceCopyingWithKnownLocalizationsFilter() async throws { + let testProject = TestProject( + "aProject", + groupTree: TestGroup( + "SomeFiles", path: "Sources", + children: [ + TestFile("Info.plist"), + TestVariantGroup("Localizable.strings", children: [ + TestFile("en.lproj/Localizable.strings", regionVariantName: "en"), + TestFile("de.lproj/Localizable.strings", regionVariantName: "de"), + TestFile("ja.lproj/Localizable.strings", regionVariantName: "ja"), + TestFile("fr.lproj/Localizable.strings", regionVariantName: "fr"), + ]), + ]), + buildConfigurations: [ + TestBuildConfiguration( + "Debug", + buildSettings: [ + "GENERATE_INFOPLIST_FILE": "YES", + "PRODUCT_NAME": "$(TARGET_NAME)", + "BUILD_ONLY_KNOWN_LOCALIZATIONS": "YES", + ]), + ], + targets: [ + TestStandardTarget( + "App", + type: .application, + buildConfigurations: [ + TestBuildConfiguration("Debug", + buildSettings: [ + "SDKROOT": "macosx", + ]), + ], + buildPhases: [ + TestResourcesBuildPhase([ + "Localizable.strings", + ]), + ]) + ], + developmentRegion: "en", + knownLocalizations: ["en", "de", "Base"] + ) + let tester = try await TaskConstructionTester(getCore(), testProject) + let SRCROOT = tester.workspace.projects[0].sourceRoot.str + + await tester.checkBuild(runDestination: .macOS) { results in + results.checkTarget("App") { target in + // Check that en.lproj/Localizable.strings is copied: + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "\(SRCROOT)/build/Debug/App.app/Contents/Resources/en.lproj/Localizable.strings", "\(SRCROOT)/Sources/en.lproj/Localizable.strings"])) { _ in } + + // Check that de.lproj/Localizable.strings is copied: + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "\(SRCROOT)/build/Debug/App.app/Contents/Resources/de.lproj/Localizable.strings", "\(SRCROOT)/Sources/de.lproj/Localizable.strings"])) { _ in } + + // Check that ja.lproj and fr.lproj are NOT copied: + results.checkNoTask(.matchTarget(target), .matchRuleItem("\(SRCROOT)/Sources/ja.lproj/Localizable.strings")) + results.checkNoTask(.matchTarget(target), .matchRuleItem("\(SRCROOT)/Sources/fr.lproj/Localizable.strings")) + } + + // Check for notes about skipped localizations: + results.checkNote(.contains("Skipping .lproj directory 'ja.lproj' because 'ja' is not in project's known localizations")) + results.checkNote(.contains("Skipping .lproj directory 'fr.lproj' because 'fr' is not in project's known localizations")) + results.checkNoDiagnostics() + } + } + } private func XCTAssertEqual(_ lhs: EnvironmentBindings, _ rhs: [String: String], file: StaticString = #filePath, line: UInt = #line) { diff --git a/Tests/SWBTaskConstructionTests/XCStringsTaskConstructionTests.swift b/Tests/SWBTaskConstructionTests/XCStringsTaskConstructionTests.swift index d1af5e84..cff1018b 100644 --- a/Tests/SWBTaskConstructionTests/XCStringsTaskConstructionTests.swift +++ b/Tests/SWBTaskConstructionTests/XCStringsTaskConstructionTests.swift @@ -621,6 +621,99 @@ fileprivate struct XCStringsTaskConstructionTests: CoreBasedTests { } } + @Test(.requireSDKs(.macOS)) + func compileMixedProject_WithKnownLocalizations() async throws { + let testProject = try await TestProject( + "Project", + groupTree: TestGroup( + "ProjectSources", + path: "Sources", + children: [ + TestFile("MyFramework.swift"), + TestFile("Localizable.xcstrings"), + TestVariantGroup("CustomTable.strings", children: [ + TestFile("en.lproj/CustomTable.strings", regionVariantName: "en"), + TestFile("de.lproj/CustomTable.strings", regionVariantName: "de"), + TestFile("ja.lproj/CustomTable.strings", regionVariantName: "ja"), + ]), + ] + ), + buildConfigurations: [ + TestBuildConfiguration("Debug", buildSettings: [ + "PRODUCT_NAME": "$(TARGET_NAME)", + "BUILD_ONLY_KNOWN_LOCALIZATIONS": "YES", + ]) + ], + targets: [ + TestStandardTarget( + "MyFramework", + type: .framework, + buildConfigurations: [ + TestBuildConfiguration("Debug", buildSettings: [ + "SKIP_INSTALL": "YES", + "SWIFT_EXEC": swiftCompilerPath.str, + "SWIFT_VERSION": "5.5", + "GENERATE_INFOPLIST_FILE": "YES", + ]), + ], + buildPhases: [ + TestSourcesBuildPhase([ + "MyFramework.swift" + ]), + TestResourcesBuildPhase([ + "Localizable.xcstrings", + "CustomTable.strings" + ]) + ] + ) + ], + developmentRegion: "en", + knownLocalizations: ["en", "de", "Base"] + ) + + // Pretend our xcstrings file contains English, German, and Japanese strings. + let xcstringsTool = MockXCStringsTool(relativeOutputFilePaths: [ + "/tmp/Test/Project/Sources/Localizable.xcstrings" : [ + "en.lproj/Localizable.strings", + "de.lproj/Localizable.strings", + "ja.lproj/Localizable.strings", + ], + ], requiredCommandLine: nil) + + let tester = try await TaskConstructionTester(getCore(), testProject) + + await tester.checkBuild(runDestination: .macOS, clientDelegate: xcstringsTool) { results in + results.checkTarget("MyFramework") { target in + // XCStrings compilation should include -l flags for known localizations + results.checkTask(.matchTarget(target), .matchRule(["CompileXCStrings", "/tmp/Test/Project/build/Project.build/Debug/MyFramework.build/", "/tmp/Test/Project/Sources/Localizable.xcstrings"])) { task in + task.checkCommandLineContains(["-l", "en"]) + task.checkCommandLineContains(["-l", "de"]) + task.checkCommandLineDoesNotContain("-l ja") + } + results.checkNoTask(.matchTarget(target), .matchRuleType("CompileXCStrings")) + + // Only en and de CopyStringsFile tasks for xcstrings outputs + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/Test/Project/build/Debug/MyFramework.framework/Versions/A/Resources/en.lproj/Localizable.strings", "/tmp/Test/Project/build/Project.build/Debug/MyFramework.build/en.lproj/Localizable.strings"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/Test/Project/build/Debug/MyFramework.framework/Versions/A/Resources/de.lproj/Localizable.strings", "/tmp/Test/Project/build/Project.build/Debug/MyFramework.build/de.lproj/Localizable.strings"])) { _ in } + + // Only en and de for CustomTable.strings (variant group) + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/Test/Project/build/Debug/MyFramework.framework/Versions/A/Resources/en.lproj/CustomTable.strings", "/tmp/Test/Project/Sources/en.lproj/CustomTable.strings"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/Test/Project/build/Debug/MyFramework.framework/Versions/A/Resources/de.lproj/CustomTable.strings", "/tmp/Test/Project/Sources/de.lproj/CustomTable.strings"])) { _ in } + + // No Japanese files + results.checkNoTask(.matchTarget(target), .matchRuleType("CopyStringsFile")) + } + + // Check for note about XCStrings language filtering + results.checkNote(.contains("XCStrings will compile languages for known regions: en, de")) + + // Check for note about skipped localization + results.checkNote(.contains("Skipping .lproj directory 'ja.lproj' because 'ja' is not in project's known localizations")) + + results.checkNoDiagnostics() + } + } + // .xcstrings and .strings/dict files in the same project are not allowed to have table overlap. @Test(.requireSDKs(.macOS)) func compileMixedProjectWithTableOverlap() async throws { @@ -970,7 +1063,8 @@ fileprivate struct XCStringsTaskConstructionTests: CoreBasedTests { ] ) ], - developmentRegion: "en" + developmentRegion: "en", + knownLocalizations: ["Base", "de"] // ignored since installloc languages take precedence in an installloc phase ) // Pretend our xcstrings file contains English and German strings, and that they have variations. @@ -1059,7 +1153,8 @@ fileprivate struct XCStringsTaskConstructionTests: CoreBasedTests { ] ) ], - developmentRegion: "en" + developmentRegion: "en", + knownLocalizations: ["Base", "de"] // ignored since installloc languages take precedence in an installloc phase ) // Pretend our xcstrings file contains English, German, and Japanese strings, and that they have variations. @@ -1476,6 +1571,123 @@ fileprivate struct XCStringsTaskConstructionTests: CoreBasedTests { } } + @Test(.requireSDKs(.macOS)) + func xcstringsInVariantGroup_WithKnownLocalizations() async throws { + let testProject = try await TestProject( + "Project", + groupTree: TestGroup( + "ProjectSources", + path: "Sources", + children: [ + TestFile("MyFramework.swift"), + TestVariantGroup("LegacyView.xib", children: [ + TestFile("Base.lproj/LegacyView.xib", regionVariantName: "Base"), + TestFile("fr.lproj/LegacyView.strings", regionVariantName: "fr"), + TestFile("de.lproj/LegacyView.strings", regionVariantName: "de"), + ]), + TestVariantGroup("View.xib", children: [ + TestFile("Base.lproj/View.xib", regionVariantName: "Base"), + TestFile("mul.lproj/View.xcstrings", regionVariantName: "mul"), + ]), + TestVariantGroup("OtherView.nib", children: [ + TestFile("Base.lproj/OtherView.nib", regionVariantName: "Base"), + TestFile("mul.lproj/OtherView.xcstrings", regionVariantName: "mul"), + ]), + TestVariantGroup("Main.storyboard", children: [ + TestFile("Base.lproj/Main.storyboard", regionVariantName: "Base"), + TestFile("mul.lproj/Main.xcstrings", regionVariantName: "mul"), + ]) + ] + ), + buildConfigurations: [ + TestBuildConfiguration("Debug", buildSettings: [ + "IBC_EXEC": ibtoolPath.str, + "PRODUCT_NAME": "$(TARGET_NAME)", + "BUILD_ONLY_KNOWN_LOCALIZATIONS": "YES", + ]) + ], + targets: [ + TestStandardTarget( + "MyFramework", + type: .framework, + buildConfigurations: [ + TestBuildConfiguration("Debug", buildSettings: [ + "SKIP_INSTALL": "YES", + "SWIFT_EXEC": swiftCompilerPath.str, + "SWIFT_VERSION": "5.5", + "GENERATE_INFOPLIST_FILE": "YES", + ]), + ], + buildPhases: [ + TestSourcesBuildPhase([ + "MyFramework.swift" + ]), + TestResourcesBuildPhase([ + "LegacyView.xib", + "View.xib", + "OtherView.nib", + "Main.storyboard", + ]) + ] + ) + ], + developmentRegion: "en", + knownLocalizations: ["en", "fr", "Base"] + ) + + // Pretend our xcstrings files contain French and German strings. + let xcstringsTool = MockXCStringsTool(relativeOutputFilePaths: [ + "/tmp/Test/Project/Sources/mul.lproj/View.xcstrings" : [ + "fr.lproj/View.strings", + "de.lproj/View.strings", + ], + "/tmp/Test/Project/Sources/mul.lproj/OtherView.xcstrings" : [ + "fr.lproj/OtherView.strings", + "de.lproj/OtherView.strings", + ], + "/tmp/Test/Project/Sources/mul.lproj/Main.xcstrings" : [ + "fr.lproj/Main.strings", + "de.lproj/Main.strings", + ], + ], requiredCommandLine: nil) + + let tester = try await TaskConstructionTester(getCore(), testProject) + + await tester.checkBuild(runDestination: .macOS, clientDelegate: xcstringsTool) { results in + results.checkTarget("MyFramework") { target in + // IB files should compile + results.checkTask(.matchTarget(target), .matchRule(["CompileXIB", "/tmp/Test/Project/Sources/Base.lproj/LegacyView.xib"])) { task in + task.checkCommandLineContains(["--companion-strings-file", "fr:/tmp/Test/Project/Sources/fr.lproj/LegacyView.strings"]) + } + results.checkTask(.matchTarget(target), .matchRule(["CompileXIB", "/tmp/Test/Project/Sources/Base.lproj/View.xib"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["CompileStoryboard", "/tmp/Test/Project/Sources/Base.lproj/Main.storyboard"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["LinkStoryboards"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["CpResource", "/tmp/Test/Project/build/Debug/MyFramework.framework/Versions/A/Resources/Base.lproj/OtherView.nib", "/tmp/Test/Project/Sources/Base.lproj/OtherView.nib"])) { _ in } + + // Each xcstrings should compile with language filtering + results.checkTask(.matchTarget(target), .matchRule(["CompileXCStrings", "/tmp/Test/Project/build/Project.build/Debug/MyFramework.build/", "/tmp/Test/Project/Sources/mul.lproj/View.xcstrings"])) { task in + task.checkCommandLineContains(["-l", "fr"]) + task.checkCommandLineDoesNotContain("-l de") + } + results.checkTask(.matchTarget(target), .matchRule(["CompileXCStrings", "/tmp/Test/Project/build/Project.build/Debug/MyFramework.build/", "/tmp/Test/Project/Sources/mul.lproj/OtherView.xcstrings"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["CompileXCStrings", "/tmp/Test/Project/build/Project.build/Debug/MyFramework.build/", "/tmp/Test/Project/Sources/mul.lproj/Main.xcstrings"])) { _ in } + + // Only French xcstrings outputs should be copied (not German) + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/Test/Project/build/Debug/MyFramework.framework/Versions/A/Resources/fr.lproj/View.strings", "/tmp/Test/Project/build/Project.build/Debug/MyFramework.build/fr.lproj/View.strings"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/Test/Project/build/Debug/MyFramework.framework/Versions/A/Resources/fr.lproj/OtherView.strings", "/tmp/Test/Project/build/Project.build/Debug/MyFramework.build/fr.lproj/OtherView.strings"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/Test/Project/build/Debug/MyFramework.framework/Versions/A/Resources/fr.lproj/Main.strings", "/tmp/Test/Project/build/Project.build/Debug/MyFramework.build/fr.lproj/Main.strings"])) { _ in } + + // LegacyView should not have CopyStringsFile tasks because ibtool is responsible for copying those .strings files: + results.checkNoTask(.matchTarget(target), .matchRuleType("CopyStringsFile")) + } + + // Check for note about skipped localizations + results.checkNote(.contains("Skipping .lproj directory 'de.lproj' because 'de' is not in project's known localizations")) + + results.checkNoDiagnostics() + } + } + @Test(.requireSDKs(.macOS)) func xcstringsInVariantGroupDuringInstallloc() async throws { let testProject = try await TestProject( @@ -1534,7 +1746,8 @@ fileprivate struct XCStringsTaskConstructionTests: CoreBasedTests { ] ) ], - developmentRegion: "en" + developmentRegion: "en", + knownLocalizations: ["Base", "de"] // ignored since installloc languages take precedence in an installloc phase ) // Pretend our xcstrings file contains English and German strings, and that they have variations. @@ -1792,4 +2005,110 @@ fileprivate struct XCStringsTaskConstructionTests: CoreBasedTests { } } + // Test BUILD_ONLY_KNOWN_LOCALIZATIONS filtering in Sources build phase with IB files and xcstrings. + @Test(.requireSDKs(.macOS)) + func sourcesPhaseKnownLocalizationsFiltering() async throws { + let testProject = try await TestProject( + "aProject", + groupTree: TestGroup( + "SomeFiles", path: "Sources", + children: [ + TestFile("MyFramework.swift"), + // a) IB file with .strings files: + TestVariantGroup("LegacyView.xib", children: [ + TestFile("Base.lproj/LegacyView.xib", regionVariantName: "Base"), + TestFile("en.lproj/LegacyView.strings", regionVariantName: "en"), + TestFile("fr.lproj/LegacyView.strings", regionVariantName: "fr"), + TestFile("de.lproj/LegacyView.strings", regionVariantName: "de"), + ]), + // b) IB file with .xcstrings: + TestVariantGroup("View.xib", children: [ + TestFile("Base.lproj/View.xib", regionVariantName: "Base"), + TestFile("mul.lproj/View.xcstrings", regionVariantName: "mul"), + ]), + // c) Variant group with .strings: + TestVariantGroup("Localizable.strings", children: [ + TestFile("en.lproj/Localizable.strings", regionVariantName: "en"), + TestFile("de.lproj/Localizable.strings", regionVariantName: "de"), + TestFile("ja.lproj/Localizable.strings", regionVariantName: "ja"), + ]), + ]), + buildConfigurations: [ + TestBuildConfiguration( + "Debug", + buildSettings: [ + "IBC_EXEC": ibtoolPath.str, + "PRODUCT_NAME": "$(TARGET_NAME)", + "BUILD_ONLY_KNOWN_LOCALIZATIONS": "YES", + ]), + ], + targets: [ + TestStandardTarget( + "MyFramework", + type: .framework, + buildConfigurations: [ + TestBuildConfiguration("Debug", buildSettings: [ + "SKIP_INSTALL": "YES", + "SWIFT_EXEC": swiftCompilerPath.str, + "SWIFT_VERSION": "5.5", + "GENERATE_INFOPLIST_FILE": "YES", + ]), + ], + buildPhases: [ + TestSourcesBuildPhase([ + "MyFramework.swift", + // IB files and strings in Sources phase: + "LegacyView.xib", + "View.xib", + "Localizable.strings", + ]), + ]) + ], + knownLocalizations: ["en", "de", "Base"]) + + // Pretend our xcstrings file contains English, French, German, and Japanese strings: + let xcstringsTool = MockXCStringsTool(relativeOutputFilePaths: [ + "/tmp/Test/aProject/Sources/mul.lproj/View.xcstrings" : [ + "en.lproj/View.strings", + "fr.lproj/View.strings", + "de.lproj/View.strings", + "ja.lproj/View.strings", + ], + ], requiredCommandLine: nil) + + let tester = try await TaskConstructionTester(getCore(), testProject) + + // Regular build: BUILD_ONLY_KNOWN_LOCALIZATIONS should filter out "ja" and "fr": + await tester.checkBuild(runDestination: .macOS, clientDelegate: xcstringsTool) { results in + results.checkTarget("MyFramework") { target in + // LegacyView.xib should only compile with en and de companion strings (not fr): + results.checkTask(.matchTarget(target), .matchRule(["CompileXIB", "/tmp/Test/aProject/Sources/Base.lproj/LegacyView.xib"])) { task in + task.checkCommandLineContains(["--companion-strings-file", "en:/tmp/Test/aProject/Sources/en.lproj/LegacyView.strings"]) + task.checkCommandLineContains(["--companion-strings-file", "de:/tmp/Test/aProject/Sources/de.lproj/LegacyView.strings"]) + task.checkCommandLineDoesNotContain("fr:/tmp/Test/aProject/Sources/fr.lproj/LegacyView.strings") + } + + // View.xib should compile (Base localization): + results.checkTask(.matchTarget(target), .matchRule(["CompileXIB", "/tmp/Test/aProject/Sources/Base.lproj/View.xib"])) { _ in } + + // xcstrings should be compiled: + results.checkTask(.matchTarget(target), .matchRule(["CompileXCStrings", "/tmp/Test/aProject/build/aProject.build/Debug/MyFramework.build/", "/tmp/Test/aProject/Sources/mul.lproj/View.xcstrings"])) { _ in } + + // Only en and de strings from xcstrings should be copied (not fr or ja): + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/Test/aProject/build/Debug/MyFramework.framework/Versions/A/Resources/en.lproj/View.strings", "/tmp/Test/aProject/build/aProject.build/Debug/MyFramework.build/en.lproj/View.strings"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/Test/aProject/build/Debug/MyFramework.framework/Versions/A/Resources/de.lproj/View.strings", "/tmp/Test/aProject/build/aProject.build/Debug/MyFramework.build/de.lproj/View.strings"])) { _ in } + results.checkNoTask(.matchRule(["CopyStringsFile", "/tmp/Test/aProject/build/Debug/MyFramework.framework/Versions/A/Resources/fr.lproj/View.strings", "/tmp/Test/aProject/build/aProject.build/Debug/MyFramework.build/fr.lproj/View.strings"])) + results.checkNoTask(.matchRule(["CopyStringsFile", "/tmp/Test/aProject/build/Debug/MyFramework.framework/Versions/A/Resources/ja.lproj/View.strings", "/tmp/Test/aProject/build/aProject.build/Debug/MyFramework.build/ja.lproj/View.strings"])) + + // Only en and de Localizable.strings should be copied (not ja): + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/Test/aProject/build/Debug/MyFramework.framework/Versions/A/Resources/en.lproj/Localizable.strings", "/tmp/Test/aProject/Sources/en.lproj/Localizable.strings"])) { _ in } + results.checkTask(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/Test/aProject/build/Debug/MyFramework.framework/Versions/A/Resources/de.lproj/Localizable.strings", "/tmp/Test/aProject/Sources/de.lproj/Localizable.strings"])) { _ in } + results.checkNoTask(.matchRule(["CopyStringsFile", "/tmp/Test/aProject/build/Debug/MyFramework.framework/Versions/A/Resources/ja.lproj/Localizable.strings", "/tmp/Test/aProject/Sources/ja.lproj/Localizable.strings"])) + } + + results.checkNote(.contains("Skipping .lproj directory 'ja.lproj'")) + results.checkNote(.contains("Skipping .lproj directory 'fr.lproj'")) + results.checkNoDiagnostics() + } + } } From 9764b31be1d21109465f52ebbacb319092a7999d Mon Sep 17 00:00:00 2001 From: Andreas Neusuess Date: Mon, 8 Dec 2025 08:51:52 -0800 Subject: [PATCH 2/2] Filter input files and add their location to notice if they don't pass the filter --- .../InterfaceBuilderShared.swift | 5 +++- Sources/SWBCore/FileToBuild.swift | 9 ++++++-- .../ResourcesTaskProducer.swift | 23 +++++++++++++++---- .../SourcesTaskProducer.swift | 21 ++++++++++++++--- 4 files changed, 48 insertions(+), 10 deletions(-) diff --git a/Sources/SWBApplePlatform/InterfaceBuilderShared.swift b/Sources/SWBApplePlatform/InterfaceBuilderShared.swift index 80bc1ffb..dd4ebdfe 100644 --- a/Sources/SWBApplePlatform/InterfaceBuilderShared.swift +++ b/Sources/SWBApplePlatform/InterfaceBuilderShared.swift @@ -70,7 +70,10 @@ extension IbtoolCompilerSupport { // Filter by knownLocalizations if BUILD_ONLY_KNOWN_LOCALIZATIONS is enabled: if let allowedLocalizations, !allowedLocalizations.contains(region) { // Skip this .strings file. - delegate.note("Skipping .lproj directory '\(region).lproj' because '\(region)' is not in project's known localizations (BUILD_ONLY_KNOWN_LOCALIZATIONS is enabled)") + if region != "mul" { + // Don't leave a note about mul.lproj + delegate.note("Skipping .lproj directory '\(region).lproj' because '\(region)' is not in project's known localizations (BUILD_ONLY_KNOWN_LOCALIZATIONS is enabled)", location: .path(ftb.absolutePath)) + } continue } diff --git a/Sources/SWBCore/FileToBuild.swift b/Sources/SWBCore/FileToBuild.swift index b9aa6237..0b0cff37 100644 --- a/Sources/SWBCore/FileToBuild.swift +++ b/Sources/SWBCore/FileToBuild.swift @@ -191,7 +191,12 @@ public extension RegionVariable { /// `BUILD_ONLY_KNOWN_LOCALIZATIONS` is disabled, or the file's region /// is present in the project's supported localizations. /// If this file should not be built, `false` is returned. - func buildSettingAllowsBuildingLocale(_ scope: MacroEvaluationScope, in project: Project?, _ delegate: (any DiagnosticProducingDelegate)?) -> Bool { + func buildSettingAllowsBuildingLocale( + _ scope: MacroEvaluationScope, + in project: Project?, + inputFileAbsolutePath: Path, + _ delegate: (any DiagnosticProducingDelegate)? + ) -> Bool { guard let project else { // We can't find the project. Allow the file to build: @@ -233,7 +238,7 @@ public extension RegionVariable { } // This region is not supported, so it shouldn't build. - delegate?.note("Skipping .lproj directory '\(regionVariantName).lproj' because '\(regionVariantName)' is not in project's known localizations (BUILD_ONLY_KNOWN_LOCALIZATIONS is enabled)") + delegate?.note("Skipping .lproj directory '\(regionVariantName).lproj' because '\(regionVariantName)' is not in project's known localizations (BUILD_ONLY_KNOWN_LOCALIZATIONS is enabled)", location: .path(inputFileAbsolutePath)) return false } } diff --git a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/ResourcesTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/ResourcesTaskProducer.swift index b1c91cd2..4b1033a5 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/ResourcesTaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/ResourcesTaskProducer.swift @@ -99,8 +99,18 @@ final class ResourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBa guard isXCStrings || group.isValidLocalizedContent(scope) else { return } } - // Check if we should filter localizations based on BUILD_ONLY_KNOWN_LOCALIZATIONS: - guard group.buildSettingAllowsBuildingLocale(scope, in: context.project, delegate) else { + var inputFiles = group.files + + // Check if we should build the input file based on BUILD_ONLY_KNOWN_LOCALIZATIONS: + inputFiles = inputFiles.filter { file in + file.buildSettingAllowsBuildingLocale( + scope, + in: context.project, + inputFileAbsolutePath: file.absolutePath, + delegate + ) + } + guard !inputFiles.isEmpty else { return } @@ -117,7 +127,7 @@ final class ResourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBa context.didProduceAssetPackSubPath(assetPackInfo, subPath) } - let cbc = CommandBuildContext(producer: context, scope: scope, inputs: group.files, isPreferredArch: buildFilesContext.belongsToPreferredArch, buildPhaseInfo: buildFilesContext.buildPhaseInfo(for: rule), resourcesDir: resourcesDir, tmpResourcesDir: tmpResourcesDir, unlocalizedResourcesDir: unlocalizedResourcesDir) + let cbc = CommandBuildContext(producer: context, scope: scope, inputs: inputFiles, isPreferredArch: buildFilesContext.belongsToPreferredArch, buildPhaseInfo: buildFilesContext.buildPhaseInfo(for: rule), resourcesDir: resourcesDir, tmpResourcesDir: tmpResourcesDir, unlocalizedResourcesDir: unlocalizedResourcesDir) await constructTasksForRule(rule, cbc, delegate) } @@ -178,7 +188,12 @@ final class ResourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBa await appendGeneratedTasks(&tasks) { delegate in // Check if we should filter localizations based on BUILD_ONLY_KNOWN_LOCALIZATIONS: - guard ftb.buildSettingAllowsBuildingLocale(scope, in: context.project, delegate) else { + guard ftb.buildSettingAllowsBuildingLocale( + scope, + in: context.project, + inputFileAbsolutePath: ftb.absolutePath, + delegate + ) else { return } diff --git a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift index 2ee61fe3..930dc0a3 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift @@ -1616,7 +1616,12 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F // For non-installLoc builds, filter based on BUILD_ONLY_KNOWN_LOCALIZATIONS: tasks = tasks.filter { task in task.inputs.allSatisfy { input in - input.path.buildSettingAllowsBuildingLocale(scope, in: context.project, nil) + input.path.buildSettingAllowsBuildingLocale( + scope, + in: context.project, + inputFileAbsolutePath: input.path, + nil + ) } } } @@ -1713,14 +1718,24 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F guard isXCStrings || group.isValidLocalizedContent(scope) else { return } } - guard group.buildSettingAllowsBuildingLocale(scope, in: context.project, delegate) else { + var inputFiles = group.files + + inputFiles = inputFiles.filter { file in + return file.buildSettingAllowsBuildingLocale( + scope, + in: context.project, + inputFileAbsolutePath: file.absolutePath, + delegate + ) + } + guard !inputFiles.isEmpty else { return } // Compute the resources directory. let resourcesDir = buildFilesContext.resourcesDir.join(group.regionVariantPathComponent) - let cbc = CommandBuildContext(producer: context, scope: scope, inputs: group.files, isPreferredArch: buildFilesContext.belongsToPreferredArch, currentArchSpec: buildFilesContext.currentArchSpec, buildPhaseInfo: buildFilesContext.buildPhaseInfo(for: rule), resourcesDir: resourcesDir, tmpResourcesDir: buildFilesContext.tmpResourcesDir, unlocalizedResourcesDir: buildFilesContext.resourcesDir) + let cbc = CommandBuildContext(producer: context, scope: scope, inputs: inputFiles, isPreferredArch: buildFilesContext.belongsToPreferredArch, currentArchSpec: buildFilesContext.currentArchSpec, buildPhaseInfo: buildFilesContext.buildPhaseInfo(for: rule), resourcesDir: resourcesDir, tmpResourcesDir: buildFilesContext.tmpResourcesDir, unlocalizedResourcesDir: buildFilesContext.resourcesDir) await constructTasksForRule(rule, cbc, delegate) }