From b9cf0bb96fef614c2d85c842a22122fa4406fa0f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 14 Aug 2026 11:08:35 -0700 Subject: [PATCH] fix(macos): package apps from paths with glob characters (#123751) * fix(macos): handle metacharacters in framework paths * fix(ci): round boundary artifact refresh mtimes --- scripts/package-mac-app.sh | 4 +- ...e-extension-package-boundary-artifacts.mts | 2 +- test/scripts/package-mac-app.test.ts | 62 +++++++++++++++++++ ...tension-package-boundary-artifacts.test.ts | 16 ++++- 4 files changed, 78 insertions(+), 6 deletions(-) diff --git a/scripts/package-mac-app.sh b/scripts/package-mac-app.sh index ee5075ccfdfa..5f803cfcc3bb 100755 --- a/scripts/package-mac-app.sh +++ b/scripts/package-mac-app.sh @@ -350,7 +350,7 @@ merge_framework_machos() { while IFS= read -r -d '' file; do if /usr/bin/file "$file" | /usr/bin/grep -q "Mach-O"; then - local rel="${file#$primary/}" + local rel="${file#"$primary"/}" local primary_archs primary_archs=$(archs_for "$file") IFS=' ' read -r -a primary_arch_array <<< "$primary_archs" @@ -371,7 +371,7 @@ merge_framework_machos() { IFS=' ' read -r -a other_arch_array <<< "$other_archs" for arch in "${other_arch_array[@]}"; do if ! arch_in_list "$arch" "${primary_arch_array[@]}"; then - local thin_file="$tmp_dir/$(echo "$rel" | tr '/' '_')-$arch" + local thin_file="$tmp_dir/${rel//\//_}-$arch" /usr/bin/lipo -thin "$arch" "$other_file" -output "$thin_file" missing_files+=("$thin_file") primary_arch_array+=("$arch") diff --git a/scripts/prepare-extension-package-boundary-artifacts.mts b/scripts/prepare-extension-package-boundary-artifacts.mts index 2a1a232afe50..a79fbbf13ecd 100644 --- a/scripts/prepare-extension-package-boundary-artifacts.mts +++ b/scripts/prepare-extension-package-boundary-artifacts.mts @@ -552,7 +552,7 @@ export function isArtifactSetFresh(params: ArtifactFreshParams) { } // Repair the mtime fast path so later invocations in this checkout skip // without re-reading every input byte. - const now = new Date(); + const now = new Date(Math.max(Date.now(), Math.ceil(newestInputMtimeMs))); for (const relativePath of params.outputPaths) { const outputPath = resolve(rootDir, relativePath); if (fs.existsSync(outputPath)) { diff --git a/test/scripts/package-mac-app.test.ts b/test/scripts/package-mac-app.test.ts index 669897a90703..5551167ee3e5 100644 --- a/test/scripts/package-mac-app.test.ts +++ b/test/scripts/package-mac-app.test.ts @@ -47,6 +47,17 @@ function getPackageManagerHelperBlock(): string { return script.slice(start, end); } +function getMergeFrameworkMachOsBlock(): string { + const script = readFileSync(scriptPath, "utf8"); + const start = script.indexOf("merge_framework_machos()"); + const end = script.indexOf('PEEKABOO_SOURCE_COMMIT="$(resolve_peekaboo_source_commit)"'); + + expect(start).toBeGreaterThanOrEqual(0); + expect(end).toBeGreaterThan(start); + + return script.slice(start, end); +} + function getSwiftToolchainBlock(): string { const script = readFileSync("scripts/lib/swift-toolchain.sh", "utf8"); const start = script.indexOf("REQUIRED_SWIFT_TOOLS_MAJOR="); @@ -749,6 +760,57 @@ describe("package-mac-app plist stamping", () => { expect(helperCopy).toContain('chmod +x "$APP_ROOT/Contents/MacOS/$MLX_TTS_HELPER_PRODUCT"'); }); + it.runIf(process.platform === "darwin")( + "merges framework Mach-O binaries when the checkout path contains glob metacharacters", + () => { + const root = tempDirs.make("openclaw-package-framework-[fixture]-"); + const primary = path.join(root, "Primary.framework"); + const secondary = path.join(root, "Secondary.framework"); + const destination = path.join(root, "Destination.framework"); + const relativeBinary = path.join("Versions", "A", "OpenClawFixture"); + + for (const framework of [primary, secondary, destination]) { + mkdirSync(path.dirname(path.join(framework, relativeBinary)), { recursive: true }); + } + + const fixtureBinary = "/bin/ls"; + const fixtureArchitectures = spawnSync("/usr/bin/lipo", ["-archs", fixtureBinary], { + encoding: "utf8", + }) + .stdout.trim() + .split(/\s+/u); + const [primaryArchitecture, secondaryArchitecture] = fixtureArchitectures; + if (!primaryArchitecture || !secondaryArchitecture) { + throw new Error(`${fixtureBinary} must contain at least two architectures`); + } + const primaryBinary = path.join(primary, relativeBinary); + const secondaryBinary = path.join(secondary, relativeBinary); + const destinationBinary = path.join(destination, relativeBinary); + expect( + spawnSync("/usr/bin/lipo", [ + "-thin", + primaryArchitecture, + fixtureBinary, + "-output", + primaryBinary, + ]).status, + ).toBe(0); + expect(spawnSync("/bin/cp", [fixtureBinary, secondaryBinary]).status).toBe(0); + writeFileSync(destinationBinary, readFileSync(primaryBinary)); + + const result = runHelper(` + set -euo pipefail + ${getMergeFrameworkMachOsBlock()} + merge_framework_machos ${JSON.stringify(primary)} ${JSON.stringify(destination)} ${JSON.stringify(secondary)} + /usr/bin/lipo -info ${JSON.stringify(destinationBinary)} + `); + + expect(result.status, result.stderr).toBe(0); + expect(result.stdout).toContain(primaryArchitecture); + expect(result.stdout).toContain(secondaryArchitecture); + }, + ); + it.each([ { title: "keeps the default backend when Xcode's Metal shim works", shimExit: 0, xcrunExit: 0 }, { diff --git a/test/scripts/prepare-extension-package-boundary-artifacts.test.ts b/test/scripts/prepare-extension-package-boundary-artifacts.test.ts index a27013b7e16e..d45ab485f48a 100644 --- a/test/scripts/prepare-extension-package-boundary-artifacts.test.ts +++ b/test/scripts/prepare-extension-package-boundary-artifacts.test.ts @@ -583,6 +583,8 @@ describe("prepare-extension-package-boundary-artifacts", () => { // Simulate checkout: inputs newer than restored outputs, bytes unchanged. fs.utimesSync(stampPath, new Date(1_000), new Date(1_000)); fs.utimesSync(outputPath, new Date(1_000), new Date(1_000)); + const repairTimeMs = Date.now(); + fs.utimesSync(inputPath, repairTimeMs / 1_000, (repairTimeMs + 0.5) / 1_000); const freshParams = { rootDir, inputPaths: ["src"], @@ -590,9 +592,17 @@ describe("prepare-extension-package-boundary-artifacts", () => { hashStampPath: "dist/.demo.stamp", }; - expect(isArtifactSetFresh(freshParams)).toBe(true); - // The hash match repairs output mtimes so the next check takes the fast path. - expect(fs.statSync(outputPath).mtimeMs).toBeGreaterThanOrEqual(fs.statSync(inputPath).mtimeMs); + vi.useFakeTimers(); + vi.setSystemTime(repairTimeMs); + try { + expect(isArtifactSetFresh(freshParams)).toBe(true); + // Round past fractional filesystem precision so the next check takes the fast path. + expect(fs.statSync(outputPath).mtimeMs).toBeGreaterThanOrEqual( + fs.statSync(inputPath).mtimeMs, + ); + } finally { + vi.useRealTimers(); + } fs.appendFileSync(inputPath, "export const demoTwo = 2;\n", "utf8"); fs.utimesSync(outputPath, new Date(1_000), new Date(1_000));