#!/usr/bin/env bash set -euo pipefail # Build and bundle OpenClaw into a minimal .app we can open. # Outputs to dist/OpenClaw.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" 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" 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}" 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 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 ]]; then SPARKLE_FEED_URL="" AUTO_CHECKS=false fi sparkle_canonical_build_from_version() { (cd "$ROOT_DIR" && node --import tsx "$ROOT_DIR/scripts/sparkle-build.ts" canonical-build "$1") } build_path_for_arch() { echo "$BUILD_ROOT/$1" } bin_for_arch() { echo "$(build_path_for_arch "$1")/$BUILD_CONFIG/$PRODUCT" } helper_build_path_for_arch() { echo "$MLX_TTS_HELPER_BUILD_ROOT/$1" } helper_bin_for_arch() { echo "$(helper_build_path_for_arch "$1")/$BUILD_CONFIG/$MLX_TTS_HELPER_PRODUCT" } sparkle_framework_for_arch() { echo "$(build_path_for_arch "$1")/$BUILD_CONFIG/Sparkle.framework" } run_with_locked_swift_packages() { local resolved_file="$ROOT_DIR/apps/macos/Package.resolved" local resolved_snapshot local command_status=0 if [[ ! -f "$resolved_file" ]]; then echo "ERROR: Swift package lockfile not found at $resolved_file" >&2 return 1 fi resolved_snapshot="$(mktemp)" cp "$resolved_file" "$resolved_snapshot" "$@" || command_status=$? if ! cmp -s "$resolved_snapshot" "$resolved_file"; then cp "$resolved_snapshot" "$resolved_file" rm "$resolved_snapshot" echo "ERROR: Swift package resolution changed Package.resolved; update it in a separate reviewed change" >&2 return 1 fi rm "$resolved_snapshot" return "$command_status" } PATCHED_SWIFTPM_RESOURCE_SOURCES=() restore_swiftpm_resource_sources() { local source_file local backup_file for source_file in "${PATCHED_SWIFTPM_RESOURCE_SOURCES[@]:-}"; do [[ -n "$source_file" ]] || continue backup_file="$source_file.openclaw-original" if [[ -f "$backup_file" ]]; then mv "$backup_file" "$source_file" fi done PATCHED_SWIFTPM_RESOURCE_SOURCES=() } patch_swiftpm_resource_lookups() { local build_path="$1" local checkout_root="$build_path/checkouts" local source_file local source_files=( "$checkout_root/KeyboardShortcuts/Sources/KeyboardShortcuts/Utilities.swift" "$checkout_root/SwiftMath/Sources/SwiftMath/MathBundle/MathFont.swift" "$checkout_root/SwiftMath/Sources/SwiftMath/MathRender/MTFont.swift" ) for source_file in "${source_files[@]}"; do if [[ ! -f "$source_file" ]]; then echo "ERROR: SwiftPM resource source not found at $source_file" >&2 return 1 fi if [[ -e "$source_file.openclaw-original" ]]; then echo "ERROR: Stale SwiftPM resource source backup at $source_file.openclaw-original" >&2 return 1 fi cp -p "$source_file" "$source_file.openclaw-original" chmod u+w "$source_file" PATCHED_SWIFTPM_RESOURCE_SOURCES+=("$source_file") done /usr/bin/python3 - "${source_files[@]}" <<'PY' from pathlib import Path import sys def replace_exact(path: Path, old: str, new: str, expected: int = 1) -> str: text = path.read_text() if text.count(old) != expected: raise SystemExit(f"Expected {expected} occurrence(s) in {path}: {old!r}") return text.replace(old, new) keyboard_shortcuts, swift_math_font, swift_math_legacy_font = map(Path, sys.argv[1:]) keyboard_text = replace_exact( keyboard_shortcuts, "NSLocalizedString(self, bundle: .module, comment: self)", "NSLocalizedString(self, bundle: .keyboardShortcutsPackagedResources, comment: self)", ) keyboard_marker = "\n\nextension Data {" keyboard_injection = """ private extension Bundle { \t// Command-line SwiftPM builds resolve Bundle.module beside the executable, which is \t// outside a valid signed .app layout. Prefer the bundle copied into Contents/Resources. \tstatic let keyboardShortcutsPackagedResources: Bundle = { \t\t#if os(macOS) \t\tif let url = Bundle.main.url( \t\t\tforResource: \"KeyboardShortcuts_KeyboardShortcuts\", \t\t\twithExtension: \"bundle\"), \t\t let bundle = Bundle(url: url) \t\t{ \t\t\treturn bundle \t\t} \t\t#endif \t\treturn .module \t}() } """ if keyboard_text.count(keyboard_marker) != 1: raise SystemExit(f"Expected one KeyboardShortcuts insertion marker in {keyboard_shortcuts}") keyboard_shortcuts.write_text(keyboard_text.replace(keyboard_marker, keyboard_injection + keyboard_marker)) swift_math_text = replace_exact( swift_math_font, "Bundle.module.url(forResource: \"mathFonts\", withExtension: \"bundle\")", "Bundle.swiftMathPackagedResources.url(forResource: \"mathFonts\", withExtension: \"bundle\")", expected=2, ) swift_math_marker = "\n#endif\n\n/// Now available for everyone to use" swift_math_injection = """ extension Bundle { // Keep SwiftMath's generated resource sidecar inside the signed app Resources directory. static let swiftMathPackagedResources: Bundle = { #if os(macOS) if let url = Bundle.main.url( forResource: \"SwiftMath_SwiftMath\", withExtension: \"bundle\"), let bundle = Bundle(url: url) { return bundle } #endif return .module }() } """ if swift_math_text.count(swift_math_marker) != 1: raise SystemExit(f"Expected one SwiftMath insertion marker in {swift_math_font}") swift_math_font.write_text( swift_math_text.replace(swift_math_marker, "\n#endif" + swift_math_injection + "\n/// Now available for everyone to use") ) legacy_text = replace_exact( swift_math_legacy_font, "Bundle.module.url(forResource: \"mathFonts\", withExtension: \"bundle\")", "Bundle.swiftMathPackagedResources.url(forResource: \"mathFonts\", withExtension: \"bundle\")", ) swift_math_legacy_font.write_text(legacy_text) PY } trap restore_swiftpm_resource_sources EXIT 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 -q "Mach-O"; 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 -q "Mach-O"; 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/$(echo "$rel" | tr '/' '_')-$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) } 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 "๐Ÿ“ฆ Building JS (pnpm build)" run_pnpm build else echo "๐Ÿ“ฆ Skipping JS build (SKIP_TSC=1)" fi if [[ "${SKIP_UI_BUILD:-0}" != "1" ]]; then echo "๐Ÿ–ฅ Building Control UI (ui:build)" (cd "$ROOT_DIR" && node scripts/ui.js build) else echo "๐Ÿ–ฅ Skipping Control UI build (SKIP_UI_BUILD=1)" fi cd "$ROOT_DIR/apps/macos" echo "๐Ÿ”จ Building $PRODUCT ($BUILD_CONFIG) [${BUILD_ARCHS[*]}]" for arch in "${BUILD_ARCHS[@]}"; do BUILD_PATH="$(build_path_for_arch "$arch")" echo "๐Ÿ“ฆ Resolving Swift packages [$arch]" run_with_locked_swift_packages swift package --scratch-path "$BUILD_PATH" resolve patch_swiftpm_resource_lookups "$BUILD_PATH" echo "๐Ÿ”จ Building $PRODUCT ($BUILD_CONFIG) [$arch]" run_with_locked_swift_packages swift build -c "$BUILD_CONFIG" --product "$PRODUCT" --build-path "$BUILD_PATH" --arch "$arch" -Xlinker -rpath -Xlinker @executable_path/../Frameworks restore_swiftpm_resource_sources echo "๐Ÿ”จ Building $MLX_TTS_HELPER_PRODUCT ($BUILD_CONFIG) [$arch]" swift build --package-path "$MLX_TTS_HELPER_ROOT" -c "$BUILD_CONFIG" --product "$MLX_TTS_HELPER_PRODUCT" --build-path "$(helper_build_path_for_arch "$arch")" --arch "$arch" done BIN_PRIMARY="$(bin_for_arch "$PRIMARY_ARCH")" echo "pkg: binary $BIN_PRIMARY" >&2 echo "๐Ÿงน Cleaning old app bundle" rm -rf "$APP_ROOT" 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" 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" if [[ "$BUILD_CONFIG" == "release" ]]; then EMBEDDED_GIT_COMMIT="$(plist_print_required "$APP_ROOT/Contents/Info.plist" OpenClawGitCommit)" if [[ "$EMBEDDED_GIT_COMMIT" != "$BUILD_GIT_COMMIT" ]]; then echo "ERROR: Release app embedded Git commit '$EMBEDDED_GIT_COMMIT', expected '$BUILD_GIT_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 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 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 "๐Ÿ–ผ Copying app icon" cp "$ROOT_DIR/apps/macos/Sources/OpenClaw/Resources/OpenClaw.icns" "$APP_ROOT/Contents/Resources/OpenClaw.icns" 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" 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 "๐ŸŒ Copying app localizations" node --import tsx "$ROOT_DIR/scripts/apple-app-i18n.ts" compile-macos \ --output "$APP_ROOT/Contents/Resources" echo "๐Ÿ“ฆ Copying Control UI assets" CONTROL_UI_SRC="$ROOT_DIR/dist/control-ui" CONTROL_UI_DEST="$APP_ROOT/Contents/Resources/control-ui" if [ -d "$CONTROL_UI_SRC" ] && [ -f "$CONTROL_UI_SRC/index.html" ]; then rm -rf "$CONTROL_UI_DEST" cp -R "$CONTROL_UI_SRC" "$CONTROL_UI_DEST" else echo "ERROR: Control UI assets missing at $CONTROL_UI_SRC. Run pnpm ui:build first." >&2 exit 1 fi echo "๐Ÿ“ฆ Copying SwiftPM resource bundles" SWIFTPM_BUILD_PRODUCTS="$(build_path_for_arch "$PRIMARY_ARCH")/$BUILD_CONFIG" # Generated Bundle.module accessors resolve from Bundle.main.bundleURL. In a packaged app, # that is the .app root, not Contents/Resources; placing a bundle there traps on first access. for resource_bundle_src in "$SWIFTPM_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 REQUIRED_SWIFTPM_RESOURCE_BUNDLES=( "GRDB_GRDB.bundle" "KeyboardShortcuts_KeyboardShortcuts.bundle" "OpenClaw_OpenClaw.bundle" "OpenClawKit_OpenClawKit.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 $SWIFTPM_BUILD_PRODUCTS/$resource_bundle" >&2 exit 1 fi done running_packaged_app_pids() { command -v pgrep >/dev/null 2>&1 || return 0 local app_binary="$APP_ROOT/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 } stop_packaged_app_if_running echo "๐Ÿ” Signing bundle (auto-selects signing identity if SIGN_IDENTITY is unset)" "$ROOT_DIR/scripts/codesign-mac-app.sh" "$APP_ROOT" echo "โœ… Bundle ready at $APP_ROOT"