#!/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 parses and 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 python3 - "$bin" "$MACOS_LIPO" "$objdump" "$minimum" "$label" <<'PY' import glob import os import plistlib import subprocess import sys bin, lipo, objdump, minimum, label = sys.argv[1:] ARCHS = {"arm64", "x86_64"} SIGNED = {"arm64"} SYSTEM = ("/System/Library/", "/usr/lib/") def run(*args): return subprocess.run(args, capture_output=True, text=True, check=True).stdout def sliceProblems(program, arch): problems = [] headers = run(objdump, "--macho", "--private-headers", "--arch=" + arch, program).split() if "LC_BUILD_VERSION" not in headers or headers[headers.index("minos") + 1] != minimum: problems.append("%s not built for macOS %s" % (arch, minimum)) if arch in SIGNED and "LC_CODE_SIGNATURE" not in headers: problems.append("%s unsigned" % arch) for line in run(objdump, "--macho", "--dylibs-used", "--arch=" + arch, program).splitlines()[1:]: library = line.split()[0] if not library.startswith(SYSTEM): problems.append("%s links %s" % (arch, library)) return problems def appProblems(app): contents = os.path.join(app, "Contents") with open(os.path.join(contents, "Info.plist"), "rb") as f: info = plistlib.load(f) problems = [] if info.get("CFBundlePackageType") != "APPL" or info.get("LSMinimumSystemVersion") != minimum: problems.append("Info.plist is not an application for macOS %s" % minimum) if not os.path.isfile(os.path.join(contents, "Resources", info.get("CFBundleIconFile", "") + ".icns")): problems.append("no icon") program = os.path.join(contents, "MacOS", info.get("CFBundleExecutable", "")) if not (os.path.isfile(program) and os.access(program, os.X_OK)): return problems + ["no program"] archs = set(run(lipo, "-archs", program).split()) if archs != ARCHS: problems.append("holds %s, not %s" % (" ".join(sorted(archs)), " ".join(sorted(ARCHS)))) for arch in sorted(archs & ARCHS): problems += sliceProblems(program, arch) return problems apps = sorted(glob.glob(os.path.join(bin, "*.app"))) fails = [] for app in apps: try: problems = appProblems(app) except (OSError, ValueError, KeyError, plistlib.InvalidFileException, subprocess.CalledProcessError) as error: problems = [str(error)] if problems: fails.append("%s: %s" % (os.path.basename(app), ", ".join(problems))) if fails: print("%s: FAIL - %s" % (label, "; ".join(fails))) sys.exit(1) print("%s: PASS (%d app%s: universal %s, arm64 signed, macOS %s on, system libraries only)" % (label, len(apps), "" if len(apps) == 1 else "s", "+".join(sorted(ARCHS)), minimum)) PY