Files
TheRock/build_tools/tests/py_packaging_test.py
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

1870 lines
73 KiB
Python

# Copyright Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
"""Tests for _therock_utils/py_packaging.py.
These tests cover:
- PopulatedFiles: per-instance isolation, dedup semantics
- Multi-arch packaging: each library package independently tracks its own files
- params.populated_packages: registration and cross-package search helpers
"""
import json
import os
import subprocess
import tempfile
import unittest
from pathlib import Path
import sys
sys.path.insert(0, os.fspath(Path(__file__).parent.parent))
from _therock_utils.artifacts import ArtifactCatalog
from _therock_utils.py_packaging import Parameters, PopulatedDistPackage, PopulatedFiles
from build_python_packages import validate_kpack_split_target_completeness
class TmpDirTestCase(unittest.TestCase):
def setUp(self):
override_temp = os.getenv("TEST_TMPDIR")
if override_temp is not None:
self.temp_context = None
self.temp_dir = Path(override_temp)
self.temp_dir.mkdir(parents=True, exist_ok=True)
else:
self.temp_context = tempfile.TemporaryDirectory()
self.temp_dir = Path(self.temp_context.name)
def tearDown(self):
if self.temp_context:
self.temp_context.cleanup()
def write_file(self, relpath: str, content: str = ""):
p = self.temp_dir / relpath
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(content)
return p
# ---------------------------------------------------------------------------
# Pure unit tests for PopulatedFiles
# ---------------------------------------------------------------------------
class PopulatedFilesTest(unittest.TestCase):
"""Tests for PopulatedFiles in isolation — no disk I/O."""
def _fake_package(self):
"""Minimal stand-in for the package argument to mark_populated."""
import types
return types.SimpleNamespace(platform_dir=Path("/fake"))
def test_has_returns_false_when_empty(self):
files = PopulatedFiles()
self.assertFalse(files.has("lib/libfoo.so.1"))
def test_has_returns_true_after_mark_populated(self):
files = PopulatedFiles()
files.mark_populated(self._fake_package(), "lib/libfoo.so.1", Path("/dest"))
self.assertTrue(files.has("lib/libfoo.so.1"))
def test_mark_populated_stores_package_and_path(self):
files = PopulatedFiles()
pkg = self._fake_package()
dest = Path("/dest/lib/libfoo.so.1")
files.mark_populated(pkg, "lib/libfoo.so.1", dest)
stored_pkg, stored_path = files.materialized_relpaths["lib/libfoo.so.1"]
self.assertIs(stored_pkg, pkg)
self.assertEqual(stored_path, dest)
def test_mark_populated_raises_on_duplicate(self):
"""Populating the same relpath twice within one package is always a bug."""
files = PopulatedFiles()
pkg = self._fake_package()
files.mark_populated(pkg, "lib/libfoo.so.1", Path("/dest"))
with self.assertRaises(AssertionError):
files.mark_populated(pkg, "lib/libfoo.so.1", Path("/dest"))
def test_two_instances_are_independent(self):
"""Regression test for the multi-arch dedup bug.
Before the fix, all packages shared a single params.files instance.
Whichever target family iterated first would claim every shared relpath,
leaving the other with an incomplete (empty) package.
After the fix, each PopulatedDistPackage has its own self.files, so
both packages can independently own the same relpath.
"""
f1 = PopulatedFiles()
f2 = PopulatedFiles()
pkg1 = self._fake_package()
pkg2 = self._fake_package()
dest1 = Path("/pkg1/lib/librocblas.so.5")
dest2 = Path("/pkg2/lib/librocblas.so.5")
f1.mark_populated(pkg1, "lib/librocblas.so.5", dest1)
# f2 must not be affected by f1's population.
self.assertFalse(f2.has("lib/librocblas.so.5"))
f2.mark_populated(pkg2, "lib/librocblas.so.5", dest2)
self.assertTrue(f2.has("lib/librocblas.so.5"))
# f1 retains its own path, unmodified.
_, path = f1.materialized_relpaths["lib/librocblas.so.5"]
self.assertEqual(path, dest1)
def test_soname_aliases_are_per_instance(self):
"""soname_aliases dict is per-instance, not shared."""
f1 = PopulatedFiles()
f2 = PopulatedFiles()
f1.soname_aliases["lib/libfoo.so"] = "libfoo.so.1"
self.assertNotIn("lib/libfoo.so", f2.soname_aliases)
# ---------------------------------------------------------------------------
# Integration tests: real artifact directories, real Parameters/PopulatedDistPackage
# ---------------------------------------------------------------------------
class MultiArchPackagingTest(TmpDirTestCase):
"""Integration tests verifying multi-arch library packaging behaviour.
These tests create minimal artifact directories on disk (text files, no ELF
binaries) so that ArtifactCatalog and populate_runtime_files work end-to-end
without patchelf being invoked.
"""
def _add_artifact(
self,
artifact_dir: Path,
name: str,
component: str,
target_family: str,
files: dict[str, str],
):
"""Create a minimal artifact directory with the given files under stage/."""
subdir = artifact_dir / f"{name}_{component}_{target_family}"
stage = subdir / "stage"
for relpath, content in files.items():
f = stage / relpath
f.parent.mkdir(parents=True, exist_ok=True)
f.write_text(content)
(subdir / "artifact_manifest.txt").write_text("stage\n")
def _make_params(self, artifact_dir: Path) -> Parameters:
dest_dir = self.temp_dir / "packages"
dest_dir.mkdir(parents=True, exist_ok=True)
return Parameters(
dest_dir=dest_dir,
version="0.0.1.test",
version_suffix="",
artifacts=ArtifactCatalog(artifact_dir),
)
def test_each_library_package_independently_owns_shared_relpaths(self):
"""Both arch-specific library packages must contain all their runtime files.
This is the end-to-end regression test for the global params.files dedup bug.
When gfx120X-all and gfx94X-dcgpu artifacts share relpaths (e.g.
lib/librocblas.txt), both packages must end up with that file in their own
self.files — neither should silently skip it because the other got there first.
"""
artifact_dir = self.temp_dir / "artifacts"
shared_files = {
"lib/librocblas.txt": "arch-specific rocblas",
"lib/libhipblas.txt": "arch-neutral hipblas wrapper",
}
self._add_artifact(artifact_dir, "blas", "lib", "gfx120X-all", shared_files)
self._add_artifact(artifact_dir, "blas", "lib", "gfx94X-dcgpu", shared_files)
params = self._make_params(artifact_dir)
for target_family in sorted(params.all_target_families):
lib = PopulatedDistPackage(
params, logical_name="libraries", target_family=target_family
)
lib.populate_runtime_files(
params.filter_artifacts(
lambda an, tf=target_family: an.name == "blas"
and an.target_family == tf
)
)
self.assertEqual(len(params.populated_packages), 2)
pkg_gfx120X = next(
p for p in params.populated_packages if p.target_family == "gfx120X-all"
)
pkg_gfx94X = next(
p for p in params.populated_packages if p.target_family == "gfx94X-dcgpu"
)
for relpath in shared_files:
self.assertTrue(
pkg_gfx120X.files.has(relpath),
f"gfx120X-all missing {relpath}",
)
self.assertTrue(
pkg_gfx94X.files.has(relpath),
f"gfx94X-dcgpu missing {relpath}",
)
def test_populate_runtime_files_registers_package(self):
"""Package is appended to params.populated_packages after populate_runtime_files."""
artifact_dir = self.temp_dir / "artifacts"
self._add_artifact(
artifact_dir, "blas", "lib", "gfx120X-all", {"lib/foo.txt": "x"}
)
params = self._make_params(artifact_dir)
self.assertEqual(len(params.populated_packages), 0)
lib = PopulatedDistPackage(
params, logical_name="libraries", target_family="gfx120X-all"
)
lib.populate_runtime_files(
params.filter_artifacts(lambda an: an.name == "blas")
)
self.assertEqual(len(params.populated_packages), 1)
self.assertIs(params.populated_packages[0], lib)
def test_find_populated_searches_across_packages(self):
"""_find_populated locates a file regardless of which package owns it."""
artifact_dir = self.temp_dir / "artifacts"
self._add_artifact(
artifact_dir,
"blas",
"lib",
"gfx120X-all",
{"lib/librocblas.txt": "gfx120X"},
)
self._add_artifact(
artifact_dir,
"blas",
"lib",
"gfx94X-dcgpu",
{"lib/librocsolver.txt": "gfx94X"},
)
params = self._make_params(artifact_dir)
lib1 = PopulatedDistPackage(
params, logical_name="libraries", target_family="gfx120X-all"
)
lib1.populate_runtime_files(
params.filter_artifacts(lambda an: an.target_family == "gfx120X-all")
)
lib2 = PopulatedDistPackage(
params, logical_name="libraries", target_family="gfx94X-dcgpu"
)
lib2.populate_runtime_files(
params.filter_artifacts(lambda an: an.target_family == "gfx94X-dcgpu")
)
# Create a devel package and use it as the search context (as in real use).
devel = PopulatedDistPackage(params, logical_name="devel")
result = devel._find_populated("lib/librocblas.txt")
self.assertIsNotNone(result)
owner, _ = result
self.assertIs(owner, lib1)
result = devel._find_populated("lib/librocsolver.txt")
self.assertIsNotNone(result)
owner, _ = result
self.assertIs(owner, lib2)
self.assertIsNone(devel._find_populated("lib/nonexistent.txt"))
def test_find_soname_alias_searches_across_packages(self):
"""_find_soname_alias finds an alias from any registered package."""
artifact_dir = self.temp_dir / "artifacts"
self._add_artifact(
artifact_dir, "blas", "lib", "gfx120X-all", {"lib/placeholder.txt": "x"}
)
params = self._make_params(artifact_dir)
lib = PopulatedDistPackage(
params, logical_name="libraries", target_family="gfx120X-all"
)
lib.populate_runtime_files(
params.filter_artifacts(lambda an: an.name == "blas")
)
# Inject a soname alias as populate_runtime_files does for real .so symlinks.
lib.files.soname_aliases["lib/librocblas.so"] = "librocblas.so.5"
devel = PopulatedDistPackage(params, logical_name="devel")
self.assertEqual(
devel._find_soname_alias("lib/librocblas.so"), "librocblas.so.5"
)
self.assertIsNone(devel._find_soname_alias("lib/nonexistent.so"))
def test_find_populated_prefers_matching_target_family(self):
"""A target-specific devel package skips runtime pkgs from a different arch.
When gfx120X-all and gfx94X-dcgpu both own the same relpath, a devel
package for gfx94X-dcgpu must return the gfx94X entry, not gfx120X.
"""
artifact_dir = self.temp_dir / "artifacts"
shared_relpath = "lib/librocblas.txt"
self._add_artifact(
artifact_dir,
"blas",
"lib",
"gfx120X-all",
{shared_relpath: "gfx120X rocblas"},
)
self._add_artifact(
artifact_dir,
"blas",
"lib",
"gfx94X-dcgpu",
{shared_relpath: "gfx94X rocblas"},
)
params = self._make_params(artifact_dir)
lib_gfx120 = PopulatedDistPackage(
params, logical_name="libraries", target_family="gfx120X-all"
)
lib_gfx120.populate_runtime_files(
params.filter_artifacts(lambda an: an.target_family == "gfx120X-all")
)
lib_gfx94 = PopulatedDistPackage(
params, logical_name="libraries", target_family="gfx94X-dcgpu"
)
lib_gfx94.populate_runtime_files(
params.filter_artifacts(lambda an: an.target_family == "gfx94X-dcgpu")
)
devel_gfx94 = PopulatedDistPackage(
params, logical_name="devel", target_family="gfx94X-dcgpu"
)
result = devel_gfx94._find_populated(shared_relpath)
self.assertIsNotNone(result)
owner, _ = result
self.assertIs(owner, lib_gfx94, "gfx94X devel must link to gfx94X libraries")
def test_find_populated_falls_back_to_generic_package(self):
"""A target-specific devel package can find files from a generic (core) package.
target_family=None on a package means it is arch-neutral; it must never
be skipped by the target-family filter.
"""
artifact_dir = self.temp_dir / "artifacts"
self._add_artifact(
artifact_dir,
"base",
"lib",
"generic",
{"lib/librocm_core.txt": "core lib"},
)
self._add_artifact(
artifact_dir,
"blas",
"lib",
"gfx94X-dcgpu",
{"lib/librocblas.txt": "gfx94X rocblas"},
)
params = self._make_params(artifact_dir)
# core package: target_family=None (no target family)
core = PopulatedDistPackage(params, logical_name="core")
core.populate_runtime_files(
params.filter_artifacts(lambda an: an.name == "base")
)
lib_gfx94 = PopulatedDistPackage(
params, logical_name="libraries", target_family="gfx94X-dcgpu"
)
lib_gfx94.populate_runtime_files(
params.filter_artifacts(lambda an: an.name == "blas")
)
devel_gfx94 = PopulatedDistPackage(
params, logical_name="devel", target_family="gfx94X-dcgpu"
)
# The core file (generic package) must be found by the gfx94X devel.
result = devel_gfx94._find_populated("lib/librocm_core.txt")
self.assertIsNotNone(result)
owner, _ = result
self.assertIs(
owner,
core,
"core (generic) file must be reachable from arch-specific devel",
)
# ---------------------------------------------------------------------------
# Tests for Parameters construction edge cases
# ---------------------------------------------------------------------------
class ParametersConstructionTest(TmpDirTestCase):
def test_no_arch_specific_artifacts_does_not_crash(self):
# Regression: Parameters.__init__ raised IndexError when all_target_families
# was empty because it did sorted(...)[0] unconditionally.
artifact_dir = self.temp_dir / "artifacts"
artifact_dir.mkdir()
params = Parameters(
dest_dir=self.temp_dir / "packages",
version="0.0.1.test",
version_suffix="",
artifacts=ArtifactCatalog(artifact_dir),
)
self.assertIsNone(params.default_target_family)
# ---------------------------------------------------------------------------
# Tests for kpack-split device packaging
# ---------------------------------------------------------------------------
class DevicePackagingTest(TmpDirTestCase):
"""Tests for kpack-split mode: arch-neutral libraries + per-ISA device wheels."""
def _add_artifact(
self,
artifact_dir: Path,
name: str,
component: str,
target_family: str,
files: dict[str, str],
):
"""Create a minimal artifact directory with the given files under stage/."""
subdir = artifact_dir / f"{name}_{component}_{target_family}"
stage = subdir / "stage"
for relpath, content in files.items():
f = stage / relpath
f.parent.mkdir(parents=True, exist_ok=True)
f.write_text(content)
(subdir / "artifact_manifest.txt").write_text("stage\n")
def _make_params(
self,
artifact_dir: Path,
kpack_split: bool = False,
version: str = "0.0.1.test",
) -> Parameters:
dest_dir = self.temp_dir / "packages"
dest_dir.mkdir(parents=True, exist_ok=True)
return Parameters(
dest_dir=dest_dir,
version=version,
version_suffix="",
artifacts=ArtifactCatalog(artifact_dir),
kpack_split=kpack_split,
)
def _setup_kpack_split_artifacts(self) -> Path:
"""Create a minimal set of generic + per-ISA artifacts."""
artifact_dir = self.temp_dir / "artifacts"
# Generic library artifact (host code)
self._add_artifact(
artifact_dir,
"blas",
"lib",
"generic",
{"lib/librocblas.txt": "host library"},
)
# Per-ISA device artifact
self._add_artifact(
artifact_dir,
"blas",
"lib",
"gfx942",
{
".kpack/blas_lib_gfx942.kpack": "kpack data",
"lib/rocblas/library/Foo_gfx942.co": "kernel object",
},
)
return artifact_dir
def test_kpack_split_libraries_is_arch_neutral(self):
"""kpack_split=True makes the libraries entry non-target-specific."""
artifact_dir = self._setup_kpack_split_artifacts()
params = self._make_params(artifact_dir, kpack_split=True)
libraries_entry = params.dist_info.ALL_PACKAGES["libraries"]
# Should not raise — libraries is no longer target-specific.
dist_name = libraries_entry.get_dist_package_name(target_family=None)
self.assertEqual(dist_name, "rocm-sdk-libraries")
# py_package_name should also work without a target.
py_name = libraries_entry.get_py_package_name(target_family=None)
self.assertTrue(py_name.startswith("_rocm_sdk_libraries"))
def test_kpack_split_libraries_package_creates_without_target(self):
"""Libraries package with target_family=None should not raise in kpack-split."""
artifact_dir = self._setup_kpack_split_artifacts()
params = self._make_params(artifact_dir, kpack_split=True)
# Should not raise.
lib = PopulatedDistPackage(params, logical_name="libraries", target_family=None)
self.assertIsNotNone(lib.path)
def test_kpack_split_libraries_setup_uses_unsuffixed_pure_package(self):
"""setup.py must not turn target_family=None into a package name suffix."""
artifact_dir = self._setup_kpack_split_artifacts()
params = self._make_params(
artifact_dir,
kpack_split=True,
version="0.0.1.dev0",
)
lib = PopulatedDistPackage(params, logical_name="libraries", target_family=None)
result = subprocess.run(
[sys.executable, "setup.py", "--name"],
cwd=lib.path,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
self.assertIn(
"Found packages: ['rocm_sdk_libraries', '_rocm_sdk_libraries']",
result.stdout,
)
self.assertNotIn("rocm_sdk_libraries_None", result.stdout)
def test_populate_device_files_copies_all_files(self):
"""populate_device_files() should copy .kpack and kernel DB files."""
artifact_dir = self._setup_kpack_split_artifacts()
params = self._make_params(artifact_dir, kpack_split=True)
# Verify xnack-suffixed targets produce valid package names by stripping
# the suffix (e.g., 'gfx942:xnack+' -> 'rocm-sdk-device-gfx942')
device_entry = params.dist_info.ALL_PACKAGES["device"]
# Test xnack+ suffix
self.assertEqual(
device_entry.get_dist_package_name("gfx942:xnack+"),
"rocm-sdk-device-gfx942",
)
# Test xnack- suffix
self.assertEqual(
device_entry.get_dist_package_name("gfx942:xnack-"),
"rocm-sdk-device-gfx942",
)
# Test base target without suffix
self.assertEqual(
device_entry.get_dist_package_name("gfx942"),
"rocm-sdk-device-gfx942",
)
# Verify get_dist_package_require also strips xnack suffix
self.assertTrue(
device_entry.get_dist_package_require("gfx942:xnack+").startswith(
"rocm-sdk-device-gfx942=="
),
)
# Verify get_py_package_name also strips xnack suffix
self.assertTrue(
device_entry.get_py_package_name("gfx942:xnack+").startswith(
"_rocm_sdk_device_gfx942"
),
)
dev = PopulatedDistPackage(
params, logical_name="device", target_family="gfx942"
)
dev.populate_device_files(
params.filter_artifacts(
lambda an: an.name == "blas"
and an.component == "lib"
and an.target_family == "gfx942"
)
)
# Both files should be materialized.
self.assertTrue(dev.files.has(".kpack/blas_lib_gfx942.kpack"))
self.assertTrue(dev.files.has("lib/rocblas/library/Foo_gfx942.co"))
# Files should exist on disk in the platform dir.
platform_dir = dev._platform_dir
self.assertTrue((platform_dir / ".kpack" / "blas_lib_gfx942.kpack").exists())
self.assertTrue(
(platform_dir / "lib" / "rocblas" / "library" / "Foo_gfx942.co").exists()
)
def test_populate_device_files_emits_devel_links_manifest(self):
"""Device wheel must ship a `_devel_links` manifest mapping each device
file to its relative hardlink target into the libraries overlay dir.
This manifest is what `rocm-sdk init` consumes to mirror per-ISA device
files (.kpack/.co/.dat/...) into the generic rocm-sdk-devel tree.
"""
artifact_dir = self._setup_kpack_split_artifacts()
params = self._make_params(artifact_dir, kpack_split=True)
dev = PopulatedDistPackage(
params, logical_name="device", target_family="gfx942"
)
dev.populate_device_files(
params.filter_artifacts(
lambda an: an.name == "blas"
and an.component == "lib"
and an.target_family == "gfx942"
)
)
libs_name = dev._platform_dir.name
manifest_path = dev._platform_dir / ".devel_links" / "gfx942.json"
self.assertTrue(
manifest_path.is_file(),
"device wheel must emit a .devel_links/<target>.json manifest",
)
data = json.loads(manifest_path.read_text())
self.assertEqual(data["version"], "0.0.1.test")
links = {entry["relpath"]: entry["target"] for entry in data["links"]}
# Every device file must have an entry with a relative target that
# backtracks out of the devel tree and into the libraries overlay.
self.assertEqual(
links[".kpack/blas_lib_gfx942.kpack"],
f"../../{libs_name}/.kpack/blas_lib_gfx942.kpack",
)
self.assertEqual(
links["lib/rocblas/library/Foo_gfx942.co"],
f"../../../../{libs_name}/lib/rocblas/library/Foo_gfx942.co",
)
# The manifest must not list itself as a device file to link.
self.assertNotIn(".devel_links/gfx942.json", links)
def test_device_platform_dir_overlays_libraries(self):
"""Device package platform dir must match libraries package platform dir name."""
artifact_dir = self._setup_kpack_split_artifacts()
params = self._make_params(artifact_dir, kpack_split=True)
lib = PopulatedDistPackage(params, logical_name="libraries", target_family=None)
dev = PopulatedDistPackage(
params, logical_name="device", target_family="gfx942"
)
# The platform dir names must match for the overlay to work.
self.assertEqual(lib._platform_dir.name, dev._platform_dir.name)
def test_device_artifact_filter(self):
"""device_artifact_filter selects only per-ISA lib artifacts."""
# Import the filter from the build script.
sys.path.insert(0, os.fspath(Path(__file__).parent.parent))
from build_python_packages import device_artifact_filter
from _therock_utils.artifacts import ArtifactName
# Should match per-ISA lib artifact.
an_gfx942 = ArtifactName("blas", "lib", "gfx942")
self.assertTrue(device_artifact_filter("gfx942", an_gfx942))
# rccl is built TARGET_NEUTRAL but BUILD_TOPOLOGY marks it
# target-specific so kpack splits produce per-arch rccl_lib_<arch>
# artifacts that must land in the device wheel.
an_rccl = ArtifactName("rccl", "lib", "gfx942")
self.assertTrue(device_artifact_filter("gfx942", an_rccl))
# hipkernelprovider is target-specific + kpack-split (its rocKE engine ships
# per-arch AOT bundles), so its per-ISA lib artifact must land in the device
# wheel.
an_hkp = ArtifactName("hipkernelprovider", "lib", "gfx942")
self.assertTrue(device_artifact_filter("gfx942", an_hkp))
# Should NOT match generic.
an_generic = ArtifactName("blas", "lib", "generic")
self.assertFalse(device_artifact_filter("gfx942", an_generic))
# Should NOT match wrong ISA.
an_gfx1100 = ArtifactName("blas", "lib", "gfx1100")
self.assertFalse(device_artifact_filter("gfx942", an_gfx1100))
# Should NOT match test component.
an_test = ArtifactName("blas", "test", "gfx942")
self.assertFalse(device_artifact_filter("gfx942", an_test))
# Should NOT match non-library artifact name.
an_core = ArtifactName("core-hip", "lib", "gfx942")
self.assertFalse(device_artifact_filter("gfx942", an_core))
# Should match xnack variant of the same base target (merges into one package).
an_xnack = ArtifactName("blas", "lib", "gfx942:xnack+")
self.assertTrue(device_artifact_filter("gfx942", an_xnack))
# Should NOT match xnack variant of a different base target.
an_xnack_other = ArtifactName("blas", "lib", "gfx950:xnack+")
self.assertFalse(device_artifact_filter("gfx942", an_xnack_other))
def test_core_artifact_filter_includes_only_rocjitsu_hotswap(self):
"""The core wheel ships the HSA hotswap hook without the rocjitsu library."""
sys.path.insert(0, os.fspath(Path(__file__).parent.parent))
from build_python_packages import core_artifact_filter
from _therock_utils.artifacts import ArtifactName
self.assertTrue(
core_artifact_filter(ArtifactName("rocjitsu-hotswap", "lib", "generic"))
)
self.assertFalse(
core_artifact_filter(ArtifactName("rocjitsu", "lib", "generic"))
)
self.assertFalse(
core_artifact_filter(ArtifactName("rocjitsu", "run", "generic"))
)
def test_device_dist_info_has_libraries_py_package_name(self):
"""Device package _dist_info.py must contain LIBRARIES_PY_PACKAGE_NAME."""
artifact_dir = self._setup_kpack_split_artifacts()
params = self._make_params(artifact_dir, kpack_split=True)
dev = PopulatedDistPackage(
params, logical_name="device", target_family="gfx942"
)
dist_info_path = (
dev.path / "src" / dev.entry.pure_py_package_name / "_dist_info.py"
)
content = dist_info_path.read_text()
self.assertIn("LIBRARIES_PY_PACKAGE_NAME", content)
self.assertIn("_rocm_sdk_libraries", content)
class KpackSplitCompletenessTest(TmpDirTestCase):
"""Tests for validating kpack-split artifact coverage before packaging."""
def _add_artifact(
self,
artifact_dir: Path,
name: str,
component: str,
target_family: str,
) -> None:
subdir = artifact_dir / f"{name}_{component}_{target_family}"
stage = subdir / "stage"
stage.mkdir(parents=True, exist_ok=True)
(stage / "placeholder.txt").write_text("x")
(subdir / "artifact_manifest.txt").write_text("stage\n")
def _make_artifact_catalog(self, target_families: list[str]) -> ArtifactCatalog:
artifact_dir = self.temp_dir / "artifacts"
for target_family in target_families:
self._add_artifact(
artifact_dir=artifact_dir,
name="blas",
component="lib",
target_family=target_family,
)
return ArtifactCatalog(artifact_dir)
def _validate_completeness(
self,
*,
kpack_split: bool,
artifacts: ArtifactCatalog,
linux_targets: list[str] | None,
windows_targets: list[str] | None,
platform_name: str,
) -> None:
validate_kpack_split_target_completeness(
kpack_split=kpack_split,
artifact_dir=self.temp_dir / "artifacts",
artifacts=artifacts,
linux_targets=linux_targets,
windows_targets=windows_targets,
platform_name=platform_name,
)
def test_linux_completeness_passes_when_targets_match(self):
artifacts = self._make_artifact_catalog(["gfx1100", "gfx1101"])
self._validate_completeness(
kpack_split=True,
artifacts=artifacts,
linux_targets=["gfx1100", "gfx1101"],
windows_targets=None,
platform_name="linux",
)
def test_linux_completeness_fails_when_target_is_missing(self):
artifacts = self._make_artifact_catalog(["gfx1100"])
with self.assertRaisesRegex(RuntimeError, "gfx1101"):
self._validate_completeness(
kpack_split=True,
artifacts=artifacts,
linux_targets=["gfx1100", "gfx1101"],
windows_targets=None,
platform_name="linux",
)
def test_windows_completeness_uses_windows_targets(self):
artifacts = self._make_artifact_catalog(["gfx1200"])
self._validate_completeness(
kpack_split=True,
artifacts=artifacts,
linux_targets=["gfx1100"],
windows_targets=["gfx1200"],
platform_name="win32",
)
def test_completeness_skips_without_platform_target_input(self):
artifacts = self._make_artifact_catalog(["gfx1100"])
self._validate_completeness(
kpack_split=True,
artifacts=artifacts,
linux_targets=None,
windows_targets=["gfx1200"],
platform_name="linux",
)
def test_completeness_skips_when_kpack_split_is_disabled(self):
artifacts = self._make_artifact_catalog(["gfx1100"])
self._validate_completeness(
kpack_split=False,
artifacts=artifacts,
linux_targets=["gfx1100", "gfx1101"],
windows_targets=None,
platform_name="linux",
)
class RequiredDistPackagesTest(TmpDirTestCase):
"""Tests for validating required files in the final dist directory."""
version = "0.0.1.test"
wheel_tag = "py3-none-linux_x86_64"
def _add_artifact(
self,
artifact_dir: Path,
name: str,
component: str,
target_family: str,
) -> None:
subdir = artifact_dir / f"{name}_{component}_{target_family}"
stage = subdir / "stage"
stage.mkdir(parents=True, exist_ok=True)
(stage / "placeholder.txt").write_text("x")
(subdir / "artifact_manifest.txt").write_text("stage\n")
def _make_artifact_catalog(
self, artifact_specs: list[tuple[str, str, str]]
) -> ArtifactCatalog:
artifact_dir = self.temp_dir / "artifacts"
for name, component, target_family in artifact_specs:
self._add_artifact(
artifact_dir=artifact_dir,
name=name,
component=component,
target_family=target_family,
)
return ArtifactCatalog(artifact_dir)
def _write_dist_file(self, filename: str) -> None:
dist_dir = self.temp_dir / "packages" / "dist"
dist_dir.mkdir(parents=True, exist_ok=True)
(dist_dir / filename).write_text("package")
def _write_required_kpack_split_runtime_files(self, target: str) -> None:
self._write_dist_file(f"rocm-{self.version}.tar.gz")
self._write_dist_file(f"rocm_sdk_core-{self.version}-{self.wheel_tag}.whl")
self._write_dist_file(f"rocm_sdk_libraries-{self.version}-{self.wheel_tag}.whl")
self._write_dist_file(
f"rocm_sdk_device_{target}-{self.version}-{self.wheel_tag}.whl"
)
def _validate_required_dist_packages(
self,
*,
artifacts: ArtifactCatalog,
kpack_split: bool,
linux_targets: list[str] | None,
windows_targets: list[str] | None,
platform_name: str,
) -> None:
sys.path.insert(0, os.fspath(Path(__file__).parent.parent))
from build_python_packages import validate_required_dist_packages
validate_required_dist_packages(
dest_dir=self.temp_dir / "packages",
version=self.version,
artifacts=artifacts,
kpack_split=kpack_split,
linux_targets=linux_targets,
windows_targets=windows_targets,
platform_name=platform_name,
)
def test_required_kpack_split_dist_packages_pass(self):
artifacts = self._make_artifact_catalog([("blas", "lib", "gfx1100")])
self._write_required_kpack_split_runtime_files("gfx1100")
self._validate_required_dist_packages(
artifacts=artifacts,
kpack_split=True,
linux_targets=["gfx1100"],
windows_targets=None,
platform_name="linux",
)
def test_required_dist_packages_fail_when_rocm_sdist_is_missing(self):
artifacts = self._make_artifact_catalog([("blas", "lib", "gfx1100")])
self._write_dist_file(f"rocm_sdk_core-{self.version}-{self.wheel_tag}.whl")
self._write_dist_file(f"rocm_sdk_libraries-{self.version}-{self.wheel_tag}.whl")
self._write_dist_file(
f"rocm_sdk_device_gfx1100-{self.version}-{self.wheel_tag}.whl"
)
with self.assertRaisesRegex(RuntimeError, f"rocm-{self.version}.tar.gz"):
self._validate_required_dist_packages(
artifacts=artifacts,
kpack_split=True,
linux_targets=["gfx1100"],
windows_targets=None,
platform_name="linux",
)
def test_required_dist_packages_fail_when_device_wheel_is_missing(self):
artifacts = self._make_artifact_catalog([("blas", "lib", "gfx1100")])
self._write_dist_file(f"rocm-{self.version}.tar.gz")
self._write_dist_file(f"rocm_sdk_core-{self.version}-{self.wheel_tag}.whl")
self._write_dist_file(f"rocm_sdk_libraries-{self.version}-{self.wheel_tag}.whl")
with self.assertRaisesRegex(RuntimeError, "rocm_sdk_device_gfx1100"):
self._validate_required_dist_packages(
artifacts=artifacts,
kpack_split=True,
linux_targets=["gfx1100"],
windows_targets=None,
platform_name="linux",
)
def test_required_dist_packages_require_devel_when_dev_artifacts_exist(self):
artifacts = self._make_artifact_catalog(
[
("blas", "lib", "gfx1100"),
("core-hip", "dev", "generic"),
]
)
self._write_required_kpack_split_runtime_files("gfx1100")
with self.assertRaisesRegex(RuntimeError, "rocm_sdk_devel"):
self._validate_required_dist_packages(
artifacts=artifacts,
kpack_split=True,
linux_targets=["gfx1100"],
windows_targets=None,
platform_name="linux",
)
def test_required_dist_packages_skip_devel_without_dev_artifacts(self):
artifacts = self._make_artifact_catalog([("blas", "lib", "gfx1100")])
self._write_required_kpack_split_runtime_files("gfx1100")
self._validate_required_dist_packages(
artifacts=artifacts,
kpack_split=True,
linux_targets=["gfx1100"],
windows_targets=None,
platform_name="linux",
)
# ---------------------------------------------------------------------------
# Unit tests for restrict_families (per-family meta package)
# ---------------------------------------------------------------------------
class RestrictFamiliesTest(TmpDirTestCase):
"""Tests for restrict_families=True in PopulatedDistPackage.
These tests verify that per-family meta (rocm) packages bake the correct
DEFAULT_TARGET_FAMILY and AVAILABLE_TARGET_FAMILIES into _dist_info.py.
"""
def _add_artifact(
self,
artifact_dir: Path,
name: str,
component: str,
target_family: str,
):
"""Create a minimal artifact directory (no files needed)."""
subdir = artifact_dir / f"{name}_{component}_{target_family}"
stage = subdir / "stage"
stage.mkdir(parents=True, exist_ok=True)
(subdir / "artifact_manifest.txt").write_text("stage\n")
def _make_params(self, artifact_dir: Path) -> Parameters:
dest_dir = self.temp_dir / "packages"
dest_dir.mkdir(parents=True, exist_ok=True)
return Parameters(
dest_dir=dest_dir,
version="0.0.1.test",
version_suffix="",
artifacts=ArtifactCatalog(artifact_dir),
)
def _exec_dist_info(self, meta: PopulatedDistPackage) -> dict:
"""Read and exec the generated _dist_info.py; return the namespace."""
dist_info_path = (
meta.path / "src" / meta.entry.pure_py_package_name / "_dist_info.py"
)
content = dist_info_path.read_text()
ns: dict = {}
exec(content, ns)
return ns
def _make_two_family_params(self) -> Parameters:
artifact_dir = self.temp_dir / "artifacts"
self._add_artifact(artifact_dir, "base", "lib", "gfx120X-all")
self._add_artifact(artifact_dir, "base", "lib", "gfx94X-dcgpu")
return self._make_params(artifact_dir)
def test_restrict_families_gfx120x_only(self):
"""restrict_families=True limits _dist_info.py to the requested family."""
params = self._make_two_family_params()
meta = PopulatedDistPackage(
params,
logical_name="meta",
target_family="gfx120X-all",
restrict_families=True,
)
ns = self._exec_dist_info(meta)
self.assertEqual(ns["DEFAULT_TARGET_FAMILY"], "gfx120X-all")
self.assertEqual(ns["AVAILABLE_TARGET_FAMILIES"], ["gfx120X-all"])
def test_restrict_families_gfx94x_only(self):
"""restrict_families=True works for the second family as well."""
params = self._make_two_family_params()
meta = PopulatedDistPackage(
params,
logical_name="meta",
target_family="gfx94X-dcgpu",
restrict_families=True,
)
ns = self._exec_dist_info(meta)
self.assertEqual(ns["DEFAULT_TARGET_FAMILY"], "gfx94X-dcgpu")
self.assertEqual(ns["AVAILABLE_TARGET_FAMILIES"], ["gfx94X-dcgpu"])
def test_no_restrict_families_lists_all(self):
"""Without restrict_families, _dist_info.py still lists all built families."""
params = self._make_two_family_params()
meta = PopulatedDistPackage(
params,
logical_name="meta",
target_family="gfx120X-all",
restrict_families=False,
)
ns = self._exec_dist_info(meta)
self.assertIn("gfx120X-all", ns["AVAILABLE_TARGET_FAMILIES"])
self.assertIn("gfx94X-dcgpu", ns["AVAILABLE_TARGET_FAMILIES"])
self.assertEqual(len(ns["AVAILABLE_TARGET_FAMILIES"]), 2)
def test_restrict_families_single_arch_build(self):
"""In a single-arch build restrict_families is a no-op (only one family anyway)."""
artifact_dir = self.temp_dir / "artifacts"
self._add_artifact(artifact_dir, "base", "lib", "gfx120X-all")
params = self._make_params(artifact_dir)
meta = PopulatedDistPackage(
params,
logical_name="meta",
target_family="gfx120X-all",
restrict_families=True,
)
ns = self._exec_dist_info(meta)
self.assertEqual(ns["DEFAULT_TARGET_FAMILY"], "gfx120X-all")
self.assertEqual(ns["AVAILABLE_TARGET_FAMILIES"], ["gfx120X-all"])
def test_restrict_families_ignored_when_target_family_is_none(self):
"""restrict_families=True with target_family=None must not modify families."""
params = self._make_two_family_params()
# This is a degenerate call (meta without a target family) but must not crash
# and must not restrict families (since there is no specific family to restrict to).
meta = PopulatedDistPackage(
params,
logical_name="meta",
target_family=None,
restrict_families=True,
)
ns = self._exec_dist_info(meta)
# Both families must still be present — the guard condition prevented restriction.
self.assertIn("gfx120X-all", ns["AVAILABLE_TARGET_FAMILIES"])
self.assertIn("gfx94X-dcgpu", ns["AVAILABLE_TARGET_FAMILIES"])
self.assertEqual(len(ns["AVAILABLE_TARGET_FAMILIES"]), 2)
def test_restrict_families_no_dead_writes(self):
"""restrict_families=True must not produce dead writes in _dist_info.py.
The generated file must contain no AVAILABLE_TARGET_FAMILIES.clear() and
must not append the non-selected family at all.
"""
params = self._make_two_family_params()
meta = PopulatedDistPackage(
params,
logical_name="meta",
target_family="gfx120X-all",
restrict_families=True,
)
dist_info_path = (
meta.path / "src" / meta.entry.pure_py_package_name / "_dist_info.py"
)
content = dist_info_path.read_text()
self.assertNotIn("AVAILABLE_TARGET_FAMILIES.clear()", content)
self.assertNotIn("gfx94X-dcgpu", content)
def test_dist_info_object_matches_generated_file(self):
"""params.dist_info (in-memory, built via direct attribute assignment
onto the static template) and the _dist_info.py written to disk (built
via repr()-encoded source text) must agree on every user-controlled
field, so the two initialization paths can't silently drift apart the
way they did when a version bug was previously introduced in only one
of the two places.
"""
artifact_dir = self.temp_dir / "artifacts"
self._add_artifact(artifact_dir, "base", "lib", "gfx942")
self._add_artifact(artifact_dir, "base", "lib", "gfx1100")
dest_dir = self.temp_dir / "packages"
dest_dir.mkdir(parents=True, exist_ok=True)
params = Parameters(
dest_dir=dest_dir,
version="7.0.0",
version_suffix="rc1",
artifacts=ArtifactCatalog(artifact_dir),
)
ns: dict = {}
exec(params.dist_info_contents, ns)
self.assertEqual(ns["__version__"], params.dist_info.__version__)
self.assertEqual(
ns["PY_PACKAGE_SUFFIX_NONCE"], params.dist_info.PY_PACKAGE_SUFFIX_NONCE
)
self.assertEqual(
ns["DEFAULT_TARGET_FAMILY"], params.dist_info.DEFAULT_TARGET_FAMILY
)
self.assertEqual(
sorted(ns["AVAILABLE_TARGET_FAMILIES"]),
sorted(params.dist_info.AVAILABLE_TARGET_FAMILIES),
)
meta = PopulatedDistPackage(params, logical_name="meta")
env = os.environ.copy()
env["ROCM_SDK_TARGET_FAMILY"] = "gfx942"
subprocess.run(
[sys.executable, "setup.py", "egg_info"],
cwd=meta.path,
env=env,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
pkg_info = (meta.path / "src" / "rocm.egg-info" / "PKG-INFO").read_text()
self.assertNotIn("Requires-Dist: rocm==7.0.0\n", pkg_info)
self.assertIn("Requires-Dist: rocm-sdk-core==7.0.0\n", pkg_info)
# ---------------------------------------------------------------------------
# Tests for cross-platform family awareness in the rocm sdist
# ---------------------------------------------------------------------------
class CrossPlatformFamiliesTest(TmpDirTestCase):
"""Tests for linux_target_families / windows_target_families kwargs.
When a multi-arch release pipeline knows the full union of GPU targets
across both Linux and Windows builds, the rocm sdist must (a) advertise
that union in AVAILABLE_TARGET_FAMILIES, (b) record each platform's
contribution separately so setup.py can attach sys_platform markers,
(c) pick a DEFAULT_TARGET_FAMILY that resolves on either OS, and
(d) produce identical dist_info_contents on both platforms so the
metadata that drives the published device extras matches regardless
of which platform's job uploaded the sdist last.
Note on naming: the kwargs and the AVAILABLE_TARGET_FAMILIES constant
use the historical "target_family" label, but in kpack-split mode
(the only mode that exhibits this bug) the values are GPU **targets**
like gfx942 / gfx1100, matching the device wheel suffixes the
artifact catalog produces and the `rocm-sdk-device-*` package names
published to the index. Tests use realistic target names accordingly.
The on-disk artifact catalog is irrelevant under these kwargs; tests
use an empty artifact tree except where the on-disk view is itself
under test (backward compat + identity-across-disjoint-artifacts).
"""
def _add_minimal_artifact(self, artifact_dir: Path, target_family: str):
subdir = artifact_dir / f"base_lib_{target_family}"
(subdir / "stage").mkdir(parents=True, exist_ok=True)
(subdir / "artifact_manifest.txt").write_text("stage\n")
def _make_params(
self,
*,
on_disk_families: list[str] | None = None,
linux_target_families: list[str] | None = None,
windows_target_families: list[str] | None = None,
) -> Parameters:
artifact_dir = self.temp_dir / "artifacts"
artifact_dir.mkdir(exist_ok=True)
for tf in on_disk_families or []:
self._add_minimal_artifact(artifact_dir, tf)
dest_dir = self.temp_dir / "packages"
dest_dir.mkdir(parents=True, exist_ok=True)
return Parameters(
dest_dir=dest_dir,
version="0.0.1.test",
version_suffix="",
artifacts=ArtifactCatalog(artifact_dir),
linux_target_families=linux_target_families,
windows_target_families=windows_target_families,
)
def _exec_dist_info(self, params: Parameters) -> dict:
ns: dict = {}
exec(params.dist_info_contents, ns)
return ns
# ----- Backward compat: no kwargs => on-disk artifact view ------------
def test_no_kwargs_uses_on_disk_artifact_view(self):
"""Without the new kwargs, available_target_families reflects the
artifact catalog. Single-platform builds keep their existing
behavior unchanged.
"""
params = self._make_params(on_disk_families=["gfx942", "gfx1100"])
self.assertEqual(
sorted(params.available_target_families),
["gfx1100", "gfx942"],
)
self.assertEqual(params.linux_target_families, [])
self.assertEqual(params.windows_target_families, [])
def test_dist_info_omits_per_platform_appends_when_kwargs_omitted(self):
"""No LINUX/WINDOWS_TARGET_FAMILIES.append() lines emitted when
neither kwarg is passed, so the generated _dist_info.py for
single-platform builds is byte-equivalent to today's.
"""
params = self._make_params(on_disk_families=["gfx942"])
ns = self._exec_dist_info(params)
# Predeclared constants must load as empty lists.
self.assertEqual(ns["LINUX_TARGET_FAMILIES"], [])
self.assertEqual(ns["WINDOWS_TARGET_FAMILIES"], [])
self.assertNotIn("LINUX_TARGET_FAMILIES.append", params.dist_info_contents)
self.assertNotIn("WINDOWS_TARGET_FAMILIES.append", params.dist_info_contents)
# ----- Union semantics ------------------------------------------------
def test_available_target_families_is_sorted_union(self):
"""available_target_families is the sorted union of linux + windows."""
params = self._make_params(
linux_target_families=["gfx942", "gfx950", "gfx1100"],
windows_target_families=["gfx1100", "gfx1102"],
)
self.assertEqual(
params.available_target_families,
["gfx1100", "gfx1102", "gfx942", "gfx950"],
)
def test_kwargs_override_on_disk_view(self):
"""When kwargs are passed, available_target_families ignores the
on-disk artifact list. The kwargs are the source of truth.
"""
params = self._make_params(
on_disk_families=["gfx942"],
linux_target_families=["gfx1100"],
windows_target_families=["gfx1100"],
)
self.assertEqual(params.available_target_families, ["gfx1100"])
def test_per_platform_lists_sorted_and_deduped(self):
"""Each platform's list is sorted and deduped on Parameters."""
params = self._make_params(
linux_target_families=["gfx950", "gfx942", "gfx942"],
windows_target_families=["gfx1100"],
)
self.assertEqual(params.linux_target_families, ["gfx942", "gfx950"])
self.assertEqual(params.windows_target_families, ["gfx1100"])
def test_only_linux_provided_union_equals_linux(self):
"""When only linux is provided, union equals linux and windows is empty."""
params = self._make_params(
linux_target_families=["gfx942", "gfx950"],
)
self.assertEqual(params.available_target_families, ["gfx942", "gfx950"])
self.assertEqual(params.linux_target_families, ["gfx942", "gfx950"])
self.assertEqual(params.windows_target_families, [])
# ----- default_target_family must be cross-platform-safe --------------
# determine_target_family() in _dist_info.py falls back to
# DEFAULT_TARGET_FAMILY when neither ROCM_SDK_TARGET_FAMILY nor
# offload-arch resolves. setup.py then plugs that target into every
# target-specific extras Requires-Dist. If DEFAULT is Linux-only, a
# Windows user without env var or offload-arch fails to resolve
# `rocm-sdk-libraries-{DEFAULT}` because no win_amd64 wheel exists
# for that target. Hence: prefer the intersection.
def test_default_target_family_prefers_intersection(self):
"""DEFAULT must come from the linux ∩ windows intersection when
non-empty, so a user without env var / offload-arch gets a target
that has wheels for their OS.
"""
params = self._make_params(
linux_target_families=["gfx942", "gfx1100", "gfx950"],
windows_target_families=["gfx1100", "gfx1102"],
)
# Intersection = {gfx1100}; that must be the DEFAULT.
self.assertEqual(params.default_target_family, "gfx1100")
def test_default_target_family_avoids_linux_only_alpha_first(self):
"""Discriminating case: alphabetical-first of the union is a
Linux-only target but the intersection points elsewhere. The
intersection must win, not the alphabetical-first.
Configuration is artificial: gfx900 / gfx942 are Linux-only in
practice; the test puts gfx942 in the Windows list purely to
construct a disagreement between sort-order and intersection.
"""
# Union sorted = [gfx900, gfx942] - alpha-first is gfx900 (Linux-only).
# Intersection = [gfx942] - DEFAULT must be gfx942.
params = self._make_params(
linux_target_families=["gfx900", "gfx942"],
windows_target_families=["gfx942"],
)
self.assertEqual(params.default_target_family, "gfx942")
def test_default_target_family_intersection_is_sorted(self):
"""When the intersection has multiple targets, DEFAULT is the
sorted-first of the intersection (deterministic).
"""
params = self._make_params(
linux_target_families=["gfx942", "gfx1100", "gfx1200"],
windows_target_families=["gfx1200", "gfx1100"],
)
# Intersection sorted = [gfx1100, gfx1200] => gfx1100.
self.assertEqual(params.default_target_family, "gfx1100")
def test_default_target_family_falls_back_to_union_when_disjoint(self):
"""When linux and windows lists are disjoint (no cross-platform
target possible), DEFAULT falls back to sorted-first-of-union as
a best-effort. Users on the "wrong" OS for that DEFAULT will need
ROCM_SDK_TARGET_FAMILY or offload-arch.
"""
params = self._make_params(
linux_target_families=["gfx942"],
windows_target_families=["gfx1100"],
)
# No intersection => sorted-union[0] = gfx1100.
self.assertEqual(params.default_target_family, "gfx1100")
def test_default_target_family_from_only_linux(self):
"""Only linux provided: DEFAULT is first of sorted linux list."""
params = self._make_params(
linux_target_families=["gfx950", "gfx942"],
)
self.assertEqual(params.default_target_family, "gfx942")
def test_default_target_family_from_only_windows(self):
"""Only windows provided: DEFAULT is first of sorted windows list."""
params = self._make_params(
windows_target_families=["gfx1200", "gfx1100"],
)
self.assertEqual(params.default_target_family, "gfx1100")
# ----- Generated _dist_info.py shape ----------------------------------
def test_dist_info_exposes_per_platform_constants(self):
"""LINUX_TARGET_FAMILIES / WINDOWS_TARGET_FAMILIES baked into
_dist_info.py so setup.py can inspect them at install time.
"""
params = self._make_params(
linux_target_families=["gfx942", "gfx950"],
windows_target_families=["gfx1100"],
)
ns = self._exec_dist_info(params)
self.assertEqual(sorted(ns["LINUX_TARGET_FAMILIES"]), ["gfx942", "gfx950"])
self.assertEqual(ns["WINDOWS_TARGET_FAMILIES"], ["gfx1100"])
self.assertEqual(
sorted(ns["AVAILABLE_TARGET_FAMILIES"]),
["gfx1100", "gfx942", "gfx950"],
)
def test_dist_info_identical_across_disjoint_artifact_sets(self):
"""Core invariant: same cross-platform inputs produce identical
dist_info_contents on both Linux and Windows machines, even when
their on-disk artifact catalogs are disjoint. Without this, the
last-writer-wins upload of rocm-X.Y.Z.tar.gz silently drops one
platform's targets from the published device extras.
"""
# Linux machine has the full Linux target set on disk.
linux_params = self._make_params(
on_disk_families=["gfx942", "gfx950", "gfx1100"],
linux_target_families=["gfx942", "gfx950", "gfx1100"],
windows_target_families=["gfx1100"],
)
# Windows machine only has Windows-supported targets on disk.
windows_params = self._make_params(
on_disk_families=["gfx1100"],
linux_target_families=["gfx942", "gfx950", "gfx1100"],
windows_target_families=["gfx1100"],
)
self.assertEqual(
linux_params.dist_info_contents,
windows_params.dist_info_contents,
)
# ---------------------------------------------------------------------------
# Tests for platform marker helper
# ---------------------------------------------------------------------------
class PlatformMarkerTest(TmpDirTestCase):
"""Tests for get_target_family_platform_marker() in _dist_info.py.
Called by the rocm setup.py at install time to decide whether a
device-gfx* Requires-Dist needs a PEP 508 sys_platform marker.
Returns the marker string for platform-exclusive families, "" for
cross-platform families or when the per-platform breakdown is
unknown (single-platform builds).
"""
def _make_dist_info(
self,
*,
linux_target_families: list[str] | None,
windows_target_families: list[str] | None,
):
artifact_dir = self.temp_dir / "artifacts"
artifact_dir.mkdir(exist_ok=True)
dest_dir = self.temp_dir / "packages"
dest_dir.mkdir(parents=True, exist_ok=True)
params = Parameters(
dest_dir=dest_dir,
version="0.0.1.test",
version_suffix="",
artifacts=ArtifactCatalog(artifact_dir),
linux_target_families=linux_target_families,
windows_target_families=windows_target_families,
)
return params.dist_info
def test_markers_in_mixed_platform_config(self):
"""In a realistic multi-arch config with Linux-only, cross-platform,
and Windows-only targets, each category gets the right marker.
Mirrors the production case where Linux and Windows ship
overlapping but unequal target sets.
"""
dist_info = self._make_dist_info(
linux_target_families=["gfx942", "gfx1100"],
windows_target_families=["gfx1100", "gfx1102"],
)
# Linux-only target gets a linux marker.
self.assertEqual(
dist_info.get_target_family_platform_marker("gfx942"),
'sys_platform == "linux"',
)
# Windows-only target gets a win32 marker.
self.assertEqual(
dist_info.get_target_family_platform_marker("gfx1102"),
'sys_platform == "win32"',
)
# Cross-platform target has no marker.
self.assertEqual(dist_info.get_target_family_platform_marker("gfx1100"), "")
# Verify xnack-suffixed targets in platform lists are matched by base target.
xnack_dist_info = self._make_dist_info(
linux_target_families=["gfx942:xnack+", "gfx1100"],
windows_target_families=["gfx1100"],
)
self.assertEqual(
xnack_dist_info.get_target_family_platform_marker("gfx942"),
'sys_platform == "linux"',
)
def test_no_marker_when_per_platform_lists_unknown(self):
"""Single-platform builds don't pass the new kwargs; no markers
are added, so existing (non-multi-arch) sdists are unchanged.
"""
dist_info = self._make_dist_info(
linux_target_families=None, windows_target_families=None
)
self.assertEqual(dist_info.get_target_family_platform_marker("gfx942"), "")
self.assertEqual(dist_info.get_target_family_platform_marker("gfx1100"), "")
def test_no_marker_when_only_one_platform_participates(self):
"""When only one platform's families are declared (the other side
is skipped in a multi-arch run, or a Linux-only build flows through
the cross-platform kwargs), markers are not attached: there is no
cross-platform variant to disambiguate from.
"""
# Linux declared, Windows not.
dist_info = self._make_dist_info(
linux_target_families=["gfx942", "gfx1100"],
windows_target_families=None,
)
self.assertEqual(dist_info.get_target_family_platform_marker("gfx942"), "")
self.assertEqual(dist_info.get_target_family_platform_marker("gfx1100"), "")
# Windows declared, Linux not.
dist_info = self._make_dist_info(
linux_target_families=None,
windows_target_families=["gfx1100", "gfx1102"],
)
self.assertEqual(dist_info.get_target_family_platform_marker("gfx1100"), "")
self.assertEqual(dist_info.get_target_family_platform_marker("gfx1102"), "")
# ---------------------------------------------------------------------------
# Tests for per-target device extras builder
# ---------------------------------------------------------------------------
class PerTargetExtrasTest(TmpDirTestCase):
"""Tests for build_per_target_extras() in _dist_info.py.
The helper produces the device-gfx* and device-all entries for the
rocm meta sdist's EXTRAS_REQUIRE. Cross-platform-aware: targets
listed only in LINUX_TARGET_FAMILIES or only in WINDOWS_TARGET_FAMILIES
get a sys_platform marker so `pip install rocm[device-all]` resolves
only to wheels actually published for the user's OS.
All tests run with kpack_split=True to mirror the multi-arch release
pipeline (the only mode that exhibits the cross-platform divergence).
In legacy mode the libraries package is also target-specific and the
helper would additionally emit libraries-{target} extras; that path
is not exercised here.
"""
def _make_dist_info(
self,
*,
linux_target_families: list[str] | None,
windows_target_families: list[str] | None,
):
artifact_dir = self.temp_dir / "artifacts"
artifact_dir.mkdir(exist_ok=True)
dest_dir = self.temp_dir / "packages"
dest_dir.mkdir(parents=True, exist_ok=True)
params = Parameters(
dest_dir=dest_dir,
version="0.0.1.test",
version_suffix="",
artifacts=ArtifactCatalog(artifact_dir),
kpack_split=True,
linux_target_families=linux_target_families,
windows_target_families=windows_target_families,
)
return params.dist_info
def test_empty_when_single_target(self):
"""No per-target extras for distributions with one target (legacy)."""
dist_info = self._make_dist_info(
linux_target_families=["gfx942"],
windows_target_families=["gfx942"],
)
self.assertEqual(dist_info.build_per_target_extras(), {})
def test_extras_emitted_for_mixed_platform_config(self):
"""In a realistic multi-arch config, the helper emits one
device-{target} entry per available target, plus a device-all
aggregating them. Linux-only and Windows-only targets carry
sys_platform markers; cross-platform targets do not.
"""
dist_info = self._make_dist_info(
linux_target_families=["gfx942", "gfx1100"],
windows_target_families=["gfx1100", "gfx1102"],
)
extras = dist_info.build_per_target_extras()
# One entry per target plus the aggregate.
self.assertEqual(
sorted(extras.keys()),
sorted(
[
"device-gfx1100",
"device-gfx1102",
"device-gfx942",
"device-all",
]
),
)
# Linux-only target carries the linux marker.
self.assertTrue(
extras["device-gfx942"][0].endswith('; sys_platform == "linux"'),
f"Expected linux marker on Linux-only target, got: "
f"{extras['device-gfx942'][0]}",
)
# Windows-only target carries the win32 marker.
self.assertTrue(
extras["device-gfx1102"][0].endswith('; sys_platform == "win32"'),
f"Expected win32 marker on Windows-only target, got: "
f"{extras['device-gfx1102'][0]}",
)
# Cross-platform target has no marker.
self.assertNotIn(";", extras["device-gfx1100"][0])
self.assertNotIn("sys_platform", extras["device-gfx1100"][0])
# device-all aggregates every per-target requirement verbatim.
self.assertEqual(
sorted(extras["device-all"]),
sorted(
extras["device-gfx942"]
+ extras["device-gfx1100"]
+ extras["device-gfx1102"]
),
)
# Verify xnack-suffixed targets produce valid extra names by stripping
# the suffix (e.g., 'device-gfx942' not 'device-gfx942:xnack+')
xnack_dist_info = self._make_dist_info(
linux_target_families=["gfx942:xnack+", "gfx1100"],
windows_target_families=["gfx1100"],
)
xnack_extras = xnack_dist_info.build_per_target_extras()
self.assertIn("device-gfx942", xnack_extras)
self.assertNotIn("device-gfx942:xnack+", xnack_extras)
xnack_req = xnack_extras["device-gfx942"][0]
self.assertTrue(
xnack_req.startswith("rocm-sdk-device-gfx942=="),
f"Expected stripped package name in requirement, got: {xnack_req}",
)
def test_no_markers_when_per_platform_lists_unknown(self):
"""Without per-platform kwargs (single-platform builds), no markers
attach so existing single-platform sdists stay unchanged.
"""
# Populate two targets via the artifact catalog so the helper
# actually emits per-target extras.
artifact_dir = self.temp_dir / "artifacts"
for t in ("gfx942", "gfx1100"):
subdir = artifact_dir / f"base_lib_{t}"
(subdir / "stage").mkdir(parents=True, exist_ok=True)
(subdir / "artifact_manifest.txt").write_text("stage\n")
dest_dir = self.temp_dir / "packages"
dest_dir.mkdir(parents=True, exist_ok=True)
params = Parameters(
dest_dir=dest_dir,
version="0.0.1.test",
version_suffix="",
artifacts=ArtifactCatalog(artifact_dir),
kpack_split=True,
)
extras = params.dist_info.build_per_target_extras()
for extra_name, requires in extras.items():
for req in requires:
self.assertNotIn(
"sys_platform",
req,
f"Single-platform build leaked a marker into {extra_name}: {req}",
)
def test_requires_dist_pins_version_and_uses_target_in_name(self):
"""Requires-Dist strings start with 'rocm-sdk-device-{target}==<ver>',
matching the canonical PackageEntry.get_dist_package_require() shape.
"""
dist_info = self._make_dist_info(
linux_target_families=["gfx942", "gfx1100"],
windows_target_families=["gfx1100"],
)
extras = dist_info.build_per_target_extras()
req = extras["device-gfx942"][0]
self.assertTrue(
req.startswith("rocm-sdk-device-gfx942==0.0.1.test"),
f"Unexpected Requires-Dist shape: {req}",
)
# ---------------------------------------------------------------------------
# Regression tests for AIPROFSYST-669: rocm-profiler wheel silently dropped
# libprofiler-hub.so*
# ---------------------------------------------------------------------------
class ProfilerWheelLibprofilerHubTest(TmpDirTestCase):
"""rocprofiler-systems' ProfilerHub.cmake vendors profiler-hub as a
runtime .so dependency (NEEDED libprofiler-hub.so.0). It stages into the
same lib/ dir as librocprof-sys*, but PROFILER_WHEEL_INCLUDES never
listed it, so it was silently dropped when the rocm-profiler wheel was
assembled from an otherwise-correct artifact - breaking every rocprof-sys
tool at load time.
"""
def _add_artifact(
self,
artifact_dir: Path,
name: str,
component: str,
target_family: str,
files: dict[str, str],
):
subdir = artifact_dir / f"{name}_{component}_{target_family}"
stage = subdir / "stage"
for relpath, content in files.items():
f = stage / relpath
f.parent.mkdir(parents=True, exist_ok=True)
f.write_text(content)
(subdir / "artifact_manifest.txt").write_text("stage\n")
def _make_params(self, artifact_dir: Path) -> Parameters:
dest_dir = self.temp_dir / "packages"
dest_dir.mkdir(parents=True, exist_ok=True)
return Parameters(
dest_dir=dest_dir,
version="0.0.1.test",
version_suffix="",
artifacts=ArtifactCatalog(artifact_dir),
)
def test_profiler_wheel_includes_libprofiler_hub(self):
"""libprofiler-hub.so* staged inside the rocprofiler-systems artifact
must be selected into the profiler wheel, same as librocprof-sys*.
"""
from build_python_packages import (
PROFILER_WHEEL_INCLUDES,
profiler_artifact_filter,
)
artifact_dir = self.temp_dir / "artifacts"
self._add_artifact(
artifact_dir,
"rocprofiler-systems",
"lib",
"generic",
{
"lib/librocprof-sys.so.1": "rocprof-sys runtime",
"lib/libprofiler-hub.so.0": "profiler-hub runtime dependency",
},
)
params = self._make_params(artifact_dir)
profiler_artifacts = params.filter_artifacts(
profiler_artifact_filter,
includes=PROFILER_WHEEL_INCLUDES,
)
profiler = PopulatedDistPackage(params, logical_name="profiler")
profiler.populate_runtime_files(profiler_artifacts)
self.assertTrue(
profiler.files.has("lib/libprofiler-hub.so.0"),
"libprofiler-hub.so.0 was dropped from the profiler wheel "
"(AIPROFSYST-669 regression)",
)
self.assertTrue(profiler.files.has("lib/librocprof-sys.so.1"))
def test_profiler_wheel_excludes_unrelated_lib_files(self):
"""PROFILER_WHEEL_INCLUDES is a targeted allowlist, not a bare lib/**
catch-all - an unrelated file must not sneak into the profiler wheel.
"""
from build_python_packages import (
PROFILER_WHEEL_INCLUDES,
profiler_artifact_filter,
)
artifact_dir = self.temp_dir / "artifacts"
self._add_artifact(
artifact_dir,
"rocprofiler-systems",
"lib",
"generic",
{
"lib/libprofiler-hub.so.0": "profiler-hub runtime dependency",
"lib/libunrelated-dependency.so.1": "should not be selected",
},
)
params = self._make_params(artifact_dir)
profiler_artifacts = params.filter_artifacts(
profiler_artifact_filter,
includes=PROFILER_WHEEL_INCLUDES,
)
profiler = PopulatedDistPackage(params, logical_name="profiler")
profiler.populate_runtime_files(profiler_artifacts)
self.assertTrue(profiler.files.has("lib/libprofiler-hub.so.0"))
self.assertFalse(profiler.files.has("lib/libunrelated-dependency.so.1"))
class EnsureProfilerLibrarySymlinksTest(unittest.TestCase):
"""Unit tests for ensure_profiler_library_symlinks() in isolation - no
real ELF binaries or ArtifactCatalog machinery needed, since it only
walks profiler.platform_dir / "lib" by filename pattern.
"""
def setUp(self):
self.temp_context = tempfile.TemporaryDirectory()
self.platform_dir = Path(self.temp_context.name)
self.lib_dir = self.platform_dir / "lib"
self.lib_dir.mkdir(parents=True)
def tearDown(self):
self.temp_context.cleanup()
def _fake_profiler(self):
import types
return types.SimpleNamespace(platform_dir=self.platform_dir)
def test_creates_unversioned_symlink_for_libprofiler_hub(self):
from build_python_packages import ensure_profiler_library_symlinks
(self.lib_dir / "libprofiler-hub.so.0").write_text("fake soname file")
ensure_profiler_library_symlinks(self._fake_profiler())
link = self.lib_dir / "libprofiler-hub.so"
self.assertTrue(link.is_symlink())
self.assertEqual(os.readlink(link), "libprofiler-hub.so.0")
def test_still_creates_unversioned_symlink_for_librocprof_sys(self):
"""Regression guard: extending the glob to cover libprofiler-hub must
not break the existing librocprof-sys* symlink behavior.
"""
from build_python_packages import ensure_profiler_library_symlinks
(self.lib_dir / "librocprof-sys.so.1").write_text("fake soname file")
ensure_profiler_library_symlinks(self._fake_profiler())
link = self.lib_dir / "librocprof-sys.so"
self.assertTrue(link.is_symlink())
self.assertEqual(os.readlink(link), "librocprof-sys.so.1")
def test_does_not_overwrite_existing_symlink(self):
"""A prior run (or another mechanism) may have already created the
unversioned symlink - don't clobber it, even if it happens to point
at a different (but real) target than we'd have picked.
"""
from build_python_packages import ensure_profiler_library_symlinks
(self.lib_dir / "libprofiler-hub.so.0").write_text("fake soname file")
(self.lib_dir / "libprofiler-hub.so.99").write_text("a different target")
(self.lib_dir / "libprofiler-hub.so").symlink_to("libprofiler-hub.so.99")
ensure_profiler_library_symlinks(self._fake_profiler())
link = self.lib_dir / "libprofiler-hub.so"
self.assertEqual(os.readlink(link), "libprofiler-hub.so.99")
if __name__ == "__main__":
unittest.main()