#!/usr/bin/env bash # Turn one locally compiled macOS product into a self-contained .app bundle. set -euo pipefail fail() { printf 'publish-app.command: error: %s\n' "$*" >&2; exit 1; } usage() { cat <<'EOF' Usage: publish-app.command --build-dir DIR --product {WiiCompiled|RetroRewind} --output-dir DIR [options] Copies a locally built product and its runtime assets into OUTPUT-DIR/.app. It bundles non-system dylibs, rewrites their install names, and ad-hoc signs the result. This is suitable for local use; a release must replace ad-hoc signing with the project's Developer ID signing and notarization process. --architecture {arm64|x86_64} Required architecture of the compiled product (default: host) --minimum-system-version VERSION App bundle minimum macOS version (default: 12.0) EOF } build_dir=""; product=""; output_dir=""; architecture=$(uname -m); minimum_system_version=12.0 while (($#)); do case "$1" in --build-dir) build_dir=${2:-}; shift 2 ;; --product) product=${2:-}; shift 2 ;; --output-dir) output_dir=${2:-}; shift 2 ;; --architecture) architecture=${2:-}; shift 2 ;; --minimum-system-version) minimum_system_version=${2:-}; shift 2 ;; -h|--help) usage; exit 0 ;; *) fail "unknown option: $1" ;; esac done [[ "$product" == WiiCompiled || "$product" == RetroRewind ]] || fail '--product must be WiiCompiled or RetroRewind' [[ "$architecture" == arm64 || "$architecture" == x86_64 ]] || fail '--architecture must be arm64 or x86_64' [[ "$minimum_system_version" =~ ^[0-9]+(\.[0-9]+){1,2}$ ]] || fail '--minimum-system-version must contain two or three period-separated integers' for tool in codesign ditto install_name_tool lipo otool; do command -v "$tool" >/dev/null || fail "required macOS tool is unavailable: $tool"; done [[ -x "$build_dir/$product" ]] || fail "missing compiled product: $build_dir/$product" lipo "$build_dir/$product" -verify_arch "$architecture" || fail "compiled product is not $architecture: $build_dir/$product" for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem wii_bootstrap; do [[ -e "$build_dir/$asset" ]] || fail "missing runtime asset: $build_dir/$asset"; done app="$output_dir/$product.app" macos="$app/Contents/MacOS" frameworks="$app/Contents/Frameworks" resources="$app/Contents/Resources" rm -rf "$app" mkdir -p "$macos" "$frameworks" "$resources" cat > "$app/Contents/Info.plist" < CFBundleDevelopmentRegionen CFBundleExecutable$product CFBundleIdentifierorg.wiicompiled.$product CFBundleInfoDictionaryVersion6.0 CFBundleName$product CFBundlePackageTypeAPPL CFBundleShortVersionString0.1.0 CFBundleVersion1 LSMinimumSystemVersion$minimum_system_version NSHighResolutionCapable EOF ditto "$build_dir/$product" "$macos/$product" for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem wii_bootstrap; do ditto "$build_dir/$asset" "$resources/$asset" ln -s "../Resources/$asset" "$macos/$asset" done # Expand each image's rpaths before inheriting them, so @loader_path stays # relative to the image that declared it rather than a descendant library. expanded_rpaths() { local target=$1 rpath while IFS= read -r rpath; do case "$rpath" in @loader_path/*) rpath="$(dirname "$target")/${rpath#@loader_path/}" ;; @loader_path) rpath="$(dirname "$target")" ;; @executable_path/*) rpath="$build_dir/${rpath#@executable_path/}" ;; @executable_path) rpath="$build_dir" ;; esac printf '%s\n' "$rpath" done < <(otool -l "$target" | awk ' /LC_RPATH/ { rpath = 1; next } rpath && /^[[:space:]]*path / { sub(/^[[:space:]]*path[[:space:]]+/, ""); sub(/[[:space:]]+\(offset[[:space:]]+[0-9]+\)$/, ""); print; rpath = 0; }') } # Resolve a non-system dependency using the current image's rpaths followed # by the inherited loader stack, matching dyld's dependency-chain search. dependency_path() { local current=$1 dependency=$2 search_rpaths=$3 rpath candidate case "$dependency" in /System/Library/*|/usr/lib/*) return 1 ;; /*) [[ -f "$dependency" ]] && { printf '%s\n' "$dependency"; return 0; } ;; @loader_path/*) candidate="$(dirname "$current")/${dependency#@loader_path/}" [[ -f "$candidate" ]] && { printf '%s\n' "$candidate"; return 0; } ;; @executable_path/*) candidate="$build_dir/${dependency#@executable_path/}" [[ -f "$candidate" ]] && { printf '%s\n' "$candidate"; return 0; } ;; @rpath/*|*.dylib) local subpath if [[ "$dependency" == @rpath/* ]]; then subpath="${dependency#@rpath/}" else subpath="$dependency" fi while IFS= read -r rpath; do [[ -n "$rpath" ]] || continue candidate="$rpath/$subpath" [[ -f "$candidate" ]] && { printf '%s\n' "$candidate"; return 0; } done <<< "$search_rpaths" ;; esac return 1 } # Build a closure of non-system dylibs. System libraries remain system # references, while every resolved dependency is copied beside the executable. queue=("$build_dir/$product") queue_rpaths=("") while ((${#queue[@]})); do current=${queue[0]} inherited_rpaths=${queue_rpaths[0]} queue=("${queue[@]:1}") queue_rpaths=("${queue_rpaths[@]:1}") current_rpaths=$(expanded_rpaths "$current") if [[ -n "$inherited_rpaths" ]]; then current_rpaths="${current_rpaths}${current_rpaths:+$'\n'}$inherited_rpaths" fi self_id=$(otool -D "$current" 2>/dev/null | tail -n +2 | sed -nE 's/^[[:space:]]*(.*)[[:space:]]*$/\1/p' || true) while IFS= read -r dependency; do [[ -z "$dependency" ]] && continue [[ -n "$self_id" && "$dependency" == "$self_id" ]] && continue case "$dependency" in /System/Library/*|/usr/lib/*) continue ;; esac dep_path=$(dependency_path "$current" "$dependency" "$current_rpaths") || fail "unresolved non-system dependency: '$dependency' needed by '$current'" name=$(basename "$dep_path") if [[ ! -f "$frameworks/$name" ]]; then ditto "$dep_path" "$frameworks/$name" install_name_tool -id "@rpath/$name" "$frameworks/$name" queue+=("$dep_path") queue_rpaths+=("$current_rpaths") fi done < <(otool -L "$current" | tail -n +2 | sed -nE 's/^[[:space:]]*(.*)[[:space:]]+\(compatibility version .*/\1/p') done while IFS= read -r binary; do while IFS= read -r old; do name=$(basename "$old") [[ -f "$frameworks/$name" ]] || continue if [[ "$binary" == "$macos/$product" ]]; then install_name_tool -change "$old" "@executable_path/../Frameworks/$name" "$binary" else install_name_tool -change "$old" "@loader_path/$name" "$binary" fi done < <(otool -L "$binary" | tail -n +2 | sed -nE 's/^[[:space:]]*(.*)[[:space:]]+\(compatibility version .*/\1/p') done < <(find "$frameworks" -type f -print; printf '%s\n' "$macos/$product") find "$frameworks" -type f -exec codesign --force --sign - {} + codesign --force --deep --sign - "$app" codesign --verify --deep --strict "$app" printf 'MKWCBUILD:APP=%s\n' "$app"