mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-15 22:13:24 +09:00
[Fix] (CI): separate CPU builds from GPU stages and repair CTest metadata
This commit is contained in:
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user