fix: stage signed Mac app replacements (#104345)

This commit is contained in:
Peter Steinberger
2026-07-11 02:26:06 -07:00
committed by GitHub
parent 518c5823d8
commit ee5834f7c4
3 changed files with 57 additions and 5 deletions

View File

@@ -8,7 +8,15 @@ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
source "$ROOT_DIR/scripts/lib/plistbuddy.sh"
source "$ROOT_DIR/scripts/lib/swift-toolchain.sh"
source "$ROOT_DIR/scripts/lib/build-metadata.sh"
APP_ROOT="$ROOT_DIR/dist/OpenClaw.app"
DEFAULT_APP_ROOT="$ROOT_DIR/dist/OpenClaw.app"
APP_ROOT="${OPENCLAW_PACKAGE_APP_ROOT:-$DEFAULT_APP_ROOT}"
case "$APP_ROOT" in
"$ROOT_DIR/dist/"*) ;;
*)
echo "ERROR: OPENCLAW_PACKAGE_APP_ROOT must stay under $ROOT_DIR/dist" >&2
exit 1
;;
esac
BUILD_ROOT="$ROOT_DIR/apps/macos/.build"
PRODUCT="OpenClaw"
MLX_TTS_HELPER_PRODUCT="openclaw-mlx-tts"

View File

@@ -27,6 +27,8 @@ TARGET_ONLY=0
TARGET_APP_BUNDLE="${ROOT_DIR}/dist/OpenClaw.app"
TARGET_EXECUTABLE="${TARGET_APP_BUNDLE}/${APP_EXECUTABLE_RELATIVE_PATH}"
INSTALLED_EXECUTABLE="/Applications/OpenClaw.app/${APP_EXECUTABLE_RELATIVE_PATH}"
STAGED_APP_DIR="${ROOT_DIR}/dist/.openclaw-replacement-${LOCK_KEY}-$$"
STAGED_APP_BUNDLE="${STAGED_APP_DIR}/OpenClaw.app"
log() { printf '%s\n' "$*"; }
fail() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
@@ -43,6 +45,9 @@ run_step() {
}
cleanup() {
if [[ -n "${STAGED_APP_DIR:-}" ]]; then
rm -rf "${STAGED_APP_DIR}"
fi
if [[ "${LOCK_HELD}" != "1" || ! -d "${LOCK_DIR}" ]]; then
return 0
fi
@@ -314,8 +319,28 @@ elif [ "$SIGN" -eq 1 ]; then
unset SIGN_IDENTITY
fi
# 3) Package app (no embedded gateway).
run_step "package app" bash -lc "cd '${ROOT_DIR}' && SKIP_TSC=${SKIP_TSC:-1} '${ROOT_DIR}/scripts/package-mac-app.sh'"
# 3) Package and sign outside the live bundle. A failed package/sign operation
# must leave the currently running and on-disk app untouched.
run_step "package app" env \
SKIP_TSC="${SKIP_TSC:-1}" \
OPENCLAW_PACKAGE_APP_ROOT="${STAGED_APP_BUNDLE}" \
"${ROOT_DIR}/scripts/package-mac-app.sh"
run_step "verify packaged app" /usr/bin/codesign --verify --deep --strict "${STAGED_APP_BUNDLE}"
install_staged_app() {
local previous="${ROOT_DIR}/dist/.OpenClaw.app.previous-$$"
rm -rf "${previous}"
if [[ -d "${TARGET_APP_BUNDLE}" ]]; then
mv "${TARGET_APP_BUNDLE}" "${previous}"
fi
if ! mv "${STAGED_APP_BUNDLE}" "${TARGET_APP_BUNDLE}"; then
if [[ -d "${previous}" && ! -d "${TARGET_APP_BUNDLE}" ]]; then
mv "${previous}" "${TARGET_APP_BUNDLE}"
fi
return 1
fi
rm -rf "${previous}" "${STAGED_APP_DIR}"
}
choose_app_bundle() {
if [[ -n "${APP_BUNDLE}" ]]; then
@@ -339,8 +364,6 @@ choose_app_bundle() {
fail "App bundle not found. Set OPENCLAW_APP_BUNDLE to your installed OpenClaw.app"
}
choose_app_bundle
# When signed, clear any previous launchagent override marker.
if [[ "$NO_SIGN" -ne 1 && "$ATTACH_ONLY" -ne 1 && -f "${LAUNCHAGENT_DISABLE_MARKER}" ]]; then
run_step "clear launchagent disable marker" /bin/rm -f "${LAUNCHAGENT_DISABLE_MARKER}"
@@ -386,6 +409,9 @@ if [[ "$TARGET_ONLY" -eq 1 ]]; then
fi
fi
run_step "install packaged app" install_staged_app
choose_app_bundle
# 4) Launch the installed app in the foreground so the menu bar extra appears.
# LaunchServices can inherit a huge environment from this shell (secrets, prompt vars, etc.).
# That can cause launchd spawn failures and is undesirable for a GUI app anyway.

View File

@@ -395,14 +395,32 @@ describe("scripts/restart-mac.sh", () => {
it("keeps the managed app alive until the signed replacement is ready", () => {
const script = readFileSync(restartScriptPath, "utf8");
const packageIndex = script.indexOf('run_step "package app"');
const verifyIndex = script.indexOf('run_step "verify packaged app"');
const switchIndex = script.indexOf('log "==> Switching managed installed');
const installIndex = script.indexOf('run_step "install packaged app"');
const launchIndex = script.indexOf('run_step "launch app"');
expect(packageIndex).toBeGreaterThan(-1);
expect(script).toContain('OPENCLAW_PACKAGE_APP_ROOT="${STAGED_APP_BUNDLE}"');
expect(verifyIndex).toBeGreaterThan(packageIndex);
expect(switchIndex).toBeGreaterThan(packageIndex);
expect(installIndex).toBeGreaterThan(switchIndex);
expect(launchIndex).toBeGreaterThan(installIndex);
expect(launchIndex).toBeGreaterThan(switchIndex);
});
it("restores the previous bundle if the staged install cannot complete", () => {
const script = readFileSync(restartScriptPath, "utf8");
const installBlock = script.slice(
script.indexOf("install_staged_app()"),
script.indexOf("choose_app_bundle()"),
);
expect(installBlock).toContain('mv "${TARGET_APP_BUNDLE}" "${previous}"');
expect(installBlock).toContain('if ! mv "${STAGED_APP_BUNDLE}" "${TARGET_APP_BUNDLE}"');
expect(installBlock).toContain('mv "${previous}" "${TARGET_APP_BUNDLE}"');
});
it("escalates only exact managed app processes when graceful shutdown stalls", () => {
const script = readFileSync(restartScriptPath, "utf8");
const managedKillBlock = script.slice(