joeylib2/scripts/check-macos-apps.sh

122 lines
4.3 KiB
Bash
Executable file

#!/usr/bin/env bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# check-macos-apps.sh - Host gate for the macOS port's apps (make macos),
# which nothing here can run: every app in build/macos/bin is one macOS can
# open. Its Info.plist names, as an application for the macOS
# make/support/macos/cc.sh builds for, a program that is in Contents/MacOS
# and an icon that is in Contents/Resources. The program is universal: an
# Apple silicon (arm64) and an Intel (x86_64) program, each built for that
# macOS and linking nothing but the system's own libraries, the Apple silicon
# one signed (zig signs it ad hoc as it links; macOS will not run an
# unsigned arm64 program at all). Read with LLVM's Mach-O tools.
#
# Usage: scripts/check-macos-apps.sh
# Exit: 0 PASS, 1 FAIL, 2 SKIP (the port not built, or no LLVM tools)
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "$repo/scripts/lib/joeyEnv.sh"
label=check-macos-apps
joeyNeed MACOS_LIPO toolchain:macos
objdump="$(dirname "$MACOS_LIPO")/llvm-objdump"
joeyNeedFile "$objdump" toolchain:macos
minimum=$("$repo/make/support/macos/cc.sh" --minimum)
bin="$repo/build/macos/bin"
if ! compgen -G "$bin/*.app" > /dev/null; then
echo "$label: SKIP - no app in $bin (make macos)"
exit 2
fi
archs="arm64 x86_64"
signed="arm64"
# joinBy SEPARATOR WORD... -- the words with SEPARATOR between them.
joinBy() {
local separator="$1"
local out="$2"
shift 2
for word in "$@"; do
out+="$separator$word"
done
printf '%s\n' "$out"
}
# plistString PLIST KEY -- the string KEY holds in an Info.plist (the
# build's own XML, each value on the line after its key).
plistString() {
awk -v key="<key>$2</key>" '
found {
if (match($0, /<string>[^<]*<\/string>/)) {
print substr($0, RSTART + length("<string>"), RLENGTH - length("<string></string>"))
}
exit
}
index($0, key) {
found = 1
}' "$1"
}
# sliceProblems PROGRAM ARCH -- what is wrong with one arch of PROGRAM.
sliceProblems() {
local headers
local library
headers=" $("$objdump" --macho --private-headers --arch="$2" "$1" | tr -s ' \t\n' ' ') "
if [[ $headers != *" LC_BUILD_VERSION "* || $headers != *" minos $minimum "* ]]; then
problems+=("$2 not built for macOS $minimum")
fi
if [[ " $signed " == *" $2 "* && $headers != *" LC_CODE_SIGNATURE "* ]]; then
problems+=("$2 unsigned")
fi
while read -r library _; do
[[ $library == /System/Library/* || $library == /usr/lib/* ]] || problems+=("$2 links $library")
done < <("$objdump" --macho --dylibs-used --arch="$2" "$1" | tail -n +2)
}
# appProblems APP -- what is wrong with APP, in problems.
appProblems() {
local contents="$1/Contents"
local plist="$contents/Info.plist"
local program
local held
local arch
problems=()
[ -f "$plist" ] || { problems=("no Info.plist"); return; }
if [ "$(plistString "$plist" CFBundlePackageType)" != APPL ] || [ "$(plistString "$plist" LSMinimumSystemVersion)" != "$minimum" ]; then
problems+=("Info.plist is not an application for macOS $minimum")
fi
[ -f "$contents/Resources/$(plistString "$plist" CFBundleIconFile).icns" ] || problems+=("no icon")
program="$contents/MacOS/$(plistString "$plist" CFBundleExecutable)"
if [ ! -f "$program" ] || [ ! -x "$program" ]; then
problems+=("no program")
return
fi
# shellcheck disable=SC2046
held=$(printf '%s\n' $("$MACOS_LIPO" -archs "$program") | sort | paste -sd' ')
[ "$held" = "$archs" ] || problems+=("holds $held, not $archs")
for arch in $held; do
[[ " $archs " == *" $arch "* ]] && sliceProblems "$program" "$arch"
done
}
fails=()
count=0
for app in "$bin"/*.app; do
count=$((count + 1))
appProblems "$app"
if [ ${#problems[@]} -gt 0 ]; then
fails+=("$(basename "$app"): $(joinBy ', ' "${problems[@]}")")
fi
done
if [ ${#fails[@]} -gt 0 ]; then
echo "$label: FAIL - $(joinBy '; ' "${fails[@]}")"
exit 1
fi
echo "$label: PASS ($count app$([ "$count" = 1 ] || echo s): universal ${archs// /+}, arm64 signed, macOS $minimum on, system libraries only)"