98 lines
4 KiB
Bash
Executable file
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
|