Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions checkbox-support/checkbox_support/helpers/host_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@
# You should have received a copy of the GNU General Public License
# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.

"""Shared utilities for host Vulkan test helpers."""
"""Shared utilities for host GPU test helpers."""

import os
import shutil
import subprocess
import sysconfig


class VulkanDetectionError(Exception):
"""Raised when a GPU/Vulkan detection step fails."""
class HostGPUDetectionError(Exception):
"""Raised when a host GPU detection step fails."""


def get_arch_triple():
Expand All @@ -38,11 +38,11 @@ def get_arch_triple():

def find_plz_run():
"""Return the path to plz-run from PATH.
Raises VulkanDetectionError if plz-run is not found.
Raises HostGPUDetectionError if plz-run is not found.
"""
path = shutil.which("plz-run")
if path is None:
raise VulkanDetectionError("plz-run not found in PATH")
raise HostGPUDetectionError("plz-run not found in PATH")
return path


Expand All @@ -69,7 +69,7 @@ def prime_selected_vendor():
"""Return the GPU vendor chosen by prime-select.

Returns one of the keys in _PRIME_VENDOR_ICD_PREFIXES.
Raises VulkanDetectionError if prime-select is not installed, fails,
Raises HostGPUDetectionError if prime-select is not installed, fails,
or returns an unrecognised value (e.g. 'on-demand').
"""
try:
Expand All @@ -83,9 +83,9 @@ def prime_selected_vendor():
.lower()
)
except (OSError, subprocess.CalledProcessError) as e:
raise VulkanDetectionError("prime-select query failed") from e
raise HostGPUDetectionError("prime-select query failed") from e
if output not in _PRIME_VENDOR_ICD_PREFIXES:
raise VulkanDetectionError(
raise HostGPUDetectionError(
"prime-select returned unrecognised value: {!r}".format(output)
)
return output
Expand All @@ -96,7 +96,7 @@ def _run_vulkaninfo(plz_run, arch_triple):

vulkaninfo is executed inside a new mount/user namespace (via plz-run)
so that it uses the host ICD stack instead of snap-bundled libraries.
Raises VulkanDetectionError if vulkaninfo fails.
Raises HostGPUDetectionError if vulkaninfo fails.
"""
ld_library_path = "/usr/lib/{arch}:/usr/lib".format(arch=arch_triple)
try:
Expand All @@ -117,7 +117,7 @@ def _run_vulkaninfo(plz_run, arch_triple):
stderr=subprocess.STDOUT,
)
except subprocess.CalledProcessError as e:
raise VulkanDetectionError("vulkaninfo failed") from e
raise HostGPUDetectionError("vulkaninfo failed") from e


def _vendor_prefixes_from_vulkaninfo(output):
Expand All @@ -127,7 +127,7 @@ def _vendor_prefixes_from_vulkaninfo(output):
Matches on the vendorID field which is unambiguous across driver versions
and device names. The field is right-padded with spaces for alignment,
so each line is stripped before matching.
Raises VulkanDetectionError if no known vendor is found.
Raises HostGPUDetectionError if no known vendor is found.
"""
for line in output.splitlines():
stripped = line.strip()
Expand All @@ -136,7 +136,7 @@ def _vendor_prefixes_from_vulkaninfo(output):
for vid, prefixes in _PCI_VENDOR_ICD_PREFIXES.items():
if vid in stripped:
return prefixes
raise VulkanDetectionError(
raise HostGPUDetectionError(
"no known GPU vendor found in vulkaninfo output"
)

Expand All @@ -146,14 +146,14 @@ def active_vendor_prefixes():

Tries prime-select first (authoritative on PRIME multi-GPU systems),
then falls back to vulkaninfo via plz-run.
Raises VulkanDetectionError if no method identifies the vendor.
Raises HostGPUDetectionError if no method identifies the vendor.
"""
# prime-select is only present on NVIDIA hybrid systems;
# absence or unrecognised output is normal — fall through.
try:
vendor = prime_selected_vendor()
return _PRIME_VENDOR_ICD_PREFIXES[vendor]
except VulkanDetectionError:
except HostGPUDetectionError:
pass

plz_run = find_plz_run()
Expand All @@ -176,7 +176,7 @@ def find_host_icd_filenames(vendor_prefixes=None):
try:
entries = sorted(os.listdir(icd_dir))
except OSError as e:
raise VulkanDetectionError(
raise HostGPUDetectionError(
"cannot read Vulkan ICD directory {}".format(icd_dir)
) from e
result = []
Expand All @@ -197,7 +197,7 @@ def check_host_gpu(plz_run, arch_triple):
"""Return True if a physical GPU is available via host Vulkan drivers."""
try:
output = _run_vulkaninfo(plz_run, arch_triple)
except VulkanDetectionError:
except HostGPUDetectionError:
return False
return any(
t in output
Expand Down
22 changes: 11 additions & 11 deletions checkbox-support/checkbox_support/helpers/tests/test_host_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def test_returns_path_when_found(self, _which):

@patch("shutil.which", return_value=None)
def test_raises_when_not_found(self, _which):
with self.assertRaises(host_utils.VulkanDetectionError):
with self.assertRaises(host_utils.HostGPUDetectionError):
host_utils.find_plz_run()


Expand Down Expand Up @@ -184,20 +184,20 @@ def test_returns_known_vendor(self, _mock):

@patch("subprocess.check_output", return_value="on-demand\n")
def test_raises_for_on_demand(self, _mock):
with self.assertRaises(host_utils.VulkanDetectionError):
with self.assertRaises(host_utils.HostGPUDetectionError):
host_utils.prime_selected_vendor()

@patch("subprocess.check_output", side_effect=FileNotFoundError)
def test_raises_when_prime_select_not_found(self, _mock):
with self.assertRaises(host_utils.VulkanDetectionError):
with self.assertRaises(host_utils.HostGPUDetectionError):
host_utils.prime_selected_vendor()

@patch(
"subprocess.check_output",
side_effect=subprocess.CalledProcessError(1, "prime-select"),
)
def test_raises_on_error(self, _mock):
with self.assertRaises(host_utils.VulkanDetectionError):
with self.assertRaises(host_utils.HostGPUDetectionError):
host_utils.prime_selected_vendor()


Expand All @@ -210,12 +210,12 @@ def test_returns_prefixes_for_known_vendor(self):

def test_raises_for_unknown_vendor(self):
output = " vendorID = 0x1234\n"
with self.assertRaises(host_utils.VulkanDetectionError):
with self.assertRaises(host_utils.HostGPUDetectionError):
host_utils._vendor_prefixes_from_vulkaninfo(output)

def test_raises_when_no_vendorid_line(self):
output = "deviceType = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU\n"
with self.assertRaises(host_utils.VulkanDetectionError):
with self.assertRaises(host_utils.HostGPUDetectionError):
host_utils._vendor_prefixes_from_vulkaninfo(output)


Expand All @@ -229,7 +229,7 @@ def test_returns_prime_vendor(self, _prime):

@patch(
"checkbox_support.helpers.host_utils.prime_selected_vendor",
side_effect=host_utils.VulkanDetectionError,
side_effect=host_utils.HostGPUDetectionError,
)
@patch(
"checkbox_support.helpers.host_utils.find_plz_run",
Expand All @@ -248,14 +248,14 @@ def test_returns_vulkaninfo_vendor(self, _vkinfo, _arch, _plz, _prime):

@patch(
"checkbox_support.helpers.host_utils.prime_selected_vendor",
side_effect=host_utils.VulkanDetectionError,
side_effect=host_utils.HostGPUDetectionError,
)
@patch(
"checkbox_support.helpers.host_utils.find_plz_run",
side_effect=host_utils.VulkanDetectionError,
side_effect=host_utils.HostGPUDetectionError,
)
def test_raises_when_all_methods_fail(self, _plz, _prime):
with self.assertRaises(host_utils.VulkanDetectionError):
with self.assertRaises(host_utils.HostGPUDetectionError):
host_utils.active_vendor_prefixes()


Expand Down Expand Up @@ -291,7 +291,7 @@ def test_skips_non_json_files(self):

def test_raises_when_icd_dir_missing(self):
with patch("os.listdir", side_effect=OSError):
with self.assertRaises(host_utils.VulkanDetectionError):
with self.assertRaises(host_utils.HostGPUDetectionError):
host_utils.find_host_icd_filenames()


Expand Down
4 changes: 2 additions & 2 deletions providers/base/bin/cl_host.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
import sys

from checkbox_support.helpers.host_utils import (
VulkanDetectionError,
HostGPUDetectionError,
find_plz_run,
get_arch_triple,
)
Expand Down Expand Up @@ -78,7 +78,7 @@ def cmd_resource():

try:
plz_run = find_plz_run()
except VulkanDetectionError as exc:
except HostGPUDetectionError as exc:
print("FAIL: {}".format(exc), file=sys.stderr)
return 1

Expand Down
4 changes: 2 additions & 2 deletions providers/base/bin/crucible_host.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
import sys

from checkbox_support.helpers.host_utils import (
VulkanDetectionError,
HostGPUDetectionError,
active_vendor_prefixes,
check_host_gpu,
find_host_icd_filenames,
Expand Down Expand Up @@ -107,7 +107,7 @@ def main():
else:
logging.error("Unknown command: %s", command)
return 1
except (RuntimeError, VulkanDetectionError) as exc:
except (RuntimeError, HostGPUDetectionError) as exc:
logging.error("%s", exc)
return 1

Expand Down
4 changes: 2 additions & 2 deletions providers/base/bin/gl_host.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
import sys

from checkbox_support.helpers.host_utils import (
VulkanDetectionError,
HostGPUDetectionError,
find_plz_run,
get_arch_triple,
)
Expand Down Expand Up @@ -142,7 +142,7 @@ def main():
else:
logging.error("Unknown command: %s", command)
return 1
except (RuntimeError, OpenGLError, VulkanDetectionError) as exc:
except (RuntimeError, OpenGLError, HostGPUDetectionError) as exc:
logging.error("%s", exc)
return 1
return 0
Expand Down
131 changes: 131 additions & 0 deletions providers/base/bin/lz_host.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/usr/bin/env python3
# This file is part of Checkbox.
#
# Copyright 2025 Canonical Ltd.
# Written by:
# Shane McKee <shane.mckee@canonical.com>
#
# Checkbox is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3,
# as published by the Free Software Foundation.
#
# Checkbox is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.

"""
Host Level Zero helper for Checkbox.

Subcommands:
resource Emit a resource record if a GPU is available via host
Level Zero drivers (used by depends:
graphics/lz_classic_gpu_avail).
validate-install Emit a resource record if the host Level Zero ICD loader
is installed (used by depends:
graphics/lz_classic_lz_avail).
run-test ARGS... Run a level-zero-tests test binary with --no-confinement,
forwarding all remaining arguments to the test.
"""

import glob
import logging
import os
import subprocess
import sys

from checkbox_support.helpers.host_utils import (
HostGPUDetectionError,
find_plz_run,
get_arch_triple,
)


def check_host_gpu(plz_run, arch_triple):
"""Check for a Level Zero GPU by probing render device nodes.

plz-run is used to escape snap confinement so that the host device
nodes and libraries are visible.
"""
render_nodes = glob.glob("/dev/dri/renderD*")
if not render_nodes:
logging.error("No render device nodes found in /dev/dri")
return False
loader = "/usr/lib/{}/libze_loader.so.1".format(arch_triple)
if not os.path.isfile(loader):
logging.error("Host Level Zero loader not found at %s", loader)
return False
logging.info("Found render device(s) and Level Zero loader at %s", loader)
return True


def cmd_resource():
arch_triple = get_arch_triple()

try:
plz_run = find_plz_run()
except HostGPUDetectionError as exc:
logging.error("%s", exc)
return 1

if check_host_gpu(plz_run, arch_triple):
print("gpu_available: True")
return 0

logging.error("No Level Zero GPU device found using host drivers")
return 1


def cmd_validate_install():
arch_triple = get_arch_triple()
host_ze = "/usr/lib/{}/libze_loader.so.1".format(arch_triple)
if os.path.isfile(host_ze):
logging.info("Host Level Zero loader found at %s", host_ze)
print("ze_loader_available: True")
return 0
logging.error("Host Level Zero loader not found at %s", host_ze)
logging.error(
"Install libze1 or equivalent before running host Level Zero tests"
)
return 1


def cmd_run_test(test_args):
snap = "/snap/level-zero-tests/current"
result = subprocess.run(
["{}/test".format(snap), "--no-confinement"] + test_args,
env=dict(os.environ, SNAP=snap),
)
return result.returncode


def main():
logging.basicConfig(
format="%(levelname)s: %(message)s", level=logging.INFO
)
if len(sys.argv) < 2:
logging.error(
"Usage: lz_host.py {resource,validate-install,run-test} [args...]"
)
return 1
command = sys.argv[1]
try:
if command == "resource":
return cmd_resource()
elif command == "validate-install":
return cmd_validate_install()
elif command == "run-test":
return cmd_run_test(sys.argv[2:])
else:
logging.error("Unknown command: %s", command)
return 1
except (RuntimeError, HostGPUDetectionError) as exc:
logging.error("%s", exc)
return 1


if __name__ == "__main__":
sys.exit(main())
Loading
Loading