Validate macOS app bundle dependencies
This commit is contained in:
parent
64e14beb70
commit
9e80740378
@ -42,6 +42,7 @@ validate_app_bundle() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bash "$ROOT_DIR/scripts/validate-macos-app-bundle.sh" "$app_path"
|
||||||
codesign --verify --deep --verbose=2 "$app_path"
|
codesign --verify --deep --verbose=2 "$app_path"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,6 +73,8 @@ if [[ ! -d "$SOURCE_APP" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
validate_app_bundle "$SOURCE_APP"
|
||||||
|
|
||||||
if [[ -d "$TARGET_APP" ]]; then
|
if [[ -d "$TARGET_APP" ]]; then
|
||||||
echo "Replacing existing app at $TARGET_APP"
|
echo "Replacing existing app at $TARGET_APP"
|
||||||
rm -rf "$TARGET_APP"
|
rm -rf "$TARGET_APP"
|
||||||
|
|||||||
@ -96,8 +96,14 @@ verify_bundle_signature() {
|
|||||||
codesign --verify --deep --verbose=2 "$app_path"
|
codesign --verify --deep --verbose=2 "$app_path"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validate_bundle_dependencies() {
|
||||||
|
local app_path="$1"
|
||||||
|
bash "$ROOT_DIR/scripts/validate-macos-app-bundle.sh" "$app_path"
|
||||||
|
}
|
||||||
|
|
||||||
echo "Validating export compliance metadata..."
|
echo "Validating export compliance metadata..."
|
||||||
bash "$ROOT_DIR/scripts/check-apple-export-compliance.sh" "$BUILD_APP_PATH"
|
bash "$ROOT_DIR/scripts/check-apple-export-compliance.sh" "$BUILD_APP_PATH"
|
||||||
|
validate_bundle_dependencies "$BUILD_APP_PATH"
|
||||||
|
|
||||||
rm -rf "$DIST_APP_PATH" "$DIST_DMG_PATH"
|
rm -rf "$DIST_APP_PATH" "$DIST_DMG_PATH"
|
||||||
ditto "$BUILD_APP_PATH" "$DIST_APP_PATH"
|
ditto "$BUILD_APP_PATH" "$DIST_APP_PATH"
|
||||||
@ -111,6 +117,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
verify_bundle_signature "$DIST_APP_PATH"
|
verify_bundle_signature "$DIST_APP_PATH"
|
||||||
|
validate_bundle_dependencies "$DIST_APP_PATH"
|
||||||
|
|
||||||
echo "Packaging DMG..."
|
echo "Packaging DMG..."
|
||||||
DMG_VOLUME_NAME="$APP_NAME" "$ROOT_DIR/scripts/create-dmg.sh" "$DIST_APP_PATH" "$DIST_DMG_PATH"
|
DMG_VOLUME_NAME="$APP_NAME" "$ROOT_DIR/scripts/create-dmg.sh" "$DIST_APP_PATH" "$DIST_DMG_PATH"
|
||||||
|
|||||||
209
scripts/validate-macos-app-bundle.sh
Normal file
209
scripts/validate-macos-app-bundle.sh
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
#!/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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
declare -A seen_dependencies=()
|
||||||
|
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ -n "$line" ]] || continue
|
||||||
|
|
||||||
|
local dependency
|
||||||
|
dependency="$(awk '{print $1}' <<< "$line")"
|
||||||
|
[[ "$dependency" == "$binary_path" ]] && continue
|
||||||
|
[[ -n "${seen_dependencies[$dependency]:-}" ]] && continue
|
||||||
|
seen_dependencies["$dependency"]=1
|
||||||
|
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"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
declare -A seen_binaries=()
|
||||||
|
failures=0
|
||||||
|
|
||||||
|
for binary_path in "${macho_files[@]}"; do
|
||||||
|
[[ -n "${seen_binaries[$binary_path]:-}" ]] && continue
|
||||||
|
seen_binaries["$binary_path"]=1
|
||||||
|
|
||||||
|
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"
|
||||||
Loading…
Reference in New Issue
Block a user