diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index f33f4f8..6cf198f 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -31,7 +31,7 @@ Use the [setup instructions](SETUP.md) from the repository root as your ordinary - **kernel** 5.14 or newer for `MADV_POPULATE_WRITE`. - **llvm** 16 or newer; 22 or 23 is what the patches are tested with. Iridium calls `LLVMGetVersion`, which LLVM 15 does not export, so an LLVM 15 runtime aborts as soon as Riot creates a Metal device ([#139](https://github.com/lucannez64/darling-lol/issues/139)). LLVM 16-21 builds with a warning. - **llvm-tools**: `llvm-objdump` and `llvm-nm`. They are now resolved as an unversioned name, as `llvm--`, or from `llvm-config --bindir`, so Ubuntu's `llvm-15-dev` no longer needs symlinks in `/usr/local/bin`. -- **vulkan** API 1.3 and timeline semaphores. Indium rejects older devices, so this stops the setup with the version `vulkaninfo` reports. Without `vulkaninfo` it only warns, because the tool may be missing even when the driver is fine. +- **vulkan** API 1.3 and timeline semaphores. Indium rejects older devices, so this stops the setup with the version `vulkaninfo` reports. An installed `vulkaninfo` that fails or times out is also required to be fixed, because the loader or the ICD files are then broken; without `vulkaninfo` at all it only warns, because the tool may be missing even when the driver is fine. - **x11** only warns: `DISPLAY` is probed with `xdpyinfo`, and the GL/EGL vendor is reported when a probe is installed. An inaccessible display is what made `iokitd` restart in a loop before [#140](https://github.com/lucannez64/darling-lol/issues/140); export `XAUTHORITY` from servers and SSH sessions. A build without a display is still valid. Run the same checks by hand, from the checkout: diff --git a/scripts/preflight.py b/scripts/preflight.py index b66f790..3dc72f3 100644 --- a/scripts/preflight.py +++ b/scripts/preflight.py @@ -27,7 +27,7 @@ MIN_LFS = (2, 13) # Debian 11 ships 2.13, the oldest release the setup is t MIN_KERNEL = (5, 14) # MADV_POPULATE_WRITE. MIN_VULKAN = (1, 3) # Indium rejects older devices and needs timeline semaphores. -SOURCE = ROOT / 'build/darling-src' +SOURCE = Path(os.environ.get('RCLOL_PREFLIGHT_SOURCE') or ROOT / 'build/darling-src') POINTER = b'version https://git-lfs.github.com/spec/v1' LIBRARY_SUFFIXES = ('.dylib', '.a', '.so', '.o') @@ -162,6 +162,12 @@ def vulkan_devices(): def check_vulkan(report): devices = vulkan_devices() + if devices is None and shutil.which('vulkaninfo'): + report('vulkan', False, 'vulkaninfo did not finish', + 'vulkaninfo is installed but exited with an error or timed out. Run ' + '`vulkaninfo --summary` for the loader or ICD error, then fix or reinstall the ' + 'driver: the runtime needs Vulkan 1.3 and timeline semaphores.') + return if devices is None: report('vulkan', False, 'vulkaninfo not available', 'Install vulkan-tools to check the device. The runtime needs Vulkan 1.3 and ' diff --git a/tests/test_preflight.py b/tests/test_preflight.py index 3cfbc29..d999061 100644 --- a/tests/test_preflight.py +++ b/tests/test_preflight.py @@ -53,7 +53,9 @@ esac''') return path def preflight(self, *arguments, kernel=KERNEL, display=':0'): - env = dict(os.environ, PATH=str(self.bin), RCLOL_PREFLIGHT_KERNEL_RELEASE=kernel) + env = dict(os.environ, PATH=str(self.bin), + RCLOL_PREFLIGHT_KERNEL_RELEASE=kernel, + RCLOL_PREFLIGHT_SOURCE=str(Path(self.tmp.name) / 'not fetched')) if display is None: env.pop('DISPLAY', None) else: @@ -157,6 +159,24 @@ esac''') self.assertIn('Vulkan 1.3 or newer is required', result.stdout) + def test_missing_vulkaninfo_only_warns(self): + (self.bin / 'vulkaninfo').unlink() + result = self.preflight() + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn('warn vulkan', result.stdout) + self.assertIn('vulkaninfo not available', result.stdout) + self.assertIn('Install vulkan-tools', result.stdout) + + + def test_broken_vulkaninfo_is_required(self): + self.stub('vulkaninfo', 'exit 1') + result = self.preflight() + self.assertEqual(result.returncode, 1) + self.assertIn('FAIL vulkan', result.stdout) + self.assertIn('vulkaninfo did not finish', result.stdout) + self.assertIn('timed out', result.stdout) + + def test_missing_timeline_semaphores_is_reported(self): self.stub('vulkaninfo', '''case "$*" in --summary) echo 'apiVersion = 1.3.289';; @@ -287,6 +307,21 @@ class LlvmToolsFixture(unittest.TestCase): self.assertEqual(self.tools.find('llvm-nm'), str(expected)) + def test_llvm_config_timeout_is_reported(self): + self.stub('llvm-config', 'exec /bin/sleep 5') + self.tools.TIMEOUT = 1 + with self.assertRaises(SystemExit) as raised: + self.tools.config('--version') + self.assertIn('did not finish within 1 seconds', str(raised.exception)) + self.assertIn('reinstall the LLVM development package', str(raised.exception)) + + + def test_llvm_config_timeout_does_not_hang_the_bindir_probe(self): + self.stub('llvm-config', 'exec /bin/sleep 5') + self.tools.TIMEOUT = 1 + self.assertIsNone(self.tools.find('llvm-objdump')) + + def test_require_explains_what_to_install(self): with self.assertRaises(SystemExit) as raised: self.tools.require('llvm-objdump') diff --git a/tools/llvm_tools.py b/tools/llvm_tools.py index e63f943..2de0b0e 100644 --- a/tools/llvm_tools.py +++ b/tools/llvm_tools.py @@ -9,6 +9,8 @@ reports. Nothing here runs a guest process. import os import subprocess +TIMEOUT = 30 # A probe that hangs must not stall the preflight or the build. + def _collect(directory, name, found): """Add every `name` and `name-` executable in the directory to found.""" @@ -41,13 +43,26 @@ def _preferred(candidates): return candidates[0 if 0 in candidates else max(candidates)] if candidates else None +def _run(command): + """Run a short probe; return (completed process, problem), one of them None.""" + try: + result = subprocess.run(command, capture_output=True, text=True, errors='replace', + timeout=TIMEOUT) + except subprocess.TimeoutExpired: + return None, f'did not finish within {TIMEOUT} seconds' + except OSError as error: + return None, f'could not be started: {error}' + return result, None + + def _bindir(): """`llvm-config --bindir` from the preferred llvm-config, or None.""" configuration = _preferred(_executables('llvm-config')) if not configuration: return None - result = subprocess.run([configuration, '--bindir'], - capture_output=True, text=True, errors='replace') + result, _ = _run([configuration, '--bindir']) + if result is None: + return None directory = result.stdout.strip() if result.returncode or not os.path.isdir(directory): return None @@ -81,7 +96,10 @@ def require(name): def config(*arguments): """Run llvm-config with the arguments, or exit with an actionable message.""" path = require('llvm-config') - result = subprocess.run([path, *arguments], capture_output=True, text=True, errors='replace') + result, problem = _run([path, *arguments]) + called = f'{path} {" ".join(arguments)}' + if problem: + raise SystemExit(f'{called} {problem}; reinstall the LLVM development package.') if result.returncode: - raise SystemExit(f'{path} {" ".join(arguments)} failed: {result.stderr.strip()}') + raise SystemExit(f'{called} failed: {result.stderr.strip()}') return result.stdout.strip()