mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
Add native computer use and Browser/Terminal launchers for prepared macOS and Windows cloud workers through Crabbox's existing authenticated transport, placement, and teardown. Launch the separately signed Mac Cloud Worker app through LaunchServices and let it own Node/CUA, desktop permissions, and the renewable idle assertion. Bind Windows enrollment and replay to the worker's interactive account and session. Preserve uncertain launch evidence for reprovisioning, and require confirmed lease teardown before downgrading readers of newer desktop metadata. Preserve chat end-follow during coalesced composer and goal resizing by carrying the actual scroll correction through the existing resize event and offset owner. Retain the canonical rail, progress, and approval lifecycle implementations. Validation: exact-head CI passed 165 jobs with seven skips; 142 focused approval cases passed locally. Historical native platform proof and the remaining current-driver, lock/account-loss, enrollment, and downgrade qualification limits are recorded in #152060 under the requested best-effort testing scope. Refs #152060. Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
664 lines
26 KiB
Bash
Executable file
664 lines
26 KiB
Bash
Executable file
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
# Build and bundle OpenClaw with its matching private worker runtime.
|
|
# Outputs to dist/OpenClaw.app, or the explicitly selected cloud-worker app.
|
|
|
|
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"
|
|
source "$ROOT_DIR/scripts/lib/mac-app-bundle.sh"
|
|
CLOUD_WORKER_HOST="${OPENCLAW_MAC_CLOUD_WORKER_HOST:-0}"
|
|
case "$CLOUD_WORKER_HOST" in
|
|
0 | 1) ;;
|
|
*) echo "ERROR: OPENCLAW_MAC_CLOUD_WORKER_HOST must be 0 or 1." >&2; exit 1 ;;
|
|
esac
|
|
APP_BUNDLE_NAME=OpenClaw.app
|
|
if [[ "$CLOUD_WORKER_HOST" == "1" ]]; then
|
|
APP_BUNDLE_NAME=OpenClawCloudWorker.app
|
|
if [[ "${BUNDLE_ID:-ai.openclaw.cloud-worker}" != ai.openclaw.cloud-worker ]]; then
|
|
echo "ERROR: Cloud worker packaging requires BUNDLE_ID=ai.openclaw.cloud-worker." >&2
|
|
exit 1
|
|
fi
|
|
if [[ "${ALLOW_ADHOC_SIGNING:-0}" == 1 || "${SIGN_IDENTITY:-}" == - ||
|
|
"${DISABLE_LIBRARY_VALIDATION:-0}" == 1 || "${SKIP_TEAM_ID_CHECK:-0}" == 1 ]]; then
|
|
echo "ERROR: Cloud worker packaging requires a complete Developer ID signature." >&2
|
|
exit 1
|
|
fi
|
|
BUNDLE_ID=ai.openclaw.cloud-worker
|
|
elif [[ "${BUNDLE_ID:-}" == ai.openclaw.cloud-worker ]]; then
|
|
echo "ERROR: Use OPENCLAW_MAC_CLOUD_WORKER_HOST=1 to build the cloud worker app." >&2
|
|
exit 1
|
|
fi
|
|
DEFAULT_APP_ROOT="$ROOT_DIR/dist/$APP_BUNDLE_NAME"
|
|
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
|
|
if [[ "$CLOUD_WORKER_HOST" == "1" && "${APP_ROOT##*/}" != OpenClawCloudWorker.app ]]; then
|
|
echo "ERROR: Cloud worker output must be named OpenClawCloudWorker.app." >&2
|
|
exit 1
|
|
fi
|
|
APP_DESTINATION="$APP_ROOT"
|
|
APP_STAGE_DIR=""
|
|
SWIFT_BUILD_PID=""
|
|
SWIFT_BUILD_RESULTS=""
|
|
BUILD_ROOT="$ROOT_DIR/apps/macos/.build"
|
|
PRODUCT="OpenClaw"
|
|
MLX_TTS_HELPER_PRODUCT="openclaw-mlx-tts"
|
|
MLX_TTS_HELPER_ROOT="$ROOT_DIR/apps/macos-mlx-tts"
|
|
MLX_TTS_HELPER_BUILD_ROOT="$MLX_TTS_HELPER_ROOT/.build"
|
|
BUNDLE_ID="${BUNDLE_ID:-ai.openclaw.mac.debug}"
|
|
PKG_VERSION="$(cd "$ROOT_DIR" && node -p "require('./package.json').version" 2>/dev/null || echo "0.0.0")"
|
|
BUILD_CONFIG="${BUILD_CONFIG:-debug}"
|
|
SIGNING_VARIANT="${OPENCLAW_MAC_SIGNING_VARIANT:-standard}"
|
|
case "$SIGNING_VARIANT" in
|
|
standard | elevation-host) ;;
|
|
*)
|
|
echo "ERROR: Unknown OPENCLAW_MAC_SIGNING_VARIANT value: $SIGNING_VARIANT (use standard|elevation-host)" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
if [[ "$CLOUD_WORKER_HOST" == "1" && "$SIGNING_VARIANT" != standard ]]; then
|
|
echo "ERROR: Cloud worker packaging cannot use the elevation-host signing variant." >&2
|
|
exit 1
|
|
fi
|
|
# OPENCLAW_SKIP_MLX_TTS=1 packages the app without the local MLX voice helper.
|
|
# The helper pulls in the full mlx-swift Metal shader stack, which some beta
|
|
# Xcode toolchains cannot compile (flaky `metal` diagnostics), needlessly
|
|
# blocking unrelated dev/proof builds. Release builds must always ship the
|
|
# helper (notarization verifies it), so refuse the skip there instead of
|
|
# producing a silently incomplete release bundle.
|
|
SKIP_MLX_TTS="${OPENCLAW_SKIP_MLX_TTS:-0}"
|
|
if [[ "$SKIP_MLX_TTS" == "1" && "$BUILD_CONFIG" == "release" ]]; then
|
|
echo "ERROR: OPENCLAW_SKIP_MLX_TTS is not allowed for release builds; the MLX voice helper must ship in release." >&2
|
|
exit 1
|
|
fi
|
|
BUILD_TS="$(openclaw_resolve_build_timestamp)"
|
|
if [[ "$BUILD_CONFIG" == "release" ]]; then
|
|
OPENCLAW_REQUIRE_BUILD_METADATA=1
|
|
fi
|
|
BUILD_GIT_COMMIT="$(openclaw_resolve_git_commit "$ROOT_DIR")"
|
|
if [[ "$BUILD_CONFIG" == "release" ]]; then
|
|
/bin/bash "$ROOT_DIR/scripts/apple-release-source-check.sh" \
|
|
--root "$ROOT_DIR" \
|
|
--expected-commit "$BUILD_GIT_COMMIT"
|
|
fi
|
|
export OPENCLAW_BUILD_TIMESTAMP="$BUILD_TS"
|
|
if openclaw_is_full_git_commit "$BUILD_GIT_COMMIT"; then
|
|
export GIT_COMMIT="$BUILD_GIT_COMMIT"
|
|
else
|
|
unset GIT_COMMIT
|
|
fi
|
|
GIT_BUILD_NUMBER=$(cd "$ROOT_DIR" && git rev-list --count HEAD 2>/dev/null || echo "0")
|
|
APP_VERSION="${APP_VERSION:-$PKG_VERSION}"
|
|
APP_BUILD="${APP_BUILD:-}"
|
|
if [[ -n "${BUILD_ARCHS:-}" ]]; then
|
|
BUILD_ARCHS_VALUE="${BUILD_ARCHS}"
|
|
elif [[ "$BUILD_CONFIG" == "release" ]]; then
|
|
# Release packaging should be universal unless explicitly overridden.
|
|
BUILD_ARCHS_VALUE="all"
|
|
else
|
|
BUILD_ARCHS_VALUE="$(uname -m)"
|
|
fi
|
|
if [[ "${BUILD_ARCHS_VALUE}" == "all" ]]; then
|
|
BUILD_ARCHS_VALUE="arm64 x86_64"
|
|
fi
|
|
IFS=' ' read -r -a BUILD_ARCHS <<< "$BUILD_ARCHS_VALUE"
|
|
PRIMARY_ARCH="${BUILD_ARCHS[0]}"
|
|
SPARKLE_PUBLIC_ED_KEY="${SPARKLE_PUBLIC_ED_KEY:-AGCY8w5vHirVfGGDGc8Szc5iuOqupZSh9pMj/Qs67XI=}"
|
|
SPARKLE_FEED_URL="${SPARKLE_FEED_URL:-https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml}"
|
|
AUTO_CHECKS=true
|
|
if [[ "$BUNDLE_ID" == *.debug || "$CLOUD_WORKER_HOST" == "1" ]]; then
|
|
SPARKLE_FEED_URL=""
|
|
AUTO_CHECKS=false
|
|
fi
|
|
|
|
resolve_peekaboo_source_commit() {
|
|
local resolved_file="$ROOT_DIR/apps/macos/Package.resolved"
|
|
local revision
|
|
revision="$(/usr/bin/python3 - "$resolved_file" <<'PY'
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import sys
|
|
|
|
resolved_file = Path(sys.argv[1])
|
|
try:
|
|
resolved = json.loads(resolved_file.read_text())
|
|
except (OSError, json.JSONDecodeError) as error:
|
|
raise SystemExit(f"ERROR: Could not parse Peekaboo source revision from {resolved_file}: {error}")
|
|
|
|
pins = resolved.get("pins") if isinstance(resolved, dict) else None
|
|
if not isinstance(pins, list):
|
|
raise SystemExit(f"ERROR: Expected a pins array in {resolved_file}")
|
|
|
|
peekaboo_pins = [pin for pin in pins if isinstance(pin, dict) and pin.get("identity") == "peekaboo"]
|
|
if len(peekaboo_pins) != 1:
|
|
raise SystemExit(f"ERROR: Expected exactly one 'peekaboo' pin in {resolved_file}; found {len(peekaboo_pins)}")
|
|
|
|
state = peekaboo_pins[0].get("state")
|
|
revision = state.get("revision") if isinstance(state, dict) else None
|
|
if not isinstance(revision, str) or re.fullmatch(r"[0-9a-f]{40}", revision) is None:
|
|
raise SystemExit(
|
|
f"ERROR: Peekaboo pin in {resolved_file} must have an exact 40-character lowercase hexadecimal revision"
|
|
)
|
|
|
|
print(revision, end="")
|
|
PY
|
|
)"
|
|
local expected="${OPENCLAW_EXPECTED_PEEKABOO_SOURCE_COMMIT:-}"
|
|
if [[ -n "$expected" && ! "$expected" =~ ^[0-9a-f]{40}$ ]]; then
|
|
echo "ERROR: OPENCLAW_EXPECTED_PEEKABOO_SOURCE_COMMIT must be a full lowercase 40-character SHA" >&2
|
|
return 1
|
|
fi
|
|
if [[ -n "$expected" && "$revision" != "$expected" ]]; then
|
|
echo "ERROR: Peekaboo pin '$revision' does not match requested release source '$expected'" >&2
|
|
return 1
|
|
fi
|
|
printf '%s' "$revision"
|
|
}
|
|
|
|
sparkle_canonical_build_from_version() {
|
|
(cd "$ROOT_DIR" && node --import tsx "$ROOT_DIR/scripts/sparkle-build.ts" canonical-build "$1")
|
|
}
|
|
|
|
source "$ROOT_DIR/scripts/lib/mac-swift-build.sh"
|
|
|
|
cleanup_package_build() {
|
|
if [[ -n "$SWIFT_BUILD_RESULTS" && ! -f "$SWIFT_BUILD_RESULTS/cleanup-complete" ]]; then
|
|
echo "ERROR: Swift cleanup was not verified; retaining $APP_STAGE_DIR for inspection" >&2
|
|
return
|
|
fi
|
|
[[ -z "$APP_STAGE_DIR" ]] || rm -rf "$APP_STAGE_DIR"
|
|
}
|
|
|
|
interrupt_package_build() {
|
|
local signal="$1" code="$2"
|
|
if [[ -n "$SWIFT_BUILD_PID" ]]; then
|
|
kill -"$signal" "$SWIFT_BUILD_PID" 2>/dev/null || true
|
|
wait "$SWIFT_BUILD_PID" || true
|
|
SWIFT_BUILD_PID=""
|
|
fi
|
|
exit "$code"
|
|
}
|
|
|
|
trap cleanup_package_build EXIT
|
|
trap 'interrupt_package_build INT 130' INT
|
|
trap 'interrupt_package_build TERM 143' TERM
|
|
trap 'interrupt_package_build HUP 129' HUP
|
|
|
|
PNPM_CMD=()
|
|
|
|
resolve_pnpm_cmd() {
|
|
if command -v corepack >/dev/null 2>&1 && (cd "$ROOT_DIR" && corepack pnpm --version >/dev/null 2>&1); then
|
|
PNPM_CMD=(corepack pnpm)
|
|
return 0
|
|
fi
|
|
|
|
if command -v pnpm >/dev/null 2>&1; then
|
|
PNPM_CMD=(pnpm)
|
|
return 0
|
|
fi
|
|
|
|
echo "ERROR: pnpm is not on PATH and corepack pnpm is unavailable. Install pnpm or run with Node/Corepack on PATH." >&2
|
|
exit 1
|
|
}
|
|
|
|
run_pnpm() {
|
|
if [[ "${#PNPM_CMD[@]}" -eq 0 ]]; then
|
|
resolve_pnpm_cmd
|
|
fi
|
|
(cd "$ROOT_DIR" && "${PNPM_CMD[@]}" "$@")
|
|
}
|
|
|
|
merge_framework_machos() {
|
|
local primary="$1"
|
|
local dest="$2"
|
|
shift 2
|
|
local others=("$@")
|
|
|
|
archs_for() {
|
|
/usr/bin/lipo -info "$1" | /usr/bin/sed -E 's/.*are: //; s/.*architecture: //'
|
|
}
|
|
|
|
arch_in_list() {
|
|
local needle="$1"
|
|
shift
|
|
for item in "$@"; do
|
|
if [[ "$item" == "$needle" ]]; then
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
while IFS= read -r -d '' file; do
|
|
if /usr/bin/file "$file" | /usr/bin/grep "Mach-O" >/dev/null; then
|
|
local rel="${file#"$primary"/}"
|
|
local primary_archs
|
|
primary_archs=$(archs_for "$file")
|
|
IFS=' ' read -r -a primary_arch_array <<< "$primary_archs"
|
|
|
|
local missing_files=()
|
|
local tmp_dir
|
|
tmp_dir=$(mktemp -d)
|
|
for fw in "${others[@]}"; do
|
|
local other_file="$fw/$rel"
|
|
if [[ ! -f "$other_file" ]]; then
|
|
echo "ERROR: Missing $rel in $fw" >&2
|
|
rm -rf "$tmp_dir"
|
|
exit 1
|
|
fi
|
|
if /usr/bin/file "$other_file" | /usr/bin/grep "Mach-O" >/dev/null; then
|
|
local other_archs
|
|
other_archs=$(archs_for "$other_file")
|
|
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/${rel//\//_}-$arch"
|
|
/usr/bin/lipo -thin "$arch" "$other_file" -output "$thin_file"
|
|
missing_files+=("$thin_file")
|
|
primary_arch_array+=("$arch")
|
|
fi
|
|
done
|
|
fi
|
|
done
|
|
|
|
if [[ "${#missing_files[@]}" -gt 0 ]]; then
|
|
/usr/bin/lipo -create "$file" "${missing_files[@]}" -output "$dest/$rel"
|
|
fi
|
|
rm -rf "$tmp_dir"
|
|
fi
|
|
done < <(find "$primary" -type f -print0)
|
|
}
|
|
|
|
PEEKABOO_SOURCE_COMMIT="$(resolve_peekaboo_source_commit)"
|
|
PEEKABOO_LOCKED_SOURCE_COMMIT="$PEEKABOO_SOURCE_COMMIT"
|
|
|
|
require_swift_toolchain
|
|
|
|
if [[ "${SKIP_PNPM_INSTALL:-0}" != "1" ]]; then
|
|
echo "📦 Ensuring deps (pnpm install --frozen-lockfile)"
|
|
run_pnpm install --frozen-lockfile --config.node-linker=hoisted
|
|
else
|
|
echo "📦 Skipping pnpm install (SKIP_PNPM_INSTALL=1)"
|
|
fi
|
|
|
|
if [[ -z "${APP_BUILD:-}" ]]; then
|
|
APP_BUILD="$GIT_BUILD_NUMBER"
|
|
if [[ "$APP_VERSION" =~ ^[0-9]{4}\.[0-9]{1,2}\.[0-9]{1,2}([.-].*)?$ ]]; then
|
|
CANONICAL_BUILD="$(sparkle_canonical_build_from_version "$APP_VERSION")" || {
|
|
echo "ERROR: Failed to derive canonical Sparkle APP_BUILD from APP_VERSION '$APP_VERSION'." >&2
|
|
exit 1
|
|
}
|
|
if [[ "$CANONICAL_BUILD" =~ ^[0-9]+$ ]] && (( CANONICAL_BUILD > APP_BUILD )); then
|
|
APP_BUILD="$CANONICAL_BUILD"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [[ "$AUTO_CHECKS" == "true" && ! "$APP_BUILD" =~ ^[0-9]+$ ]]; then
|
|
echo "ERROR: APP_BUILD must be numeric for Sparkle compare (CFBundleVersion). Got: $APP_BUILD" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "${SKIP_TSC:-0}" == "1" ]]; then
|
|
echo "📦 SKIP_TSC no longer skips the app's private runtime; using the content-checked build cache"
|
|
fi
|
|
echo "📦 Building JS (pnpm build)"
|
|
run_pnpm build
|
|
|
|
node - "$ROOT_DIR/dist/build-info.json" "$APP_VERSION" "$BUILD_GIT_COMMIT" "$BUILD_TS" <<'NODE'
|
|
const fs = require("node:fs");
|
|
const [file, version, commit, builtAt] = process.argv.slice(2);
|
|
const actual = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
if (actual.version !== version || actual.commit !== commit || actual.builtAt !== builtAt || !actual.buildId) {
|
|
throw new Error("JavaScript build provenance does not match this app. Rebuild from matching package inputs.");
|
|
}
|
|
NODE
|
|
|
|
node "$ROOT_DIR/scripts/prepare-apple-mermaid.mjs"
|
|
|
|
# pnpm build owns the Control UI and content-checked build stamps as well.
|
|
# Private Swift and worker staging must stay outside the published dist tree.
|
|
mkdir -p "$(dirname "$APP_DESTINATION")" "$ROOT_DIR/.artifacts"
|
|
APP_STAGE_DIR="$(mktemp -d "$ROOT_DIR/.artifacts/.openclaw-package.XXXXXX")"
|
|
APP_ROOT="$APP_STAGE_DIR/$APP_BUNDLE_NAME"
|
|
|
|
echo "🔨 Building $PRODUCT ($BUILD_CONFIG) [${BUILD_ARCHS[*]}]"
|
|
SWIFT_BUILD_RESULTS="$APP_STAGE_DIR/swift-builds"
|
|
node "$ROOT_DIR/scripts/build-mac-swift.mts" "$ROOT_DIR" "$BUILD_CONFIG" \
|
|
"$PEEKABOO_LOCKED_SOURCE_COMMIT" "$SKIP_MLX_TTS" "$SWIFT_BUILD_RESULTS" "${BUILD_ARCHS[@]}" &
|
|
SWIFT_BUILD_PID=$!
|
|
if wait "$SWIFT_BUILD_PID"; then
|
|
SWIFT_BUILD_PID=""
|
|
PEEKABOO_SOURCE_COMMIT="$PEEKABOO_LOCKED_SOURCE_COMMIT"
|
|
else
|
|
build_status=$?
|
|
SWIFT_BUILD_PID=""
|
|
exit "$build_status"
|
|
fi
|
|
|
|
BIN_PRIMARY="$(bin_for_arch "$PRIMARY_ARCH")"
|
|
echo "pkg: binary $BIN_PRIMARY" >&2
|
|
echo "📦 Assembling replacement app bundle"
|
|
mkdir -p "$APP_ROOT/Contents/MacOS"
|
|
mkdir -p "$APP_ROOT/Contents/Resources"
|
|
mkdir -p "$APP_ROOT/Contents/Frameworks"
|
|
|
|
echo "📄 Copying Info.plist template"
|
|
INFO_PLIST_SRC="$ROOT_DIR/apps/macos/Sources/OpenClaw/Resources/Info.plist"
|
|
if [ ! -f "$INFO_PLIST_SRC" ]; then
|
|
echo "ERROR: Info.plist template missing at $INFO_PLIST_SRC" >&2
|
|
exit 1
|
|
fi
|
|
cp "$INFO_PLIST_SRC" "$APP_ROOT/Contents/Info.plist"
|
|
PORT_GUARDIAN_STORAGE_VERSION="$(plist_print_required "$APP_ROOT/Contents/Info.plist" OpenClawPortGuardianStorageVersion)"
|
|
if [[ ! "$PORT_GUARDIAN_STORAGE_VERSION" =~ ^[1-9][0-9]*$ ]]; then
|
|
echo "ERROR: OpenClawPortGuardianStorageVersion must be a positive integer." >&2
|
|
exit 1
|
|
fi
|
|
plist_set_string_required "$APP_ROOT/Contents/Info.plist" CFBundleIdentifier "$BUNDLE_ID"
|
|
if [[ "$CLOUD_WORKER_HOST" == "1" ]]; then
|
|
plist_set_string_required "$APP_ROOT/Contents/Info.plist" CFBundleName "OpenClaw Cloud Worker"
|
|
plist_set_or_add_string "$APP_ROOT/Contents/Info.plist" CFBundleDisplayName "OpenClaw Cloud Worker"
|
|
/usr/libexec/PlistBuddy -c 'Add :OpenClawCloudWorkerHostVersion integer 1' "$APP_ROOT/Contents/Info.plist"
|
|
/usr/libexec/PlistBuddy -c 'Delete :CFBundleURLTypes' "$APP_ROOT/Contents/Info.plist"
|
|
fi
|
|
plist_set_string_required "$APP_ROOT/Contents/Info.plist" CFBundleShortVersionString "$APP_VERSION"
|
|
plist_set_string_required "$APP_ROOT/Contents/Info.plist" CFBundleVersion "$APP_BUILD"
|
|
plist_set_string_required "$APP_ROOT/Contents/Info.plist" OpenClawBuildTimestamp "$BUILD_TS"
|
|
plist_set_string_required "$APP_ROOT/Contents/Info.plist" OpenClawGitCommit "$BUILD_GIT_COMMIT"
|
|
WORKER_BUILD_ID="$(node -e 'console.log(require(process.argv[1]).buildId)' "$ROOT_DIR/dist/build-info.json")"
|
|
plist_set_or_add_string "$APP_ROOT/Contents/Info.plist" OpenClawWorkerBuildID "$WORKER_BUILD_ID"
|
|
plist_set_string_required "$APP_ROOT/Contents/Info.plist" PeekabooSourceCommit "$PEEKABOO_SOURCE_COMMIT"
|
|
if [[ "$BUILD_CONFIG" == "release" ]]; then
|
|
EMBEDDED_GIT_COMMIT="$(plist_print_required "$APP_ROOT/Contents/Info.plist" OpenClawGitCommit)"
|
|
BRIDGE_SOURCE_COMMIT="$(plist_print_required "$APP_ROOT/Contents/Info.plist" PeekabooSourceCommit)"
|
|
if [[ "$EMBEDDED_GIT_COMMIT" != "$BUILD_GIT_COMMIT" ]]; then
|
|
echo "ERROR: Release app OpenClaw source mismatch: OpenClawGitCommit='$EMBEDDED_GIT_COMMIT', expected='$BUILD_GIT_COMMIT'." >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$BRIDGE_SOURCE_COMMIT" != "$PEEKABOO_SOURCE_COMMIT" ]]; then
|
|
echo "ERROR: Release app Peekaboo source mismatch: PeekabooSourceCommit='$BRIDGE_SOURCE_COMMIT', expected='$PEEKABOO_SOURCE_COMMIT'." >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
plist_set_or_add_string "$APP_ROOT/Contents/Info.plist" SUFeedURL "$SPARKLE_FEED_URL"
|
|
plist_set_or_add_string "$APP_ROOT/Contents/Info.plist" SUPublicEDKey "$SPARKLE_PUBLIC_ED_KEY"
|
|
plist_set_or_add_bool "$APP_ROOT/Contents/Info.plist" SUEnableAutomaticChecks "$AUTO_CHECKS"
|
|
|
|
echo "🚚 Copying binary"
|
|
cp "$BIN_PRIMARY" "$APP_ROOT/Contents/MacOS/OpenClaw"
|
|
if [[ "${#BUILD_ARCHS[@]}" -gt 1 ]]; then
|
|
BIN_INPUTS=()
|
|
for arch in "${BUILD_ARCHS[@]}"; do
|
|
BIN_INPUTS+=("$(bin_for_arch "$arch")")
|
|
done
|
|
/usr/bin/lipo -create "${BIN_INPUTS[@]}" -output "$APP_ROOT/Contents/MacOS/OpenClaw"
|
|
fi
|
|
chmod +x "$APP_ROOT/Contents/MacOS/OpenClaw"
|
|
# SwiftPM outputs ad-hoc signed binaries; strip the signature before install_name_tool to avoid warnings.
|
|
/usr/bin/codesign --remove-signature "$APP_ROOT/Contents/MacOS/OpenClaw" 2>/dev/null || true
|
|
|
|
echo "🚚 Copying macOS control CLI"
|
|
cp "$(mac_cli_bin_for_arch "$PRIMARY_ARCH")" "$APP_ROOT/Contents/MacOS/openclaw-mac"
|
|
if [[ "${#BUILD_ARCHS[@]}" -gt 1 ]]; then
|
|
MAC_CLI_BIN_INPUTS=()
|
|
for arch in "${BUILD_ARCHS[@]}"; do
|
|
MAC_CLI_BIN_INPUTS+=("$(mac_cli_bin_for_arch "$arch")")
|
|
done
|
|
/usr/bin/lipo -create "${MAC_CLI_BIN_INPUTS[@]}" -output "$APP_ROOT/Contents/MacOS/openclaw-mac"
|
|
fi
|
|
chmod +x "$APP_ROOT/Contents/MacOS/openclaw-mac"
|
|
/usr/bin/codesign --remove-signature "$APP_ROOT/Contents/MacOS/openclaw-mac" 2>/dev/null || true
|
|
|
|
if [[ "$SKIP_MLX_TTS" == "1" ]]; then
|
|
echo "🔇 Skipping MLX TTS helper copy (OPENCLAW_SKIP_MLX_TTS=1) — bundle omits Contents/MacOS/$MLX_TTS_HELPER_PRODUCT"
|
|
else
|
|
echo "🚚 Copying MLX TTS helper"
|
|
cp "$(helper_bin_for_arch "$PRIMARY_ARCH")" "$APP_ROOT/Contents/MacOS/$MLX_TTS_HELPER_PRODUCT"
|
|
if [[ "${#BUILD_ARCHS[@]}" -gt 1 ]]; then
|
|
HELPER_BIN_INPUTS=()
|
|
for arch in "${BUILD_ARCHS[@]}"; do
|
|
HELPER_BIN_INPUTS+=("$(helper_bin_for_arch "$arch")")
|
|
done
|
|
/usr/bin/lipo -create "${HELPER_BIN_INPUTS[@]}" -output "$APP_ROOT/Contents/MacOS/$MLX_TTS_HELPER_PRODUCT"
|
|
fi
|
|
chmod +x "$APP_ROOT/Contents/MacOS/$MLX_TTS_HELPER_PRODUCT"
|
|
/usr/bin/codesign --remove-signature "$APP_ROOT/Contents/MacOS/$MLX_TTS_HELPER_PRODUCT" 2>/dev/null || true
|
|
fi
|
|
|
|
SPARKLE_FRAMEWORK_PRIMARY="$(sparkle_framework_for_arch "$PRIMARY_ARCH")"
|
|
if [ -d "$SPARKLE_FRAMEWORK_PRIMARY" ]; then
|
|
echo "✨ Embedding Sparkle.framework"
|
|
cp -R "$SPARKLE_FRAMEWORK_PRIMARY" "$APP_ROOT/Contents/Frameworks/"
|
|
if [[ "${#BUILD_ARCHS[@]}" -gt 1 ]]; then
|
|
OTHER_FRAMEWORKS=()
|
|
for arch in "${BUILD_ARCHS[@]}"; do
|
|
if [[ "$arch" == "$PRIMARY_ARCH" ]]; then
|
|
continue
|
|
fi
|
|
OTHER_FRAMEWORKS+=("$(sparkle_framework_for_arch "$arch")")
|
|
done
|
|
merge_framework_machos "$SPARKLE_FRAMEWORK_PRIMARY" "$APP_ROOT/Contents/Frameworks/Sparkle.framework" "${OTHER_FRAMEWORKS[@]}"
|
|
fi
|
|
chmod -R a+rX "$APP_ROOT/Contents/Frameworks/Sparkle.framework"
|
|
fi
|
|
|
|
echo "📦 Copying Swift 6.2 compatibility libraries"
|
|
SWIFT_COMPAT_LIB="$(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-6.2/macosx/libswiftCompatibilitySpan.dylib"
|
|
if [ -f "$SWIFT_COMPAT_LIB" ]; then
|
|
cp "$SWIFT_COMPAT_LIB" "$APP_ROOT/Contents/Frameworks/"
|
|
chmod +x "$APP_ROOT/Contents/Frameworks/libswiftCompatibilitySpan.dylib"
|
|
elif [[ "$BUILD_CONFIG" == "release" ]]; then
|
|
echo "ERROR: Swift compatibility library not found at $SWIFT_COMPAT_LIB" >&2
|
|
exit 1
|
|
else
|
|
echo "WARN: Swift compatibility library not found at $SWIFT_COMPAT_LIB (continuing)" >&2
|
|
fi
|
|
|
|
echo "🖼 Compiling app icon"
|
|
xcrun actool "$ROOT_DIR/apps/macos/Icon.icon" \
|
|
--compile "$APP_ROOT/Contents/Resources" \
|
|
--output-format human-readable-text --notices --warnings --errors \
|
|
--output-partial-info-plist "$APP_STAGE_DIR/icon.plist" \
|
|
--app-icon Icon --include-all-app-icons --enable-on-demand-resources NO \
|
|
--development-region en --target-device mac \
|
|
--minimum-deployment-target "$(plist_print_required "$APP_ROOT/Contents/Info.plist" LSMinimumSystemVersion)" \
|
|
--platform macosx
|
|
mv "$APP_ROOT/Contents/Resources/Icon.icns" "$APP_ROOT/Contents/Resources/OpenClaw.icns"
|
|
cp -R "$ROOT_DIR/apps/macos/Sources/OpenClaw/Resources/AppIcons" "$APP_ROOT/Contents/Resources/AppIcons"
|
|
|
|
echo "📦 Copying device model resources"
|
|
rm -rf "$APP_ROOT/Contents/Resources/DeviceModels"
|
|
cp -R "$ROOT_DIR/apps/macos/Sources/OpenClaw/Resources/DeviceModels" "$APP_ROOT/Contents/Resources/DeviceModels"
|
|
|
|
echo "📦 Copying provider icon resources"
|
|
PROVIDER_ICONS_SRC="$ROOT_DIR/apps/macos/Sources/OpenClaw/Resources/ProviderIcons"
|
|
if [ ! -d "$PROVIDER_ICONS_SRC" ]; then
|
|
echo "ERROR: Provider icon resources missing at $PROVIDER_ICONS_SRC" >&2
|
|
exit 1
|
|
fi
|
|
rm -rf "$APP_ROOT/Contents/Resources/ProviderIcons"
|
|
cp -R "$PROVIDER_ICONS_SRC" "$APP_ROOT/Contents/Resources/ProviderIcons"
|
|
|
|
if [[ "$SIGNING_VARIANT" == "elevation-host" ]]; then
|
|
echo "🖥 Omitting embedded CUA driver from elevation-host package"
|
|
else
|
|
echo "🖥 Staging embedded CUA driver"
|
|
CUA_DRIVER="$APP_ROOT/Contents/Resources/cua-driver"
|
|
"$ROOT_DIR/scripts/stage-cua-driver-macos.sh" "$CUA_DRIVER"
|
|
if [[ "${#BUILD_ARCHS[@]}" -eq 1 ]]; then
|
|
CUA_ARCH="${BUILD_ARCHS[0]}"
|
|
CUA_DRIVER_THIN="${CUA_DRIVER}.thin"
|
|
echo "🖥 Thinning embedded CUA driver [$CUA_ARCH]"
|
|
lipo "$CUA_DRIVER" -thin "$CUA_ARCH" -output "$CUA_DRIVER_THIN"
|
|
chmod 0755 "$CUA_DRIVER_THIN"
|
|
mv "$CUA_DRIVER_THIN" "$CUA_DRIVER"
|
|
[[ "$(lipo -archs "$CUA_DRIVER")" == "$CUA_ARCH" ]] || {
|
|
echo "ERROR: CUA driver architecture did not match requested build: $CUA_ARCH" >&2
|
|
exit 1
|
|
}
|
|
fi
|
|
fi
|
|
|
|
echo "📦 Staging browser sign-in helper"
|
|
for arch in "${BUILD_ARCHS[@]}"; do
|
|
/bin/bash "$ROOT_DIR/scripts/stage-cloudflared-macos.sh" "$arch" "$APP_ROOT/Contents/Resources/cloudflared"
|
|
done
|
|
|
|
echo "📦 Copying CLI installer"
|
|
INSTALL_CLI_SRC="$ROOT_DIR/scripts/install-cli.sh"
|
|
if [ ! -f "$INSTALL_CLI_SRC" ]; then
|
|
echo "ERROR: CLI installer missing at $INSTALL_CLI_SRC" >&2
|
|
exit 1
|
|
fi
|
|
cp "$INSTALL_CLI_SRC" "$APP_ROOT/Contents/Resources/install-cli.sh"
|
|
chmod 0644 "$APP_ROOT/Contents/Resources/install-cli.sh"
|
|
|
|
echo "📦 Provisioning the matching private node worker [${BUILD_ARCHS[*]}]"
|
|
/bin/bash "$ROOT_DIR/scripts/stage-mac-node-worker.sh" "$APP_ROOT/Contents/Resources/node-worker" "${BUILD_ARCHS[@]}"
|
|
|
|
echo "🌐 Copying app localizations"
|
|
node --import tsx "$ROOT_DIR/scripts/apple-app-i18n.ts" compile-macos \
|
|
--output "$APP_ROOT/Contents/Resources"
|
|
|
|
# The native dashboard loads the Gateway-served HTTP UI. Neither the app bundle
|
|
# nor its private `node worker` runtime serves a second Control UI copy.
|
|
if [[ -e "$APP_ROOT/Contents/Resources/control-ui" || -L "$APP_ROOT/Contents/Resources/control-ui" ]]; then
|
|
echo "ERROR: Standalone Control UI assets must not be embedded in OpenClaw.app" >&2
|
|
exit 1
|
|
fi
|
|
for arch in "${BUILD_ARCHS[@]}"; do
|
|
worker_ui="$APP_ROOT/Contents/Resources/node-worker/$arch/lib/node_modules/openclaw/dist/control-ui"
|
|
if [[ -e "$worker_ui" || -L "$worker_ui" ]]; then
|
|
echo "ERROR: Private node worker must not embed Control UI assets: $worker_ui" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
echo "📦 Copying SwiftPM resource bundles"
|
|
SWIFTPM_BUILD_PRODUCTS=("$(build_path_for_arch "$PRIMARY_ARCH")/$BUILD_CONFIG")
|
|
if [[ "$SKIP_MLX_TTS" != "1" ]]; then
|
|
SWIFTPM_BUILD_PRODUCTS+=("$(helper_products_for_arch "$PRIMARY_ARCH")")
|
|
fi
|
|
# Main app and helper dependencies share the signed Resources directory.
|
|
# MLX loads its compiled Metal library from its resource bundle there.
|
|
for build_products in "${SWIFTPM_BUILD_PRODUCTS[@]}"; do
|
|
for resource_bundle_src in "$build_products"/*.bundle; do
|
|
[[ -d "$resource_bundle_src" ]] || continue
|
|
resource_bundle="${resource_bundle_src##*/}"
|
|
rm -rf "$APP_ROOT/Contents/Resources/$resource_bundle"
|
|
cp -R "$resource_bundle_src" "$APP_ROOT/Contents/Resources/$resource_bundle"
|
|
done
|
|
done
|
|
REQUIRED_SWIFTPM_RESOURCE_BUNDLES=(
|
|
"GRDB_GRDB.bundle"
|
|
"KeyboardShortcuts_KeyboardShortcuts.bundle"
|
|
"OpenClaw_OpenClaw.bundle"
|
|
"OpenClawKit_OpenClawKit.bundle"
|
|
"OpenClawKit_OpenClawChatUI.bundle"
|
|
"SwiftMath_SwiftMath.bundle"
|
|
)
|
|
for resource_bundle in "${REQUIRED_SWIFTPM_RESOURCE_BUNDLES[@]}"; do
|
|
if [[ ! -d "$APP_ROOT/Contents/Resources/$resource_bundle" ]]; then
|
|
echo "ERROR: Required SwiftPM resource bundle not found at $APP_ROOT/Contents/Resources/$resource_bundle" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
if [[ "$SKIP_MLX_TTS" != "1" && ! -f "$APP_ROOT/Contents/Resources/mlx-swift_Cmlx.bundle/Contents/Resources/default.metallib" ]]; then
|
|
echo "ERROR: Required MLX shaders not found at $APP_ROOT/Contents/Resources/mlx-swift_Cmlx.bundle/Contents/Resources/default.metallib" >&2
|
|
exit 1
|
|
fi
|
|
|
|
running_packaged_app_pids() {
|
|
command -v pgrep >/dev/null 2>&1 || return 0
|
|
local app_binary="$APP_DESTINATION/Contents/MacOS/OpenClaw"
|
|
local pid
|
|
pgrep -x "$PRODUCT" 2>/dev/null | while IFS= read -r pid; do
|
|
[[ "$pid" =~ ^[0-9]+$ ]] || continue
|
|
if command -v lsof >/dev/null 2>&1 &&
|
|
lsof -a -p "$pid" -d txt -Fn 2>/dev/null | sed 's/^n//' | grep -Fx "$app_binary" >/dev/null; then
|
|
printf '%s\n' "$pid"
|
|
continue
|
|
fi
|
|
local command_line
|
|
command_line="$(ps -p "$pid" -o command= 2>/dev/null || true)"
|
|
if [[ "$command_line" == "$app_binary" || "$command_line" == "$app_binary "* ]]; then
|
|
printf '%s\n' "$pid"
|
|
fi
|
|
done
|
|
}
|
|
|
|
stop_packaged_app_if_running() {
|
|
local pids=()
|
|
local pid
|
|
while IFS= read -r pid; do
|
|
[[ -n "$pid" ]] && pids+=("$pid")
|
|
done < <(running_packaged_app_pids)
|
|
if [[ "${#pids[@]}" -eq 0 ]]; then
|
|
return 0
|
|
fi
|
|
|
|
echo "⏹ Stopping packaged OpenClaw bundle (${pids[*]})"
|
|
kill "${pids[@]}" 2>/dev/null || true
|
|
for _ in $(seq 1 40); do
|
|
local alive=0
|
|
for pid in "${pids[@]}"; do
|
|
if kill -0 "$pid" 2>/dev/null; then
|
|
alive=1
|
|
fi
|
|
done
|
|
[[ "$alive" == "0" ]] && return 0
|
|
sleep 0.25
|
|
done
|
|
kill -KILL "${pids[@]}" 2>/dev/null || true
|
|
for _ in $(seq 1 20); do
|
|
local alive=0
|
|
for pid in "${pids[@]}"; do
|
|
if kill -0 "$pid" 2>/dev/null; then
|
|
alive=1
|
|
fi
|
|
done
|
|
[[ "$alive" == "0" ]] && return 0
|
|
sleep 0.1
|
|
done
|
|
echo "ERROR: Packaged OpenClaw bundle did not exit: ${pids[*]}" >&2
|
|
return 1
|
|
}
|
|
|
|
if [[ -n "${SIGN_IDENTITY:-}" ]]; then
|
|
echo "🔏 Signing bundle with explicit SIGN_IDENTITY"
|
|
else
|
|
echo "🔏 Signing bundle (auto-selecting signing identity)"
|
|
fi
|
|
"$ROOT_DIR/scripts/codesign-mac-app.sh" "$APP_ROOT"
|
|
codesign --verify --deep --strict "$APP_ROOT"
|
|
if [[ "${CLOUD_WORKER_HOST:-0}" == "1" ]]; then
|
|
signing_metadata="$(codesign -dv --verbose=4 "$APP_ROOT" 2>&1)"
|
|
if ! printf '%s\n' "$signing_metadata" | grep -q '^Authority=Developer ID Application:' ||
|
|
! printf '%s\n' "$signing_metadata" | grep -Eq '^TeamIdentifier=[A-Z0-9]{10}$'; then
|
|
echo "ERROR: Cloud worker app did not receive a Developer ID Application signature." >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
for arch in "${BUILD_ARCHS[@]}"; do
|
|
env -i HOME="$APP_STAGE_DIR" PATH="/usr/bin:/bin:/usr/sbin:/sbin" TMPDIR="${TMPDIR:-/tmp}" \
|
|
"$APP_ROOT/Contents/Resources/node-worker/$arch/bin/node" \
|
|
"$ROOT_DIR/scripts/verify-mac-node-worker.mjs" \
|
|
"$APP_ROOT/Contents/Resources/node-worker/$arch" "$ROOT_DIR/dist/build-info.json"
|
|
done
|
|
codesign --verify --deep --strict "$APP_ROOT"
|
|
|
|
# Nothing touches the previous app until build, provisioning and signing pass.
|
|
stop_packaged_app_if_running
|
|
replace_mac_app_bundle "$APP_ROOT" "$APP_DESTINATION"
|
|
|
|
echo "✅ Bundle ready at $APP_DESTINATION"
|