From 6a4402c1b3a5e0629f6eb40ce818ec0d5f7a97cc Mon Sep 17 00:00:00 2001 From: Matt Seaman Date: Thu, 2 Oct 2025 15:04:30 -0700 Subject: [PATCH] Introduce INSTALLLOC_DIRECTORY_CONTENTS to allow installloc to support installing ad-hoc bundle sub-contents --- Sources/SWBCore/Settings/BuiltinMacros.swift | 2 + .../CopyFilesTaskProducer.swift | 4 + .../ResourcesTaskProducer.swift | 4 + Sources/SWBTestSupport/TestWorkspaces.swift | 4 + .../InstallLocTaskConstructionTests.swift | 226 +++++++++++++++++- 5 files changed, 239 insertions(+), 1 deletion(-) diff --git a/Sources/SWBCore/Settings/BuiltinMacros.swift b/Sources/SWBCore/Settings/BuiltinMacros.swift index ddf8d3cf..247f44de 100644 --- a/Sources/SWBCore/Settings/BuiltinMacros.swift +++ b/Sources/SWBCore/Settings/BuiltinMacros.swift @@ -780,6 +780,7 @@ public final class BuiltinMacros { public static let INPUT_FILE_SUFFIX = BuiltinMacros.declareStringMacro("INPUT_FILE_SUFFIX") public static let INSTALLHDRS_COPY_PHASE = BuiltinMacros.declareBooleanMacro("INSTALLHDRS_COPY_PHASE") public static let INSTALLHDRS_SCRIPT_PHASE = BuiltinMacros.declareBooleanMacro("INSTALLHDRS_SCRIPT_PHASE") + public static let INSTALLLOC_DIRECTORY_CONTENTS = BuiltinMacros.declareBooleanMacro("INSTALLLOC_DIRECTORY_CONTENTS") public static let INSTALLLOC_LANGUAGE = BuiltinMacros.declareStringListMacro("INSTALLLOC_LANGUAGE") public static let INSTALLLOC_SCRIPT_PHASE = BuiltinMacros.declareBooleanMacro("INSTALLLOC_SCRIPT_PHASE") public static let INSTALL_DIR = BuiltinMacros.declarePathMacro("INSTALL_DIR") @@ -1877,6 +1878,7 @@ public final class BuiltinMacros { INSTALLED_PRODUCT_ASIDES, INSTALLHDRS_COPY_PHASE, INSTALLHDRS_SCRIPT_PHASE, + INSTALLLOC_DIRECTORY_CONTENTS, INSTALLLOC_LANGUAGE, INSTALLLOC_SCRIPT_PHASE, INSTALL_DIR, diff --git a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/CopyFilesTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/CopyFilesTaskProducer.swift index 04b6f569..559c1e20 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/CopyFilesTaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/CopyFilesTaskProducer.swift @@ -226,6 +226,10 @@ class CopyFilesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBasedBui return } } + } else if scope.evaluate(BuiltinMacros.INSTALLLOC_DIRECTORY_CONTENTS), !ftb.isValidLocalizedContent(scope), context.workspaceContext.fs.isDirectory(ftb.absolutePath) { + // Treat any package or directory the same as a copied bundle and copy over the relevant lproj directories. + await addTasksForEmbeddedLocalizedBundle(ftb, buildFilesContext, scope, &tasks) + return } guard ftb.isValidLocalizedContent(scope) || targetBundleProduct else { return } } diff --git a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/ResourcesTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/ResourcesTaskProducer.swift index 848d3d57..aee06f4f 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/ResourcesTaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/ResourcesTaskProducer.swift @@ -163,6 +163,10 @@ final class ResourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBa await addTasksForEmbeddedLocalizedBundle(ftb, buildFilesContext, scope, &tasks) return } + } else if scope.evaluate(BuiltinMacros.INSTALLLOC_DIRECTORY_CONTENTS), !ftb.isValidLocalizedContent(scope), context.workspaceContext.fs.isDirectory(ftb.absolutePath) { + // Treat any package or directory the same as a copied bundle and copy over the relevant lproj directories. + await addTasksForEmbeddedLocalizedBundle(ftb, buildFilesContext, scope, &tasks) + return } guard ftb.isValidLocalizedContent(scope) || targetBundleProduct else { return } } diff --git a/Sources/SWBTestSupport/TestWorkspaces.swift b/Sources/SWBTestSupport/TestWorkspaces.swift index 34d55abf..219269b3 100644 --- a/Sources/SWBTestSupport/TestWorkspaces.swift +++ b/Sources/SWBTestSupport/TestWorkspaces.swift @@ -315,6 +315,10 @@ package final class TestFile: TestInternalStructureItem, CustomStringConvertible return "folder.documentationcatalog" case ".tightbeam": return "sourcecode.tightbeam" + case ".myPackage": + return "com.apple.package" + case "": + return "public.directory" case let ext where ext.hasPrefix(".fake-"): // If this is a fake extension, just return "file". return "file" diff --git a/Tests/SWBTaskConstructionTests/InstallLocTaskConstructionTests.swift b/Tests/SWBTaskConstructionTests/InstallLocTaskConstructionTests.swift index 1ace97f1..c0452319 100644 --- a/Tests/SWBTaskConstructionTests/InstallLocTaskConstructionTests.swift +++ b/Tests/SWBTaskConstructionTests/InstallLocTaskConstructionTests.swift @@ -630,7 +630,7 @@ fileprivate struct InstallLocTaskConstructionTests: CoreBasedTests { results.checkNoDiagnostics() } - // INSTALLLOC_LANGUAGE set to "ja" + // INSTALLLOC_LANGUAGE set to "zh_TW" and "ja" let specificLangs = ["zh_TW", "ja"] await tester.checkBuild(BuildParameters(action: .installLoc, configuration: "Debug", overrides: ["INSTALLLOC_LANGUAGE": specificLangs.joined(separator: " ")]), runDestination: .iOS, fs: fs) { results in // Ignore all Gate, build directory, MkDir, and SymLink tasks. @@ -653,6 +653,230 @@ fileprivate struct InstallLocTaskConstructionTests: CoreBasedTests { } } + /// Test an App that has a directory or package (not an explicit bundle) in resources phase + @Test(.requireSDKs(.macOS)) + func embeddedDirectoriesInResources() async throws { + try await withTemporaryDirectory { tmpDir in + let srcRoot = tmpDir.join("srcroot") + + let testProject = TestProject( + "aProject", + sourceRoot: srcRoot, + groupTree: TestGroup( + "SomeFiles", path: "Sources", + children: [ + TestFile("MyFolder"), + TestFile("MyPackage.myPackage"), + ]), + buildConfigurations: [ + TestBuildConfiguration( + "Debug", + buildSettings: [ + "PRODUCT_NAME": "$(TARGET_NAME)", + "INSTALLLOC_DIRECTORY_CONTENTS": "YES" + ]), + ], + targets: [ + TestStandardTarget( + "App", + type: .application, + buildPhases: [ + TestResourcesBuildPhase([ + "MyFolder", + "MyPackage.myPackage" + ], onlyForDeployment: false) + ] + ) + ]) + let tester = try await TaskConstructionTester(getCore(), testProject) + let fs = PseudoFS() + var languageComponentPathGroupings: [(lang: String, component: String, path: Path)] = [] + for pathComponent in ["MyFolder", "MyPackage.myPackage"] { + let bundlePath = srcRoot.join("Sources/\(pathComponent)", preserveRoot: true, normalize: true) + try fs.createDirectory(bundlePath, recursive: true) + try fs.write(bundlePath.join("Info.plist"), contents: "LocTest") + + for lang in ["en", "ja", "zh_TW"] { + let path = bundlePath.join("\(lang).lproj", preserveRoot: true, normalize: true) + try fs.createDirectory(path, recursive: false) + try fs.write(path.join("Localizable.strings"), contents: "LocTest") + languageComponentPathGroupings += [(lang, pathComponent, path)] + } + } + + await tester.checkBuild(BuildParameters(action: .installLoc, configuration: "Debug"), runDestination: .iOS, fs: fs) { results in + // Ignore all Gate, build directory, SymLink, and MkDir tasks. + results.checkTasks(.matchRuleType("Gate")) { _ in } + results.checkTasks(.matchRuleType("CreateBuildDirectory")) { _ in } + results.checkTasks(.matchRuleType("SymLink")) { _ in } + results.checkTasks(.matchRuleType("MkDir")) { _ in } + results.checkTasks(.matchRuleType("WriteAuxiliaryFile")) { _ in } + results.checkTasks(.matchRuleType("ProcessInfoPlistFile")) { _ in } + + results.checkTarget("App") { target in + for (language, component, path) in languageComponentPathGroupings { + results.checkTask(.matchTarget(target), .matchRule(["CpResource", "/tmp/aProject.dst/Applications/App.app/Contents/Resources/\(component)/\(language).lproj", path.str])) { _ in } + } + } + results.checkNoTask() + results.checkNoDiagnostics() + } + + // INSTALLLOC_LANGUAGE set to "ja" + await tester.checkBuild(BuildParameters(action: .installLoc, configuration: "Debug", overrides: ["INSTALLLOC_LANGUAGE": "ja"]), runDestination: .iOS, fs: fs) { 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 + for (language, component, path) in languageComponentPathGroupings where language == "ja" { + results.checkTask(.matchTarget(target), .matchRule(["CpResource", "/tmp/aProject.dst/Applications/App.app/Contents/Resources/\(component)/\(language).lproj", path.str])) { _ in } + } + } + + results.checkNoTask() + results.checkNoDiagnostics() + } + + // INSTALLLOC_LANGUAGE set to "zh_TW" and "ja" + let specificLangs = ["zh_TW", "ja"] + await tester.checkBuild(BuildParameters(action: .installLoc, configuration: "Debug", overrides: ["INSTALLLOC_LANGUAGE": specificLangs.joined(separator: " ")]), runDestination: .iOS, fs: fs) { 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 + for (language, component, path) in languageComponentPathGroupings where specificLangs.contains(language) { + results.checkTask(.matchTarget(target), .matchRule(["CpResource", "/tmp/aProject.dst/Applications/App.app/Contents/Resources/\(component)/\(language).lproj", path.str])) { _ in } + } + } + + results.checkNoTask() + results.checkNoDiagnostics() + } + } + } + + /// Test an App that has a directory or package (not an explicit bundle) in copy files phase + @Test(.requireSDKs(.macOS)) + func embeddedDirectoriesInCopyFiles() async throws { + try await withTemporaryDirectory { tmpDir in + let srcRoot = tmpDir.join("srcroot") + + let testProject = TestProject( + "aProject", + sourceRoot: srcRoot, + groupTree: TestGroup( + "SomeFiles", path: "Sources", + children: [ + TestFile("MyFolder"), + TestFile("MyPackage.myPackage"), + ]), + buildConfigurations: [ + TestBuildConfiguration( + "Debug", + buildSettings: [ + "PRODUCT_NAME": "$(TARGET_NAME)", + "INSTALLLOC_DIRECTORY_CONTENTS": "YES" + ]), + ], + targets: [ + TestStandardTarget( + "App", + type: .application, + buildPhases: [ + TestCopyFilesBuildPhase([ + "MyFolder", + "MyPackage.myPackage" + ], destinationSubfolder: .absolute, destinationSubpath: "/tmp/CustomPath", onlyForDeployment: false) + ] + ) + ]) + let tester = try await TaskConstructionTester(getCore(), testProject) + let fs = PseudoFS() + var languageComponentPathGroupings: [(lang: String, component: String, path: Path)] = [] + for pathComponent in ["MyFolder", "MyPackage.myPackage"] { + let bundlePath = srcRoot.join("Sources/\(pathComponent)", preserveRoot: true, normalize: true) + try fs.createDirectory(bundlePath, recursive: true) + try fs.write(bundlePath.join("Info.plist"), contents: "LocTest") + + for lang in ["en", "ja", "zh_TW"] { + let path = bundlePath.join("\(lang).lproj", preserveRoot: true, normalize: true) + try fs.createDirectory(path, recursive: false) + try fs.write(path.join("Localizable.strings"), contents: "LocTest") + languageComponentPathGroupings += [(lang, pathComponent, path)] + } + } + + await tester.checkBuild(BuildParameters(action: .installLoc, configuration: "Debug"), runDestination: .iOS, fs: fs) { results in + // Ignore all Gate, build directory, SymLink, and MkDir tasks. + results.checkTasks(.matchRuleType("Gate")) { _ in } + results.checkTasks(.matchRuleType("CreateBuildDirectory")) { _ in } + results.checkTasks(.matchRuleType("SymLink")) { _ in } + results.checkTasks(.matchRuleType("MkDir")) { _ in } + results.checkTasks(.matchRuleType("WriteAuxiliaryFile")) { _ in } + results.checkTasks(.matchRuleType("ProcessInfoPlistFile")) { _ in } + + results.checkTarget("App") { target in + for (language, component, path) in languageComponentPathGroupings { + results.checkTask(.matchTarget(target), .matchRule(["Copy", "/tmp/aProject.dst/tmp/CustomPath/\(component)/\(language).lproj", path.str])) { _ in } + } + } + results.checkNoTask() + results.checkNoDiagnostics() + } + + // INSTALLLOC_LANGUAGE set to "ja" + await tester.checkBuild(BuildParameters(action: .installLoc, configuration: "Debug", overrides: ["INSTALLLOC_LANGUAGE": "ja"]), runDestination: .iOS, fs: fs) { 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 + for (language, component, path) in languageComponentPathGroupings where language == "ja" { + results.checkTask(.matchTarget(target), .matchRule(["Copy", "/tmp/aProject.dst/tmp/CustomPath/\(component)/\(language).lproj", path.str])) { _ in } + } + } + + results.checkNoTask() + results.checkNoDiagnostics() + } + + // INSTALLLOC_LANGUAGE set to "zh_TW" and "ja" + let specificLangs = ["zh_TW", "ja"] + await tester.checkBuild(BuildParameters(action: .installLoc, configuration: "Debug", overrides: ["INSTALLLOC_LANGUAGE": specificLangs.joined(separator: " ")]), runDestination: .iOS, fs: fs) { 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 + for (language, component, path) in languageComponentPathGroupings where specificLangs.contains(language) { + results.checkTask(.matchTarget(target), .matchRule(["Copy", "/tmp/aProject.dst/tmp/CustomPath/\(component)/\(language).lproj", path.str])) { _ in } + } + } + + results.checkNoTask() + results.checkNoDiagnostics() + } + } + } + /// Test an App that has a bundle (not the product of a target) in copy files phase @Test(.requireSDKs(.iOS)) func warningForMissingExternalBundles() async throws {