joeylib2/scripts/check-macos-apps.sh

98 lines
4 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 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