From 9adfaf010f3ae38285c3f2b2a340556c000529a2 Mon Sep 17 00:00:00 2001 From: Nicolai Bruns Date: Sat, 17 Oct 2026 22:09:29 +0200 Subject: [PATCH] fix(setup): use consistent runtime paths and reject incomplete installs --- docs/SETUP.md | 17 +- scripts/configure.py | 7 +- scripts/install-runtime.sh | 10 +- tests/test_setup_portability.py | 222 +++++++++++++++++++++ tools/check-darwin-tlv-calls.py | 3 + tools/install-darling-system-profiler.py | 6 +- tools/install-riot-appkit-runtime.sh | 2 +- tools/install-riot-carboncore-runtime.sh | 4 +- tools/install-riot-foundation-runtime.py | 9 +- tools/install-riot-gpu-runtime.sh | 27 +-- tools/install-riot-libs-runtime.py | 15 +- tools/install-riot-memory-runtime.py | 9 +- tools/install-riot-presentation-runtime.sh | 14 +- tools/riot-launcher-offline-check.py | 6 +- tools/run-riot-client.sh | 16 +- tools/runtime-paths.sh | 18 ++ tools/runtime_paths.py | 24 +++ tools/validate-riot-runtime.py | 78 ++++++++ 18 files changed, 439 insertions(+), 48 deletions(-) create mode 100644 tests/test_setup_portability.py create mode 100644 tools/runtime-paths.sh create mode 100644 tools/runtime_paths.py create mode 100644 tools/validate-riot-runtime.py diff --git a/docs/SETUP.md b/docs/SETUP.md index 1b5a89a..10d22b9 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -12,7 +12,22 @@ python3 scripts/configure.py --guest-user my-linux-user python3 scripts/configure.py --sdk /usr/local/libexec/darling ``` -The normal wrappers currently support the default `~/.darling` prefix and `/usr/local` base installation. The configurator exposes a custom prefix for developers, but several archived tools assume the default layout. Custom prefixes/install locations have not been tested end to end. +The runtime installation wrappers resolve `DPREFIX` first, otherwise the saved +`configure.py --prefix` setting. The configured guest account supplies the +default guest home. `RIOT_GUEST_HOME`, when set, is an absolute **host** path +inside the selected prefix, not a guest `/Users/...` path. Installers, runtime +validation and the Riot launcher's guest library paths use that selection. +Paths containing spaces are supported by isolated regression fixtures. Explicit +environment overrides win over saved configuration; clear stale values before +selecting a different installation. + +A complete real installation using a custom prefix has not been verified. +Archived tools may still assume the default layout. Runtime validation fails on +missing expected binaries, unreadable/missing directories or checker errors; +an empty input set is not a successful validation. + +Runtime installation requires the selected runtime to stop before overlay +replacement. Failed shutdown blocks replacement. The build uses Unix Makefiles because the incremental runtime helpers read CMake `flags.make` and `link.txt`. Ninja is not a drop-in replacement for those helpers. diff --git a/scripts/configure.py b/scripts/configure.py index 3ea8d2f..7fcce7b 100755 --- a/scripts/configure.py +++ b/scripts/configure.py @@ -17,7 +17,12 @@ build=ROOT/'build';build.mkdir(exist_ok=True) folder=build/'runtime/tools';folder.mkdir(parents=True,exist_ok=True) for source in (ROOT/'tools').iterdir(): if not source.is_file():continue - target=folder/source.name;target.write_text(render(source.read_text(),c));os.chmod(target,source.stat().st_mode & 0o777) + target=folder/source.name + # Unlink the directory entry, including dangling symlinks. Any other error + # must abort rather than following a leftover link or writing through it. + target.unlink(missing_ok=True) + target.write_text(render(source.read_text(),c)) + os.chmod(target,source.stat().st_mode & 0o777) link=folder.parent/'build' if not link.exists():link.symlink_to(build,target_is_directory=True) print('Configured',len(list(folder.iterdir())),'runtime sources for',c['guest_home']) diff --git a/scripts/install-runtime.sh b/scripts/install-runtime.sh index ad26e21..4bb394f 100755 --- a/scripts/install-runtime.sh +++ b/scripts/install-runtime.sh @@ -4,6 +4,7 @@ set -eu repo=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) cd "$repo" python3 scripts/configure.py +. build/runtime/tools/runtime-paths.sh if [ -d build/venv/bin ]; then PATH="$repo/build/venv/bin:$PATH"; export PATH; fi # A server killed by the base install leaves a stale .init.pid that makes darling shell fail. python3 build/runtime/tools/ensure-darling-runtime.py @@ -11,8 +12,9 @@ python3 build/runtime/tools/ensure-darling-runtime.py darling shell /usr/bin/true # The prefix has no /private/tmp of its own, so the guest falls through to the root-owned # installed layer and every write there (the Riot installer's temp files) fails with EACCES. -mkdir -p "$HOME/.darling/private/tmp" -chmod 1777 "$HOME/.darling/private/tmp" +prefix=$prefix_dir +mkdir -p "$prefix/private/tmp" +chmod 1777 "$prefix/private/tmp" python3 build/runtime/tools/stop-darling-runtime.py sh build/runtime/tools/install-riot-presentation-runtime.sh sh build/runtime/tools/install-riot-gpu-runtime.sh @@ -23,6 +25,4 @@ if [ -f build/runtime/tools/build-riot-libs-runtime.sh ]; then fi python3 build/runtime/tools/install-darling-system-profiler.py # Every guest process loads these overlays; refuse a set the compiler broke (check-darwin-tlv-calls.py). -find "$HOME/.darling/Users/$(id -un)/RuntimeLibraries" "$HOME/.darling/Users/$(id -un)/RuntimeFrameworks" \ - -type f ! -name '*.before-*' ! -name '*.rt-*' ! -name '*.llvm*' -size +4k -print0 | - xargs -0 python3 build/runtime/tools/check-darwin-tlv-calls.py +python3 build/runtime/tools/validate-riot-runtime.py diff --git a/tests/test_setup_portability.py b/tests/test_setup_portability.py new file mode 100644 index 0000000..47ded2b --- /dev/null +++ b/tests/test_setup_portability.py @@ -0,0 +1,222 @@ +"""Filesystem/subprocess fixtures: never invoke a host runtime or compiler.""" +import importlib.util +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] + + +VALIDATION_INPUTS = ( + 'RuntimeFrameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvDSP.dylib', + 'libDarlingXShm.dylib', + *(f'RuntimeLibraries/{name}.dylib' for name in ( + 'libsystem_kernel', 'libsystem_c', 'libsystem_platform', 'libsystem_malloc', + 'libsystem_pthread', 'libobjc.A', 'libc++abi', 'libc++.1', + 'libGL', 'libindium', 'libiridium')), + *(f'RuntimeFrameworks/{name}.framework/Versions/{version}/{name}' for name, version in ( + ('AppKit', 'C'), ('QuartzCore', 'A'), ('CoreFoundation', 'A'), + ('SystemConfiguration', 'A'), ('CFNetwork', 'A'), ('IOSurface', 'A'), + ('OpenGL', 'A'), ('CoreVideo', 'A'), ('CoreAudio', 'A'), ('AudioToolbox', 'A'), + ('CoreGraphics', 'A'), ('CoreText', 'A'), ('Metal', 'A'), ('CarbonCore', 'A'), + ('LaunchServices', 'A'))), + 'RuntimeFrameworks/AppKit.framework/Versions/C/Resources/Backends/X11.backend/Contents/MacOS/X11', + 'RuntimeFrameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib', + 'Library/Audio/Plug-Ins/Components/DarlingRiotAudio.component/Contents/MacOS/CoreAudio', +) + + +class SetupFixture(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory(prefix='setup fixture ', dir=os.environ.get('TMPDIR')) + self.addCleanup(self.tmp.cleanup) + self.root = Path(self.tmp.name) / 'project space' + self.root.mkdir() + for name in ('scripts', 'tools'): + shutil.copytree(ROOT / name, self.root / name) + self.home = Path(self.tmp.name) / 'host home' + self.home.mkdir() + self.prefix = Path(self.tmp.name) / 'custom prefix' + self.guest = self.prefix / 'Users/Guest Account' + self.bin = Path(self.tmp.name) / 'bin' + self.bin.mkdir() + self.env = dict(os.environ, HOME=str(self.home), TMPDIR=self.tmp.name, + DPREFIX=str(self.prefix), PYTHONDONTWRITEBYTECODE='1', + PATH=str(self.bin) + ':' + os.environ['PATH']) + self.config = dict(project_root=str(self.root), host_home=str(self.home), + prefix=str(self.prefix), guest_user='Guest Account', + guest_home='/Users/Guest Account', sdk=str(self.root / 'sdk'), jobs=1) + (self.root / 'build').mkdir() + (self.root / 'build/config.json').write_text(json.dumps(self.config)) + self.log = Path(self.tmp.name) / 'events' + self.env['FIXTURE_LOG'] = str(self.log) + + + def run_script(self, path, *args): + return subprocess.run([sys.executable if path.endswith('.py') else 'sh', + str(self.root / path), *args], env=self.env, + text=True, capture_output=True, timeout=20) + + + def configure(self): + result = self.run_script('scripts/configure.py') + self.assertEqual(result.returncode, 0, result.stderr) + + + def stub(self, name, body): + path = self.bin / name + path.write_text('#!/bin/sh\nset -eu\n' + body + '\n') + path.chmod(0o755) + + + def runtime_stubs(self): + # Nested Python is a strict allowlist: no accidental live helper execution. + self.stub('python3', '''printf 'python %s\\n' "$*" >> "$FIXTURE_LOG" +case "$1" in + -|*runtime_paths.py) exec REAL_PYTHON "$@";; + *stop-darling-runtime.py) [ "${STOP_FAIL:-0}" = 0 ] || exit 9; printf stopped\\n >> "$FIXTURE_LOG";; + *build-*.py|*ensure-darling-runtime.py|*install-darling-system-profiler.py|*install-riot-*.py) :;; + *) exit 90;; +esac'''.replace('REAL_PYTHON', sys.executable)) + self.stub('darling', '''printf 'darling %s\\n' "$*" >> "$FIXTURE_LOG" +if [ "$1" = shutdown ]; then sleep "${SHUTDOWN_DELAY:-0}"; exit "${SHUTDOWN_FAIL:-0}"; fi''') + self.stub('clang', '''last=; for arg do if [ "$last" = -o ]; then printf binary > "$arg"; fi; last=$arg; done''') + self.stub('sudo', 'exit 1') + self.stub('make', ':') + self.stub('cc', 'exec clang "$@"') + self.stub('pkg-config', ':') + self.stub('install', '''case "$(cat "$FIXTURE_LOG")" in *stopped*) :;; *) exit 88;; esac +exec REAL_INSTALL "$@"'''.replace('REAL_INSTALL', shutil.which('install'))) + + + def test_configure_replaces_readonly_and_repeats(self): + self.configure() + generated = self.root / 'build/runtime/tools/run-riot-client.sh' + generated.chmod(0o444) + self.configure() + self.configure() + self.assertTrue(generated.stat().st_mode & 0o200) + + + def test_configure_unlinks_dangling_symlink_without_writing_victim(self): + self.configure() + target = self.root / 'build/runtime/tools/run-riot-client.sh' + target.unlink() + victim = self.home / 'must not be created' + target.symlink_to(victim) + self.configure() + self.assertFalse(victim.exists(), 'configure followed the dangling generated-file symlink') + self.assertFalse(target.is_symlink()) + + + def test_presentation_creates_prerequisites_and_repeat_install(self): + self.configure() + self.runtime_stubs() + self.presentation_inputs() + for _ in range(2): + result = self.run_script('build/runtime/tools/install-riot-presentation-runtime.sh') + self.assertEqual(result.returncode, 0, result.stderr) + self.assertTrue((self.guest / 'libDarlingXShm.dylib').is_file()) + self.assertTrue((self.guest / 'RuntimeLibraries/libsystem_kernel.dylib').is_file()) + self.assertIn('stopped', self.log.read_text()) + + + def presentation_inputs(self): + for name in ('external/cocotron/AppKit/AppKit', 'external/cocotron/AppKit/X11', + 'external/cocotron/QuartzCore/QuartzCore', + 'external/corefoundation/CoreFoundation', + 'external/configd/SystemConfiguration.fproj/SystemConfiguration', + 'external/cfnetwork/src/CFNetwork', + 'external/libplatform/libsystem_platform.dylib', + 'external/xnu/darling/src/libsystem_kernel/libsystem_kernel.dylib'): + path = self.root / 'build/darling-src/build/src' / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text('library') + for framework, version in (('AppKit', 'C'), ('QuartzCore', 'A'), + ('CoreFoundation', 'A'), ('SystemConfiguration', 'A'), + ('CFNetwork', 'A')): + path = self.root / 'sdk/System/Library/Frameworks' / (framework + '.framework') + binary = path / 'Versions' / version / framework + binary.parent.mkdir(parents=True) + binary.write_text('sdk binary') + binary.chmod(0o555) + (path / 'Versions/Current').symlink_to(version) + (path / framework).symlink_to('Versions/Current/' + framework) + if framework == 'AppKit': + backend = binary.parent / 'Resources/Backends/X11.backend/Contents/MacOS/X11' + backend.parent.mkdir(parents=True) + backend.write_text('sdk backend') + p = self.root / 'build/libcxx-install/lib' + p.mkdir(parents=True) + (p / 'libc++.1.0.dylib').write_text('library') + + + def prepare_validation(self): + self.configure() + for relative in VALIDATION_INPUTS: + path = self.guest / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(b'\xcf\xfa\xed\xfe' + b'\0' * 5000) + self.stub('llvm-objdump', '''printf '%s\\n' "$*" >> "$FIXTURE_LOG" +exit "${CHECK_FAIL:-0}"''') + + + def test_launcher_selects_paths_before_prefix_helpers(self): + source = (ROOT / 'tools/run-riot-client.sh').read_text() + self.assertLess(source.index('runtime-paths.sh'), source.index('ensure-darling-runtime.py')) + + def test_paths_reject_symlink_escape(self): + self.configure() + outside = self.home / 'outside' + outside.mkdir() + self.prefix.mkdir(parents=True, exist_ok=True) + (self.prefix / 'escape').symlink_to(outside, target_is_directory=True) + self.env['RIOT_GUEST_HOME'] = str(self.prefix / 'escape/new-user') + result = self.run_script('build/runtime/tools/validate-riot-runtime.py') + self.assertNotEqual(result.returncode, 0) + self.assertIn('inside DPREFIX', result.stderr) + result = self.run_script('build/runtime/tools/runtime-paths.sh') + self.assertNotEqual(result.returncode, 0) + + def test_validation_missing_empty_and_checker_failure(self): + self.prepare_validation() + self.env['CHECK_FAIL'] = '1' + self.assertNotEqual(self.run_script('build/runtime/tools/validate-riot-runtime.py').returncode, 0) + self.env.pop('CHECK_FAIL') + vdsp = self.guest / 'RuntimeFrameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvDSP.dylib' + vdsp.unlink(missing_ok=True) + self.assertNotEqual(self.run_script('build/runtime/tools/validate-riot-runtime.py').returncode, 0) + shutil.rmtree(self.guest) + self.assertNotEqual(self.run_script('build/runtime/tools/validate-riot-runtime.py').returncode, 0) + + def test_validation_uses_host_guest_override(self): + self.prepare_validation() + override = self.prefix / 'Users/Override Guest' + self.guest.rename(override) + self.env['RIOT_GUEST_HOME'] = str(override) + result = self.run_script('build/runtime/tools/validate-riot-runtime.py') + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn(str(override), self.log.read_text()) + + + def test_failed_stop_with_all_compile_outputs_available(self): + self.configure() + self.runtime_stubs() + self.presentation_inputs() + self.guest.mkdir(parents=True) + self.env['RIOT_GUEST_HOME'] = str(self.guest) + self.env.update(STOP_FAIL='1', SHUTDOWN_FAIL='1') + result = self.run_script('build/runtime/tools/install-riot-presentation-runtime.sh') + self.assertNotEqual(result.returncode, 0) + self.assertIn('stop-darling-runtime.py', self.log.read_text()) + self.assertFalse((self.guest / 'libDarlingXShm.dylib').exists()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/check-darwin-tlv-calls.py b/tools/check-darwin-tlv-calls.py index c23a4f2..74cca28 100644 --- a/tools/check-darwin-tlv-calls.py +++ b/tools/check-darwin-tlv-calls.py @@ -51,6 +51,9 @@ def bad_accesses(path): return found +if len(sys.argv) < 2: + sys.exit('No runtime binaries supplied for validation') + status = 0 for binary in sys.argv[1:]: problems = bad_accesses(binary) diff --git a/tools/install-darling-system-profiler.py b/tools/install-darling-system-profiler.py index c6b6fbe..9ba07b9 100644 --- a/tools/install-darling-system-profiler.py +++ b/tools/install-darling-system-profiler.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 """Install the host-backed system_profiler into the Darling prefix (no sudo).""" -import os +import json from pathlib import Path import shutil +from runtime_paths import runtime_paths + root = Path(__file__).resolve().parent -prefix = Path(os.environ.get("DPREFIX", str(Path.home() / ".darling"))) +prefix, _ = runtime_paths(json.loads((root.parent / "build/config.json").read_text())) target = prefix / "usr/sbin/system_profiler" target.parent.mkdir(parents=True, exist_ok=True) staged = target.with_name(target.name + ".new") diff --git a/tools/install-riot-appkit-runtime.sh b/tools/install-riot-appkit-runtime.sh index 18ecc50..376af0c 100755 --- a/tools/install-riot-appkit-runtime.sh +++ b/tools/install-riot-appkit-runtime.sh @@ -3,7 +3,7 @@ # Manually exit the game and both clients, then shut down Darling before use. set -eu project_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) -guest_home=${RIOT_GUEST_HOME:-$HOME/.darling@GUEST_HOME@} +. "$project_dir/tools/runtime-paths.sh" framework="$guest_home/RuntimeFrameworks/AppKit.framework/Versions/C" backup_dir=${RIOT_APPKIT_BACKUP_DIR:-$project_dir/build/appkit-runtime-backups} # Preflight both artifacts and destinations before replacing either binary. diff --git a/tools/install-riot-carboncore-runtime.sh b/tools/install-riot-carboncore-runtime.sh index de9d6b0..f15cdcc 100755 --- a/tools/install-riot-carboncore-runtime.sh +++ b/tools/install-riot-carboncore-runtime.sh @@ -3,8 +3,7 @@ # Exit the game and both clients, then run darling shutdown before this script. set -eu project_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) -prefix_dir=${DPREFIX:-@DPREFIX@} -guest_home=${RIOT_GUEST_HOME:-$prefix_dir@GUEST_HOME@} +. "$project_dir/tools/runtime-paths.sh" artifact=${RIOT_CARBONCORE_ARTIFACT:-$project_dir/build/riot-CarbonCore} backup_dir=${RIOT_CARBONCORE_BACKUP_DIR:-$project_dir/build/carboncore-runtime-backups} sdk_framework="@DARLING_SDK@/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework" @@ -12,6 +11,7 @@ framework="$guest_home/RuntimeFrameworks/CarbonCore.framework" prefix_binary="$prefix_dir/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/CarbonCore" [ -f "$artifact" ] || { echo "Build CarbonCore first: $artifact" >&2; exit 1; } [ -f "$sdk_framework/Versions/A/CarbonCore" ] || { echo "Missing SDK CarbonCore framework" >&2; exit 1; } +python3 "$project_dir/tools/stop-darling-runtime.py" mkdir -p "$guest_home/RuntimeFrameworks" "$backup_dir" if [ ! -d "$framework" ]; then cp -a "$sdk_framework" "$framework" diff --git a/tools/install-riot-foundation-runtime.py b/tools/install-riot-foundation-runtime.py index 4f803ed..2b5a2b1 100644 --- a/tools/install-riot-foundation-runtime.py +++ b/tools/install-riot-foundation-runtime.py @@ -17,7 +17,9 @@ import shutil import stat import tempfile -ROOT = Path(__file__).resolve().parent.parent +from runtime_paths import runtime_paths + +ROOT = Path(__file__).resolve().parents[1] DEFAULT_ARTIFACT = ROOT / "build/riot-Foundation" @@ -110,9 +112,8 @@ def main(): parser.exit(1, "The library to install does not exist: " + str(args.binary) + "; build it first: " "python3 build/runtime/tools/build-riot-foundation-runtime.py\n") config = json.loads((ROOT / "build/config.json").read_text()) - prefix = Path(config["prefix"]) - guest_home = config["guest_home"].lstrip("/") - framework = prefix / guest_home / "RuntimeFrameworks/Foundation.framework" + _, guest_home = runtime_paths(config) + framework = guest_home / "RuntimeFrameworks/Foundation.framework" sdk_framework = Path(config["sdk"]) / "System/Library/Frameworks/Foundation.framework" if not sdk_framework.exists(): parser.exit(1, "The SDK framework does not exist: " + str(sdk_framework) + "\n") diff --git a/tools/install-riot-gpu-runtime.sh b/tools/install-riot-gpu-runtime.sh index d7baf18..03057bd 100755 --- a/tools/install-riot-gpu-runtime.sh +++ b/tools/install-riot-gpu-runtime.sh @@ -2,8 +2,9 @@ # Build/install the paired GPU, NV12 and display-clock runtime overrides. set -eu project_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) -guest_home=${RIOT_GUEST_HOME:-$HOME/.darling@GUEST_HOME@} +. "$project_dir/tools/runtime-paths.sh" cd "$project_dir" +mkdir -p build python3 tools/build-m58f333-iosurface.py # Rebuild the native wrapper with floating-point-safe symbol resolvers. make -C build/darling-src/build/src/native GL -j4 @@ -31,6 +32,7 @@ python3 tools/build-riot-server-runtime.py build/riot-host-message-check cc -shared -fPIC -O2 -Wall -Wextra -Wl,-soname,"$project_dir/build/libdarling-xshm.so" tools/m58f299-xshm.c -o build/libdarling-xshm.so.next -pthread $(pkg-config --cflags --libs x11 x11-xcb xcb-shm xext) python3 tools/stop-darling-runtime.py +mkdir -p "$guest_home/RuntimeFrameworks" # Host fileports own SCM_RIGHTS descriptors and must die with their last sender. if sudo -n true 2>/dev/null; then sudo make -C "$project_dir/build/darling-src/build/src/external/darlingserver" install -j12 @@ -70,7 +72,7 @@ mkdir -p "$guest_home/RuntimeLibraries" gl_wrapper="$guest_home/RuntimeLibraries/libGL.dylib" if [ ! -f "$gl_wrapper.before-float-resolver" ]; then if [ -f "$gl_wrapper" ]; then cp -p "$gl_wrapper" "$gl_wrapper.before-float-resolver" - else cp -p @DARLING_SDK@/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib "$gl_wrapper.before-float-resolver"; fi + else cp -p "@DARLING_SDK@"/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib "$gl_wrapper.before-float-resolver"; fi fi install -m755 build/darling-src/build/src/native/libGL.dylib "$gl_wrapper.next" mv "$gl_wrapper.next" "$gl_wrapper" @@ -78,7 +80,7 @@ install -m755 build/darling-src/build/src/native/libGL.dylib "$guest_home/Runtim mv "$guest_home/RuntimeFrameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib.next" "$guest_home/RuntimeFrameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib" launchservices="$guest_home/RuntimeFrameworks/LaunchServices.framework" if [ ! -d "$launchservices" ]; then - cp -a @DARLING_SDK@/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework "$launchservices" + cp -a "@DARLING_SDK@"/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework "$launchservices" fi launchservices_binary="$launchservices/Versions/A/LaunchServices" if [ ! -f "$launchservices_binary.before-null-error" ]; then cp -p "$launchservices_binary" "$launchservices_binary.before-null-error"; fi @@ -86,12 +88,11 @@ install -m755 build/darling-src/build/src/frameworks/CoreServices/src/LaunchServ mv "$launchservices_binary.next" "$launchservices_binary" # Electron opens this subframework by its absolute path, bypassing the # framework search override. Replace the same binary in the stopped prefix. -prefix_dir="${DPREFIX:-$HOME/.darling}" launchservices_prefix="$prefix_dir/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/LaunchServices" mkdir -p "$(dirname "$launchservices_prefix")" if [ ! -f "$launchservices_prefix.before-null-error" ]; then if [ -f "$launchservices_prefix" ]; then cp -p "$launchservices_prefix" "$launchservices_prefix.before-null-error" - else cp -p @DARLING_SDK@/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/LaunchServices "$launchservices_prefix.before-null-error"; fi + else cp -p "@DARLING_SDK@"/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/LaunchServices "$launchservices_prefix.before-null-error"; fi fi install -m755 build/darling-src/build/src/frameworks/CoreServices/src/LaunchServices/LaunchServices "$launchservices_prefix.next" mv "$launchservices_prefix.next" "$launchservices_prefix" @@ -100,7 +101,7 @@ vdsp_prefix="$prefix_dir/System/Library/Frameworks/Accelerate.framework/Versions mkdir -p "$(dirname "$vdsp_prefix")" if [ ! -f "$vdsp_prefix.before-vdsp" ]; then if [ -f "$vdsp_prefix" ]; then cp -p "$vdsp_prefix" "$vdsp_prefix.before-vdsp" - else cp -p @DARLING_SDK@/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvDSP.dylib "$vdsp_prefix.before-vdsp"; fi + else cp -p "@DARLING_SDK@"/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvDSP.dylib "$vdsp_prefix.before-vdsp"; fi fi install -m755 build/darling-src/build/src/frameworks/Accelerate/vecLib/vDSP/libvDSP.dylib "$vdsp_prefix.next" mv "$vdsp_prefix.next" "$vdsp_prefix" @@ -108,7 +109,7 @@ mv "$vdsp_prefix.next" "$vdsp_prefix" component="$guest_home/Library/Audio/Plug-Ins/Components/DarlingRiotAudio.component" if [ ! -d "$component" ]; then mkdir -p "$(dirname "$component")" - cp -a @DARLING_SDK@/System/Library/Components/CoreAudio.component "$component" + cp -a "@DARLING_SDK@"/System/Library/Components/CoreAudio.component "$component" fi component_binary="$component/Contents/MacOS/CoreAudio" if [ ! -f "$component_binary.before-gpu-direct" ]; then cp -p "$component_binary" "$component_binary.before-gpu-direct"; fi @@ -139,7 +140,7 @@ mv "$kernel.next" "$kernel" allocator="$guest_home/RuntimeLibraries/libsystem_malloc.dylib" if [ ! -f "$allocator.before-gpu-direct" ]; then if [ -f "$allocator" ]; then cp -p "$allocator" "$allocator.before-gpu-direct" - else cp -p @DARLING_SDK@/usr/lib/system/libsystem_malloc.dylib "$allocator.before-gpu-direct"; fi + else cp -p "@DARLING_SDK@"/usr/lib/system/libsystem_malloc.dylib "$allocator.before-gpu-direct"; fi fi install -m755 build/riot-libsystem_malloc.dylib "$allocator.next" mv "$allocator.next" "$allocator" @@ -149,7 +150,7 @@ mv build/libdarling-xshm.so.next build/libdarling-xshm.so objc_library="$guest_home/RuntimeLibraries/libobjc.A.dylib" if [ ! -f "$objc_library.before-m761" ]; then if [ -f "$objc_library" ]; then cp -p "$objc_library" "$objc_library.before-m761" - else cp -p @DARLING_SDK@/usr/lib/libobjc.A.dylib "$objc_library.before-m761"; fi + else cp -p "@DARLING_SDK@"/usr/lib/libobjc.A.dylib "$objc_library.before-m761"; fi fi install -m755 build/riot-libobjc.A.dylib "$objc_library.next" mv "$objc_library.next" "$objc_library" @@ -157,7 +158,7 @@ mv "$objc_library.next" "$objc_library" cxxabi_library="$guest_home/RuntimeLibraries/libc++abi.dylib" if [ ! -f "$cxxabi_library.before-m765" ]; then if [ -f "$cxxabi_library" ]; then cp -p "$cxxabi_library" "$cxxabi_library.before-m765" - else cp -p @DARLING_SDK@/usr/lib/libc++abi.dylib "$cxxabi_library.before-m765"; fi + else cp -p "@DARLING_SDK@"/usr/lib/libc++abi.dylib "$cxxabi_library.before-m765"; fi fi install -m755 build/riot-libc++abi.dylib "$cxxabi_library.next" mv "$cxxabi_library.next" "$cxxabi_library" @@ -165,7 +166,7 @@ mv "$cxxabi_library.next" "$cxxabi_library" indium_library="$guest_home/RuntimeLibraries/libindium.dylib" if [ ! -f "$indium_library.before-m766" ]; then if [ -f "$indium_library" ]; then cp -p "$indium_library" "$indium_library.before-m766" - else cp -p @DARLING_SDK@/usr/lib/darling/libindium.dylib "$indium_library.before-m766"; fi + else cp -p "@DARLING_SDK@"/usr/lib/darling/libindium.dylib "$indium_library.before-m766"; fi fi install -m755 build/riot-libindium.dylib "$indium_library.next" mv "$indium_library.next" "$indium_library" @@ -174,7 +175,7 @@ mv "$indium_library.next" "$indium_library" iridium_library="$guest_home/RuntimeLibraries/libiridium.dylib" if [ ! -f "$iridium_library.before-m767" ]; then if [ -f "$iridium_library" ]; then cp -p "$iridium_library" "$iridium_library.before-m767" - else cp -p @DARLING_SDK@/usr/lib/darling/libiridium.dylib "$iridium_library.before-m767"; fi + else cp -p "@DARLING_SDK@"/usr/lib/darling/libiridium.dylib "$iridium_library.before-m767"; fi fi install -m755 build/riot-libiridium.dylib "$iridium_library.next" mv "$iridium_library.next" "$iridium_library" @@ -182,7 +183,7 @@ mv "$iridium_library.next" "$iridium_library" pthread_library="$guest_home/RuntimeLibraries/libsystem_pthread.dylib" if [ ! -f "$pthread_library.before-gpu-direct" ]; then if [ -f "$pthread_library" ]; then cp -p "$pthread_library" "$pthread_library.before-gpu-direct" - else cp -p @DARLING_SDK@/usr/lib/system/libsystem_pthread.dylib "$pthread_library.before-gpu-direct"; fi + else cp -p "@DARLING_SDK@"/usr/lib/system/libsystem_pthread.dylib "$pthread_library.before-gpu-direct"; fi fi install -m755 build/riot-libsystem_pthread.dylib "$pthread_library.next" mv "$pthread_library.next" "$pthread_library" diff --git a/tools/install-riot-libs-runtime.py b/tools/install-riot-libs-runtime.py index bf495d0..e50cbba 100644 --- a/tools/install-riot-libs-runtime.py +++ b/tools/install-riot-libs-runtime.py @@ -23,7 +23,9 @@ import shutil import stat import tempfile -ROOT = Path(__file__).resolve().parent.parent +from runtime_paths import runtime_paths + +ROOT = Path(__file__).resolve().parents[1] # (built artifact, file name in the guest's RuntimeLibraries directory). The # artifacts are the ones tools/build-riot-libs-runtime.sh builds. @@ -91,6 +93,7 @@ def install(libraries, backup_folder): else: shutil.copy2(destination, backup) metadata = destination.stat() if destination.is_file() else None + destination.parent.mkdir(parents=True, exist_ok=True) descriptor, temporary = tempfile.mkstemp(prefix="." + destination.name + "-staged-", dir=destination.parent) staged.append((Path(temporary), destination)) @@ -186,9 +189,13 @@ def main(argv=None): parser.exit(1, str(error) + "\n") return config = json.loads((ROOT / "build/config.json").read_text()) - prefix = arguments.prefix or config["prefix"] - guest_home = arguments.guest_home or config["guest_home"] - libraries = destinations(prefix, guest_home) + prefix, host_guest_home = runtime_paths(config) + prefix = arguments.prefix or prefix + if arguments.prefix and not os.environ.get("RIOT_GUEST_HOME"): + host_guest_home = prefix / config["guest_home"].lstrip("/") + if arguments.guest_home: + host_guest_home = prefix / arguments.guest_home.lstrip("/") + libraries = destinations(host_guest_home, "") if arguments.only: wanted = set(arguments.only.split(",")) unknown = wanted - {name for _, name in LIBRARIES} diff --git a/tools/install-riot-memory-runtime.py b/tools/install-riot-memory-runtime.py index c71e232..55f3fb4 100644 --- a/tools/install-riot-memory-runtime.py +++ b/tools/install-riot-memory-runtime.py @@ -10,7 +10,9 @@ import shutil import stat import tempfile -ROOT = Path(__file__).resolve().parent.parent +from runtime_paths import runtime_paths + +ROOT = Path(__file__).resolve().parents[1] # The optimized relink from tools/build-riot-kqueue-runtime.py is the library the # client runtime is supposed to run; the plain CMake build (no -O flag) is not. DEFAULT_ARTIFACT = ROOT / "build/riot-libsystem_kernel.dylib" @@ -84,10 +86,11 @@ def prefix_destinations(config, binary, artifact): install the artifact itself leaves it untouched. That is what keeps the optimized relink from being silently replaced by the plain CMake build. """ + prefix, guest_home = runtime_paths(config) destinations = [ - Path(config["prefix"]) / config["guest_home"].lstrip("/") / "RuntimeLibraries/libsystem_kernel.dylib", + guest_home / "RuntimeLibraries/libsystem_kernel.dylib", # Keep the per-prefix bootstrap copy consistent; leave the system SDK alone. - Path(config["prefix"]) / "usr/lib/system/libsystem_kernel.dylib", + prefix / "usr/lib/system/libsystem_kernel.dylib", ] if not artifact.exists(): return destinations, "the prepared artifact " + str(artifact) + " does not exist yet" diff --git a/tools/install-riot-presentation-runtime.sh b/tools/install-riot-presentation-runtime.sh index d0078f8..220f394 100644 --- a/tools/install-riot-presentation-runtime.sh +++ b/tools/install-riot-presentation-runtime.sh @@ -4,7 +4,8 @@ set -eu project_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) source_dir="$project_dir/build/darling-src" build_dir="$source_dir/build" -guest_home="${RIOT_GUEST_HOME:-$HOME/.darling@GUEST_HOME@}" +. "$project_dir/tools/runtime-paths.sh" +mkdir -p "$project_dir/build" make -C "$build_dir/src/external/xnu/darling/src/libsystem_kernel" system_kernel -j8 make -C "$build_dir/src/external/libplatform" platform -j8 make -C "$build_dir/src/external/corefoundation" CoreFoundation_x86_64 -j8 @@ -17,14 +18,16 @@ cc -shared -fPIC -O3 -march=native -mtune=generic -Wall -Wextra \ "$project_dir/tools/m58f299-xshm.c" -o "$project_dir/build/libdarling-xshm.next.so" \ -pthread $(pkg-config --cflags --libs x11 x11-xcb xcb-shm xext) clang -target x86_64-apple-macosx11.0 -fuse-ld=lld -dynamiclib -nostdlib -O3 -march=native -mtune=generic \ - -isysroot @DARLING_SDK@ \ + -isysroot "@DARLING_SDK@" \ -I"$source_dir/src/startup/mldr/elfcalls" \ "-DDARLING_XSHM_HOST_LIBRARY=\"$project_dir/build/libdarling-xshm.so\"" \ - -Wl,-undefined,dynamic_lookup -Wl,-install_name,@GUEST_HOME@/libDarlingXShm.dylib \ - "$project_dir/tools/m58f299-native-wrapper.c" -o "$guest_home/libDarlingXShm.next.dylib" + -Wl,-undefined,dynamic_lookup "-Wl,-install_name,@GUEST_HOME@/libDarlingXShm.dylib" \ + "$project_dir/tools/m58f299-native-wrapper.c" -o "$project_dir/build/libDarlingXShm.next.dylib" # Stop before changing the overlay's upper directory, even with atomic writes. -darling shutdown || true +python3 "$project_dir/tools/stop-darling-runtime.py" +mkdir -p "$guest_home" mv "$project_dir/build/libdarling-xshm.next.so" "$project_dir/build/libdarling-xshm.so" +install -m755 "$project_dir/build/libDarlingXShm.next.dylib" "$guest_home/libDarlingXShm.next.dylib" mv "$guest_home/libDarlingXShm.next.dylib" "$guest_home/libDarlingXShm.dylib" mkdir -p "$guest_home/RuntimeFrameworks" for framework in AppKit QuartzCore CoreFoundation SystemConfiguration CFNetwork; do @@ -52,7 +55,6 @@ atomic_install "$build_dir/src/external/xnu/darling/src/libsystem_kernel/libsyst # Bootstrap services can start before the app's DYLD_LIBRARY_PATH is applied. # The prefix overlay is user-owned; install the same corrected kernel there. -prefix_dir="${DPREFIX:-$HOME/.darling}" mkdir -p "$prefix_dir/usr/lib/system" atomic_install "$build_dir/src/external/xnu/darling/src/libsystem_kernel/libsystem_kernel.dylib" "$prefix_dir/usr/lib/system/libsystem_kernel.dylib" diff --git a/tools/riot-launcher-offline-check.py b/tools/riot-launcher-offline-check.py index 248444d..1f08239 100644 --- a/tools/riot-launcher-offline-check.py +++ b/tools/riot-launcher-offline-check.py @@ -39,6 +39,10 @@ with open({str(log)!r}, "a") as output: prefix = folder / "prefix" (prefix / "Applications/Riot Client.app").mkdir(parents=True) env["DPREFIX"] = str(prefix) + env.pop("RIOT_GUEST_HOME", None) + (folder / "runtime-paths.sh").write_text( + (ROOT / "tools/runtime-paths.sh").read_text() + .replace("@DPREFIX@", str(prefix)).replace("@GUEST_HOME@", "/Users/offline-check")) env["DYLD_INSERT_LIBRARIES"] = "/Users/offline-check/zzlogdir.dylib:/tmp/inherited.dylib" env["RCLOL_APP_ARGS"] = "--inherited-shim-argument" env["RCLOL_APP_ARG2"] = "--inherited-shim-argument-2" @@ -60,7 +64,7 @@ with open({str(log)!r}, "a") as output: records = [json.loads(line) for line in log.read_text().splitlines()] maintenance = [r for r in records if r["command"] == "python3"] assert [Path(r["args"][0]).name for r in maintenance] == [ - "ensure-darling-runtime.py", "ensure-darling-service-accounts.py", "ensure-league-runtime-layout.py" + "runtime_paths.py", "ensure-darling-runtime.py", "ensure-darling-service-accounts.py", "ensure-league-runtime-layout.py" ], "unexpected automatic helper" launches = [r for r in records if r["command"] == "darling"] assert len(launches) == 1, "unexpected launch count" diff --git a/tools/run-riot-client.sh b/tools/run-riot-client.sh index 74b480c..da66ec0 100755 --- a/tools/run-riot-client.sh +++ b/tools/run-riot-client.sh @@ -8,6 +8,7 @@ if [ "${RCLOL_CORE_DUMPS:-0}" != 1 ]; then ulimit -c 0 fi runner_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +. "$runner_dir/runtime-paths.sh" python3 "$runner_dir/ensure-darling-runtime.py" python3 "$runner_dir/ensure-darling-service-accounts.py" python3 "$runner_dir/ensure-league-runtime-layout.py" @@ -21,7 +22,12 @@ for option in RCLOL_GPU_DIRECT_PRESENT RCLOL_GPU_DIRECT_ASYNC RCLOL_GPU_DIRECT_V eval 'value=${'"$option"'-1}' case "$value" in 0|'') unset "$option";; *) export "$option=$value";; esac done -prefix=${DPREFIX:-$HOME/.darling} +prefix=$prefix_dir +# RIOT_GUEST_HOME is host-side; DYLD paths must be relative to the guest root. +case "$guest_home" in + "$prefix"/*) guest_path=/${guest_home#"$prefix"/} ;; + *) echo "RIOT_GUEST_HOME must be inside DPREFIX" >&2; exit 1 ;; +esac # The first-time installer puts the client in /Users/Shared/Riot Games and then # hands off through LaunchServices, which does not start it under Darling. Link # the installed client where the normal launch expects it. @@ -85,9 +91,9 @@ exec env -u ELECTRON_RUN_AS_NODE -u DYLD_INSERT_LIBRARIES EGL_PLATFORM=x11 \ LANG="${RCLOL_GUEST_LOCALE:-en_US.UTF-8}" LC_ALL="${RCLOL_GUEST_LOCALE:-en_US.UTF-8}" \ mesa_glthread="${RCLOL_MESA_GLTHREAD-${mesa_glthread-false}}" \ XMODIFIERS="${RCLOL_XMODIFIERS:-@im=none}" DISPLAY="${DISPLAY:-:1}" \ - DYLD_LIBRARY_PATH="${DYLD_LIBRARY_PATH:-@GUEST_HOME@/RuntimeLibraries}" \ - DYLD_FRAMEWORK_PATH="${DYLD_FRAMEWORK_PATH:-@GUEST_HOME@/RuntimeFrameworks}" \ - DARLING_XSHM_LIBRARY="${DARLING_XSHM_LIBRARY:-@GUEST_HOME@/libDarlingXShm.dylib}" \ + DYLD_LIBRARY_PATH="${DYLD_LIBRARY_PATH:-$guest_path/RuntimeLibraries}" \ + DYLD_FRAMEWORK_PATH="${DYLD_FRAMEWORK_PATH:-$guest_path/RuntimeFrameworks}" \ + DARLING_XSHM_LIBRARY="${DARLING_XSHM_LIBRARY:-$guest_path/libDarlingXShm.dylib}" \ darling "$A" --region="${RCLOL_REGION:-EUW}" --locale="${RCLOL_LOCALE:-en_GB}" \ "$@" \ - --log-dir="@GUEST_HOME@/Library/Logs/Riot Games/Riot Client" + --log-dir="$guest_path/Library/Logs/Riot Games/Riot Client" diff --git a/tools/runtime-paths.sh b/tools/runtime-paths.sh new file mode 100644 index 0000000..74c40d0 --- /dev/null +++ b/tools/runtime-paths.sh @@ -0,0 +1,18 @@ +# Sourced by configured runtime installers; RIOT_GUEST_HOME is a host path. +# Keep these defaults aligned with runtime_paths.py. +prefix_dir=${DPREFIX:-"@DPREFIX@"} +guest_home=${RIOT_GUEST_HOME:-"$prefix_dir@GUEST_HOME@"} +case "$prefix_dir" in + /*) : ;; + *) echo "DPREFIX must be an absolute host path" >&2; exit 1 ;; +esac +prefix_dir=${prefix_dir%/} +case "$guest_home" in + "$prefix_dir"/*) : ;; + *) echo "RIOT_GUEST_HOME must be inside DPREFIX" >&2; exit 1 ;; +esac +case "$guest_home/" in + */../*|*/./*) echo "RIOT_GUEST_HOME must not contain dot components" >&2; exit 1 ;; +esac +export DPREFIX="$prefix_dir" RIOT_GUEST_HOME="$guest_home" +python3 "@PROJECT_ROOT@/build/runtime/tools/runtime_paths.py" "$prefix_dir" || exit 1 diff --git a/tools/runtime_paths.py b/tools/runtime_paths.py new file mode 100644 index 0000000..d50e4d4 --- /dev/null +++ b/tools/runtime_paths.py @@ -0,0 +1,24 @@ +"""Shared host-side destinations for configured runtime installers.""" +import os +from pathlib import Path + + +def runtime_paths(config): + prefix = Path(os.environ.get("DPREFIX") or config["prefix"]) + guest_home = Path(os.environ.get("RIOT_GUEST_HOME") or + str(prefix / config["guest_home"].lstrip("/"))) + if not prefix.is_absolute() or not guest_home.is_absolute(): + raise ValueError("Runtime paths must be absolute host paths") + if ".." in guest_home.parts or not guest_home.is_relative_to(prefix) or guest_home == prefix: + raise ValueError("RIOT_GUEST_HOME must be inside DPREFIX") + if not guest_home.resolve().is_relative_to(prefix.resolve()) or guest_home.resolve() == prefix.resolve(): + raise ValueError("RIOT_GUEST_HOME must resolve inside DPREFIX") + return prefix, guest_home + + +if __name__ == '__main__': + import sys + try: + runtime_paths({'prefix': sys.argv[1], 'guest_home': '/'},) + except (ValueError, OSError) as error: + raise SystemExit(str(error)) diff --git a/tools/validate-riot-runtime.py b/tools/validate-riot-runtime.py new file mode 100644 index 0000000..1e5da1a --- /dev/null +++ b/tools/validate-riot-runtime.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Validate installed overlays, refusing missing, empty or unreadable inputs.""" +import json +import os +from pathlib import Path +import subprocess +import sys + +from runtime_paths import runtime_paths + +ROOT = Path(__file__).resolve().parents[1] +REQUIRED = ( + 'RuntimeFrameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvDSP.dylib', + 'libDarlingXShm.dylib', + *(f'RuntimeLibraries/{name}' for name in ( + 'libsystem_kernel.dylib', 'libsystem_c.dylib', 'libsystem_platform.dylib', + 'libsystem_malloc.dylib', 'libsystem_pthread.dylib', + 'libobjc.A.dylib', 'libc++abi.dylib', 'libc++.1.dylib', + 'libGL.dylib', 'libindium.dylib', 'libiridium.dylib')), + *(f'RuntimeFrameworks/{name}.framework/Versions/{version}/{name}' + for name, version in ( + ('AppKit', 'C'), ('QuartzCore', 'A'), ('CoreFoundation', 'A'), + ('SystemConfiguration', 'A'), ('CFNetwork', 'A'), ('IOSurface', 'A'), + ('OpenGL', 'A'), ('CoreVideo', 'A'), ('CoreAudio', 'A'), + ('AudioToolbox', 'A'), ('CoreGraphics', 'A'), ('CoreText', 'A'), + ('Metal', 'A'), ('CarbonCore', 'A'), ('LaunchServices', 'A'))), + 'RuntimeFrameworks/AppKit.framework/Versions/C/Resources/Backends/X11.backend/Contents/MacOS/X11', + 'RuntimeFrameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib', + 'Library/Audio/Plug-Ins/Components/DarlingRiotAudio.component/Contents/MacOS/CoreAudio', +) +MACHO = (b'\xcf\xfa\xed\xfe', b'\xca\xfe\xba\xbe') + + +def inputs(guest_home): + def enumeration_error(error): + raise error + + def is_macho(path): + with path.open('rb') as source: + return source.read(4) in MACHO + + required = [guest_home / relative for relative in REQUIRED] + for path in required: + if not is_macho(path): + raise ValueError('Expected installed Mach-O runtime binary: ' + str(path)) + binaries = set(required) + for name in ('RuntimeLibraries', 'RuntimeFrameworks'): + folder = guest_home / name + if not folder.is_dir(): + raise ValueError('Missing runtime directory: ' + str(folder)) + found = [] + for directory, _, files in os.walk(folder, onerror=enumeration_error): + for name in files: + if any(marker in name for marker in ('.before-', '.rt-', '.llvm')): + continue + path = Path(directory) / name + if is_macho(path): + found.append(path) + if not found: + raise ValueError('No Mach-O runtime binaries found in ' + str(folder)) + binaries.update(found) + return sorted(binaries) + + +def main(): + try: + _, guest_home = runtime_paths(json.loads((ROOT / 'build/config.json').read_text())) + binaries = inputs(guest_home) + subprocess.run([sys.executable, str(ROOT / 'tools/check-darwin-tlv-calls.py'), + *map(str, binaries)], check=True) + except (OSError, ValueError, subprocess.CalledProcessError) as error: + print('Runtime validation failed: ' + str(error), file=sys.stderr) + return 1 + return 0 + + +if __name__ == '__main__': + raise SystemExit(main())