[Fix] (CI): separate CPU builds from GPU stages and repair CTest metadata

This commit is contained in:
2026-10-05 10:00:48 -04:00
parent 4eee6ae251
commit 039b342b97
7 changed files with 719 additions and 387 deletions
+12 -1
View File
@@ -1,15 +1,25 @@
name: Configure Test runner
description: Select the installed compiler and graphics stack without modifying the host
inputs:
gpu:
description: Require the native GPU stack when running on 8845
default: "false"
runs:
using: composite
steps:
- shell: bash
env:
NEED_GPU: ${{ inputs.gpu }}
run: |
if [ "$RUNNER_ENVIRONMENT" = "self-hosted" ]; then
if [ "$NEED_GPU" != true ] && [ -f /.dockerenv ]; then
bash "$GITHUB_WORKSPACE/scripts/ci/setup_cpu_container.sh"
fi
if [ "$RUNNER_ENVIRONMENT" = "self-hosted" ] && [ "$NEED_GPU" = true ]; then
for tool in clang clang++ cmake ninja ccache; do command -v "$tool"; done
test -r /usr/share/vulkan/icd.d/dzn_icd.json
test -e /dev/dxg
{
echo 'MOBILEGL_CI_NATIVE_GPU=1'
echo 'MOBILEGL_CI_CC=clang'
echo 'MOBILEGL_CI_CXX=clang++'
echo 'MOBILEGL_CI_LLD=ld.lld'
@@ -23,6 +33,7 @@ runs:
} >> "$GITHUB_ENV"
else
{
echo 'MOBILEGL_CI_NATIVE_GPU=0'
echo 'MOBILEGL_CI_CC=clang-20'
echo 'MOBILEGL_CI_CXX=clang++-20'
echo 'MOBILEGL_CI_LLD=ld.lld-20'
+509 -362
View File
File diff suppressed because it is too large Load Diff
+35 -22
View File
@@ -111,28 +111,41 @@ Clash must be running for the runner's configured GitHub connection to work.
## Main Test workflow
`test.yml` uses a small GitHub-hosted `select-runner` job to query this repository's
runner inventory. Every subsequent job uses its single decision, so artifacts built
against Arch's compiler/libc/Mesa cannot accidentally be consumed by Ubuntu jobs.
An online runner is preferred even when busy. Offline, unregistered, or mismatched
labels select `ubuntu-latest` for the whole run. Test failures do not cause fallback.
CPU jobs do not query GPU availability. They default to GitHub `ubuntu-latest`.
Repository variable `CI_CPU_RUNNER` may specify a JSON runner label array, for example
`["self-hosted", "Linux", "X64", "cpu"]`. A self-hosted CPU runner needs Docker and
permission to run job containers. CPU builds/tests run in `ubuntu:24.04` on either
host type, with Clang 20 and the same userspace ABI. They do not require `/dev/dxg`.
GitHub Actions has no native "any hosted OR self-hosted" selector; the variable
chooses the CPU pool independently, without reserving this GPU machine.
Configure repository Actions secret `RUNNER_STATUS_TOKEN` with a fine-grained PAT
restricted to this repository and **Administration: Read-only**. The ordinary
`GITHUB_TOKEN` cannot generally list repository runners. A missing/expired token or
API failure stops selection with an error: unknown availability is not treated as
an offline machine. The token is passed only to the GitHub-hosted selector.
After each runtime build completes, a small GitHub-hosted selection job queries
this repository's runner inventory for the corresponding GPU stage. Integration,
verify, split and retrace select independently. Benchmark uses the integration
selection. `integration-verify-split` has a separate CPU build and GPU test job.
Retrace compilation and fixture preparation no longer wait for integration tests.
Availability is sampled at workflow start. If the machine disconnects after
selection, GitHub queues its remaining jobs; rerunning the workflow while it is
offline chooses Ubuntu. GitHub Actions does not migrate already scheduled jobs
between runner types. Keeping one platform per run avoids incompatible artifacts.
An online 8845 runner is preferred even when busy. Offline, unregistered, or
mismatched labels select `ubuntu-latest`. Test failures never trigger fallback.
Configure Actions secret `RUNNER_STATUS_TOKEN` with repository Administration:
Read-only access. The secret is passed only to the selection jobs. A missing token,
expired credential or API failure is an error, not evidence that the runner is offline.
The local composite setup action uses Arch's installed Clang/CMake and Dozen ICD.
Ubuntu package installation, swap provisioning, SDK download, and privileged core
pattern changes are skipped on this no-sudo host. Compiler caches are separated by
runner pool. Trace tests disable forced software-rendering environment overrides
on this machine; the Ubuntu fallback retains its existing Mesa software settings.
Hardware capability differences remain visible as test results, not fallback triggers.
The dedicated `wsl-gpu-smoke.yml` workflow has been removed; the host's installed
probe remains available to its operator.
`prepare_test_runtime.py` runs when producing and consuming runtime artifacts. It
records/relocates workspace paths, resolves CMake module paths on the execution
host, and repairs escaped ENVIRONMENT/LABELS separators before GoogleTest discovery.
This prevents a backend value like `DirectVulkan;MOBILEGL_TRANSPORT=inproc;...`.
At test time it replaces the built-in ICD path and removes software-rendering
and Mesa version overrides on the hardware runner. Thus an Ubuntu build can be
tested on Arch WSL with Dozen/D3D12 without recompiling or using the build host's
software driver configuration. Capability failures remain visible test failures.
Each GPU stage samples availability after its inputs are ready. A disconnection
later still leaves already scheduled jobs queued: GitHub cannot migrate those
jobs automatically. Rerun the workflow to select again. Arch-built native artifacts
are not mixed with Ubuntu consumers; CPU artifacts use the common Ubuntu container.
Native GPU jobs use Arch's installed tools and do not install packages, provision
swap, download an SDK or change privileged core settings. The old standalone
`wsl-gpu-smoke.yml` has been removed; the host's installed probe remains available
for manual diagnostics.
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""Relocate packaged CTest metadata and select the executing runner's drivers."""
import argparse
import os
from pathlib import Path
import re
import subprocess
def normalize(text, old_root, new_root, cmake_root, hardware=False, icd=None):
if old_root:
text = text.replace(old_root, new_root)
# Artifact builds and test hosts can have different CMake installations.
text = re.sub(r'"[^"\n]*/bin/cmake"', '"cmake"', text)
text = re.sub(r'/[^"\[\]\s;()]*/Modules/(GoogleTest(?:AddTests)?\.cmake|GoogleTest/LaunchTest\.cmake)',
lambda m: cmake_root + '/Modules/' + m[1], text)
lines = []
for line in text.splitlines(keepends=True):
if re.search(r'ENVIRONMENT|LABELS', line):
# GoogleTest discovery receives list properties, not one value with
# escaped separators. Otherwise BACKEND becomes "DirectVulkan;...".
line = line.replace('\\;', ';')
if icd:
line = re.sub(r'VK_ICD_FILENAMES=[^;"\]\s]+', 'VK_ICD_FILENAMES=' + icd, line)
if hardware:
for value in ('LIBGL_ALWAYS_SOFTWARE=1', 'MESA_GL_VERSION_OVERRIDE=3.3',
'MESA_GLSL_VERSION_OVERRIDE=330'):
line = line.replace(value + ';', '').replace(';' + value, '').replace(value, '')
lines.append(line)
return ''.join(lines)
def prepare(build, workspace, cmake_root, hardware, icd):
marker = build / '.ci-source-root'
old_root = marker.read_text().strip() if marker.exists() else str(workspace)
changed = 0
for path in list(build.rglob('*.cmake')) + list(build.rglob('runtime-mode.json')):
original = path.read_text()
revised = normalize(original, old_root, str(workspace), cmake_root, hardware, icd)
if original != revised:
path.write_text(revised)
changed += 1
# Drop discovery caches so the repaired properties are used on this host.
for path in build.rglob('*_tests*.json'):
path.unlink()
marker.write_text(str(workspace) + '\n')
print(f'Prepared {build}: {changed} metadata files; hardware={hardware}; ICD={icd}')
def main():
parser = argparse.ArgumentParser()
parser.add_argument('build', nargs='+', type=Path)
args = parser.parse_args()
info = subprocess.check_output(['cmake', '--system-information'], text=True)
cmake_root = re.search(r'^CMAKE_ROOT "([^"]+)"', info, re.M)[1]
workspace = Path(os.environ.get('GITHUB_WORKSPACE', os.getcwd())).resolve()
for build in args.build:
prepare(build, workspace, cmake_root, os.environ.get('MOBILEGL_CI_NATIVE_GPU') == '1',
os.environ.get('VK_ICD_FILENAMES'))
if __name__ == '__main__':
main()
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Choose one execution platform for the entire Test artifact dependency graph."""
"""Choose a GPU runner after the consuming stage has its build artifacts."""
import json
import os
import sys
@@ -48,7 +48,7 @@ def main():
labels, pool, reason = choose(runners)
except (ValueError, KeyError, OSError, urllib.error.URLError) as error:
# An API/auth error is not evidence that the machine is offline. Do not
# silently send the whole matrix to GitHub when the status cannot be read.
# silently send GPU tests to GitHub when the status cannot be read.
print(f"::error::Cannot determine runner availability: {error}", file=sys.stderr)
return 1
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
# CPU runners may be GitHub-hosted or self-hosted; the artifact ABI stays Ubuntu.
test -f /.dockerenv
. /etc/os-release
test "$ID" = ubuntu && test "$VERSION_ID" = 24.04
apt-get update
apt-get install -y curl gnupg tar xz-utils zstd build-essential gh jq unzip zip wget pkg-config cmake
curl --fail --silent --show-error https://apt.llvm.org/llvm-snapshot.gpg.key \
| gpg --dearmor -o /usr/share/keyrings/llvm-archive-keyring.gpg
echo 'deb [signed-by=/usr/share/keyrings/llvm-archive-keyring.gpg] https://apt.llvm.org/noble/ llvm-toolchain-noble-20 main' \
> /etc/apt/sources.list.d/llvm.list
apt-get update
apt-get install -y libc++1-20 libc++abi1-20 libunwind-20
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
import importlib.util
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import tempfile
import unittest
spec = importlib.util.spec_from_file_location('runtime', Path(__file__).with_name('prepare_test_runtime.py'))
runtime = importlib.util.module_from_spec(spec)
spec.loader.exec_module(runtime)
class MetadataTest(unittest.TestCase):
def test_environment_and_labels_are_lists(self):
text = r'[[ENVIRONMENT]] [[MOBILEGL_BACKEND_TYPE=DirectVulkan\;MOBILEGL_TRANSPORT=inproc]] [[LABELS]] [[gpu\;split]]'
fixed = runtime.normalize(text, '', '/new', '/cmake')
self.assertNotIn(r'\;', fixed)
self.assertIn('DirectVulkan;MOBILEGL_TRANSPORT=inproc', fixed)
def test_other_escaped_values_are_preserved(self):
text = r'COMMAND "argument\;with-semicolon"'
self.assertEqual(runtime.normalize(text, '', '/new', '/cmake'), text)
def test_relocation_and_cmake_modules(self):
text = '"/old/build/test" "/old/tool/bin/cmake" "/old/tool/share/cmake/Modules/GoogleTest.cmake"'
fixed = runtime.normalize(text, '/old', '/new', '/usr/share/cmake')
self.assertIn('"/new/build/test"', fixed)
self.assertIn('"cmake"', fixed)
self.assertIn('/usr/share/cmake/Modules/GoogleTest.cmake', fixed)
def test_hardware_replaces_icd_and_removes_software_overrides(self):
text = 'ENVIRONMENT "VK_ICD_FILENAMES=/lvp.json;LIBGL_ALWAYS_SOFTWARE=1;MESA_GL_VERSION_OVERRIDE=3.3;MESA_GLSL_VERSION_OVERRIDE=330;EGL_PLATFORM=surfaceless"'
fixed = runtime.normalize(text, '', '/new', '/cmake', True, '/dzn.json')
self.assertIn('VK_ICD_FILENAMES=/dzn.json', fixed)
self.assertIn('EGL_PLATFORM=surfaceless', fixed)
self.assertNotIn('SOFTWARE', fixed)
self.assertNotIn('VERSION_OVERRIDE', fixed)
self.assertIn('LIBGL_ALWAYS_SOFTWARE=1', runtime.normalize(text, '', '/new', '/cmake'))
def test_preparation_is_idempotent_and_drops_discovery_cache(self):
with tempfile.TemporaryDirectory() as folder:
root = Path(folder)
(root / '.ci-source-root').write_text('/old\n')
(root / 'tests.cmake').write_text('ENVIRONMENT "BACKEND=x\\;TRANSPORT=y"\n"/old/build"')
(root / 'x_tests.json').write_text('{}')
runtime.prepare(root, Path('/new'), '/cmake', False, None)
first = (root / 'tests.cmake').read_text()
runtime.prepare(root, Path('/new'), '/cmake', False, None)
self.assertEqual(first, (root / 'tests.cmake').read_text())
self.assertFalse((root / 'x_tests.json').exists())
@unittest.skipUnless(os.name == 'posix' and shutil.which('cmake'), 'requires POSIX CMake')
def test_real_ctest_discovery_separates_environment(self):
with tempfile.TemporaryDirectory() as folder:
root = Path(folder)
probe = root / 'probe.py'
probe.write_text('#!/usr/bin/env python3\nimport sys, os\n'
'if "--gtest_list_tests" in sys.argv:\n print("Probe.\\n Environment")\n'
'else:\n assert os.environ.get("MOBILEGL_BACKEND_TYPE") == "DirectVulkan", repr(dict(os.environ))\n assert os.environ.get("MOBILEGL_TRANSPORT") == "inproc"\n')
probe.chmod(0o755)
(root / 'CMakeLists.txt').write_text(
'cmake_minimum_required(VERSION 3.22)\nproject(EnvProbe NONE)\nenable_testing()\ninclude(GoogleTest)\n'
f'add_executable(probe IMPORTED)\nset_target_properties(probe PROPERTIES IMPORTED_LOCATION "{probe}")\n'
'gtest_discover_tests(probe DISCOVERY_MODE PRE_TEST PROPERTIES '
'ENVIRONMENT "MOBILEGL_BACKEND_TYPE=DirectVulkan\\;MOBILEGL_TRANSPORT=inproc" LABELS "gpu\\;split")\n')
build = root / 'build'
subprocess.run(['cmake', '-S', str(root), '-B', str(build)], check=True, capture_output=True)
info = subprocess.check_output(['cmake', '--system-information'], text=True)
cmake_root = re.search(r'^CMAKE_ROOT "([^"]+)"', info, re.M)[1]
runtime.prepare(build, root, cmake_root, False, None)
inventory = json.loads(subprocess.check_output(['ctest', '--test-dir', str(build), '--show-only=json-v1'], text=True))
props = {p['name']: p['value'] for p in inventory['tests'][0]['properties']}
self.assertEqual(props['ENVIRONMENT'], ['MOBILEGL_BACKEND_TYPE=DirectVulkan', 'MOBILEGL_TRANSPORT=inproc'])
self.assertEqual(set(props['LABELS']), {'gpu', 'split'})
result = subprocess.run(['ctest', '--test-dir', str(build), '--output-on-failure'], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
if __name__ == '__main__':
unittest.main()