mirror of
https://github.com/lucannez64/darling-lol.git
synced 2026-10-26 19:47:42 +00:00
setup failures reported in #137, #138 and #143 happened only after the long fetch/build or at Riot startup. scripts/preflight.py now runs from scripts/build.sh (before configure.py, so an old interpreter gets this message and not a traceback) and from scripts/install-runtime.sh, and checks the host once: - python 3.10+, cmake 3.20+ (the bundled indium CMakeLists requires it), git-lfs 2.13+, kernel 5.14+ and LLVM 16+ exit nonzero with one actionable line each. RCLOL_SKIP_PREFLIGHT=1 bypasses the gate. - llvm-objdump and llvm-nm are required, and unresolved names are explained. - Vulkan 1.3 and timeline semaphores are required when vulkaninfo is installed; they only warn when it is not, because a missing tool does not mean a missing driver. - the X display is probed with xdpyinfo in the environment Darling inherits. It only warns: a build does not need a display. - a prepared build/darling-src is scanned for small .dylib/.a/.so/.o files that still contain Git LFS pointer text, which links but crashes the guest. - --json prints the same report for the diagnostic tools. The installers no longer call hashlib.file_digest (Python 3.11+), so Ubuntu 22.04's Python 3.10 works instead of failing after the overlays are built (#137). A streaming tools/file_digest.py replaces it. LLVM command line tools are resolved by tools/llvm_tools.py: an unversioned name on PATH first, then the highest llvm-<tool>-<N>, then llvm-config --bindir. Debian/Ubuntu install llvm-objdump and llvm-nm only under /usr/lib/llvm-<N>/bin, which broke the runtime validation (#138). Six tools now use it. The LLVM 16 floor also stops the LLVM 15 runtime abort from #139 before anything is built. Two test helpers used APIs added after Python 3.10; they are shimmed so the same suite runs on 3.10, 3.11 and 3.14. Adds tests/test_preflight.py (24 tests, fake host tools only).
305 lines
12 KiB
Python
305 lines
12 KiB
Python
"""Preflight and LLVM tool discovery, with fake host tools and no Darling runtime."""
|
|
import hashlib
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
PREFLIGHT = ROOT / 'scripts/preflight.py'
|
|
KERNEL = '6.1.0-test'
|
|
|
|
|
|
def load(name, path):
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
class PreflightFixture(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory(prefix='preflight fixture ', dir=os.environ.get('TMPDIR'))
|
|
self.addCleanup(self.tmp.cleanup)
|
|
self.bin = Path(self.tmp.name) / 'bin'
|
|
self.bin.mkdir()
|
|
self.llvm_bindir = Path(self.tmp.name) / 'llvm bin'
|
|
self.llvm_bindir.mkdir()
|
|
self.good()
|
|
|
|
def good(self):
|
|
self.stub('llvm-config', '''case "$1" in
|
|
--version) echo 23.1.5;;
|
|
--bindir) printf '%s\\n' "LLVM_BINDIR";;
|
|
esac'''.replace('LLVM_BINDIR', str(self.llvm_bindir)))
|
|
self.stub('llvm-objdump', 'exit 0')
|
|
self.stub('llvm-nm', 'exit 0')
|
|
self.stub('cmake', 'echo "cmake version 3.31.6"')
|
|
self.stub('git', 'case "$*" in "lfs version") echo "git-lfs/3.7.0 (GitHub; linux amd64)";; esac')
|
|
self.stub('vulkaninfo', '''case "$*" in
|
|
--summary) printf 'apiVersion = 1.3.289\\ndeviceName = Test GPU\\n';;
|
|
*) printf 'timelineSemaphore = true\\n';;
|
|
esac''')
|
|
self.stub('xdpyinfo', 'exit 0')
|
|
|
|
def stub(self, name, body, folder=None):
|
|
path = (folder or self.bin) / name
|
|
path.write_text('#!/bin/sh\nset -eu\n' + body + '\n')
|
|
path.chmod(0o755)
|
|
return path
|
|
|
|
def preflight(self, *arguments, kernel=KERNEL, display=':0'):
|
|
env = dict(os.environ, PATH=str(self.bin), RCLOL_PREFLIGHT_KERNEL_RELEASE=kernel)
|
|
if display is None:
|
|
env.pop('DISPLAY', None)
|
|
else:
|
|
env['DISPLAY'] = display
|
|
return subprocess.run([sys.executable, str(PREFLIGHT), *arguments], env=env,
|
|
text=True, capture_output=True, timeout=120)
|
|
|
|
def test_all_required_checks_pass(self):
|
|
result = self.preflight()
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
self.assertIn('preflight: ', result.stdout)
|
|
self.assertNotIn('FAIL', result.stdout)
|
|
self.assertIn('ok llvm-tools', result.stdout)
|
|
|
|
|
|
def test_llvm_15_stops_with_the_reason(self):
|
|
self.stub('llvm-config', 'echo 15.0.7')
|
|
result = self.preflight()
|
|
self.assertEqual(result.returncode, 1)
|
|
self.assertIn('FAIL llvm', result.stdout)
|
|
self.assertIn('LLVM 16 or newer is required', result.stdout)
|
|
self.assertIn('LLVMGetVersion', result.stdout)
|
|
self.assertIn('RCLOL_SKIP_PREFLIGHT=1', result.stderr)
|
|
|
|
|
|
def test_untested_llvm_major_warns_but_builds(self):
|
|
self.stub('llvm-config', 'case "$1" in --version) echo 17.0.6;; esac')
|
|
result = self.preflight()
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
self.assertIn('warn llvm-tested', result.stdout)
|
|
self.assertIn('1 warning', result.stdout)
|
|
|
|
|
|
def test_versioned_llvm_tools_are_found(self):
|
|
(self.bin / 'llvm-objdump').unlink()
|
|
(self.bin / 'llvm-nm').unlink()
|
|
self.stub('llvm-objdump-15', 'exit 0')
|
|
self.stub('llvm-nm-15', 'exit 0')
|
|
result = self.preflight()
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
self.assertIn('llvm-objdump-15', result.stdout)
|
|
self.assertIn('llvm-nm-15', result.stdout)
|
|
|
|
|
|
def test_llvm_tools_come_from_llvm_config_bindir(self):
|
|
(self.bin / 'llvm-objdump').unlink()
|
|
(self.bin / 'llvm-nm').unlink()
|
|
self.stub('llvm-objdump-23', 'exit 0', folder=self.llvm_bindir)
|
|
self.stub('llvm-nm-23', 'exit 0', folder=self.llvm_bindir)
|
|
result = self.preflight()
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
self.assertIn('llvm bin', result.stdout)
|
|
|
|
|
|
def test_missing_llvm_tool_names_what_to_install(self):
|
|
(self.bin / 'llvm-nm').unlink()
|
|
result = self.preflight()
|
|
self.assertEqual(result.returncode, 1)
|
|
self.assertIn('FAIL llvm-tools', result.stdout)
|
|
self.assertIn('llvm-nm not found', result.stdout)
|
|
self.assertIn('llvm23-tools', result.stdout)
|
|
|
|
|
|
def test_missing_llvm_config_names_the_package(self):
|
|
(self.bin / 'llvm-config').unlink()
|
|
result = self.preflight()
|
|
self.assertEqual(result.returncode, 1)
|
|
self.assertIn('FAIL llvm', result.stdout)
|
|
self.assertIn('llvm-config not found', result.stdout)
|
|
self.assertIn('llvm-dev', result.stdout)
|
|
|
|
|
|
def test_old_cmake_stops_the_setup(self):
|
|
self.stub('cmake', 'echo "cmake version 3.16.3"')
|
|
result = self.preflight()
|
|
self.assertEqual(result.returncode, 1)
|
|
self.assertIn('FAIL cmake', result.stdout)
|
|
self.assertIn('CMake 3.20', result.stdout)
|
|
|
|
|
|
def test_old_git_lfs_stops_the_setup(self):
|
|
self.stub('git', 'case "$*" in "lfs version") echo "git-lfs/2.9.2 (GitHub; linux amd64)";; esac')
|
|
result = self.preflight()
|
|
self.assertEqual(result.returncode, 1)
|
|
self.assertIn('FAIL git-lfs', result.stdout)
|
|
self.assertIn('Git LFS 2.13', result.stdout)
|
|
|
|
|
|
def test_old_kernel_is_required(self):
|
|
result = self.preflight(kernel='5.10.0-28-amd64')
|
|
self.assertEqual(result.returncode, 1)
|
|
self.assertIn('FAIL kernel', result.stdout)
|
|
self.assertIn('Linux 5.14', result.stdout)
|
|
|
|
|
|
def test_old_vulkan_stops_the_setup(self):
|
|
self.stub('vulkaninfo', 'echo "apiVersion = 1.2.170"')
|
|
result = self.preflight()
|
|
self.assertEqual(result.returncode, 1)
|
|
self.assertIn('FAIL vulkan', result.stdout)
|
|
self.assertIn('Vulkan 1.3 or newer is required', result.stdout)
|
|
|
|
|
|
def test_missing_timeline_semaphores_is_reported(self):
|
|
self.stub('vulkaninfo', '''case "$*" in
|
|
--summary) echo 'apiVersion = 1.3.289';;
|
|
*) echo 'timelineSemaphore = false';;
|
|
esac''')
|
|
result = self.preflight()
|
|
self.assertEqual(result.returncode, 1)
|
|
self.assertIn('FAIL vulkan-timeline', result.stdout)
|
|
|
|
|
|
def test_unreachable_display_only_warns(self):
|
|
self.stub('xdpyinfo', 'exit 1')
|
|
result = self.preflight(display=':99')
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
self.assertIn('warn x11', result.stdout)
|
|
self.assertIn('XAUTHORITY', result.stdout)
|
|
|
|
|
|
def test_headless_host_only_warns(self):
|
|
result = self.preflight(display=None)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
self.assertIn('DISPLAY unset', result.stdout)
|
|
|
|
|
|
def test_json_report_names_every_check(self):
|
|
result = self.preflight('--json')
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
report = json.loads(result.stdout)
|
|
self.assertTrue(report['ok'])
|
|
names = {check['check'] for check in report['checks']}
|
|
self.assertTrue({'python', 'cmake', 'git-lfs', 'lfs-pointer', 'kernel', 'llvm',
|
|
'llvm-tools', 'vulkan', 'x11'} <= names)
|
|
self.assertTrue(all('required' in check and 'detail' in check for check in report['checks']))
|
|
|
|
|
|
class LfsPointerFixture(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory(prefix='lfs pointer ', dir=os.environ.get('TMPDIR'))
|
|
self.addCleanup(self.tmp.cleanup)
|
|
self.source = Path(self.tmp.name) / 'darling-src'
|
|
self.source.mkdir()
|
|
self.preflight = load('preflight_lfs', PREFLIGHT)
|
|
self.preflight.SOURCE = self.source
|
|
|
|
def check(self):
|
|
reports = []
|
|
self.preflight.check_lfs_pointers(lambda *arguments, **keywords: reports.append(
|
|
(arguments, keywords)))
|
|
self.assertEqual(len(reports), 1)
|
|
arguments, keywords = reports[0]
|
|
name, ok, detail = arguments[:3]
|
|
hint = arguments[3] if len(arguments) > 3 else keywords.get('hint')
|
|
return name, ok, detail, hint
|
|
|
|
def write(self, name, content):
|
|
path = self.source / name
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_bytes(content)
|
|
return path
|
|
|
|
def test_pointer_text_in_a_library_is_reported(self):
|
|
self.write('lib/libswiftCore.dylib',
|
|
b'version https://git-lfs.github.com/spec/v1\noid sha256:abc\nsize 123\n')
|
|
self.write('lib/libreal.dylib', b'\xcf\xfa\xed\xfe' + b'\0' * 64)
|
|
name, ok, detail, hint = self.check()
|
|
self.assertEqual(name, 'lfs-pointer')
|
|
self.assertFalse(ok)
|
|
self.assertIn('lib/libswiftCore.dylib', detail)
|
|
self.assertNotIn('libreal', detail)
|
|
self.assertIn('git lfs pull', hint)
|
|
|
|
def test_pointer_text_outside_a_library_is_ignored(self):
|
|
self.write('notes.dylib.txt', b'version https://git-lfs.github.com/spec/v1\n')
|
|
self.write('lib/libreal.dylib', b'not a pointer')
|
|
_, ok, detail, _ = self.check()
|
|
self.assertTrue(ok, detail)
|
|
|
|
def test_missing_checkout_is_not_a_failure(self):
|
|
self.preflight.SOURCE = self.source / 'not fetched'
|
|
_, ok, detail, _ = self.check()
|
|
self.assertTrue(ok)
|
|
self.assertIn('no prepared checkout', detail)
|
|
|
|
|
|
class LlvmToolsFixture(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory(prefix='llvm tools ', dir=os.environ.get('TMPDIR'))
|
|
self.addCleanup(self.tmp.cleanup)
|
|
self.bin = Path(self.tmp.name) / 'bin'
|
|
self.bin.mkdir()
|
|
self.saved = os.environ.get('PATH')
|
|
os.environ['PATH'] = str(self.bin)
|
|
self.addCleanup(self.restore)
|
|
self.tools = load('llvm_tools', ROOT / 'tools/llvm_tools.py')
|
|
|
|
def restore(self):
|
|
os.environ['PATH'] = self.saved or ''
|
|
|
|
def stub(self, name, body='exit 0', folder=None):
|
|
path = (folder or self.bin) / name
|
|
path.write_text('#!/bin/sh\n' + body + '\n')
|
|
path.chmod(0o755)
|
|
return path
|
|
|
|
def test_prefers_the_unversioned_name(self):
|
|
unversioned = self.stub('llvm-objdump')
|
|
self.stub('llvm-objdump-23')
|
|
self.assertEqual(self.tools.find('llvm-objdump'), str(unversioned))
|
|
|
|
|
|
def test_prefers_the_highest_versioned_name(self):
|
|
self.stub('llvm-objdump-15')
|
|
newest = self.stub('llvm-objdump-23')
|
|
self.stub('llvm-objdump-9')
|
|
self.assertEqual(self.tools.find('llvm-objdump'), str(newest))
|
|
|
|
|
|
def test_ignores_a_name_that_is_not_a_version(self):
|
|
self.stub('llvm-objdump-tools')
|
|
self.assertIsNone(self.tools.find('llvm-objdump'))
|
|
|
|
|
|
def test_falls_back_to_llvm_config_bindir(self):
|
|
libexec = Path(self.tmp.name) / 'libexec'
|
|
libexec.mkdir()
|
|
expected = self.stub('llvm-nm-22', folder=libexec)
|
|
self.stub('llvm-config', 'echo "%s"' % libexec)
|
|
self.assertEqual(self.tools.find('llvm-nm'), str(expected))
|
|
|
|
|
|
def test_require_explains_what_to_install(self):
|
|
with self.assertRaises(SystemExit) as raised:
|
|
self.tools.require('llvm-objdump')
|
|
self.assertIn('llvm-objdump not found', str(raised.exception))
|
|
self.assertIn('llvm-config --bindir', str(raised.exception))
|
|
|
|
|
|
class FileDigestFixture(unittest.TestCase):
|
|
def test_sha256_matches_hashlib(self):
|
|
digest = load('file_digest', ROOT / 'tools/file_digest.py')
|
|
with tempfile.TemporaryDirectory(prefix='digest ', dir=os.environ.get('TMPDIR')) as folder:
|
|
path = Path(folder) / 'payload'
|
|
payload = b'darling' * 400_000 # More than one chunk.
|
|
path.write_bytes(payload)
|
|
self.assertEqual(digest.sha256(path), hashlib.sha256(payload).hexdigest())
|