#!/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="$2" ' found { if (match($0, /[^<]*<\/string>/)) { print substr($0, RSTART + length(""), RLENGTH - length("")) } 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)"