mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
SwiftPM builds each dependency for its own floored platform, so KeyboardShortcuts (macOS 10.15, built for 12.0) emits its weak Clock.sleep(for:) ContinuousClock specialization with a 112-byte async frame while modules built for macOS 13+ emit a 128-byte one. The linker coalesces the body and the frame descriptor independently, and the 2026.9.7 arm64 build paired a body that writes byte 119 with the 112-byte descriptor: the 2026.9.6 launch-abort shape that the release async-frame audit now rejects. Pass -target <arch>-apple-macosx<LSMinimumSystemVersion> to every Swift module in both release build commands so all coalesced copies share one frame layout, independent of dependency pins or link order.
564 lines
22 KiB
Bash
564 lines
22 KiB
Bash
#!/bin/bash
|
|
|
|
build_path_for_arch() {
|
|
echo "$BUILD_ROOT/$1"
|
|
}
|
|
|
|
bin_for_arch() {
|
|
echo "$(build_path_for_arch "$1")/$BUILD_CONFIG/$PRODUCT"
|
|
}
|
|
|
|
mac_cli_bin_for_arch() {
|
|
echo "$(build_path_for_arch "$1")/$BUILD_CONFIG/openclaw-mac"
|
|
}
|
|
|
|
helper_build_path_for_arch() {
|
|
echo "$MLX_TTS_HELPER_BUILD_ROOT/$1"
|
|
}
|
|
|
|
helper_products_for_arch() {
|
|
cat "$SWIFT_BUILD_RESULTS/$1/helper-products"
|
|
}
|
|
|
|
helper_bin_for_arch() {
|
|
echo "$(helper_products_for_arch "$1")/$MLX_TTS_HELPER_PRODUCT"
|
|
}
|
|
|
|
build_mlx_tts_helper() {
|
|
local arch="$1"
|
|
shift
|
|
# Swift 6.3 needs Swift Build for Metal and --show-bin-path for its output directory.
|
|
swift build --build-system swiftbuild \
|
|
--package-path "$MLX_TTS_HELPER_ROOT" \
|
|
-c "$BUILD_CONFIG" \
|
|
--product "$MLX_TTS_HELPER_PRODUCT" \
|
|
--build-path "$(helper_build_path_for_arch "$arch")" \
|
|
--arch "$arch" \
|
|
--jobs "$SWIFT_BUILD_JOBS" \
|
|
"$@"
|
|
}
|
|
|
|
sparkle_framework_for_arch() {
|
|
echo "$(build_path_for_arch "$1")/$BUILD_CONFIG/Sparkle.framework"
|
|
}
|
|
|
|
run_with_locked_swift_packages() {
|
|
local resolved_file="${SWIFT_PACKAGE_ROOT:-$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"
|
|
}
|
|
|
|
compiled_peekaboo_commit() {
|
|
local checkout_or_build_path="$1" expected="$2"
|
|
local checkout="$checkout_or_build_path"
|
|
if [[ ! -d "$checkout/.git" && ! -f "$checkout/.git" ]]; then
|
|
checkout="$checkout_or_build_path/checkouts/Peekaboo"
|
|
fi
|
|
[[ -d "$checkout/.git" || -f "$checkout/.git" ]] || {
|
|
echo "ERROR: Resolved Peekaboo checkout not found at $checkout" >&2
|
|
return 1
|
|
}
|
|
local commit
|
|
if ! commit="$(git --no-replace-objects -C "$checkout" rev-parse HEAD)"; then
|
|
echo "ERROR: Could not inspect compiled Peekaboo checkout revision" >&2
|
|
return 1
|
|
fi
|
|
[[ "$commit" == "$expected" ]] || {
|
|
echo "ERROR: Compiled Peekaboo checkout '$commit' does not match locked source '$expected'" >&2
|
|
return 1
|
|
}
|
|
if ! /usr/bin/python3 - "$checkout" "$commit" <<'PY'
|
|
import hashlib
|
|
import os
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
|
|
checkout = os.fsencode(sys.argv[1])
|
|
commit = os.fsencode(sys.argv[2])
|
|
visited: set[tuple[bytes, bytes]] = set()
|
|
|
|
def run_git(repository: bytes, *arguments: str) -> bytes:
|
|
return subprocess.run(
|
|
["git", "-c", "core.commitGraph=false", "--no-replace-objects", "-C", os.fsdecode(repository), *arguments],
|
|
check=True,
|
|
stdout=subprocess.PIPE,
|
|
).stdout
|
|
|
|
def object_format_for(object_id: bytes) -> str:
|
|
if len(object_id) == 40:
|
|
return "sha1"
|
|
if len(object_id) == 64:
|
|
return "sha256"
|
|
raise SystemExit(1)
|
|
|
|
def read_verified_object(repository: bytes, object_type: str, object_id: bytes) -> bytes:
|
|
try:
|
|
contents = run_git(repository, "cat-file", object_type, object_id.decode("ascii"))
|
|
except (OSError, subprocess.CalledProcessError, UnicodeError):
|
|
raise SystemExit(2) from None
|
|
digest = hashlib.new(object_format_for(object_id))
|
|
digest.update(f"{object_type} {len(contents)}\0".encode("ascii"))
|
|
digest.update(contents)
|
|
if digest.hexdigest().encode("ascii") != object_id:
|
|
raise SystemExit(1)
|
|
return contents
|
|
|
|
def verify_repository(repository: bytes, expected_commit: bytes) -> None:
|
|
if not os.path.isdir(repository) or os.path.islink(repository):
|
|
raise SystemExit(1)
|
|
identity = (os.path.realpath(repository), expected_commit)
|
|
if identity in visited:
|
|
raise SystemExit(1)
|
|
visited.add(identity)
|
|
|
|
try:
|
|
head = run_git(repository, "rev-parse", "HEAD").strip()
|
|
run_git(repository, "fsck", "--full", "--strict", "--no-dangling", expected_commit.decode("ascii"))
|
|
except (OSError, subprocess.CalledProcessError):
|
|
raise SystemExit(2) from None
|
|
if head != expected_commit:
|
|
raise SystemExit(1)
|
|
|
|
commit = read_verified_object(repository, "commit", expected_commit)
|
|
tree_line = commit.split(b"\n", 1)[0]
|
|
if not tree_line.startswith(b"tree "):
|
|
raise SystemExit(1)
|
|
tree_id = tree_line.removeprefix(b"tree ")
|
|
object_format = object_format_for(tree_id)
|
|
if object_format != object_format_for(expected_commit):
|
|
raise SystemExit(1)
|
|
read_verified_object(repository, "tree", tree_id)
|
|
try:
|
|
listing = run_git(repository, "ls-tree", "-rz", tree_id.decode("ascii"))
|
|
except (OSError, subprocess.CalledProcessError, UnicodeError):
|
|
raise SystemExit(2) from None
|
|
expected: dict[bytes, tuple[bytes, bytes]] = {}
|
|
gitlinks: dict[bytes, bytes] = {}
|
|
for record in listing.split(b"\0"):
|
|
if not record:
|
|
continue
|
|
metadata, path = record.split(b"\t", 1)
|
|
mode, object_type, object_id = metadata.split(b" ", 2)
|
|
if object_type == b"blob":
|
|
expected[path] = (mode, object_id)
|
|
elif object_type == b"commit":
|
|
gitlinks[path] = object_id
|
|
|
|
def is_gitlink_path(path: bytes) -> bool:
|
|
return any(path == gitlink or path.startswith(gitlink + b"/") for gitlink in gitlinks)
|
|
|
|
actual: set[bytes] = set()
|
|
for root, directories, files in os.walk(repository, topdown=True, followlinks=False):
|
|
relative_root = os.path.relpath(root, repository)
|
|
relative_root = b"" if relative_root == b"." else relative_root
|
|
kept_directories: list[bytes] = []
|
|
for directory in directories:
|
|
relative = os.path.join(relative_root, directory) if relative_root else directory
|
|
absolute = os.path.join(root, directory)
|
|
if relative == b".git" or is_gitlink_path(relative):
|
|
continue
|
|
if os.path.islink(absolute):
|
|
actual.add(relative)
|
|
else:
|
|
kept_directories.append(directory)
|
|
directories[:] = kept_directories
|
|
for filename in files:
|
|
relative = os.path.join(relative_root, filename) if relative_root else filename
|
|
if relative == b".git" or is_gitlink_path(relative):
|
|
continue
|
|
actual.add(relative)
|
|
|
|
if actual != set(expected):
|
|
raise SystemExit(1)
|
|
|
|
for path, (mode, expected_id) in expected.items():
|
|
absolute = os.path.join(repository, path)
|
|
file_stat = os.lstat(absolute)
|
|
if mode == b"120000":
|
|
if not stat.S_ISLNK(file_stat.st_mode):
|
|
raise SystemExit(1)
|
|
contents = os.fsencode(os.readlink(absolute))
|
|
else:
|
|
if not stat.S_ISREG(file_stat.st_mode):
|
|
raise SystemExit(1)
|
|
executable = bool(file_stat.st_mode & stat.S_IXUSR)
|
|
if executable != (mode == b"100755"):
|
|
raise SystemExit(1)
|
|
with open(absolute, "rb") as source:
|
|
contents = source.read()
|
|
digest = hashlib.new(object_format)
|
|
digest.update(f"blob {len(contents)}\0".encode("ascii"))
|
|
digest.update(contents)
|
|
if digest.hexdigest().encode("ascii") != expected_id:
|
|
raise SystemExit(1)
|
|
|
|
for path, object_id in gitlinks.items():
|
|
verify_repository(os.path.join(repository, path), object_id)
|
|
|
|
verify_repository(checkout, commit)
|
|
PY
|
|
then
|
|
echo "ERROR: Compiled Peekaboo checkout does not exactly match its committed source" >&2
|
|
return 1
|
|
fi
|
|
printf '%s' "$commit"
|
|
}
|
|
|
|
PEEKABOO_SNAPSHOT_ROOT=""
|
|
PEEKABOO_SNAPSHOT_IMAGE=""
|
|
PEEKABOO_SNAPSHOT_MOUNT=""
|
|
SWIFT_PACKAGE_CONTAINER=""
|
|
SWIFT_PACKAGE_ROOT=""
|
|
SWIFT_PACKAGE_LOCK_BASELINE=""
|
|
|
|
prepare_swift_package_root() {
|
|
SWIFT_PACKAGE_CONTAINER="$SWIFT_WORK_ROOT/package"
|
|
SWIFT_PACKAGE_ROOT="$SWIFT_PACKAGE_CONTAINER/apps/macos"
|
|
SWIFT_PACKAGE_LOCK_BASELINE="$SWIFT_PACKAGE_CONTAINER/Package.resolved.committed"
|
|
mkdir -p "$SWIFT_PACKAGE_ROOT"
|
|
cp "$ROOT_DIR/apps/macos/Package.swift" "$SWIFT_PACKAGE_ROOT/Package.swift"
|
|
cp "$ROOT_DIR/apps/macos/Package.resolved" "$SWIFT_PACKAGE_LOCK_BASELINE"
|
|
cp "$SWIFT_PACKAGE_LOCK_BASELINE" "$SWIFT_PACKAGE_ROOT/Package.resolved"
|
|
chmod 0400 "$SWIFT_PACKAGE_LOCK_BASELINE"
|
|
ln -s "$ROOT_DIR/apps/macos/Sources" "$SWIFT_PACKAGE_ROOT/Sources"
|
|
ln -s "$ROOT_DIR/apps/macos/Tests" "$SWIFT_PACKAGE_ROOT/Tests"
|
|
ln -s "$ROOT_DIR/apps/shared" "$SWIFT_PACKAGE_CONTAINER/apps/shared"
|
|
ln -s "$ROOT_DIR/apps/swabble" "$SWIFT_PACKAGE_CONTAINER/apps/swabble"
|
|
MLX_TTS_HELPER_ROOT="$SWIFT_PACKAGE_CONTAINER/apps/macos-mlx-tts"
|
|
mkdir -p "$MLX_TTS_HELPER_ROOT"
|
|
cp "$ROOT_DIR/apps/macos-mlx-tts/Package.swift" "$MLX_TTS_HELPER_ROOT/Package.swift"
|
|
cp "$ROOT_DIR/apps/macos-mlx-tts/Package.resolved" "$MLX_TTS_HELPER_ROOT/Package.resolved"
|
|
ln -s "$ROOT_DIR/apps/macos-mlx-tts/Sources" "$MLX_TTS_HELPER_ROOT/Sources"
|
|
ln -s "$ROOT_DIR/apps/macos-mlx-tts/Tests" "$MLX_TTS_HELPER_ROOT/Tests"
|
|
}
|
|
|
|
clear_peekaboo_edit() {
|
|
local build_path="$1"
|
|
swift package --scratch-path "$build_path" unedit --force Peekaboo >/dev/null 2>&1 || true
|
|
}
|
|
|
|
edit_peekaboo_from_snapshot() {
|
|
local build_path="$1"
|
|
swift package --scratch-path "$build_path" edit Peekaboo --path "$PEEKABOO_SNAPSHOT_MOUNT"
|
|
}
|
|
|
|
verify_snapshot_swift_lock() {
|
|
/usr/bin/python3 - \
|
|
"$SWIFT_PACKAGE_LOCK_BASELINE" \
|
|
"$SWIFT_PACKAGE_ROOT/Package.resolved" <<'PY'
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
committed_path, snapshot_path = map(Path, sys.argv[1:])
|
|
committed = json.loads(committed_path.read_text())
|
|
snapshot = json.loads(snapshot_path.read_text())
|
|
|
|
def fail(reason):
|
|
raise SystemExit(f"ERROR: Peekaboo snapshot resolution does not match the committed Package.resolved: {reason}")
|
|
|
|
def pins(document, label):
|
|
values = document.get("pins")
|
|
if not isinstance(values, list):
|
|
fail(f"{label} has no pins list")
|
|
result = {}
|
|
for pin in values:
|
|
if not isinstance(pin, dict):
|
|
fail(f"{label} has a malformed pin")
|
|
identity = pin.get("identity")
|
|
if not isinstance(identity, str) or not identity or identity in result:
|
|
fail(f"{label} has a missing or duplicate pin identity {identity!r}")
|
|
result[identity] = pin
|
|
return result
|
|
|
|
def describe(pin):
|
|
state = pin.get("state")
|
|
if not isinstance(state, dict):
|
|
return json.dumps(pin, sort_keys=True)
|
|
parts = [str(state[key]) for key in ("version", "branch", "revision") if key in state]
|
|
location = pin.get("location")
|
|
return " ".join(parts + ([f"from {location}"] if location else []))
|
|
|
|
if committed.get("version") != snapshot.get("version"):
|
|
fail(f"format version {committed.get('version')!r} became {snapshot.get('version')!r}")
|
|
|
|
committed_pins = pins(committed, "committed lock")
|
|
snapshot_pins = pins(snapshot, "snapshot lock")
|
|
if "peekaboo" not in committed_pins:
|
|
fail("committed lock has no peekaboo pin")
|
|
if "peekaboo" in snapshot_pins:
|
|
fail("snapshot lock still pins peekaboo instead of the edited snapshot")
|
|
del committed_pins["peekaboo"]
|
|
if committed_pins != snapshot_pins:
|
|
changes = []
|
|
for identity in sorted(committed_pins.keys() | snapshot_pins.keys()):
|
|
before, after = committed_pins.get(identity), snapshot_pins.get(identity)
|
|
if before == after:
|
|
continue
|
|
if after is None:
|
|
changes.append(f"{identity}: {describe(before)} -> removed")
|
|
elif before is None:
|
|
changes.append(f"{identity}: added {describe(after)}")
|
|
else:
|
|
old, new = describe(before), describe(after)
|
|
if old == new:
|
|
old, new = (json.dumps(pin, sort_keys=True) for pin in (before, after))
|
|
changes.append(f"{identity}: {old} -> {new}")
|
|
fail("; ".join(changes))
|
|
PY
|
|
}
|
|
|
|
create_verified_peekaboo_snapshot() {
|
|
local build_path="$1" expected="$2" source_checkout source_commit snapshot_commit
|
|
source_checkout="$build_path/checkouts/Peekaboo"
|
|
source_commit="$(compiled_peekaboo_commit "$source_checkout" "$expected")" || return 1
|
|
PEEKABOO_SNAPSHOT_ROOT="$SWIFT_WORK_ROOT/snapshot"
|
|
mkdir -p "$PEEKABOO_SNAPSHOT_ROOT"
|
|
PEEKABOO_SNAPSHOT_IMAGE="$PEEKABOO_SNAPSHOT_ROOT/Peekaboo.dmg"
|
|
mkdir "$PEEKABOO_SNAPSHOT_MOUNT"
|
|
# -quiet suppresses failure stderr too; discard only routine stdout.
|
|
hdiutil create -fs APFS -format UDRO \
|
|
-srcfolder "$source_checkout" \
|
|
-volname OpenClawPeekabooSnapshot \
|
|
"$PEEKABOO_SNAPSHOT_IMAGE" >/dev/null
|
|
hdiutil attach -readonly -nobrowse \
|
|
-mountpoint "$PEEKABOO_SNAPSHOT_MOUNT" \
|
|
"$PEEKABOO_SNAPSHOT_IMAGE" >/dev/null
|
|
snapshot_commit="$(compiled_peekaboo_commit "$PEEKABOO_SNAPSHOT_MOUNT" "$expected")" || return 1
|
|
[[ "$snapshot_commit" == "$source_commit" ]] || return 1
|
|
}
|
|
|
|
swiftpm_resource_sources() {
|
|
local checkout_root="$1/checkouts"
|
|
printf '%s\n' \
|
|
"$checkout_root/KeyboardShortcuts/Sources/KeyboardShortcuts/Utilities.swift" \
|
|
"$checkout_root/SwiftMath/Sources/SwiftMath/MathBundle/MathFont.swift" \
|
|
"$checkout_root/SwiftMath/Sources/SwiftMath/MathRender/MTFont.swift"
|
|
}
|
|
|
|
restore_swiftpm_resource_sources() {
|
|
local source_file index=0 backup_file
|
|
while IFS= read -r source_file; do
|
|
backup_file="$SWIFT_WORK_ROOT/resource-backups/$index"
|
|
if [[ -f "$backup_file" ]]; then
|
|
mv "$backup_file" "$source_file" || return 1
|
|
fi
|
|
index=$((index + 1))
|
|
done < <(swiftpm_resource_sources "$BUILD_PATH")
|
|
}
|
|
|
|
patch_swiftpm_resource_lookups() {
|
|
local build_path="$1" source_file index=0
|
|
local source_files=()
|
|
mkdir "$SWIFT_WORK_ROOT/resource-backups"
|
|
while IFS= read -r source_file; do
|
|
if [[ ! -f "$source_file" ]]; then
|
|
echo "ERROR: SwiftPM resource source not found at $source_file" >&2
|
|
return 1
|
|
fi
|
|
cp -p "$source_file" "$SWIFT_WORK_ROOT/resource-backups/$index"
|
|
chmod u+w "$source_file"
|
|
source_files+=("$source_file")
|
|
index=$((index + 1))
|
|
done < <(swiftpm_resource_sources "$build_path")
|
|
|
|
/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
|
|
}
|
|
|
|
cleanup_swift_architecture() {
|
|
local cleanup_status=0
|
|
restore_swiftpm_resource_sources || cleanup_status=1
|
|
if [[ -d "$SWIFT_PACKAGE_ROOT" ]]; then
|
|
(cd "$SWIFT_PACKAGE_ROOT" && clear_peekaboo_edit "$BUILD_PATH") || cleanup_status=1
|
|
fi
|
|
if [[ -d "$PEEKABOO_SNAPSHOT_MOUNT" ]]; then
|
|
hdiutil detach -quiet "$PEEKABOO_SNAPSHOT_MOUNT" >/dev/null 2>&1 || {
|
|
# An unattached directory is safe to remove; a live mount must be retained.
|
|
local mounts
|
|
if ! mounts="$(mount)"; then
|
|
cleanup_status=1
|
|
elif printf '%s\n' "$mounts" | grep -F " on $PEEKABOO_SNAPSHOT_MOUNT (" >/dev/null; then
|
|
cleanup_status=1
|
|
fi
|
|
}
|
|
fi
|
|
if [[ "$cleanup_status" == "0" ]]; then
|
|
rm -rf "$PEEKABOO_SNAPSHOT_ROOT" "$PEEKABOO_SNAPSHOT_MOUNT" "$SWIFT_PACKAGE_CONTAINER" "$SWIFT_WORK_ROOT/resource-backups"
|
|
fi
|
|
return "$cleanup_status"
|
|
}
|
|
|
|
build_swift_architecture() {
|
|
local arch="$1" arch_peekaboo_commit
|
|
prepare_swift_package_root
|
|
cd "$SWIFT_PACKAGE_ROOT"
|
|
BUILD_PATH="$(build_path_for_arch "$arch")"
|
|
echo "📦 Resolving Swift packages [$arch]"
|
|
# The reused scratch path still records local packages under the previous run's
|
|
# package root. A full resolve (any resolve with Peekaboo edited, or a stale
|
|
# originHash) drops pins it cannot reach and floats them, e.g. swift-cmark via
|
|
# OpenClawKit. Lock-file mode rebinds local packages, checks out exactly the
|
|
# committed pins and returns an interrupted run's edited Peekaboo to its pin.
|
|
run_with_locked_swift_packages swift package --scratch-path "$BUILD_PATH" resolve --force-resolved-versions
|
|
echo "🔒 Freezing authenticated Peekaboo sources in a read-only snapshot [$arch]"
|
|
create_verified_peekaboo_snapshot "$BUILD_PATH" "$PEEKABOO_LOCKED_SOURCE_COMMIT"
|
|
edit_peekaboo_from_snapshot "$BUILD_PATH"
|
|
swift package --scratch-path "$BUILD_PATH" resolve
|
|
verify_snapshot_swift_lock
|
|
patch_swiftpm_resource_lookups "$BUILD_PATH"
|
|
# SwiftPM compiles each dependency for its own floored platform (KeyboardShortcuts: macOS 12),
|
|
# and below macOS 13 the weak Clock.sleep(for:) specialization gets a smaller async frame.
|
|
# The linker coalesces bodies and frame descriptors separately, so mixed targets can pair a
|
|
# 128-byte body with a 112-byte descriptor. Compile every module for the app's minimum.
|
|
local deployment_target
|
|
deployment_target="$(/usr/libexec/PlistBuddy -c 'Print :LSMinimumSystemVersion' "$ROOT_DIR/apps/macos/Sources/OpenClaw/Resources/Info.plist")"
|
|
local target_flags=(-Xswiftc -target -Xswiftc "$arch-apple-macosx$deployment_target")
|
|
echo "🔨 Building $PRODUCT ($BUILD_CONFIG) [$arch]"
|
|
verify_snapshot_swift_lock
|
|
swift build -c "$BUILD_CONFIG" --jobs "$SWIFT_BUILD_JOBS" --product "$PRODUCT" --build-path "$BUILD_PATH" --arch "$arch" "${target_flags[@]}" -Xlinker -rpath -Xlinker @executable_path/../Frameworks
|
|
verify_snapshot_swift_lock
|
|
echo "🔨 Building openclaw-mac ($BUILD_CONFIG) [$arch]"
|
|
swift build -c "$BUILD_CONFIG" --jobs "$SWIFT_BUILD_JOBS" --product openclaw-mac --build-path "$BUILD_PATH" --arch "$arch" "${target_flags[@]}" -Xlinker -rpath -Xlinker @executable_path/../Frameworks
|
|
verify_snapshot_swift_lock
|
|
arch_peekaboo_commit="$(compiled_peekaboo_commit "$PEEKABOO_SNAPSHOT_MOUNT" "$PEEKABOO_LOCKED_SOURCE_COMMIT")"
|
|
printf '%s\n' "$arch_peekaboo_commit" > "$SWIFT_WORK_ROOT/peekaboo-commit"
|
|
restore_swiftpm_resource_sources
|
|
clear_peekaboo_edit "$BUILD_PATH"
|
|
cd "$ROOT_DIR/apps/macos"
|
|
if [[ "$SKIP_MLX_TTS" == "1" ]]; then
|
|
echo "🔇 Skipping $MLX_TTS_HELPER_PRODUCT (OPENCLAW_SKIP_MLX_TTS=1) — app will lack the local MLX voice helper [$arch]"
|
|
else
|
|
echo "🔨 Building $MLX_TTS_HELPER_PRODUCT ($BUILD_CONFIG) [$arch]"
|
|
local helper_lock="$MLX_TTS_HELPER_ROOT/Package.resolved"
|
|
cp "$helper_lock" "$SWIFT_WORK_ROOT/mlx-lock"
|
|
build_mlx_tts_helper "$arch"
|
|
build_mlx_tts_helper "$arch" --show-bin-path > "$SWIFT_WORK_ROOT/helper-products"
|
|
cmp "$SWIFT_WORK_ROOT/mlx-lock" "$helper_lock" || {
|
|
echo "ERROR: MLX package resolution changed Package.resolved" >&2
|
|
return 1
|
|
}
|
|
fi
|
|
}
|
|
|
|
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
set -euo pipefail
|
|
operation="$1"
|
|
ROOT_DIR="$2"
|
|
arch="$3"
|
|
BUILD_CONFIG="$4"
|
|
SWIFT_BUILD_JOBS="$5"
|
|
PEEKABOO_LOCKED_SOURCE_COMMIT="$6"
|
|
SKIP_MLX_TTS="$7"
|
|
SWIFT_WORK_ROOT="$8"
|
|
PRODUCT=OpenClaw
|
|
MLX_TTS_HELPER_PRODUCT=openclaw-mlx-tts
|
|
BUILD_ROOT="$ROOT_DIR/apps/macos/.build"
|
|
MLX_TTS_HELPER_BUILD_ROOT="$ROOT_DIR/apps/macos-mlx-tts/.build"
|
|
BUILD_PATH="$(build_path_for_arch "$arch")"
|
|
SWIFT_PACKAGE_CONTAINER="$SWIFT_WORK_ROOT/package"
|
|
SWIFT_PACKAGE_ROOT="$SWIFT_PACKAGE_CONTAINER/apps/macos"
|
|
PEEKABOO_SNAPSHOT_ROOT="$SWIFT_WORK_ROOT/snapshot"
|
|
PEEKABOO_SNAPSHOT_MOUNT="$9"
|
|
case "$operation" in
|
|
build) build_swift_architecture "$arch" ;;
|
|
cleanup) cleanup_swift_architecture ;;
|
|
*) echo "Unknown Swift build operation: $operation" >&2; exit 1 ;;
|
|
esac
|
|
fi
|