diff --git a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/CopyFilesTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/CopyFilesTaskProducer.swift index 559c1e20..54daab89 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/CopyFilesTaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/CopyFilesTaskProducer.swift @@ -169,9 +169,15 @@ class CopyFilesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBasedBui override func constructTasksForRule(_ rule: any BuildRuleAction, _ group: FileToBuildGroup, _ buildFilesContext: BuildFilesProcessingContext, _ scope: MacroEvaluationScope, _ delegate: any TaskGenerationDelegate) async { let dstFolder = computeOutputDirectory(scope) - // FIXME: Merge the region variant. + // Merge the region variant. + // Since this behavior was not always here, some people have hardcoded lproj directories in their destination folder path. + // Only add an additional component if there isn't one already. + var locDST = dstFolder + if !locDST.containsRegionVariantPathComponent { + locDST = dstFolder.join(group.regionVariantPathComponent) + } - let cbc = CommandBuildContext(producer: context, scope: scope, inputs: group.files, isPreferredArch: buildFilesContext.belongsToPreferredArch, buildPhaseInfo: buildFilesContext.buildPhaseInfo(for: rule), resourcesDir: dstFolder, unlocalizedResourcesDir: dstFolder) + let cbc = CommandBuildContext(producer: context, scope: scope, inputs: group.files, isPreferredArch: buildFilesContext.belongsToPreferredArch, buildPhaseInfo: buildFilesContext.buildPhaseInfo(for: rule), resourcesDir: locDST, unlocalizedResourcesDir: dstFolder) await constructTasksForRule(rule, cbc, delegate) } diff --git a/Sources/SWBUtil/Path.swift b/Sources/SWBUtil/Path.swift index 28294f29..94bf7134 100644 --- a/Sources/SWBUtil/Path.swift +++ b/Sources/SWBUtil/Path.swift @@ -404,6 +404,24 @@ public struct Path: Serializable, Sendable { return nil } + /// `true` if the path contains any .lproj directories as path components. + public var containsRegionVariantPathComponent: Bool { + var path = self.join("File.strings") // since regionVariantName looks at parent dir + while !path.isRoot && !path.isEmpty { + if path.regionVariantName != nil { + return true + } else { + let parent = path.dirname + if parent == path { + break + } else { + path = parent + } + } + } + return false + } + /// Return true if the pathname is conformant to path restrictions on the platform. /// /// Check the Unicode string representation of the path for reserved characters that cannot be represented as a path. diff --git a/Tests/SWBTaskConstructionTests/InstallLocTaskConstructionTests.swift b/Tests/SWBTaskConstructionTests/InstallLocTaskConstructionTests.swift index 83432e46..0cd157d0 100644 --- a/Tests/SWBTaskConstructionTests/InstallLocTaskConstructionTests.swift +++ b/Tests/SWBTaskConstructionTests/InstallLocTaskConstructionTests.swift @@ -1014,6 +1014,83 @@ fileprivate struct InstallLocTaskConstructionTests: CoreBasedTests { } } + @Test(.requireSDKs(.macOS)) + func copyFilesRulesMergeRegion() async throws { + let testProject = TestProject( + "aProject", + groupTree: TestGroup( + "SomeFiles", path: "Sources", + children: [ + TestVariantGroup("Localizable.strings", children: [ + TestFile("en.lproj/Localizable.strings", regionVariantName: "en"), + TestFile("ja.lproj/Localizable.strings", regionVariantName: "ja"), + TestFile("zh_TW.lproj/Localizable.strings", regionVariantName: "zh_TW"), + ]), + ]), + buildConfigurations: [ + TestBuildConfiguration( + "Debug", + buildSettings: [ + "PRODUCT_NAME": "$(TARGET_NAME)", + "GENERATE_INFOPLIST_FILE": "YES", + "APPLY_RULES_IN_COPY_FILES": "YES" + ]), + ], + targets: [ + TestStandardTarget( + "CoreFoo", type: .framework, + buildPhases: [ + TestCopyFilesBuildPhase([ + "Localizable.strings", + ], destinationSubfolder: .absolute, destinationSubpath: "/System/Library/Bundles/MyBundle.bundle", onlyForDeployment: true), + TestCopyFilesBuildPhase([ + "Localizable.strings", + ], destinationSubfolder: .builtProductsDir, destinationSubpath: "OtherProduct.bundle", onlyForDeployment: false), + ] + ) + ]) + let tester = try await TaskConstructionTester(getCore(), testProject) + + // installloc single language + await tester.checkBuild(BuildParameters(action: .installLoc, configuration: "Release", overrides: ["INSTALLLOC_LANGUAGE": "ja"]), runDestination: .macOS) { results in + results.checkTarget("CoreFoo") { target in + results.checkTaskExists(.matchTarget(target), .matchRule(["Copy", "/tmp/Test/aProject/build/Debug/OtherProduct.bundle/ja.lproj/Localizable.strings", "/tmp/Test/aProject/Sources/ja.lproj/Localizable.strings"])) + results.checkTaskExists(.matchTarget(target), .matchRule(["Copy", "/tmp/aProject.dst/System/Library/Bundles/MyBundle.bundle/ja.lproj/Localizable.strings", "/tmp/Test/aProject/Sources/ja.lproj/Localizable.strings"])) + } + + results.checkNoDiagnostics() + } + + // installloc multi-language + await tester.checkBuild(BuildParameters(action: .installLoc, configuration: "Release", overrides: ["INSTALLLOC_LANGUAGE": "ja zh_TW"]), runDestination: .macOS) { results in + results.checkTarget("CoreFoo") { target in + results.checkTaskExists(.matchTarget(target), .matchRule(["Copy", "/tmp/Test/aProject/build/Debug/OtherProduct.bundle/ja.lproj/Localizable.strings", "/tmp/Test/aProject/Sources/ja.lproj/Localizable.strings"])) + results.checkTaskExists(.matchTarget(target), .matchRule(["Copy", "/tmp/aProject.dst/System/Library/Bundles/MyBundle.bundle/ja.lproj/Localizable.strings", "/tmp/Test/aProject/Sources/ja.lproj/Localizable.strings"])) + + results.checkTaskExists(.matchTarget(target), .matchRule(["Copy", "/tmp/Test/aProject/build/Debug/OtherProduct.bundle/zh_TW.lproj/Localizable.strings", "/tmp/Test/aProject/Sources/zh_TW.lproj/Localizable.strings"])) + results.checkTaskExists(.matchTarget(target), .matchRule(["Copy", "/tmp/aProject.dst/System/Library/Bundles/MyBundle.bundle/zh_TW.lproj/Localizable.strings", "/tmp/Test/aProject/Sources/zh_TW.lproj/Localizable.strings"])) + } + + results.checkNoDiagnostics() + } + + // install + await tester.checkBuild(BuildParameters(action: .install, configuration: "Release"), runDestination: .macOS) { results in + results.checkTarget("CoreFoo") { target in + results.checkTaskExists(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/Test/aProject/build/Debug/OtherProduct.bundle/en.lproj/Localizable.strings", "/tmp/Test/aProject/Sources/en.lproj/Localizable.strings"])) + results.checkTaskExists(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/aProject.dst/System/Library/Bundles/MyBundle.bundle/en.lproj/Localizable.strings", "/tmp/Test/aProject/Sources/en.lproj/Localizable.strings"])) + + results.checkTaskExists(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/Test/aProject/build/Debug/OtherProduct.bundle/ja.lproj/Localizable.strings", "/tmp/Test/aProject/Sources/ja.lproj/Localizable.strings"])) + results.checkTaskExists(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/aProject.dst/System/Library/Bundles/MyBundle.bundle/ja.lproj/Localizable.strings", "/tmp/Test/aProject/Sources/ja.lproj/Localizable.strings"])) + + results.checkTaskExists(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/Test/aProject/build/Debug/OtherProduct.bundle/zh_TW.lproj/Localizable.strings", "/tmp/Test/aProject/Sources/zh_TW.lproj/Localizable.strings"])) + results.checkTaskExists(.matchTarget(target), .matchRule(["CopyStringsFile", "/tmp/aProject.dst/System/Library/Bundles/MyBundle.bundle/zh_TW.lproj/Localizable.strings", "/tmp/Test/aProject/Sources/zh_TW.lproj/Localizable.strings"])) + } + + results.checkNoDiagnostics() + } + } + @Test(.requireSDKs(.iOS)) func installLocForFramework() async throws { let testProject = TestProject(