#!/usr/bin/env bash set -euo pipefail # Create a universal (arm64+x86_64) .app by merging two already-deployed app bundles # that have identical layouts, one built on Apple Silicon and one built on Intel. # # Usage: # ./build_tools/macos/make_universal_app.sh \ # --arm-app /path/to/arm64/strawberry.app \ # --x86-app /path/to/x86_64/strawberry.app \ # --out-app /path/to/output/strawberry.app \ # [--clean] # # Notes: # - Do NOT sign the per-arch apps first; signatures will be invalidated by lipo anyway. # - Both inputs must be the same app version/config with the same enabled features, # so the file lists match. ts() { date +"%H:%M:%S"; } usage() { cat <<'EOF' Usage: ./build_tools/macos/make_universal_app.sh --arm-app --x86-app --out-app [--clean] What it does: - Copies the arm64 app to --out-app - For every Mach-O file in the copied app, finds the corresponding file in the x86_64 app - Uses lipo to combine the two slices into a universal binary at the same relative path Required: --arm-app Path to arm64 Strawberry.app (built+deployed on Apple Silicon) --x86-app Path to x86_64 Strawberry.app (built+deployed on Intel) --out-app Output path for universal Strawberry.app Optional: --clean Delete --out-app if it already exists EOF } if [[ "$(uname -s)" != "Darwin" ]]; then echo "Error: This script is for macOS only." >&2 exit 1 fi arm_app="" x86_app="" out_app="" do_clean=0 while [[ $# -gt 0 ]]; do case "$1" in --arm-app) arm_app="${2:-}"; shift 2 ;; --x86-app) x86_app="${2:-}"; shift 2 ;; --out-app) out_app="${2:-}"; shift 2 ;; --clean) do_clean=1; shift ;; -h|--help) usage; exit 0 ;; *) echo "Unknown arg: $1" >&2; usage; exit 2 ;; esac done if [[ -z "$arm_app" || -z "$x86_app" || -z "$out_app" ]]; then echo "Error: missing required args." >&2 usage exit 2 fi if [[ ! -d "$arm_app" ]]; then echo "Error: --arm-app not found: $arm_app" >&2 exit 2 fi if [[ ! -d "$x86_app" ]]; then echo "Error: --x86-app not found: $x86_app" >&2 exit 2 fi for cmd in /usr/bin/file /usr/bin/lipo /usr/bin/ditto; do if [[ ! -x "$cmd" ]]; then echo "Error: required tool not found: $cmd" >&2 exit 1 fi done out_parent="$(cd -- "$(dirname -- "$out_app")" && pwd)" out_name="$(basename -- "$out_app")" out_app="${out_parent}/${out_name}" if [[ -e "$out_app" && "$do_clean" -eq 1 ]]; then echo "==> [$(ts)] Removing existing output app: $out_app" rm -rf "$out_app" fi if [[ -e "$out_app" ]]; then echo "Error: output already exists: $out_app (use --clean to overwrite)" >&2 exit 2 fi echo "==> [$(ts)] Copying arm64 app to output" /usr/bin/ditto "$arm_app" "$out_app" # Remove any existing signatures in the copied app; we'll re-sign after creating universal binaries. echo "==> [$(ts)] Removing existing code signature metadata (will be re-signed later)" find "$out_app" -type d -name "_CodeSignature" -print0 2>/dev/null | while IFS= read -r -d '' d; do rm -rf "$d" || true done echo "==> [$(ts)] Merging Mach-O files with lipo" merged=0 skipped=0 # Traverse output app and lipo-merge any Mach-O file with its counterpart in the x86 app. while IFS= read -r -d '' f; do # Only operate on regular files. if [[ ! -f "$f" ]]; then continue fi ft="$(/usr/bin/file -b "$f" 2>/dev/null || true)" if [[ "$ft" != *"Mach-O"* ]]; then skipped=$((skipped + 1)) continue fi rel="${f#"$out_app"/}" other="${x86_app}/${rel}" if [[ ! -f "$other" ]]; then echo "Error: missing matching file in x86 app for:" >&2 echo " $rel" >&2 echo "Expected at:" >&2 echo " $other" >&2 exit 1 fi other_ft="$(/usr/bin/file -b "$other" 2>/dev/null || true)" if [[ "$other_ft" != *"Mach-O"* ]]; then echo "Error: file is Mach-O in arm app but not Mach-O in x86 app:" >&2 echo " $rel" >&2 echo "arm64: $ft" >&2 echo "x86_64: $other_ft" >&2 exit 1 fi # Validate architectures. arm_archs="$(/usr/bin/lipo -archs "$f" 2>/dev/null || true)" x86_archs="$(/usr/bin/lipo -archs "$other" 2>/dev/null || true)" if [[ "$arm_archs" != *"arm64"* ]]; then echo "Error: expected arm64 slice in arm app file:" >&2 echo " $rel" >&2 echo " archs: $arm_archs" >&2 exit 1 fi if [[ "$x86_archs" != *"x86_64"* ]]; then echo "Error: expected x86_64 slice in x86 app file:" >&2 echo " $rel" >&2 echo " archs: $x86_archs" >&2 exit 1 fi tmp="$(mktemp -t strawberry-universal.XXXXXX)" /usr/bin/lipo -create "$f" "$other" -output "$tmp" chmod --reference="$f" "$tmp" 2>/dev/null || true mv -f "$tmp" "$f" merged=$((merged + 1)) done < <(find "$out_app" -type f -print0 2>/dev/null) echo "==> [$(ts)] Done" echo "Merged Mach-O files: $merged" echo "Non-Mach-O files skipped: $skipped" echo "Output app:" echo " $out_app"