#!/usr/bin/env bash set -euo pipefail APP_PATH="${1:-}" if [[ -z "$APP_PATH" ]]; then echo "Usage: $0 /path/to/App.app" >&2 exit 1 fi if [[ ! -d "$APP_PATH" ]]; then echo "App bundle not found: $APP_PATH" >&2 exit 1 fi APP_PATH="$(cd "$APP_PATH" && pwd -P)" INFO_PLIST="$APP_PATH/Contents/Info.plist" if [[ ! -f "$INFO_PLIST" ]]; then echo "Info.plist not found: $INFO_PLIST" >&2 exit 1 fi APP_EXECUTABLE="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$INFO_PLIST" 2>/dev/null || true)" if [[ -z "$APP_EXECUTABLE" ]]; then echo "Unable to read CFBundleExecutable from $INFO_PLIST" >&2 exit 1 fi MAIN_EXECUTABLE="$APP_PATH/Contents/MacOS/$APP_EXECUTABLE" if [[ ! -x "$MAIN_EXECUTABLE" ]]; then echo "Main executable not found: $MAIN_EXECUTABLE" >&2 exit 1 fi resolve_special_path() { local path="$1" local executable_dir="$2" local loader_dir="$3" path="${path//@executable_path/$executable_dir}" path="${path//@loader_path/$loader_dir}" printf '%s\n' "$path" } normalize_existing_path() { local path="$1" local dir dir="$(cd "$(dirname "$path")" && pwd -P)" printf '%s/%s\n' "$dir" "$(basename "$path")" } extract_rpaths() { local binary_path="$1" local executable_dir="$2" local loader_dir="$3" otool -l "$binary_path" 2>/dev/null | awk ' $1 == "cmd" && $2 == "LC_RPATH" { want = 1; next } want && $1 == "path" { print $2; want = 0 } ' | while IFS= read -r rpath; do resolve_special_path "$rpath" "$executable_dir" "$loader_dir" done } is_macho_file() { local path="$1" otool -L "$path" >/dev/null 2>&1 } list_contains_line() { local list="$1" local needle="$2" grep -Fxq "$needle" <<< "$list" } validate_no_test_only_frameworks() { local app_path="$1" local frameworks_dir="$app_path/Contents/Frameworks" [[ -d "$frameworks_dir" ]] || return 0 local forbidden_framework for forbidden_framework in patrol.framework; do if [[ -e "$frameworks_dir/$forbidden_framework" ]]; then echo "Test-only framework must not be bundled in packaged app: $frameworks_dir/$forbidden_framework" >&2 return 1 fi done } resolve_dependency() { local dependency="$1" local binary_path="$2" local app_path="$3" local executable_dir="$4" local loader_dir loader_dir="$(cd "$(dirname "$binary_path")" && pwd)" case "$dependency" in /System/Library/*|/usr/lib/*) return 0 ;; @executable_path/*|@loader_path/*) local resolved resolved="$(resolve_special_path "$dependency" "$executable_dir" "$loader_dir")" [[ -e "$resolved" ]] && { normalize_existing_path "$resolved" return 0 } return 1 ;; @rpath/*) while IFS= read -r rpath; do local candidate="${dependency/@rpath/$rpath}" candidate="$(resolve_special_path "$candidate" "$executable_dir" "$loader_dir")" if [[ -e "$candidate" ]]; then normalize_existing_path "$candidate" return 0 fi done < <( { extract_rpaths "$binary_path" "$executable_dir" "$loader_dir" printf '%s\n' "$app_path/Contents/Frameworks" } | awk '!seen[$0]++' ) return 1 ;; /*) if [[ -e "$dependency" ]]; then normalize_existing_path "$dependency" return 0 fi return 1 ;; *) return 1 ;; esac } validate_binary() { local binary_path="$1" local app_path="$2" local executable_dir="$3" local failures=0 local seen_dependencies="" while IFS= read -r line; do [[ -n "$line" ]] || continue local dependency dependency="$(awk '{print $1}' <<< "$line")" [[ "$dependency" == "$binary_path" ]] && continue list_contains_line "$seen_dependencies" "$dependency" && continue seen_dependencies+="${dependency}"$'\n' local is_weak=0 [[ "$line" == *" weak)"* ]] && is_weak=1 if [[ "$dependency" == "/System/Library/"* || "$dependency" == "/usr/lib/"* ]]; then continue fi if ! resolve_dependency "$dependency" "$binary_path" "$app_path" "$executable_dir" >/dev/null; then if (( is_weak )); then echo "Warning: unresolved weak dependency in $binary_path -> $dependency" >&2 continue fi echo "Missing dependency in app bundle:" >&2 echo " Binary: $binary_path" >&2 echo " Dependency: $dependency" >&2 ((failures++)) continue fi local resolved_path resolved_path="$(resolve_dependency "$dependency" "$binary_path" "$app_path" "$executable_dir")" case "$resolved_path" in /System/Library/*|/usr/lib/*) ;; "$app_path"/*) ;; *) if (( ! is_weak )); then echo "Non-system dependency resolves outside the app bundle:" >&2 echo " Binary: $binary_path" >&2 echo " Dependency: $dependency" >&2 echo " Resolved: $resolved_path" >&2 ((failures++)) fi ;; esac done < <( otool -L "$binary_path" 2>/dev/null | tail -n +2 | sed 's/^[[:space:]]*//' ) return "$failures" } echo "Validating macOS app bundle dynamic dependencies: $APP_PATH" validate_no_test_only_frameworks "$APP_PATH" EXECUTABLE_DIR="$(cd "$APP_PATH/Contents/MacOS" && pwd)" declare -a macho_files=("$MAIN_EXECUTABLE") if [[ -d "$APP_PATH/Contents/Frameworks" ]]; then while IFS= read -r -d '' candidate; do if is_macho_file "$candidate"; then macho_files+=("$candidate") fi done < <(find "$APP_PATH/Contents/Frameworks" -type f -print0) fi seen_binaries="" failures=0 for binary_path in "${macho_files[@]}"; do list_contains_line "$seen_binaries" "$binary_path" && continue seen_binaries+="${binary_path}"$'\n' if ! validate_binary "$binary_path" "$APP_PATH" "$EXECUTABLE_DIR"; then failures=1 fi done if (( failures != 0 )); then echo "App bundle dependency validation failed: $APP_PATH" >&2 exit 1 fi echo "App bundle dependency validation passed: $APP_PATH"