From d71a24f3a4b7a03c060e01993afbb201cd024d26 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 4 Jul 2026 07:53:18 +0200 Subject: [PATCH] fix(mac): share Swift packaging preflight --- scripts/check-changed.mjs | 2 +- scripts/ci-changed-scope.mjs | 2 +- scripts/lib/swift-toolchain.sh | 33 +++++++++++++++ scripts/package-mac-app.sh | 32 +-------------- scripts/package-mac-dist.sh | 9 ++++- scripts/test-projects.test-support.mjs | 4 ++ src/scripts/ci-changed-scope.test.ts | 1 + test/scripts/changed-lanes.test.ts | 1 + test/scripts/package-mac-app.test.ts | 5 ++- test/scripts/package-mac-dist.test.ts | 56 ++++++++++++++++++++++++++ test/scripts/test-projects.test.ts | 8 ++++ 11 files changed, 117 insertions(+), 36 deletions(-) create mode 100644 scripts/lib/swift-toolchain.sh diff --git a/scripts/check-changed.mjs b/scripts/check-changed.mjs index cb66e4f3ffe0..5fb9b8b3b918 100644 --- a/scripts/check-changed.mjs +++ b/scripts/check-changed.mjs @@ -69,7 +69,7 @@ const ANDROID_VERSION_SYNC_PATHS = new Set([ "apps/android/version.json", ]); const MACOS_APP_CI_PATH_RE = - /^(?:apps\/(?:macos|macos-mlx-tts|shared|swabble)\/|Swabble\/|scripts\/(?:codesign-mac-app|create-dmg|notarize-mac-artifact|package-mac-app|package-mac-dist)\.sh$|scripts\/lib\/plistbuddy\.sh$|test\/scripts\/(?:codesign-mac-app|create-dmg|notarize-mac-artifact|package-mac-app|package-mac-dist)\.test\.ts$)/u; + /^(?:apps\/(?:macos|macos-mlx-tts|shared|swabble)\/|Swabble\/|scripts\/(?:codesign-mac-app|create-dmg|notarize-mac-artifact|package-mac-app|package-mac-dist)\.sh$|scripts\/lib\/(?:plistbuddy|swift-toolchain)\.sh$|test\/scripts\/(?:codesign-mac-app|create-dmg|notarize-mac-artifact|package-mac-app|package-mac-dist)\.test\.ts$)/u; let corepackPnpmShimDir; let corepackPnpmShimCleanupRegistered = false; diff --git a/scripts/ci-changed-scope.mjs b/scripts/ci-changed-scope.mjs index 4550eb0d21dd..988fe67d3ca8 100644 --- a/scripts/ci-changed-scope.mjs +++ b/scripts/ci-changed-scope.mjs @@ -37,7 +37,7 @@ const NATIVE_PROTOCOL_GEN_RE = /^apps\/shared\/OpenClawKit\/Sources\/OpenClawPro const MACOS_NATIVE_RE = /^(apps\/macos\/|apps\/macos-mlx-tts\/|apps\/ios\/|apps\/shared\/|apps\/swabble\/|Swabble\/)/; const MACOS_SCRIPT_SCOPE_RE = - /^(?:scripts\/(?:codesign-mac-app|create-dmg|notarize-mac-artifact|package-mac-app|package-mac-dist)\.sh|scripts\/lib\/plistbuddy\.sh|test\/scripts\/(?:codesign-mac-app|create-dmg|notarize-mac-artifact|package-mac-app|package-mac-dist)\.test\.ts)$/; + /^(?:scripts\/(?:codesign-mac-app|create-dmg|notarize-mac-artifact|package-mac-app|package-mac-dist)\.sh|scripts\/lib\/(?:plistbuddy|swift-toolchain)\.sh|test\/scripts\/(?:codesign-mac-app|create-dmg|notarize-mac-artifact|package-mac-app|package-mac-dist)\.test\.ts)$/; const IOS_BUILD_RE = /^(apps\/ios\/|apps\/shared\/|apps\/swabble\/|Swabble\/|config\/(?:swiftformat|swiftlint\.yml)$|scripts\/ios-(?:configure-signing|team-id|write-version-xcconfig)\.sh$|scripts\/ios-version\.ts$|scripts\/lib\/(?:ios-version\.ts|npm-publish-plan\.mjs|version-script-args\.ts)$)/; const ANDROID_NATIVE_RE = /^(apps\/android\/|apps\/shared\/)/; diff --git a/scripts/lib/swift-toolchain.sh b/scripts/lib/swift-toolchain.sh new file mode 100644 index 000000000000..edff1fc5e957 --- /dev/null +++ b/scripts/lib/swift-toolchain.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +REQUIRED_SWIFT_TOOLS_MAJOR=6 +REQUIRED_SWIFT_TOOLS_MINOR=2 + +require_swift_toolchain() { + local swift_version + if ! swift_version="$(swift --version 2>&1)"; then + printf '%s\n' "$swift_version" >&2 + echo "ERROR: OpenClaw macOS app packaging requires Swift tools ${REQUIRED_SWIFT_TOOLS_MAJOR}.${REQUIRED_SWIFT_TOOLS_MINOR}+." >&2 + echo " Install/select Xcode 26.x or newer before running macOS packaging scripts." >&2 + return 1 + fi + + local major_minor + major_minor="$(printf '%s\n' "$swift_version" | sed -nE 's/.*Apple Swift version ([0-9]+)\.([0-9]+).*/\1 \2/p' | head -n 1)" + if [[ -z "$major_minor" ]]; then + printf '%s\n' "$swift_version" >&2 + echo "ERROR: Could not parse selected Swift toolchain version." >&2 + echo " OpenClaw macOS app packaging requires Swift tools ${REQUIRED_SWIFT_TOOLS_MAJOR}.${REQUIRED_SWIFT_TOOLS_MINOR}+." >&2 + return 1 + fi + + local major minor + read -r major minor <<< "$major_minor" + if (( major < REQUIRED_SWIFT_TOOLS_MAJOR )) || + (( major == REQUIRED_SWIFT_TOOLS_MAJOR && minor < REQUIRED_SWIFT_TOOLS_MINOR )); then + printf '%s\n' "$swift_version" >&2 + echo "ERROR: OpenClaw macOS app packaging requires Swift tools ${REQUIRED_SWIFT_TOOLS_MAJOR}.${REQUIRED_SWIFT_TOOLS_MINOR}+." >&2 + echo " Current Swift is ${major}.${minor}; install/select Xcode 26.x or newer." >&2 + return 1 + fi +} diff --git a/scripts/package-mac-app.sh b/scripts/package-mac-app.sh index bcfa3a962d08..f4ff41e6612b 100755 --- a/scripts/package-mac-app.sh +++ b/scripts/package-mac-app.sh @@ -6,6 +6,7 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" source "$ROOT_DIR/scripts/lib/plistbuddy.sh" +source "$ROOT_DIR/scripts/lib/swift-toolchain.sh" APP_ROOT="$ROOT_DIR/dist/OpenClaw.app" BUILD_ROOT="$ROOT_DIR/apps/macos/.build" PRODUCT="OpenClaw" @@ -40,8 +41,6 @@ if [[ "$BUNDLE_ID" == *.debug ]]; then SPARKLE_FEED_URL="" AUTO_CHECKS=false fi -REQUIRED_SWIFT_TOOLS_MAJOR=6 -REQUIRED_SWIFT_TOOLS_MINOR=2 sparkle_canonical_build_from_version() { (cd "$ROOT_DIR" && node --import tsx "$ROOT_DIR/scripts/sparkle-build.ts" canonical-build "$1") @@ -91,35 +90,6 @@ run_pnpm() { (cd "$ROOT_DIR" && "${PNPM_CMD[@]}" "$@") } -require_swift_toolchain() { - local swift_version - if ! swift_version="$(swift --version 2>&1)"; then - printf '%s\n' "$swift_version" >&2 - echo "ERROR: OpenClaw macOS app packaging requires Swift tools ${REQUIRED_SWIFT_TOOLS_MAJOR}.${REQUIRED_SWIFT_TOOLS_MINOR}+." >&2 - echo " Install/select Xcode 26.x or newer before running scripts/package-mac-app.sh." >&2 - return 1 - fi - - local major_minor - major_minor="$(printf '%s\n' "$swift_version" | sed -nE 's/.*Apple Swift version ([0-9]+)\.([0-9]+).*/\1 \2/p' | head -n 1)" - if [[ -z "$major_minor" ]]; then - printf '%s\n' "$swift_version" >&2 - echo "ERROR: Could not parse selected Swift toolchain version." >&2 - echo " OpenClaw macOS app packaging requires Swift tools ${REQUIRED_SWIFT_TOOLS_MAJOR}.${REQUIRED_SWIFT_TOOLS_MINOR}+." >&2 - return 1 - fi - - local major minor - read -r major minor <<< "$major_minor" - if (( major < REQUIRED_SWIFT_TOOLS_MAJOR )) || - (( major == REQUIRED_SWIFT_TOOLS_MAJOR && minor < REQUIRED_SWIFT_TOOLS_MINOR )); then - printf '%s\n' "$swift_version" >&2 - echo "ERROR: OpenClaw macOS app packaging requires Swift tools ${REQUIRED_SWIFT_TOOLS_MAJOR}.${REQUIRED_SWIFT_TOOLS_MINOR}+." >&2 - echo " Current Swift is ${major}.${minor}; install/select Xcode 26.x or newer." >&2 - return 1 - fi -} - merge_framework_machos() { local primary="$1" local dest="$2" diff --git a/scripts/package-mac-dist.sh b/scripts/package-mac-dist.sh index 82721eebc166..86043d54520f 100755 --- a/scripts/package-mac-dist.sh +++ b/scripts/package-mac-dist.sh @@ -10,11 +10,12 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" source "$ROOT_DIR/scripts/lib/plistbuddy.sh" +source "$ROOT_DIR/scripts/lib/swift-toolchain.sh" BUILD_ROOT="$ROOT_DIR/apps/macos/.build" PRODUCT="OpenClaw" BUILD_CONFIG="${BUILD_CONFIG:-release}" -APP_VERSION_INPUT="${APP_VERSION:-$(cd "$ROOT_DIR" && node -p "require('./package.json').version" 2>/dev/null || echo "0.0.0")}" +APP_VERSION_INPUT="${APP_VERSION:-}" # Default to universal binary for distribution builds (supports both Apple Silicon and Intel Macs) export BUILD_ARCHS="${BUILD_ARCHS:-all}" @@ -128,6 +129,12 @@ correction_build_from_exact_tag() { # Local fallback releases must not silently fall back to a git-rev-count build number. # For correction tags, pass a higher explicit APP_BUILD than the canonical floor. +require_swift_toolchain + +if [[ -z "$APP_VERSION_INPUT" ]]; then + APP_VERSION_INPUT="$(cd "$ROOT_DIR" && node -p "require('./package.json').version" 2>/dev/null || echo "0.0.0")" +fi + if [[ -z "${APP_BUILD:-}" && "$BUILD_CONFIG" == "release" ]]; then CANONICAL_APP_BUILD="$(require_canonical_sparkle_build "$APP_VERSION_INPUT")" APP_BUILD="$(correction_build_from_exact_tag "$APP_VERSION_INPUT" "$CANONICAL_APP_BUILD")" diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index 72c23fdb57d5..06fff758063a 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -1233,6 +1233,10 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([ "test/scripts/package-mac-dist.test.ts", ], ], + [ + "scripts/lib/swift-toolchain.sh", + ["test/scripts/package-mac-app.test.ts", "test/scripts/package-mac-dist.test.ts"], + ], [ "scripts/lib/plugin-npm-runtime-build.mjs", ["test/scripts/plugin-npm-runtime-build-args.test.ts", "test/plugin-npm-runtime-build.test.ts"], diff --git a/src/scripts/ci-changed-scope.test.ts b/src/scripts/ci-changed-scope.test.ts index e047a8cda3b0..630248a64a32 100644 --- a/src/scripts/ci-changed-scope.test.ts +++ b/src/scripts/ci-changed-scope.test.ts @@ -372,6 +372,7 @@ describe("detectChangedScope", () => { "scripts/codesign-mac-app.sh", "scripts/create-dmg.sh", "scripts/lib/plistbuddy.sh", + "scripts/lib/swift-toolchain.sh", "scripts/notarize-mac-artifact.sh", "scripts/package-mac-app.sh", "scripts/package-mac-dist.sh", diff --git a/test/scripts/changed-lanes.test.ts b/test/scripts/changed-lanes.test.ts index eff72647b2f9..dafa3101d6db 100644 --- a/test/scripts/changed-lanes.test.ts +++ b/test/scripts/changed-lanes.test.ts @@ -1581,6 +1581,7 @@ describe("scripts/changed-lanes", () => { "scripts/codesign-mac-app.sh", "scripts/create-dmg.sh", "scripts/lib/plistbuddy.sh", + "scripts/lib/swift-toolchain.sh", "scripts/notarize-mac-artifact.sh", "scripts/package-mac-app.sh", "scripts/package-mac-dist.sh", diff --git a/test/scripts/package-mac-app.test.ts b/test/scripts/package-mac-app.test.ts index 9c7b96d411bc..81fe9445965a 100644 --- a/test/scripts/package-mac-app.test.ts +++ b/test/scripts/package-mac-app.test.ts @@ -49,9 +49,9 @@ function getPackageManagerHelperBlock(): string { } function getSwiftToolchainBlock(): string { - const script = readFileSync(scriptPath, "utf8"); + const script = readFileSync("scripts/lib/swift-toolchain.sh", "utf8"); const start = script.indexOf("REQUIRED_SWIFT_TOOLS_MAJOR="); - const end = script.indexOf("merge_framework_machos()"); + const end = script.length; expect(start).toBeGreaterThanOrEqual(0); expect(end).toBeGreaterThan(start); @@ -301,6 +301,7 @@ describe("package-mac-app plist stamping", () => { const installIndex = script.indexOf('if [[ "${SKIP_PNPM_INSTALL:-0}" != "1" ]]'); const preInstallBlock = script.slice(0, installIndex); + expect(script).toContain('source "$ROOT_DIR/scripts/lib/swift-toolchain.sh"'); expect(preInstallBlock).toContain("\nrequire_swift_toolchain\n"); }); diff --git a/test/scripts/package-mac-dist.test.ts b/test/scripts/package-mac-dist.test.ts index c152b03e6964..b505d517fdbe 100644 --- a/test/scripts/package-mac-dist.test.ts +++ b/test/scripts/package-mac-dist.test.ts @@ -113,6 +113,62 @@ describe("package-mac-dist plist validation", () => { expect(script).not.toContain('canonical_sparkle_build "$VERSION" 2>/dev/null || true'); }); + it("checks Swift before Sparkle metadata or dependency bootstrap work", () => { + const script = readFileSync(scriptPath, "utf8"); + const swiftIndex = script.indexOf("\nrequire_swift_toolchain\n"); + const versionIndex = script.indexOf('if [[ -z "$APP_VERSION_INPUT" ]]'); + const appBuildIndex = script.indexOf( + 'if [[ -z "${APP_BUILD:-}" && "$BUILD_CONFIG" == "release" ]]', + ); + const packageAppIndex = script.indexOf('"$ROOT_DIR/scripts/package-mac-app.sh"'); + const preSwiftBlock = script.slice(0, swiftIndex); + + expect(script).toContain('source "$ROOT_DIR/scripts/lib/swift-toolchain.sh"'); + expect(swiftIndex).toBeGreaterThanOrEqual(0); + expect(versionIndex).toBeGreaterThan(swiftIndex); + expect(appBuildIndex).toBeGreaterThan(versionIndex); + expect(packageAppIndex).toBeGreaterThan(appBuildIndex); + expect(preSwiftBlock).not.toContain("node -p"); + }); + + it("fails on old Swift before reading package metadata", () => { + const toolsDir = mkdtempSync(path.join(tmpdir(), "openclaw-dist-swift-tools-")); + tempDirs.push(toolsDir); + + writeFileSync( + path.join(toolsDir, "swift"), + [ + "#!/usr/bin/env bash", + "echo 'swift-driver version: 1.115.1 Apple Swift version 6.0.3 (swiftlang-6.0.3.1.10 clang-1600.0.30.1)'", + "", + ].join("\n"), + "utf8", + ); + chmodSync(path.join(toolsDir, "swift"), 0o755); + writeFileSync( + path.join(toolsDir, "node"), + [ + "#!/usr/bin/env bash", + "echo 'node should not run before Swift preflight' >&2", + "exit 42", + "", + ].join("\n"), + "utf8", + ); + chmodSync(path.join(toolsDir, "node"), 0o755); + + const result = runHelper(` + set -euo pipefail + PATH=${JSON.stringify(`${toolsDir}:/usr/bin:/bin`)} + BUILD_CONFIG=release bash ${scriptPath} + `); + + expect(result.status).toBe(1); + expect(result.stderr).toContain("OpenClaw macOS app packaging requires Swift tools 6.2+"); + expect(result.stderr).toContain("Current Swift is 6.0"); + expect(result.stderr).not.toContain("node should not run before Swift preflight"); + }); + it("prefers repo Corepack pnpm over a global pnpm shim", () => { const helperBlock = getPackageManagerHelperBlock(); const tempRoot = mkdtempSync(path.join(tmpdir(), "openclaw-dist-pnpm-root-")); diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index 0d35552b1b4e..21a1ae5182da 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -1468,6 +1468,10 @@ describe("scripts/test-projects changed-target routing", () => { ["scripts/make_appcast.sh", ["test/scripts/make-appcast.test.ts"]], ["scripts/package-mac-app.sh", ["test/scripts/package-mac-app.test.ts"]], ["scripts/package-mac-dist.sh", ["test/scripts/package-mac-dist.test.ts"]], + [ + "scripts/lib/swift-toolchain.sh", + ["test/scripts/package-mac-app.test.ts", "test/scripts/package-mac-dist.test.ts"], + ], ["scripts/e2e/bun-global-install-smoke.sh", ["test/scripts/test-install-sh-docker.test.ts"]], [ "scripts/sparkle-build.ts", @@ -1962,6 +1966,10 @@ describe("scripts/test-projects changed-target routing", () => { "test/scripts/package-mac-dist.test.ts", ], ], + [ + "scripts/lib/swift-toolchain.sh", + ["test/scripts/package-mac-app.test.ts", "test/scripts/package-mac-dist.test.ts"], + ], [ "scripts/lib/npm-publish-plan.mjs", [