From 5337a065f1ccf9a19997a14624de957840a8f0b6 Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Wed, 22 Jul 2026 12:00:00 -0500 Subject: [PATCH 01/35] fix(amdsmi): restore amd-smi-lib dependency on the tests package - The tests package ships amdsmitst, which dynamically links libamd_smi.so from the main amd-smi-lib package. - generic_package() sets the tests package to depend on amd-smi-lib, but the packaging rework overwrote that value with the system runtime deps, dropping the amd-smi-lib dependency entirely. - Result: amd-smi-lib-tests could install without the library, and amdsmitst would fail at runtime with a missing libamd_smi.so. - Keep amd-smi-lib first and append the runtime deps for both DEB and RPM. Signed-off-by: Maisam Arif --- projects/amdsmi/CMakeLists.txt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/projects/amdsmi/CMakeLists.txt b/projects/amdsmi/CMakeLists.txt index 26339c26bd2d..153e355445ed 100644 --- a/projects/amdsmi/CMakeLists.txt +++ b/projects/amdsmi/CMakeLists.txt @@ -481,7 +481,11 @@ set(CPACK_DEBIAN_DEV_PACKAGE_RECOMMENDS ${CPACK_DEBIAN_PACKAGE_RECOMMENDS}) set(CPACK_DEBIAN_PACKAGE_DEPENDS "libdrm-dev, sudo, libc6, python3 (>= 3.6.8)") set(CPACK_DEBIAN_ASAN_PACKAGE_DEPENDS ${CPACK_DEBIAN_PACKAGE_DEPENDS}) set(CPACK_DEBIAN_DEV_PACKAGE_DEPENDS ${CPACK_DEBIAN_PACKAGE_DEPENDS}) -set(CPACK_DEBIAN_TESTS_PACKAGE_DEPENDS ${CPACK_DEBIAN_PACKAGE_DEPENDS}) +# The tests package ships amdsmitst, which dynamically links libamd_smi.so from +# the main package. Keep amd-smi-lib (set by generic_package()) as the first +# dependency and append the runtime deps, so the tests package is never +# installable without the library it links against. +set(CPACK_DEBIAN_TESTS_PACKAGE_DEPENDS "${CPACK_PACKAGE_NAME}, ${CPACK_DEBIAN_PACKAGE_DEPENDS}") # $CURRENT_YEAR is used by copyright.in string(TIMESTAMP CURRENT_YEAR "%Y") @@ -590,7 +594,10 @@ message(STATUS "amdsmi RPM python interpreter dependency: ${_amdsmi_py_req}") set(CPACK_RPM_PACKAGE_REQUIRES "libdrm-devel, sudo, ${_amdsmi_py_req}") set(CPACK_RPM_DEV_PACKAGE_REQUIRES ${CPACK_RPM_PACKAGE_REQUIRES}) set(CPACK_RPM_ASAN_PACKAGE_REQUIRES ${CPACK_RPM_PACKAGE_REQUIRES}) -set(CPACK_RPM_TESTS_PACKAGE_REQUIRES ${CPACK_RPM_PACKAGE_REQUIRES}) +# amdsmitst dynamically links libamd_smi.so; keep amd-smi-lib (set by +# generic_package()) first and append the runtime requires so the tests RPM +# cannot install without the library it links against. +set(CPACK_RPM_TESTS_PACKAGE_REQUIRES "${CPACK_PACKAGE_NAME}, ${CPACK_RPM_PACKAGE_REQUIRES}") # don't terminate if bytecompile of python files fails set(CPACK_RPM_SPEC_MORE_DEFINE "%define _python_bytecompile_errors_terminate_build 0") From d8a237580214df8c51c8cba97826d77341e71264 Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Wed, 22 Jul 2026 12:00:58 -0500 Subject: [PATCH 02/35] test(amdsmi): install disable_system_fallback.py with the tests - test_abi_compat.py verifies the wheel loader refuses to fall back to a system libamd_smi.so, which is the wheel's core safety property. - Those tests invoke tools/disable_system_fallback.py, resolved relative to the shared install prefix in the installed layout. - The tool was never installed, so in CI the tests silently skipped and the fallback-disabled behavior went unverified. - Install the tool under share/amd_smi/tools/ with the tests component. Signed-off-by: Maisam Arif --- projects/amdsmi/tests/python/CMakeLists.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/projects/amdsmi/tests/python/CMakeLists.txt b/projects/amdsmi/tests/python/CMakeLists.txt index 5246e2bf5802..ef9785381ce5 100644 --- a/projects/amdsmi/tests/python/CMakeLists.txt +++ b/projects/amdsmi/tests/python/CMakeLists.txt @@ -18,3 +18,14 @@ install( FILES_MATCHING PATTERN "*.py" ) + +# test_abi_compat.py exercises the wheel loader's system-fallback guard by +# invoking tools/disable_system_fallback.py. In the installed layout the test +# resolves that tool relative to SHARE_INSTALL_PREFIX (its REPO_ROOT), so it +# must be shipped alongside the tests -- otherwise the fallback-disabled tests +# silently skip and the wheel's core safety property goes unverified in CI. +install( + PROGRAMS ${PROJECT_SOURCE_DIR}/tools/disable_system_fallback.py + DESTINATION ${SHARE_INSTALL_PREFIX}/tools/ + COMPONENT ${TESTS_COMPONENT} +) From 9b4f4fe2d028dcd9631e79d49277377315fba233 Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Wed, 22 Jul 2026 12:05:18 -0500 Subject: [PATCH 03/35] test(amdsmi): run the SONAME conflict check in CI and the build harness - run_amdsmi_pkg_conflict_test.py asserts the system libamd_smi.so and the wheel libamd_smi_python.so keep distinct SONAMEs, so a process with both cannot double-load one library and crash in static init. - The check ran nowhere: no workflow or harness invoked it, so the invariant was unverified. - Add a --build-root option so the libraries can be located in an out-of-tree build (the wheel job builds into /tmp/amdsmi-build); the system library falls back to the build tree when no rpm/deb is installed. - Gate it in the manylinux wheel workflow, where both libraries are built. - Run it in run_amdsmi_build.py after install, skipping cleanly when no wheel library was built. Signed-off-by: Maisam Arif --- .github/workflows/amdsmi-manylinux-build.yml | 6 +++ .../tests/amdsmi_build/run_amdsmi_build.py | 35 +++++++++++++++++ .../tests/run_amdsmi_pkg_conflict_test.py | 39 +++++++++++++++---- 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/.github/workflows/amdsmi-manylinux-build.yml b/.github/workflows/amdsmi-manylinux-build.yml index b402c8ad22f7..386166072066 100644 --- a/.github/workflows/amdsmi-manylinux-build.yml +++ b/.github/workflows/amdsmi-manylinux-build.yml @@ -94,6 +94,12 @@ jobs: exit 1 fi + - name: Verify system/wheel SONAMEs are distinct + run: | + ${{matrix.target.python-bin}} \ + "$GITHUB_WORKSPACE/projects/amdsmi/tests/run_amdsmi_pkg_conflict_test.py" \ + --build-root /tmp/amdsmi-build + - name: Smoke test (install + import) run: | ${{matrix.target.python-bin}} -m pip install --upgrade pip diff --git a/projects/amdsmi/tests/amdsmi_build/run_amdsmi_build.py b/projects/amdsmi/tests/amdsmi_build/run_amdsmi_build.py index 629603a6cc0e..37a658a2e733 100755 --- a/projects/amdsmi/tests/amdsmi_build/run_amdsmi_build.py +++ b/projects/amdsmi/tests/amdsmi_build/run_amdsmi_build.py @@ -1004,6 +1004,27 @@ def verify_wheel_site_packages(cfg: "RunnerConfig") -> None: ) +def verify_soname_distinct(cfg: "RunnerConfig") -> None: + """Assert the system and wheel libraries keep distinct SONAMEs. + + Runs the standalone SONAME conflict check against the build tree. Only + meaningful when the wheel library was built (BUILD_PYTHON_WHEEL=ON), so a + build tree without libamd_smi_python.so is skipped rather than failed. + """ + wheel_libs = list(cfg.build_dir.glob("**/libamd_smi_python.so.*")) + if not wheel_libs: + print("Skipping SONAME conflict check: no wheel library in the build tree") + return + + test_script = cfg.project_dir / "tests" / "run_amdsmi_pkg_conflict_test.py" + run_command( + ["python3", str(test_script), "--build-root", str(cfg.build_dir)], + name="pkg-conflict-soname", + retries=1, + log_dir=cfg.log_dir, + ) + + @dataclass class RunnerConfig: project_dir: Path @@ -1366,6 +1387,20 @@ def main() -> None: report_and_raise("VERIFY WHEEL", exc) _write_result(cfg.test_results_dir, "verify_wheel_result.txt", "VERIFY WHEEL PASSED") + # 8. SONAME distinctness (system vs wheel library) + if not cfg.skip_install: + try: + verify_soname_distinct(cfg) + except CommandError as exc: + _write_result( + cfg.test_results_dir, + "pkg_conflict_result.txt", + f"SONAME CHECK FAILED: {exc.name} exited {exc.code}\n\n" + f"Log ({exc.log_path}):\n{read_log(exc.log_path)}", + ) + report_and_raise("SONAME CHECK", exc) + _write_result(cfg.test_results_dir, "pkg_conflict_result.txt", "SONAME CHECK PASSED") + print("AMDSMI workflow complete") diff --git a/projects/amdsmi/tests/run_amdsmi_pkg_conflict_test.py b/projects/amdsmi/tests/run_amdsmi_pkg_conflict_test.py index dc8b9be32975..c82dd776fd5d 100644 --- a/projects/amdsmi/tests/run_amdsmi_pkg_conflict_test.py +++ b/projects/amdsmi/tests/run_amdsmi_pkg_conflict_test.py @@ -39,6 +39,7 @@ /.../libamd_smi_python.so. (wheel) """ +import argparse import os import re import shutil @@ -78,33 +79,55 @@ def _rocm_lib_dir() -> Path: return Path(rocm_path) / "lib" -def find_system_lib() -> Path: +def find_system_lib(build_root: Path) -> Path: + # Prefer the installed system library (rpm/deb layout). When it is absent + # -- e.g. the manylinux wheel job builds both libraries but installs no + # system package -- fall back to the build tree, which holds the same + # libamd_smi.so. the package would ship. lib_dir = _rocm_lib_dir() candidates = sorted( (c for c in lib_dir.glob("libamd_smi.so.*") if not c.is_symlink()), key=_version_key ) if not candidates: - sys.exit(f"System libamd_smi.so not found under {lib_dir}; install the rpm/deb first.") + candidates = sorted( + (c for c in build_root.glob("**/libamd_smi.so.*") if not c.is_symlink()), + key=_version_key, + ) + if not candidates: + sys.exit( + f"System libamd_smi.so not found under {lib_dir} or the build tree " + f"({build_root}); install the rpm/deb or build first." + ) return candidates[-1] -def find_wheel_lib(repo_root: Path) -> Path: +def find_wheel_lib(build_root: Path) -> Path: candidates = sorted( - (c for c in repo_root.glob("**/libamd_smi_python.so.*") if not c.is_symlink()), + (c for c in build_root.glob("**/libamd_smi_python.so.*") if not c.is_symlink()), key=_version_key, ) if not candidates: sys.exit( - "libamd_smi_python.so not found under the build tree; " + f"libamd_smi_python.so not found under {build_root}; " "build with -DBUILD_PYTHON_WHEEL=ON first." ) return candidates[-1] def main() -> int: - repo_root = Path(__file__).resolve().parent.parent - sys_lib = find_system_lib() - wheel_lib = find_wheel_lib(repo_root) + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--build-root", + type=Path, + default=Path(__file__).resolve().parent.parent, + help="Directory to search for the built libraries " + "(default: the amdsmi project root; use e.g. /tmp/amdsmi-build when the " + "wheel was built out-of-tree).", + ) + args = parser.parse_args() + build_root = args.build_root.resolve() + sys_lib = find_system_lib(build_root) + wheel_lib = find_wheel_lib(build_root) sys_son = soname(sys_lib) wheel_son = soname(wheel_lib) From e1bf1df0dac6b4f33647433f64f572ba20523ce8 Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Wed, 22 Jul 2026 12:09:46 -0500 Subject: [PATCH 04/35] test(amdsmi): guard against drift between the two installed module copies - The system DEB/RPM installs the amdsmi module twice: into site-packages and under share/amd_smi. Both copies are required -- TheRock ships only /opt/rocm and cannot reach the /usr site-packages tree, so a redirector or symlink is not viable. - Two independent copies can drift (partial upgrade, stray edit), after which import resolves to a different version depending on sys.path order. - Add run_amdsmi_dual_copy_test.py, which asserts the two copies are byte-identical, and run it from the build harness after install. - Add unit tests for the comparison logic so the guard itself is verified without an installed package or GPU. Signed-off-by: Maisam Arif --- .../tests/amdsmi_build/run_amdsmi_build.py | 36 +++++ .../tests/python/test_dual_copy_guard.py | 103 ++++++++++++++ .../amdsmi/tests/run_amdsmi_dual_copy_test.py | 127 ++++++++++++++++++ 3 files changed, 266 insertions(+) create mode 100644 projects/amdsmi/tests/python/test_dual_copy_guard.py create mode 100644 projects/amdsmi/tests/run_amdsmi_dual_copy_test.py diff --git a/projects/amdsmi/tests/amdsmi_build/run_amdsmi_build.py b/projects/amdsmi/tests/amdsmi_build/run_amdsmi_build.py index 37a658a2e733..417dedf0c90e 100755 --- a/projects/amdsmi/tests/amdsmi_build/run_amdsmi_build.py +++ b/projects/amdsmi/tests/amdsmi_build/run_amdsmi_build.py @@ -1025,6 +1025,28 @@ def verify_soname_distinct(cfg: "RunnerConfig") -> None: ) +def verify_dual_copy(cfg: "RunnerConfig") -> None: + """Assert the system package's two module copies are byte-identical. + + The DEB/RPM installs amdsmi into both site-packages and share/amd_smi. Only + runs when the share/amd_smi copy exists (system package installed), skipping + on a wheel-only install where there is a single copy. + """ + rocm_path = os.environ.get("ROCM_PATH") or os.environ.get("ROCM_HOME") or "/opt/rocm" + share_copy = Path(rocm_path) / "share" / "amd_smi" / "amdsmi" + if not share_copy.is_dir(): + print(f"Skipping dual-copy check: {share_copy} not present") + return + + test_script = cfg.project_dir / "tests" / "run_amdsmi_dual_copy_test.py" + run_command( + ["python3", str(test_script)], + name="dual-copy-guard", + retries=1, + log_dir=cfg.log_dir, + ) + + @dataclass class RunnerConfig: project_dir: Path @@ -1401,6 +1423,20 @@ def main() -> None: report_and_raise("SONAME CHECK", exc) _write_result(cfg.test_results_dir, "pkg_conflict_result.txt", "SONAME CHECK PASSED") + # 9. Dual-copy drift guard (system package installs the module twice) + if not cfg.skip_install: + try: + verify_dual_copy(cfg) + except CommandError as exc: + _write_result( + cfg.test_results_dir, + "dual_copy_result.txt", + f"DUAL COPY CHECK FAILED: {exc.name} exited {exc.code}\n\n" + f"Log ({exc.log_path}):\n{read_log(exc.log_path)}", + ) + report_and_raise("DUAL COPY CHECK", exc) + _write_result(cfg.test_results_dir, "dual_copy_result.txt", "DUAL COPY CHECK PASSED") + print("AMDSMI workflow complete") diff --git a/projects/amdsmi/tests/python/test_dual_copy_guard.py b/projects/amdsmi/tests/python/test_dual_copy_guard.py new file mode 100644 index 000000000000..b1e77f0b277e --- /dev/null +++ b/projects/amdsmi/tests/python/test_dual_copy_guard.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +# +# Copyright (C) Advanced Micro Devices. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Unit tests for the dual-copy drift guard's compare_trees logic. + +Exercises the comparison used by run_amdsmi_dual_copy_test.py against synthetic +trees, so the guard is proven to catch drift without needing an installed +package or GPU. +""" + +import importlib.util +import tempfile +import unittest +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def _load_guard(): + # In the source tree the guard lives at tests/run_amdsmi_dual_copy_test.py; + # in the installed tests layout REPO_ROOT is share/amd_smi and it lives at + # tests/run_amdsmi_dual_copy_test.py there too. + for cand in ( + REPO_ROOT / "tests" / "run_amdsmi_dual_copy_test.py", + REPO_ROOT / "run_amdsmi_dual_copy_test.py", + ): + if cand.is_file(): + spec = importlib.util.spec_from_file_location("amdsmi_dual_copy_guard", cand) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + raise unittest.SkipTest("run_amdsmi_dual_copy_test.py not found") + + +def _write(root: Path, rel: str, text: str) -> None: + path = root / rel + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + + +class CompareTreesTest(unittest.TestCase): + def setUp(self): + self.guard = _load_guard() + self._tmp = tempfile.TemporaryDirectory() + self.base = Path(self._tmp.name) + self.a = self.base / "a" + self.b = self.base / "b" + + def tearDown(self): + self._tmp.cleanup() + + def test_identical_trees_report_no_drift(self): + _write(self.a, "amdsmi_interface.py", "x = 1\n") + _write(self.b, "amdsmi_interface.py", "x = 1\n") + missing, differing = self.guard.compare_trees(self.a, self.b) + self.assertEqual(missing, []) + self.assertEqual(differing, []) + + def test_differing_contents_are_flagged(self): + _write(self.a, "amdsmi_interface.py", "x = 1\n") + _write(self.b, "amdsmi_interface.py", "x = 2\n") + missing, differing = self.guard.compare_trees(self.a, self.b) + self.assertEqual(missing, []) + self.assertIn("amdsmi_interface.py", differing) + + def test_missing_file_is_flagged(self): + _write(self.a, "amdsmi_interface.py", "x = 1\n") + _write(self.a, "extra.py", "y = 3\n") + _write(self.b, "amdsmi_interface.py", "x = 1\n") + missing, differing = self.guard.compare_trees(self.a, self.b) + self.assertIn("extra.py", missing) + self.assertEqual(differing, []) + + def test_non_python_files_are_ignored(self): + _write(self.a, "amdsmi_interface.py", "x = 1\n") + _write(self.b, "amdsmi_interface.py", "x = 1\n") + # a bundled .so differing between trees must not count as drift + (self.a / "libamd_smi_python.so").write_bytes(b"AAAA") + missing, differing = self.guard.compare_trees(self.a, self.b) + self.assertEqual(missing, []) + self.assertEqual(differing, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/projects/amdsmi/tests/run_amdsmi_dual_copy_test.py b/projects/amdsmi/tests/run_amdsmi_dual_copy_test.py new file mode 100644 index 000000000000..de1398372742 --- /dev/null +++ b/projects/amdsmi/tests/run_amdsmi_dual_copy_test.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# +# Copyright (C) Advanced Micro Devices. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +run_amdsmi_dual_copy_test.py +============================ + +The system DEB/RPM installs the amdsmi Python module twice: once into the +interpreter's site-packages (so a bare ``import amdsmi`` works) and once under +``share/amd_smi`` (captured by the TheRock artifact flow and used by downstream +tools that ``sys.path.insert(ROCM_PATH + "/share/amd_smi")``). Both copies are +necessary -- TheRock ships only ``/opt/rocm`` and cannot reach the ``/usr`` +site-packages tree, so a redirector/symlink is not an option. + +Two independent copies can silently drift (a partial upgrade, a stray edit), +after which ``import amdsmi`` resolves to a different version depending on +sys.path order. This guard asserts the two installed copies are byte-identical +so any drift fails a build instead of shipping. + +The bundled ``libamd_smi*.so`` is excluded from both install trees, so only the +Python sources are compared. +""" + +import argparse +import filecmp +import os +import sys +from pathlib import Path +from typing import List, Tuple + + +def _py_files(root: Path) -> List[Path]: + return sorted(p.relative_to(root) for p in root.rglob("*.py")) + + +def compare_trees(tree_a: Path, tree_b: Path) -> Tuple[List[str], List[str]]: + """Compare the .py files of two module trees. + + Returns ``(missing, differing)``: relative paths present in one tree but not + the other, and relative paths whose contents differ. Empty lists mean the + two trees are byte-identical. + """ + files_a = set(_py_files(tree_a)) + files_b = set(_py_files(tree_b)) + + missing = sorted(str(p) for p in files_a.symmetric_difference(files_b)) + differing = [] + for rel in sorted(files_a & files_b): + if not filecmp.cmp(tree_a / rel, tree_b / rel, shallow=False): + differing.append(str(rel)) + return missing, differing + + +def _rocm_share_dir() -> Path: + rocm_path = os.environ.get("ROCM_PATH") or os.environ.get("ROCM_HOME") or "/opt/rocm" + return Path(rocm_path) / "share" / "amd_smi" / "amdsmi" + + +def _sitelib_dir() -> Path: + import amdsmi + + return Path(amdsmi.__file__).resolve().parent + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--sitelib-tree", + type=Path, + default=None, + help="site-packages amdsmi dir (default: resolved from `import amdsmi`).", + ) + parser.add_argument( + "--share-tree", + type=Path, + default=None, + help="share/amd_smi amdsmi dir (default: ROCM_PATH/share/amd_smi/amdsmi).", + ) + args = parser.parse_args() + + share_tree = (args.share_tree or _rocm_share_dir()).resolve() + try: + sitelib_tree = (args.sitelib_tree or _sitelib_dir()).resolve() + except ImportError: + sys.exit("could not `import amdsmi`; install the package or pass --sitelib-tree") + + if not share_tree.is_dir(): + sys.exit(f"share/amd_smi copy not found at {share_tree}") + if not sitelib_tree.is_dir(): + sys.exit(f"site-packages copy not found at {sitelib_tree}") + + print(f"site-packages : {sitelib_tree}") + print(f"share/amd_smi : {share_tree}") + + missing, differing = compare_trees(sitelib_tree, share_tree) + if missing or differing: + msg = ["the two installed amdsmi copies have drifted:"] + if missing: + msg.append(" only in one tree: " + ", ".join(missing)) + if differing: + msg.append(" differing contents: " + ", ".join(differing)) + sys.exit("\n".join(msg)) + + print("PASS: site-packages and share/amd_smi copies are byte-identical.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From a2557c7d68059ce925289ac6df50c9a576666093 Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Wed, 22 Jul 2026 12:10:27 -0500 Subject: [PATCH 05/35] fix(amdsmi): fail the build when the system python3 cannot be detected - The module install path is auto-detected from the default system python3 (/usr/libexec/platform-python or /usr/bin/python3). - When neither exists on a system-package build, the old code silently fell back to Python3_SITELIB -- the path of whatever interpreter cmake happened to find (e.g. a manylinux build python), which no target host has on sys.path. The package installed but `import amdsmi` failed. - Emit a FATAL_ERROR in that case so the misconfiguration surfaces at configure time. Wheel builds (which legitimately run in such containers) are exempt. Signed-off-by: Maisam Arif --- projects/amdsmi/py-interface/CMakeLists.txt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/projects/amdsmi/py-interface/CMakeLists.txt b/projects/amdsmi/py-interface/CMakeLists.txt index 675b58e13907..0d67f06a1be8 100644 --- a/projects/amdsmi/py-interface/CMakeLists.txt +++ b/projects/amdsmi/py-interface/CMakeLists.txt @@ -259,6 +259,20 @@ if(NOT AMDSMI_SYSTEM_PYTHON_SITELIB) ) set(_amdsmi_detected_sitelib "${Python3_SITELIB}") endif() + elseif(NOT BUILD_PYTHON_WHEEL) + # System-package build with no default system python3: Python3_SITELIB + # points at whatever interpreter cmake found (e.g. a manylinux + # /opt/python/... build python), a path no target host has on sys.path. + # The module would install "successfully" yet `import amdsmi` would fail + # with ModuleNotFoundError. Fail loudly instead. Wheel builds (which run + # in exactly such containers) are exempt and set the destination there. + message( + FATAL_ERROR + "Cannot detect the system python3 for the module install path: neither " + "/usr/libexec/platform-python nor /usr/bin/python3 exists. Set " + "-DAMDSMI_SYSTEM_PYTHON_SITELIB= explicitly, or build the wheel " + "with -DBUILD_PYTHON_WHEEL=ON." + ) else() set(_amdsmi_detected_sitelib "${Python3_SITELIB}") endif() From 50ee0a53f054c62084619167bd97e94655409f5c Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Wed, 22 Jul 2026 12:11:05 -0500 Subject: [PATCH 06/35] test(amdsmi): run the wheel install-location check without a GPU - The wheel verification asserted the install location (site-packages / dist-packages) only after amdsmi_init(), which opens the GPU driver. - On GPU-less runners (manylinux, containers) amdsmi_init() failed first, so the location assertion never ran and a genuine wrong-path install was masked as a hardware problem. - Run the hardware-independent location check unconditionally, and gate the init/enumerate smoke test behind a GPU probe that skips cleanly when no driver is present. Signed-off-by: Maisam Arif --- .../tests/amdsmi_build/run_amdsmi_build.py | 48 +++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/projects/amdsmi/tests/amdsmi_build/run_amdsmi_build.py b/projects/amdsmi/tests/amdsmi_build/run_amdsmi_build.py index 417dedf0c90e..8839b1095172 100755 --- a/projects/amdsmi/tests/amdsmi_build/run_amdsmi_build.py +++ b/projects/amdsmi/tests/amdsmi_build/run_amdsmi_build.py @@ -944,25 +944,6 @@ def verify_wheel_site_packages(cfg: "RunnerConfig") -> None: log_dir=cfg.log_dir, ) - smoke_test = ( - "import amdsmi\n" - "print('PASS: import amdsmi OK')\n" - "amdsmi.amdsmi_init()\n" - "print('PASS: amdsmi_init() OK')\n" - "devs = amdsmi.amdsmi_get_processor_handles()\n" - "print('PASS: Found %d device(s)' % len(devs))\n" - "amdsmi.amdsmi_shut_down()\n" - "print('PASS: amdsmi_shut_down() OK')\n" - "print('=== Wheel verification passed ===')\n" - ) - run_command( - ["python3", "-c", smoke_test], - name="wheel-smoke-test", - cwd=Path("/tmp"), - retries=1, - log_dir=cfg.log_dir, - ) - run_command( ["python3", "-m", "pip", "show", "amdsmi"], name="pip-show-amdsmi", @@ -978,6 +959,10 @@ def verify_wheel_site_packages(cfg: "RunnerConfig") -> None: # unconditionally precedes site-packages, so leaving it set would test the # environment's path config rather than the package. A bare interpreter # (no PYTHONPATH) is the real scripting scenario where pip must win. + # This check is hardware-independent and runs before the GPU smoke test + # below, so it executes even on GPU-less runners (manylinux, containers) + # where amdsmi_init() would otherwise fail first and mask a wrong-path + # install. priority_check = ( "import os, subprocess, amdsmi\n" "out = subprocess.check_output(['python3', '-m', 'pip', 'show', 'amdsmi'], text=True)\n" @@ -1003,6 +988,31 @@ def verify_wheel_site_packages(cfg: "RunnerConfig") -> None: log_dir=cfg.log_dir, ) + # GPU-dependent smoke test: initialize the library and enumerate devices. + # Skip cleanly when no GPU/driver is present so packaging-only runners are + # not failed by the absence of hardware. + smoke_test = ( + "import amdsmi\n" + "try:\n" + " amdsmi.amdsmi_init()\n" + "except amdsmi.AmdSmiException as e:\n" + " print('SKIP: amdsmi_init() failed (no GPU/driver?): %s' % e)\n" + " raise SystemExit(0)\n" + "print('PASS: amdsmi_init() OK')\n" + "devs = amdsmi.amdsmi_get_processor_handles()\n" + "print('PASS: Found %d device(s)' % len(devs))\n" + "amdsmi.amdsmi_shut_down()\n" + "print('PASS: amdsmi_shut_down() OK')\n" + "print('=== Wheel GPU smoke test passed ===')\n" + ) + run_command( + ["python3", "-c", smoke_test], + name="wheel-smoke-test", + cwd=Path("/tmp"), + retries=1, + log_dir=cfg.log_dir, + ) + def verify_soname_distinct(cfg: "RunnerConfig") -> None: """Assert the system and wheel libraries keep distinct SONAMEs. From 36b6fb18d4ae9d68e2d9e658c045f76ad0cf73c7 Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Wed, 22 Jul 2026 12:11:59 -0500 Subject: [PATCH 07/35] test(amdsmi): make the older-.so ABI test exercise real behavior - test_stable_symbols_resolve_against_older_so only checked hasattr for the same symbols the fake library was told to expose, so it could never fail. - Rewrite it to model a user running against an older/mismatched .so: assert every common (stable) symbol still resolves and is callable, and that a symbol the older library does not export raises AttributeError instead of silently acting as a no-op. Signed-off-by: Maisam Arif --- .../amdsmi/tests/python/test_abi_compat.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/projects/amdsmi/tests/python/test_abi_compat.py b/projects/amdsmi/tests/python/test_abi_compat.py index 2f2eedc9792f..db20c78c107f 100644 --- a/projects/amdsmi/tests/python/test_abi_compat.py +++ b/projects/amdsmi/tests/python/test_abi_compat.py @@ -210,14 +210,24 @@ def test_override_routes_through_loader(self): ) def test_stable_symbols_resolve_against_older_so(self): - # Older .so: only the stable subset is exported. Wrapper must - # successfully bind every stable amdsmi_* it touches at module - # init -- regression here means wrappers built against newer - # headers cannot be used against older libraries at all. + # Simulate a user with an OLDER/mismatched .so loaded: it exports only + # the stable subset, none of the newer symbols. The contract is: + # * every common (stable) symbol the user requests still works, and + # * a symbol the older .so does not export fails cleanly, not silently. with _Patch(STABLE_SYMBOLS): w = _import_fresh_wrapper() + lib = w._libraries["libamd_smi.so"] + self.assertIsInstance(lib, FakeCDLL) + + # Common symbols resolve and are callable against the older .so. for sym in STABLE_SYMBOLS: self.assertTrue(hasattr(w, sym), "wrapper lost stable symbol %s" % sym) + fn = getattr(lib, sym) + self.assertEqual(fn(), 0, "stable symbol %s not callable against older .so" % sym) + + # A newer symbol the older .so lacks must raise, not return a no-op. + with self.assertRaises(AttributeError): + lib.amdsmi_get_gpu_a_future_only_symbol() def test_unguarded_bindings_are_stable(self): # Contract: any amdsmi_* binding the wrapper performs WITHOUT a From 087370a7c16bfcf86f67ae52ded7b4d7255997ed Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Wed, 22 Jul 2026 12:13:53 -0500 Subject: [PATCH 08/35] fix(amdsmi): quote the install prefix when writing ld.so.conf.d on RPM - The RPM post script wrote the ld.so.conf.d entry with an unquoted $RPM_INSTALL_PREFIX0, so a prefix containing spaces or glob characters would expand incorrectly. - Quote the expansion, matching how the Debian postinst uses a fixed constant. Signed-off-by: Maisam Arif --- projects/amdsmi/RPM/post.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/amdsmi/RPM/post.in b/projects/amdsmi/RPM/post.in index d9b8776201b0..9af8c0eb70b1 100755 --- a/projects/amdsmi/RPM/post.in +++ b/projects/amdsmi/RPM/post.in @@ -66,7 +66,7 @@ EOF do_ldconfig() { # left-hand term originates from ENABLE_LDCONFIG = ON/OFF at package build if [ "@ENABLE_LDCONFIG@" == "ON" ]; then - echo $RPM_INSTALL_PREFIX0/@CMAKE_INSTALL_LIBDIR@ > /etc/ld.so.conf.d/x86_64-libamd_smi_lib.conf + echo "$RPM_INSTALL_PREFIX0/@CMAKE_INSTALL_LIBDIR@" > /etc/ld.so.conf.d/x86_64-libamd_smi_lib.conf ldconfig fi } From 6cedf9df0c06bdeaf40e983a5e4f5ef66aaa6d1a Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Wed, 22 Jul 2026 12:14:17 -0500 Subject: [PATCH 09/35] fix(amdsmi): make the operational log group-writable, not world-writable - The post-install scripts set the AMD SMI log file to a+rw, so any local user could truncate or inject entries into a root-owned log (CWE-732). - Use 0664 instead: owner and group can write, others can only read. Non-root appenders share the file's group. Signed-off-by: Maisam Arif --- projects/amdsmi/DEBIAN/postinst.in | 12 ++++++------ projects/amdsmi/RPM/post.in | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/projects/amdsmi/DEBIAN/postinst.in b/projects/amdsmi/DEBIAN/postinst.in index 2f5583b8a94b..20ebb1d4071a 100755 --- a/projects/amdsmi/DEBIAN/postinst.in +++ b/projects/amdsmi/DEBIAN/postinst.in @@ -8,12 +8,12 @@ do_configureLogrotate() { mkdir -p "${logPath}" touch "${logFile}" - # Relax permissions on the operational log file only so non-root - # processes can append to it. Do NOT use chmod -R here: recursing - # would also make the log directory and sibling files (e.g. - # postinst.log) world-writable, which would let a local user create - # or tamper with files under a root-owned log directory (CWE-732). - chmod a+rw "${logFile}" + # Relax permissions on the operational log file only so non-root processes + # in the same group can append to it. 0664 keeps it group-writable but not + # world-writable. Do NOT use chmod -R here: recursing would also make the log + # directory and sibling files (e.g. postinst.log) writable, which would let a + # local user tamper with files under a root-owned log directory (CWE-732). + chmod 0664 "${logFile}" if ! command -v logrotate &>/dev/null; then echo "[WARNING] Detected logrotate is not installed."\ diff --git a/projects/amdsmi/RPM/post.in b/projects/amdsmi/RPM/post.in index 9af8c0eb70b1..c566b0f6db76 100755 --- a/projects/amdsmi/RPM/post.in +++ b/projects/amdsmi/RPM/post.in @@ -8,12 +8,12 @@ do_configureLogrotate() { mkdir -p "${logPath}" touch "${logFile}" - # Relax permissions on the operational log file only so non-root - # processes can append to it. Do NOT use chmod -R here: recursing - # would also make the log directory and sibling files (e.g. - # postinst.log) world-writable, which would let a local user create - # or tamper with files under a root-owned log directory (CWE-732). - chmod a+rw "${logFile}" + # Relax permissions on the operational log file only so non-root processes + # in the same group can append to it. 0664 keeps it group-writable but not + # world-writable. Do NOT use chmod -R here: recursing would also make the log + # directory and sibling files (e.g. postinst.log) writable, which would let a + # local user tamper with files under a root-owned log directory (CWE-732). + chmod 0664 "${logFile}" if ! command -v logrotate &>/dev/null; then echo "[WARNING] Detected logrotate is not installed."\ From b4a349e003596c564d55e29f7a2c556fb7ceb65d Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Wed, 22 Jul 2026 12:15:02 -0500 Subject: [PATCH 10/35] fix(amdsmi): only strip release libraries when a strip tool is available - The release strip commands ran ${CMAKE_STRIP} unconditionally. When CMAKE_STRIP is empty (some cross-compile toolchains or stripped sysroots), the command degrades to executing the .so itself, giving a confusing failure. - Guard both strip commands on CMAKE_STRIP being set. Signed-off-by: Maisam Arif --- projects/amdsmi/src/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/amdsmi/src/CMakeLists.txt b/projects/amdsmi/src/CMakeLists.txt index a91e1ab89501..360df5686f3b 100644 --- a/projects/amdsmi/src/CMakeLists.txt +++ b/projects/amdsmi/src/CMakeLists.txt @@ -213,7 +213,7 @@ set_property(TARGET ${AMD_SMI} PROPERTY SOVERSION "${MAJOR}") set_property(TARGET ${AMD_SMI} PROPERTY VERSION "${SO_VERSION_STRING}") ## If the library is a release, strip the target library -if("${CMAKE_BUILD_TYPE}" STREQUAL Release) +if("${CMAKE_BUILD_TYPE}" STREQUAL Release AND CMAKE_STRIP) if(BUILD_BOTH_LIBS OR BUILD_SHARED_LIBS) if(TARGET ${AMD_SMI}) add_custom_command(TARGET ${AMD_SMI} POST_BUILD COMMAND ${CMAKE_STRIP} $) @@ -274,7 +274,7 @@ if(BUILD_PYTHON_WHEEL) set_property(TARGET ${AMD_SMI}_python APPEND PROPERTY LINK_DEPENDS "${VERSION_SCRIPT}") ## Strip the wheel-shipped library in Release so the published wheel does ## not carry DWARF debug symbols (mirrors the system-library strip above). - if("${CMAKE_BUILD_TYPE}" STREQUAL Release) + if("${CMAKE_BUILD_TYPE}" STREQUAL Release AND CMAKE_STRIP) add_custom_command(TARGET ${AMD_SMI}_python POST_BUILD COMMAND ${CMAKE_STRIP} $) endif() endif() From a4ebca507b8f3ef9995cddc0f0a02cdf5cba1a5a Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Wed, 22 Jul 2026 12:16:52 -0500 Subject: [PATCH 11/35] refactor(amdsmi): share common target setup between the libraries - The wheel-only libamd_smi_python target duplicated the link libraries, include directories and link dirs already applied to the main/static libraries, so any change had to be made in two places. - Extract the shared setup into amdsmi_configure_common_target() and call it from both the main target loop and the python target. - The python target keeps only its extras: the two NIC interface includes, UALOE flags, version script, -Bsymbolic-functions, SOVERSION and output name. Signed-off-by: Maisam Arif --- projects/amdsmi/src/CMakeLists.txt | 45 +++++++++++------------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/projects/amdsmi/src/CMakeLists.txt b/projects/amdsmi/src/CMakeLists.txt index 360df5686f3b..46bbca68517a 100644 --- a/projects/amdsmi/src/CMakeLists.txt +++ b/projects/amdsmi/src/CMakeLists.txt @@ -138,12 +138,16 @@ else() set(_AMD_SMI_ALL_TARGETS ${AMD_SMI}) endif() -foreach(_TARGET IN LISTS _AMD_SMI_ALL_TARGETS) +# Apply the include dirs, link libraries and link dirs shared by every amd_smi +# library target (the main/static libraries and the wheel-only +# libamd_smi_python). Target-specific settings -- version script, SOVERSION, +# -Bsymbolic-functions, the extra NIC interface includes -- are applied by the +# caller after this. +function(amdsmi_configure_common_target _TARGET) target_link_libraries( ${_TARGET} PRIVATE rt Threads::Threads ${CMAKE_DL_LIBS} amdsminic ${FILESYSTEM_LIB} ${UALOE_LINK_LIBRARIES} ) - target_include_directories( ${_TARGET} PRIVATE @@ -157,15 +161,17 @@ foreach(_TARGET IN LISTS _AMD_SMI_ALL_TARGETS) ${NIC_INCLUDE_DIRS} ${UALOE_INCLUDE_DIR} ) - target_include_directories( ${_TARGET} PUBLIC "$" "$" ) - target_link_directories(${_TARGET} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/nic/ai-nic/amdsmi_unified/build/) +endfunction() + +foreach(_TARGET IN LISTS _AMD_SMI_ALL_TARGETS) + amdsmi_configure_common_target(${_TARGET}) endforeach() # @@ -224,11 +230,15 @@ endif() ## Create Python-specific library if BUILD_PYTHON_WHEEL is enabled if(BUILD_PYTHON_WHEEL) add_library(${AMD_SMI}_python ${SRC_LIST} ${INC_LIST}) - target_link_libraries( + amdsmi_configure_common_target(${AMD_SMI}_python) + # Extra NIC interface includes the wheel target needs on top of the common + # set applied above. + target_include_directories( ${AMD_SMI}_python - PRIVATE rt Threads::Threads ${CMAKE_DL_LIBS} amdsminic ${FILESYSTEM_LIB} ${UALOE_LINK_LIBRARIES} + PRIVATE + ${PROJECT_SOURCE_DIR}/src/nic/ai-nic/amdsmi_unified/inc + ${PROJECT_SOURCE_DIR}/src/nic/ai-nic/amdsmi_unified/interface ) - target_link_directories(${AMD_SMI}_python PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/nic/ai-nic/amdsmi_unified/build/) # Apply the same UALOE netlink compile flags (e.g. -I.../libnl3 for # and -DUALOE_NETLINK) that the other amd_smi targets receive; # this target is created after the shared _AMD_SMI_ALL_TARGETS loop above and @@ -237,27 +247,6 @@ if(BUILD_PYTHON_WHEEL) separate_arguments(UALOE_FLAGS_PYTHON NATIVE_COMMAND "${UALOE_COMPILE_FLAGS}") target_compile_options(${AMD_SMI}_python PRIVATE ${UALOE_FLAGS_PYTHON}) endif() - target_include_directories( - ${AMD_SMI}_python - PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_CURRENT_SOURCE_DIR}/../ - ${PROJECT_SOURCE_DIR}/rocm_smi/include - ${PROJECT_SOURCE_DIR}/common/shared_mutex - ${RAS_DECODE_INC_DIR} - ${DRM_INCLUDE_DIRS} - ${DRM_AMDGPU_INCLUDE_DIRS} - ${NIC_INCLUDE_DIRS} - ${PROJECT_SOURCE_DIR}/src/nic/ai-nic/amdsmi_unified/inc - ${PROJECT_SOURCE_DIR}/src/nic/ai-nic/amdsmi_unified/interface - ${UALOE_INCLUDE_DIR} - ) - target_include_directories( - ${AMD_SMI}_python - PUBLIC - "$" - "$" - ) set_property(TARGET ${AMD_SMI}_python PROPERTY SOVERSION "${MAJOR}") set_property(TARGET ${AMD_SMI}_python PROPERTY VERSION "${SO_VERSION_STRING}") set_target_properties(${AMD_SMI}_python PROPERTIES OUTPUT_NAME "amd_smi_python") From ec8ffc9ff2f27e418cda5a4f1959be840d4a309d Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Wed, 22 Jul 2026 12:17:56 -0500 Subject: [PATCH 12/35] fix(amdsmi): remove the unused PY_WRAPPER_INSTALL_DIR cache variable - The packaging rework moved both module installs to AMDSMI_SYSTEM_PYTHON_SITELIB and SHARE_INSTALL_PREFIX directly, leaving PY_WRAPPER_INSTALL_DIR referenced nowhere. - A -D override of it silently did nothing. Delete the dead declaration. Signed-off-by: Maisam Arif --- projects/amdsmi/py-interface/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/projects/amdsmi/py-interface/CMakeLists.txt b/projects/amdsmi/py-interface/CMakeLists.txt index 0d67f06a1be8..74593762d15b 100644 --- a/projects/amdsmi/py-interface/CMakeLists.txt +++ b/projects/amdsmi/py-interface/CMakeLists.txt @@ -11,7 +11,6 @@ set(PY_BUILD_DIR "python_package") # amdsmi part of this string is the directory containing all python files # additionally defined in pyproject.toml set(PY_PACKAGE_DIR "${PY_BUILD_DIR}/amdsmi") -set(PY_WRAPPER_INSTALL_DIR "${SHARE_INSTALL_PREFIX}" CACHE STRING "Wrapper installation directory") # AMDSMI_PYTHON_LIB_NAME is overloaded: when BUILD_PYTHON_WHEEL=OFF it # resolves to the system libamd_smi.so (the python_package staging tree # just copies it for in-tree imports); when ON it resolves to the From ded24e8f5ba6cff844e218d40cf563228b3422f1 Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Wed, 22 Jul 2026 12:18:41 -0500 Subject: [PATCH 13/35] fix(amdsmi): pin permissions on the installed Python module - install(DIRECTORY) preserved the build tree's permissions, so a permissive build umask could leave group- or world-writable .py files under a system site-packages directory, where code runs as root via the CLI. - Set explicit permissions (files 0644, dirs 0755) on both module installs so the result is deterministic regardless of the build umask. Signed-off-by: Maisam Arif --- projects/amdsmi/py-interface/CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/projects/amdsmi/py-interface/CMakeLists.txt b/projects/amdsmi/py-interface/CMakeLists.txt index 74593762d15b..8821e052c7d8 100644 --- a/projects/amdsmi/py-interface/CMakeLists.txt +++ b/projects/amdsmi/py-interface/CMakeLists.txt @@ -313,6 +313,10 @@ install( DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${PY_PACKAGE_DIR} DESTINATION ${AMDSMI_SYSTEM_PYTHON_SITELIB} COMPONENT dev + # Pin permissions so a permissive build umask cannot leave group/other + # writable .py files under a system directory (files 0644, dirs 0755). + FILE_PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ + DIRECTORY_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE PATTERN "libamd_smi*.so" EXCLUDE ) @@ -334,5 +338,7 @@ install( DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${PY_PACKAGE_DIR} DESTINATION ${SHARE_INSTALL_PREFIX} COMPONENT dev + FILE_PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ + DIRECTORY_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE PATTERN "libamd_smi*.so" EXCLUDE ) From 36718e8a3cb76e984c5ae48df6d6ee2e128ce5b4 Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Wed, 22 Jul 2026 12:20:17 -0500 Subject: [PATCH 14/35] docs(amdsmi): document the packaging and install paths - Add docs/packaging.md: the delivery paths, the loader order, the two installed copies and why both are needed, the coexistence/precedence rules, a support matrix, upgrade/downgrade behavior, and the RPM interpreter dependency. - Link it from the docs index and the install-paths toctree. - Point the manual-install section at the new page instead of only the py-interface README. Signed-off-by: Maisam Arif --- projects/amdsmi/docs/index.md | 1 + projects/amdsmi/docs/install/install.md | 5 +- projects/amdsmi/docs/packaging.md | 116 ++++++++++++++++++++++++ projects/amdsmi/docs/sphinx/_toc.yml.in | 2 + 4 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 projects/amdsmi/docs/packaging.md diff --git a/projects/amdsmi/docs/index.md b/projects/amdsmi/docs/index.md index 7613b18e4273..03164421f9da 100644 --- a/projects/amdsmi/docs/index.md +++ b/projects/amdsmi/docs/index.md @@ -32,6 +32,7 @@ AMD SMI is the successor to [ROCm SMI](https://github.com/ROCm/rocm-systems/tree :::{grid-item-card} Install * [Library and CLI tool installation](./install/install.md) * [Build from source](./install/build.md) +* [Packaging and install paths](./packaging.md) ::: :::{grid-item-card} How to diff --git a/projects/amdsmi/docs/install/install.md b/projects/amdsmi/docs/install/install.md index 57042ee28787..368c69882085 100644 --- a/projects/amdsmi/docs/install/install.md +++ b/projects/amdsmi/docs/install/install.md @@ -243,8 +243,9 @@ install needs to be uninstalled manually. The wheel ships its own SONAME-renamed `libamd_smi_python.so` next to the wrapper, so it does not depend on `/opt/rocm` being present. - See `py-interface/README.md` in the source tree for the full - install-paths matrix and the `AMDSMI_LIB_OVERRIDE` override. + See `py-interface/README.md` in the source tree, or + [Packaging and install paths](../packaging.md) for the full install-paths + matrix, coexistence rules, and the `AMDSMI_LIB_OVERRIDE` override. > **Note:** `sudo` may be required for the system-package path. For pip, > use `--break-system-packages` only if installing into a non-venv diff --git a/projects/amdsmi/docs/packaging.md b/projects/amdsmi/docs/packaging.md new file mode 100644 index 000000000000..5af50cefe8d2 --- /dev/null +++ b/projects/amdsmi/docs/packaging.md @@ -0,0 +1,116 @@ +# AMD SMI packaging and install paths + +AMD SMI ships through several delivery channels. This page describes each one, +how the Python module locates the native library in each, which combinations +are supported, and how upgrades and downgrades behave. It is the reference for +the loader contract that `py-interface/amdsmi_wrapper.py` implements. + +## Delivery paths + +| Path | Native library (`.so`) | Python module | How the module finds the `.so` | +| ---- | ---------------------- | ------------- | ------------------------------ | +| System package (deb/rpm) | `/opt/rocm/lib/libamd_smi.so.` (+ `ld.so.conf.d` entry) | Installed into the system interpreter's `site-packages`/`dist-packages` **and** `share/amd_smi` | SONAME via the dynamic linker | +| Tarball | Present in the extracted tree | Not installed | n/a — CLI and `.so` work; `import amdsmi` is not provided | +| ROCm via pip (TheRock `rocm_sdk_core`) | `/lib/libamd_smi.so.` | `/share/amd_smi/amdsmi` | Resolved relative to the wrapper (`../../../lib`) | +| ROCm via pip in a venv | Same as above, inside the venv | Same as above, inside the venv | Same as above | +| PyPI wheel | Bundled `libamd_smi_python.so` next to the wrapper | Interpreter `site-packages` | The bundled `.so`; system fallback is disabled | + +## The loader + +`py-interface/amdsmi_wrapper.py` selects the library in this order: + +1. `AMDSMI_LIB_OVERRIDE` — explicit path, for ABI tests. +2. A bundled `libamd_smi_python.so` next to the wrapper — the PyPI wheel. +3. The SONAME resolved relative to the wrapper (`parents[3]/lib`) — the TheRock + `share/amd_smi` layout, where a venv has no `ld.so.conf.d` entry. +4. The bare SONAME via the dynamic linker — the system deb/rpm. + +Steps 3 and 4 are skipped when `_AMDSMI_ALLOW_SYSTEM_FALLBACK` is `False`. The +committed wrapper and the system package keep it `True`; the wheel build flips +it to `False`, so a wheel never loads a system `libamd_smi.so` (which could be a +different version and would risk symbol conflicts inside processes such as +PyTorch or JAX that ship their own copy). + +The wheel's `libamd_smi_python.so` has a distinct SONAME and is linked with +`-Bsymbolic-functions`, so the system and wheel libraries can be loaded in the +same process without the dynamic linker interposing one on the other. + +## Two installed copies (system package) + +The deb/rpm installs the module into **both** the interpreter's site-packages +and `share/amd_smi`. Both are required: + +- site-packages makes a plain `import amdsmi` work. +- `share/amd_smi` is captured by the TheRock artifact flow (which packages only + `/opt/rocm`, so it cannot reach the `/usr` site-packages tree) and is used by + downstream tools that `sys.path.insert(ROCM_PATH + "/share/amd_smi")`. + +A redirector or symlink is not viable because TheRock ships only `/opt/rocm`. +The build harness runs a guard (`tests/run_amdsmi_dual_copy_test.py`) asserting +the two copies stay byte-identical, so drift fails a build instead of shipping. + +## Coexistence and precedence + +A user installs one delivery path. When a PyPI wheel is installed alongside a +system package, the wheel wins and the package uninstall does not remove it, +because they live in separate, file-manager-owned trees and `sys.path` favors +the wheel: + +| Installed together | `import amdsmi` resolves to | Package uninstall removes the wheel? | +| ------------------ | --------------------------- | ------------------------------------ | +| deb + pip wheel (Debian) | wheel in `/usr/local/.../dist-packages` (precedes `/usr/lib`) | No — dpkg removes only its own files | +| rpm + pip `--user` wheel (RHEL) | wheel in `~/.local/.../site-packages` (precedes system) | No — rpm removes only its own files | +| deb/rpm + venv wheel | wheel in the venv (isolated) | No — separate tree | + +## Support matrix + +Legend: ✅ supported and tested · 🟡 supported, pick one recommended · ⛔ unsupported. + +### Single path + +| Path | `amd-smi` CLI | `import amdsmi` | +| ---- | ------------- | --------------- | +| deb/rpm | ✅ | ✅ | +| tarball | ✅ | ⛔ (module not installed) | +| ROCm pip | ✅ | ✅ | +| ROCm pip in venv | ✅ | ✅ | +| PyPI wheel | ✅ | ✅ | + +### Two paths together + +| Combination | Coexist? | +| ----------- | -------- | +| deb/rpm + PyPI wheel | ✅ (wheel wins; package uninstall keeps the wheel) | +| deb/rpm + ROCm pip, same interpreter | 🟡 (discouraged; two library families) | +| PyPI wheel + ROCm pip | 🟡 (discouraged) | +| tarball + any pip/wheel | ✅ (module comes from pip; tarball `.so` unused by the module) | +| ROCm pip + ROCm pip venv | ✅ (venv wins while active) | + +### Unsupported + +- Two system packages of different major SOVERSION on one prefix. +- Relying on the tarball to provide `import amdsmi`. +- A wheel loading a system `/opt/rocm` library (blocked by design). + +## Upgrade and downgrade (deb/rpm) + +| Transition | Behavior | +| ---------- | -------- | +| pre-7.14 (pip-era) → 7.14+ package | The old package's prerm still `pip uninstall`s the legacy module and removes its `.pth`; the new package owns the site-packages files. | +| 7.14+ → 7.14+ | Plain file replacement by the package manager. | +| 7.14+ → pre-7.14 (downgrade) | The old package re-adds the pip install; a user-installed PyPI wheel in `/usr/local` or `~/.local` still wins and survives. | +| package removed, then newest PyPI wheel | Package removal deletes only its own files; the wheel is self-contained (bundled `.so`, fallback disabled). | + +## RPM interpreter dependency + +On RPM distros the module installs into a version-specific site-packages +(e.g. `/usr/lib64/python3.9/site-packages`). The package therefore declares a +dependency on the matching interpreter so it installs only where that +interpreter (and thus the baked path) exists: + +- RHEL/CentOS/Fedora/AlmaLinux/AzureLinux: `python(abi) = X.Y`. +- SLES/openSUSE: `pythonXY` (e.g. `python311`). + +Debian's `dist-packages` is version-agnostic, so the deb keeps the loose +`python3 (>= 3.6.8)` dependency and the `#!/usr/bin/python3` CLI shebang serves +every python3 minor. diff --git a/projects/amdsmi/docs/sphinx/_toc.yml.in b/projects/amdsmi/docs/sphinx/_toc.yml.in index 37f0d54aedfe..2da387bd10fa 100644 --- a/projects/amdsmi/docs/sphinx/_toc.yml.in +++ b/projects/amdsmi/docs/sphinx/_toc.yml.in @@ -15,6 +15,8 @@ subtrees: title: Install AMD SMI - file: install/build.md title: Build from source + - file: packaging.md + title: Packaging and install paths - caption: How to entries: From b46153f2851743aaa9f5e759c32760ee27c67589 Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Wed, 22 Jul 2026 15:54:45 -0500 Subject: [PATCH 15/35] test(amdsmi): install the dual-copy runner so its guard tests run in CI - test_dual_copy_guard.py loads run_amdsmi_dual_copy_test.py relative to its REPO_ROOT, which is SHARE_INSTALL_PREFIX in the installed test layout. - The runner was never installed, so all four guard tests raised SkipTest in the installed test run and the drift-guard logic went unexercised in CI. - Install run_amdsmi_dual_copy_test.py under tests/ with the tests component. Signed-off-by: Maisam Arif --- projects/amdsmi/tests/python/CMakeLists.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/projects/amdsmi/tests/python/CMakeLists.txt b/projects/amdsmi/tests/python/CMakeLists.txt index ef9785381ce5..b47d073c280e 100644 --- a/projects/amdsmi/tests/python/CMakeLists.txt +++ b/projects/amdsmi/tests/python/CMakeLists.txt @@ -29,3 +29,13 @@ install( DESTINATION ${SHARE_INSTALL_PREFIX}/tools/ COMPONENT ${TESTS_COMPONENT} ) + +# test_dual_copy_guard.py loads run_amdsmi_dual_copy_test.py relative to its +# REPO_ROOT (SHARE_INSTALL_PREFIX in the installed layout), so the runner must +# ship under tests/ -- otherwise the guard's four unit tests raise SkipTest and +# the drift-guard logic is never exercised in the installed test run. +install( + PROGRAMS ${PROJECT_SOURCE_DIR}/tests/run_amdsmi_dual_copy_test.py + DESTINATION ${SHARE_INSTALL_PREFIX}/tests/ + COMPONENT ${TESTS_COMPONENT} +) From ddb1139f2aa1bf90bdff06ef499e18a57a523ced Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Wed, 22 Jul 2026 20:03:31 -0500 Subject: [PATCH 16/35] fix(amdsmi): stop the logger from restoring world-writable log permissions - The postinst sets the operational log to 0664, but the logger reset it to 0666 (S_IWOTH) on every open, so the first root run made the root-owned log world-writable again and negated the CWE-732 fix. - Drop S_IWOTH so the runtime mode matches the 0664 the package installs: owner and group may write, others read-only. Signed-off-by: Maisam Arif --- projects/amdsmi/rocm_smi/src/rocm_smi_logger.cc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/projects/amdsmi/rocm_smi/src/rocm_smi_logger.cc b/projects/amdsmi/rocm_smi/src/rocm_smi_logger.cc index 2eedcc0ccdf2..2e66e7909952 100644 --- a/projects/amdsmi/rocm_smi/src/rocm_smi_logger.cc +++ b/projects/amdsmi/rocm_smi/src/rocm_smi_logger.cc @@ -499,7 +499,11 @@ void ROCmLogging::Logger::initialize_resources() { if (m_File.fail()) { std::cout << "WARNING: Failed opening log file." << std::endl; } - chmod(logFileName, S_IRUSR | S_IRGRP | S_IROTH | S_IWUSR | S_IWGRP | S_IWOTH); + // 0664: owner and group may write, others read-only. Must match the mode the + // package postinst sets; granting world-write here (S_IWOTH) would let any + // local user tamper with a root-owned log (CWE-732) and silently undo the + // packaging permission on the first root-run. + chmod(logFileName, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH); } void ROCmLogging::Logger::destroy_resources() { m_File.close(); } From 83d20f45b81626306582fba57e651c7c714628ff Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Wed, 22 Jul 2026 22:16:14 -0500 Subject: [PATCH 17/35] fix(amdsmi): stop the RPM from co-owning system python directories - The module installs under an absolute system site-packages path (e.g. /usr/lib64/python3.9/site-packages) that the OS python package owns. - CPack's RPM auto-filelist declared %dir ownership of that directory and its /usr parents with the default group-writable 0775 mode, co-owning system directories the package should not touch. - Walk the sitelib's ancestors and add them to CPACK_RPM_EXCLUDE_FROM_AUTO_FILELIST_ADDITION so the RPM owns only its own /amdsmi subtree; verified the /usr parents no longer appear in the built RPM's file list. Signed-off-by: Maisam Arif --- projects/amdsmi/CMakeLists.txt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/projects/amdsmi/CMakeLists.txt b/projects/amdsmi/CMakeLists.txt index 153e355445ed..3f2077ea255d 100644 --- a/projects/amdsmi/CMakeLists.txt +++ b/projects/amdsmi/CMakeLists.txt @@ -531,9 +531,25 @@ install( ) # RPM package specific variables +# The Python module installs under an absolute system site-packages path +# (e.g. /usr/lib64/python3.9/site-packages) that the OS python package already +# owns. Left alone, CPack's RPM generator declares %dir ownership of that +# directory and its parents -- co-owning system directories, with the default +# group-writable (0775) mode. Exclude the sitelib and its ancestors from the +# auto filelist so the RPM owns only its own /amdsmi subtree, not the +# shared /usr python directories. +set(_amdsmi_sitelib_excludes "") +if(AMDSMI_SYSTEM_PYTHON_SITELIB) + set(_amdsmi_walk "${AMDSMI_SYSTEM_PYTHON_SITELIB}") + while(_amdsmi_walk AND NOT _amdsmi_walk STREQUAL "/" AND NOT _amdsmi_walk STREQUAL "/usr") + list(APPEND _amdsmi_sitelib_excludes "${_amdsmi_walk}") + get_filename_component(_amdsmi_walk "${_amdsmi_walk}" DIRECTORY) + endwhile() +endif() set(CPACK_RPM_EXCLUDE_FROM_AUTO_FILELIST_ADDITION "${CPACK_PACKAGING_INSTALL_PREFIX}" "${CPACK_PACKAGING_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}" + ${_amdsmi_sitelib_excludes} ) #Set rpm distro if(CPACK_RPM_PACKAGE_RELEASE) From 3747053ba9acc2c221ff1c79e0cf5968abe181a5 Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Wed, 22 Jul 2026 22:18:06 -0500 Subject: [PATCH 18/35] test(amdsmi): resolve the loader test wrapper deterministically - The wheel-loader tests copy amdsmi_wrapper.py expecting the committed system variant (_AMDSMI_ALLOW_SYSTEM_FALLBACK = True), then flip it to False. - _find_py_interface() fell back to `import amdsmi`, which in the supported wheel-plus-system coexistence resolves to the wheel's wrapper (already False), making the flip test fail on a valid configuration. - Prefer the co-installed share/amd_smi/amdsmi copy (always the system variant, immune to a wheel shadowing sys.path) before the imported package. Signed-off-by: Maisam Arif --- projects/amdsmi/tests/python/test_abi_compat.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/projects/amdsmi/tests/python/test_abi_compat.py b/projects/amdsmi/tests/python/test_abi_compat.py index db20c78c107f..bdbac56fcbfd 100644 --- a/projects/amdsmi/tests/python/test_abi_compat.py +++ b/projects/amdsmi/tests/python/test_abi_compat.py @@ -54,12 +54,18 @@ def _find_py_interface() -> Path: """Directory holding amdsmi_wrapper.py, across source and installed layouts. - Source tree: ``py-interface/`` under ``projects/amdsmi``. Installed - package: the ``amdsmi`` package in site-packages -- in CI this test runs - from the installed tests directory, where ``py-interface/`` is absent and - the wrapper instead ships inside the importable ``amdsmi`` package. + Resolution order is deliberate: the wheel-loader tests copy this wrapper + expecting the committed system variant (``_AMDSMI_ALLOW_SYSTEM_FALLBACK = + True``), so it must NOT resolve to a coexisting pip wheel's wrapper (which + ships the flag flipped to ``False``). + + 1. ``py-interface/`` in the source checkout. + 2. The co-installed ``share/amd_smi/amdsmi`` copy (installed alongside the + tests, always the system variant, and immune to a wheel shadowing + ``import amdsmi`` on sys.path). + 3. Only then the imported ``amdsmi`` package, as a last resort. """ - candidates = [REPO_ROOT / "py-interface"] + candidates = [REPO_ROOT / "py-interface", REPO_ROOT / "amdsmi"] try: import amdsmi From 901c64e3278ee6b9d6ce5b01a172a04f70607dea Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Wed, 22 Jul 2026 22:19:24 -0500 Subject: [PATCH 19/35] fix(amdsmi): skip the system site-packages install for wheel builds - The install(DIRECTORY) to AMDSMI_SYSTEM_PYTHON_SITELIB ran even with BUILD_PYTHON_WHEEL=ON. On a manylinux build host the detected sitelib can be a container-internal path (e.g. /opt/python/cp312-cp312/...), so a cpack run in that configuration would ship the module at a path no target system has. - The wheel packages its own module tree, so guard the system install with if(NOT BUILD_PYTHON_WHEEL). Signed-off-by: Maisam Arif --- projects/amdsmi/py-interface/CMakeLists.txt | 27 +++++++++++++-------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/projects/amdsmi/py-interface/CMakeLists.txt b/projects/amdsmi/py-interface/CMakeLists.txt index 8821e052c7d8..82d738d30f58 100644 --- a/projects/amdsmi/py-interface/CMakeLists.txt +++ b/projects/amdsmi/py-interface/CMakeLists.txt @@ -309,16 +309,23 @@ set(AMDSMI_SYSTEM_PYTHON_ABI FORCE ) -install( - DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${PY_PACKAGE_DIR} - DESTINATION ${AMDSMI_SYSTEM_PYTHON_SITELIB} - COMPONENT dev - # Pin permissions so a permissive build umask cannot leave group/other - # writable .py files under a system directory (files 0644, dirs 0755). - FILE_PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ - DIRECTORY_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE - PATTERN "libamd_smi*.so" EXCLUDE -) +# Install the module into the system interpreter's site-packages for the +# DEB/RPM. Skipped when building the wheel: the wheel packages its own module +# tree, and on a manylinux-style build host the detected sitelib can be a +# container-internal path (e.g. /opt/python/cp312-cp312/...) that exists on no +# target system, so a cpack run in that configuration would ship files nowhere. +if(NOT BUILD_PYTHON_WHEEL) + install( + DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${PY_PACKAGE_DIR} + DESTINATION ${AMDSMI_SYSTEM_PYTHON_SITELIB} + COMPONENT dev + # Pin permissions so a permissive build umask cannot leave group/other + # writable .py files under a system directory (files 0644, dirs 0755). + FILE_PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ + DIRECTORY_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE + PATTERN "libamd_smi*.so" EXCLUDE + ) +endif() # Also stage the module under share/amd_smi (the historical location). site- # packages above is the primary, importable location for the DEB/RPM and for From 3b7e213a9fb910c947618c6c5e2c11dbc792cb6e Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Wed, 22 Jul 2026 22:20:16 -0500 Subject: [PATCH 20/35] fix(amdsmi): reject regenerating the wrapper against the wheel library - With BUILD_WRAPPER=ON and BUILD_PYTHON_WHEEL=ON the generator is fed -l libamd_smi_python.so, so ctypeslib emits bindings keyed on 'libamd_smi_python.so' while the loader still populates 'libamd_smi.so' -- every API call would then raise KeyError. - The wheel reuses the committed wrapper (only flipping the fallback flag) and never needs regeneration, so fail configure with a clear message instead of emitting a broken wrapper. Signed-off-by: Maisam Arif --- projects/amdsmi/py-interface/CMakeLists.txt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/projects/amdsmi/py-interface/CMakeLists.txt b/projects/amdsmi/py-interface/CMakeLists.txt index 82d738d30f58..d3242ca4894e 100644 --- a/projects/amdsmi/py-interface/CMakeLists.txt +++ b/projects/amdsmi/py-interface/CMakeLists.txt @@ -25,6 +25,19 @@ set(AMDSMI_PYTHON_LIB_PATH "${PROJECT_BINARY_DIR}/src/${AMDSMI_PYTHON_LIB_NAME}" set(GOOD_CLANG_FOUND FALSE) # only try to re-generate amdsmi_wrapper.py if BUILD_WRAPPER is on/true if(BUILD_WRAPPER) + # The committed wrapper is canonical: its loader and every ctypes binding + # key on 'libamd_smi.so'. With BUILD_PYTHON_WHEEL=ON the generator is fed + # -l libamd_smi_python.so, so ctypeslib would emit bindings keyed on + # 'libamd_smi_python.so' while the loader still populates 'libamd_smi.so' -- + # every API call would then KeyError. The wheel reuses the committed wrapper + # (only flipping the fallback flag), so it never needs regeneration. Reject + # the combination rather than emit a broken wrapper. + if(BUILD_PYTHON_WHEEL) + message( + FATAL_ERROR + "BUILD_WRAPPER=ON is incompatible with BUILD_PYTHON_WHEEL=ON: regenerating the wrapper against libamd_smi_python.so produces bindings that mismatch the canonical libamd_smi.so loader. Regenerate the wrapper with BUILD_PYTHON_WHEEL=OFF, then build the wheel from the committed wrapper." + ) + endif() find_program(clang NAMES clang REQUIRED) # extract clang version manually because find_package(clang) doesn't work execute_process(COMMAND ${clang} --version OUTPUT_VARIABLE clang_full_version_string) From efe2694a330895bc2a114420454e2093f3104964 Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Wed, 22 Jul 2026 22:21:33 -0500 Subject: [PATCH 21/35] ci(amdsmi): assert the wheel disables system fallback and pin the build dir - The manylinux smoke test only checked that `import amdsmi` works, so a build-step ordering regression that shipped a wheel with the system-library fallback still enabled would pass silently. Add an assertion that the built wheel's _AMDSMI_ALLOW_SYSTEM_FALLBACK is False. - The SONAME check read the build tree from a hardcoded /tmp/amdsmi-build that only matched build_wheel.py's default by coincidence. Set the build dir once in a job-level env var and pass it to both the wheel build and the SONAME check so the coupling is explicit. Signed-off-by: Maisam Arif --- .github/workflows/amdsmi-manylinux-build.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/amdsmi-manylinux-build.yml b/.github/workflows/amdsmi-manylinux-build.yml index 386166072066..c61973e27028 100644 --- a/.github/workflows/amdsmi-manylinux-build.yml +++ b/.github/workflows/amdsmi-manylinux-build.yml @@ -37,6 +37,12 @@ jobs: name: ${{matrix.target.name}} runs-on: ${{matrix.target.runs-on}} container: ${{matrix.target.container}} + # Shared build directory: build_wheel.py writes the CMake build tree here, + # and the SONAME conflict check reads both libraries back from it. Setting + # it explicitly (rather than relying on build_wheel.py's default) keeps the + # two steps coupled to one value. + env: + AMDSMI_WHEEL_BUILD_DIR: /tmp/amdsmi-build strategy: fail-fast: false @@ -82,6 +88,7 @@ jobs: ${{matrix.target.python-bin}} \ "$GITHUB_WORKSPACE/projects/amdsmi/tools/build_wheel.py" \ --project-dir "$GITHUB_WORKSPACE/projects/amdsmi" \ + --build-dir "$AMDSMI_WHEEL_BUILD_DIR" \ --output-dir "$GITHUB_WORKSPACE/wheels" \ --os-variant ${{matrix.target.os-variant}} \ ${{matrix.target.repair-flag}} @@ -98,13 +105,17 @@ jobs: run: | ${{matrix.target.python-bin}} \ "$GITHUB_WORKSPACE/projects/amdsmi/tests/run_amdsmi_pkg_conflict_test.py" \ - --build-root /tmp/amdsmi-build + --build-root "$AMDSMI_WHEEL_BUILD_DIR" - name: Smoke test (install + import) run: | ${{matrix.target.python-bin}} -m pip install --upgrade pip ${{matrix.target.python-bin}} -m pip install "$GITHUB_WORKSPACE"/wheels/*.whl ${{matrix.target.python-bin}} -c "import amdsmi; print('amdsmi import OK')" + # The wheel must ship with the system-library fallback disabled, so a + # build-step ordering regression that leaves it enabled fails here + # rather than shipping a wheel that can silently load a system .so. + ${{matrix.target.python-bin}} -c "import amdsmi.amdsmi_wrapper as w; assert not w._AMDSMI_ALLOW_SYSTEM_FALLBACK, 'wheel shipped with system fallback enabled'; print('fallback disabled OK')" - name: Upload wheels (by SHA) uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 From bdda9976317c48ca1a60231fa6d940b78055e290 Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Thu, 23 Jul 2026 10:12:57 -0500 Subject: [PATCH 22/35] build(amdsmi): build one wheel per platform, not per interpreter - the wheel bundles a prebuilt .so loaded via ctypes and carries no python c-extension, so it is identical (py3-none) for every cpython; building it once per platform is sufficient - add select_interpreters() to pick a single build interpreter and reduce the remaining oldest/newest ones to build-smoke checks - replace the per-interpreter build_wheels() with build_wheel() plus build_smoke() so the manylinux image no longer emits ~8 identical wheels Signed-off-by: Maisam Arif --- projects/amdsmi/tools/build_wheel.py | 103 +++++++++++++++++++-------- 1 file changed, 72 insertions(+), 31 deletions(-) diff --git a/projects/amdsmi/tools/build_wheel.py b/projects/amdsmi/tools/build_wheel.py index f9fc3c8c12d4..38b63f877c0c 100644 --- a/projects/amdsmi/tools/build_wheel.py +++ b/projects/amdsmi/tools/build_wheel.py @@ -15,8 +15,9 @@ 1. (optional) Register the project as a git safe.directory 2. cmake configure (-DBUILD_PYTHON_WHEEL=ON) 3. make -j$(nproc) - 4. pip wheel --no-deps --no-build-isolation (per interpreter) - 5. auditwheel repair (optional, for manylinux tags) + 4. pip wheel --no-deps --no-build-isolation (once; the wheel is py3-none) + 5. build-smoke the package with the oldest/newest interpreters + 6. auditwheel repair (optional, for manylinux tags) Requirements ------------ @@ -49,9 +50,11 @@ import glob import logging import os +import re import shutil import subprocess import sys +import tempfile from pathlib import Path # --------------------------------------------------------------------------- @@ -264,35 +267,71 @@ def cmake_and_build(project_dir, build_dir, cmake_python, args, extra_env): run(["make", "-j" + str(os.cpu_count() or 4)], cwd=str(build_dir), env=extra_env or None) -def build_wheels(pkg_dir, raw_wheels_dir, interpreters): - """Run ``pip wheel`` for each interpreter into *raw_wheels_dir*.""" - log.info("Building wheels for %d interpreter(s) ...", len(interpreters)) +def select_interpreters(candidates): + """Split *candidates* into the one build interpreter and the smoke set. + + The wheel bundles a prebuilt .so and loads it via ctypes, so it is + py3-none (identical for every CPython). Building it once is enough; the + remaining oldest/newest interpreters only build-smoke the package to catch + setuptools compatibility breakage across the supported range. + """ + if not candidates: + return None, [] + + def _key(path): + m = re.search(r"cp3(\d+)", path) + return int(m.group(1)) if m else -1 + + ordered = sorted(candidates, key=_key) + build_py = next((p for p in ordered if "cp310" in p), ordered[0]) + smoke = [] + for p in (ordered[0], ordered[-1]): + if p != build_py and p not in smoke: + smoke.append(p) + return build_py, smoke + + +def build_wheel(pkg_dir, raw_wheels_dir, py): + """Run ``pip wheel`` once into *raw_wheels_dir* with interpreter *py*.""" + log.info("--- Building wheel with %s ---", py) + best_effort_pip_upgrade(py, ["pip", "setuptools", "wheel"]) + + for pattern in ("*.whl", "*.egg-info", "build", "dist"): + for path in pkg_dir.glob(pattern): + shutil.rmtree(path) if path.is_dir() else path.unlink() + + run( + [ + py, + "-m", + "pip", + "wheel", + "--no-deps", + "--no-build-isolation", + "-w", + str(raw_wheels_dir), + ".", + ], + cwd=str(pkg_dir), + ) + + +def build_smoke(pkg_dir, interpreters): + """Build (and discard) the package with each interpreter as a compat check.""" for py in interpreters: if not os.path.isfile(py) or not os.access(py, os.X_OK): - log.warning("Skipping missing/non-executable interpreter: %s", py) + log.warning("Skipping missing/non-executable smoke interpreter: %s", py) continue - - log.info("--- Building wheel with %s ---", py) + log.info("--- Build-smoke with %s ---", py) best_effort_pip_upgrade(py, ["pip", "setuptools", "wheel"]) - - for pattern in ("*.whl", "*.egg-info", "build", "dist"): - for path in pkg_dir.glob(pattern): - shutil.rmtree(path) if path.is_dir() else path.unlink() - - run( - [ - py, - "-m", - "pip", - "wheel", - "--no-deps", - "--no-build-isolation", - "-w", - str(raw_wheels_dir), - ".", - ], - cwd=str(pkg_dir), - ) + with tempfile.TemporaryDirectory() as tmp: + for pattern in ("*.egg-info", "build", "dist"): + for path in pkg_dir.glob(pattern): + shutil.rmtree(path) if path.is_dir() else path.unlink() + run( + [py, "-m", "pip", "wheel", "--no-deps", "--no-build-isolation", "-w", tmp, "."], + cwd=str(pkg_dir), + ) def repair_or_copy(raw_wheels, output_dir, cmake_python, repair): @@ -310,7 +349,6 @@ def repair_or_copy(raw_wheels, output_dir, cmake_python, repair): ).returncode == 0 ) - if not auditwheel_ok: log.warning("auditwheel not available; copying raw wheels.") for whl in raw_wheels: @@ -430,7 +468,8 @@ def main(): interpreters = collect_interpreters(args.python_bins) if not interpreters: abort("No Python interpreters found. Pass --python-bins explicitly.") - cmake_python = interpreters[0] + build_py, smoke_pys = select_interpreters(interpreters) + cmake_python = build_py log.info("OS variant : %s", args.os_variant or "(unspecified)") log.info("Project dir : %s", project_dir) @@ -438,7 +477,8 @@ def main(): log.info("Raw wheels : %s", raw_wheels_dir) log.info("Output dir : %s", output_dir) log.info("Python (cmake): %s", cmake_python) - log.info("Python targets: %s", ", ".join(interpreters)) + log.info("Publish wheel : %s", build_py) + log.info("Build-smoke : %s", ", ".join(smoke_pys) or "(none)") # esmi_ib_library_temp is generated by a stale build; remove it before # reconfiguring so cmake won't pick it up. @@ -470,7 +510,8 @@ def main(): cmake_python, ["pip", "setuptools", "wheel"] + (["auditwheel"] if args.repair else []) ) - build_wheels(pkg_dir, raw_wheels_dir, interpreters) + build_wheel(pkg_dir, raw_wheels_dir, build_py) + build_smoke(pkg_dir, smoke_pys) raw_wheels = sorted(raw_wheels_dir.glob("*.whl")) if not raw_wheels: From 7e68fa9a71d61f93d5ba82d12dc90818ad456174 Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Thu, 23 Jul 2026 10:13:43 -0500 Subject: [PATCH 23/35] build(amdsmi): tag the python wheel manylinux_2_28 and verify the bundled .so - the wheel ships a prebuilt libamd_smi_python.so, so a py3-none-any tag lets pip install it on incompatible platforms and fail at load time - add a setup.py that marks the distribution binary and overrides bdist_wheel to emit a non-pure py3-none platform wheel, which auditwheel can then stamp with the concrete manylinux tag - stage setup.py into the wheel package tree from cmake - fail the build when --repair is set but auditwheel is missing or the repair fails, instead of silently shipping an un-audited wheel Signed-off-by: Maisam Arif --- projects/amdsmi/py-interface/CMakeLists.txt | 3 ++ projects/amdsmi/py-interface/setup.py | 33 +++++++++++++++++++++ projects/amdsmi/tools/build_wheel.py | 20 ++++++++----- 3 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 projects/amdsmi/py-interface/setup.py diff --git a/projects/amdsmi/py-interface/CMakeLists.txt b/projects/amdsmi/py-interface/CMakeLists.txt index d3242ca4894e..d5760163d42b 100644 --- a/projects/amdsmi/py-interface/CMakeLists.txt +++ b/projects/amdsmi/py-interface/CMakeLists.txt @@ -113,8 +113,10 @@ add_custom_command( ${PY_PACKAGE_DIR}/amdsmi_interface_utils.py ${PY_PACKAGE_DIR}/README.md ${PY_PACKAGE_DIR}/LICENSE + ${PY_BUILD_DIR}/setup.py DEPENDS python_wrapper COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/__init__.py ${PY_PACKAGE_DIR}/ + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/setup.py ${PY_BUILD_DIR}/ COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/amdsmi_exception.py ${PY_PACKAGE_DIR}/ COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/amdsmi_interface.py ${PY_PACKAGE_DIR}/ COMMAND @@ -142,6 +144,7 @@ add_custom_target( ${PY_PACKAGE_DIR}/amdsmi_interface_utils.py ${PY_PACKAGE_DIR}/README.md ${PY_PACKAGE_DIR}/LICENSE + ${PY_BUILD_DIR}/setup.py ${PY_PACKAGE_DIR}/${AMDSMI_PYTHON_LIB_NAME} ) if(BUILD_PYTHON_WHEEL) diff --git a/projects/amdsmi/py-interface/setup.py b/projects/amdsmi/py-interface/setup.py new file mode 100644 index 000000000000..5502bd3aeee0 --- /dev/null +++ b/projects/amdsmi/py-interface/setup.py @@ -0,0 +1,33 @@ +# The wheel bundles a prebuilt libamd_smi_python.so and loads it via ctypes. +# It carries no Python C-extension, so it is ABI-independent across CPython +# versions (py3-none) but platform-specific (the .so is glibc/arch bound). +# Setuptools would otherwise tag it py3-none-any, which lets pip install it on +# any platform and then fail at load time. Force a non-pure, py3-none wheel so +# the platform tag reflects the bundled .so; auditwheel later stamps the +# concrete manylinux tag. +from setuptools import Distribution, setup + +try: + from wheel.bdist_wheel import bdist_wheel as _bdist_wheel +except ImportError: # wheel >= 0.44 relocated the module + from setuptools.command.bdist_wheel import bdist_wheel as _bdist_wheel + + +class BinaryDistribution(Distribution): + # Report a binary distribution so the bundled .so lands in platlib, which + # auditwheel requires to inspect and stamp the manylinux tag. + def has_ext_modules(self): + return True + + +class bdist_wheel(_bdist_wheel): + def finalize_options(self): + super().finalize_options() + self.root_is_pure = False + + def get_tag(self): + _, _, plat = super().get_tag() + return "py3", "none", plat + + +setup(cmdclass={"bdist_wheel": bdist_wheel}, distclass=BinaryDistribution) diff --git a/projects/amdsmi/tools/build_wheel.py b/projects/amdsmi/tools/build_wheel.py index 38b63f877c0c..db2f0c41ee10 100644 --- a/projects/amdsmi/tools/build_wheel.py +++ b/projects/amdsmi/tools/build_wheel.py @@ -335,7 +335,11 @@ def build_smoke(pkg_dir, interpreters): def repair_or_copy(raw_wheels, output_dir, cmake_python, repair): - """Run ``auditwheel repair`` if requested; otherwise just copy raw wheels.""" + """Copy raw wheels, or run ``auditwheel repair`` when *repair* is set. + + With *repair* the output must be a manylinux wheel; a missing auditwheel or + a failed repair is fatal rather than silently shipping the un-audited wheel. + """ if not repair: for whl in raw_wheels: shutil.copy2(whl, output_dir) @@ -350,10 +354,9 @@ def repair_or_copy(raw_wheels, output_dir, cmake_python, repair): == 0 ) if not auditwheel_ok: - log.warning("auditwheel not available; copying raw wheels.") - for whl in raw_wheels: - shutil.copy2(whl, output_dir) - return + abort( + "auditwheel requested (--repair) but not available; cannot produce a manylinux wheel." + ) log.info("Repairing wheels with auditwheel ...") for whl in raw_wheels: @@ -362,8 +365,11 @@ def repair_or_copy(raw_wheels, output_dir, cmake_python, repair): check=False, ) if result.returncode != 0: - log.warning("auditwheel repair failed for %s; copying raw wheel.", whl.name) - shutil.copy2(whl, output_dir) + abort( + "auditwheel repair failed for " + + whl.name + + "; refusing to ship an un-audited wheel." + ) # --------------------------------------------------------------------------- From 07d5ab99b977ed723fd13f236d65db317ac41ca1 Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Thu, 23 Jul 2026 10:39:33 -0500 Subject: [PATCH 24/35] fix(amdsmi): fall through to the system library when the TheRock lib fails to load - the loader returned early once the relative TheRock library path existed, so a present-but-unloadable file (missing dependencies) shadowed the dynamic-linker SONAME lookup and aborted the load - catch OSError from the relative load and continue to the system SONAME - apply the same fix to generator.py so a regenerated wrapper keeps it Signed-off-by: Maisam Arif --- projects/amdsmi/py-interface/amdsmi_wrapper.py | 9 +++++++-- projects/amdsmi/tools/generator.py | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/projects/amdsmi/py-interface/amdsmi_wrapper.py b/projects/amdsmi/py-interface/amdsmi_wrapper.py index 7f48c18ce834..93340ec0ef64 100644 --- a/projects/amdsmi/py-interface/amdsmi_wrapper.py +++ b/projects/amdsmi/py-interface/amdsmi_wrapper.py @@ -237,12 +237,17 @@ def _load_library(): # Relocatable ROCm tree: /lib/, one fixed location relative # to this file, tried before the bare-SONAME linker lookup (not a search). - # amdsmi_interface.py resolves librocm-core.so the same way. + # amdsmi_interface.py resolves librocm-core.so the same way. A + # present-but-unloadable file (missing deps) must not shadow the system + # linker lookup, so fall through on OSError. here = Path(__file__).resolve() if len(here.parents) > 3: relocatable = here.parents[3] / "lib" / _AMDSMI_LIB_SONAME if relocatable.exists(): - return ctypes.CDLL(str(relocatable), mode=mode), str(relocatable) + try: + return ctypes.CDLL(str(relocatable), mode=mode), str(relocatable) + except OSError: + pass return ctypes.CDLL(_AMDSMI_LIB_SONAME, mode=mode), _AMDSMI_LIB_SONAME diff --git a/projects/amdsmi/tools/generator.py b/projects/amdsmi/tools/generator.py index 4bd549fade2f..1e70a5f0e5bb 100644 --- a/projects/amdsmi/tools/generator.py +++ b/projects/amdsmi/tools/generator.py @@ -309,12 +309,17 @@ def _load_library(): # Relocatable ROCm tree: /lib/, one fixed location relative # to this file, tried before the bare-SONAME linker lookup (not a search). - # amdsmi_interface.py resolves librocm-core.so the same way. + # amdsmi_interface.py resolves librocm-core.so the same way. A + # present-but-unloadable file (missing deps) must not shadow the system + # linker lookup, so fall through on OSError. here = Path(__file__).resolve() if len(here.parents) > 3: relocatable = here.parents[3] / "lib" / _AMDSMI_LIB_SONAME if relocatable.exists(): - return ctypes.CDLL(str(relocatable), mode=mode), str(relocatable) + try: + return ctypes.CDLL(str(relocatable), mode=mode), str(relocatable) + except OSError: + pass return ctypes.CDLL(_AMDSMI_LIB_SONAME, mode=mode), _AMDSMI_LIB_SONAME From 7c4c9377c70c49dad983b1c0470bbde6b69fb235 Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Thu, 23 Jul 2026 10:40:17 -0500 Subject: [PATCH 25/35] fix(amdsmi): restrict package preun/prerm cleanup to a full erase - the leftovers, log folder, test dir and logrotate cleanups ran on upgrade as well, deleting the incoming package's state - gate them on a full erase (RPM $1 -eq 0, DEB the remove action) so only an uninstall triggers them - keep the old gpuv-smi symlink removal on both erase and upgrade --- projects/amdsmi/DEBIAN/prerm.in | 24 ++++++++++++------------ projects/amdsmi/RPM/preun.in | 8 ++++++-- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/projects/amdsmi/DEBIAN/prerm.in b/projects/amdsmi/DEBIAN/prerm.in index d5a24cc17137..c5b0001798a1 100755 --- a/projects/amdsmi/DEBIAN/prerm.in +++ b/projects/amdsmi/DEBIAN/prerm.in @@ -89,19 +89,8 @@ rm_logrotateConfig() { case "$1" in ( remove | upgrade) - # remove old gpuv-smi symlink + # remove old gpuv-smi symlink on both remove and upgrade rm -f @CPACK_PACKAGING_INSTALL_PREFIX@/bin/gpuv-smi &> /dev/null - echo "Removing AMDSMI Lib Packages..." - rm_ldconfig - echo "ldconfig removed" - rm_leftovers - echo "leftovers removed" - rm_logFolder - echo "log folder removed" - rm_rocm_tests_dir - echo "rocm tests directory removed" - rm_logrotateConfig - echo "logrotate configuration removed" ;; ( purge ) ;; @@ -109,3 +98,14 @@ case "$1" in exit 0 ;; esac + +if [ "$1" = "remove" ]; then + # only on a full remove -- on upgrade these would delete the incoming + # package's ldconfig entry, logs, test dir and logrotate config + echo "Removing AMDSMI Lib Packages..." + rm_ldconfig + rm_leftovers + rm_logFolder + rm_rocm_tests_dir + rm_logrotateConfig +fi diff --git a/projects/amdsmi/RPM/preun.in b/projects/amdsmi/RPM/preun.in index 78544d1b5303..ecc4d7e57844 100755 --- a/projects/amdsmi/RPM/preun.in +++ b/projects/amdsmi/RPM/preun.in @@ -61,9 +61,13 @@ rm_logrotateConfig() { } if [ "$1" -le 1 ]; then - # perform the below actions for rpm remove($1=0) or upgrade($1=1) operations - # remove old gpuv-smi symlink + # remove old gpuv-smi symlink on both remove($1=0) and upgrade($1=1) rm -f $RPM_INSTALL_PREFIX0/bin/gpuv-smi &> /dev/null +fi + +if [ "$1" -eq 0 ]; then + # only on a full erase -- on upgrade($1=1) these would delete the + # incoming package's logs, test dir and logrotate config rm_leftovers rm_logFolder rm_rocm_tests_dir From 9c1c087af54b8d9927c96ca5a2b1b0616489abc6 Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Thu, 23 Jul 2026 10:40:49 -0500 Subject: [PATCH 26/35] docs(amdsmi): correct the log-permission comment for root ownership - the comment implied the group-write bit lets same-group processes write, but the log is root-owned so no non-root user is in its group - restate that 0664 only matches the postinst mode and that withholding world-write is the security-relevant part Signed-off-by: Maisam Arif --- projects/amdsmi/rocm_smi/src/rocm_smi_logger.cc | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/projects/amdsmi/rocm_smi/src/rocm_smi_logger.cc b/projects/amdsmi/rocm_smi/src/rocm_smi_logger.cc index 2e66e7909952..d3c02773ac88 100644 --- a/projects/amdsmi/rocm_smi/src/rocm_smi_logger.cc +++ b/projects/amdsmi/rocm_smi/src/rocm_smi_logger.cc @@ -499,10 +499,11 @@ void ROCmLogging::Logger::initialize_resources() { if (m_File.fail()) { std::cout << "WARNING: Failed opening log file." << std::endl; } - // 0664: owner and group may write, others read-only. Must match the mode the - // package postinst sets; granting world-write here (S_IWOTH) would let any - // local user tamper with a root-owned log (CWE-732) and silently undo the - // packaging permission on the first root-run. + // 0664 to match the mode the package postinst sets and avoid churn on the + // first root-run. The log is root-owned, so the group-write bit has no + // practical effect; the security-relevant part is withholding world-write + // (S_IWOTH), which would let any local user tamper with a root-owned log + // (CWE-732). chmod(logFileName, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH); } From 233f03ea9da1d202ba3dd35fd3a353e4e47513ee Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Thu, 23 Jul 2026 10:46:09 -0500 Subject: [PATCH 27/35] build(amdsmi): use a clean PyPI version and expose the commit as metadata - the wheel version carried a +hash local suffix, which PyPI rejects on upload - add AMDSMI_WHEEL_RELEASE (and build_wheel.py --release) to emit a clean MAJOR.MINOR.PATCH version for the published wheel - keep +hash by default so per-commit CI wheels stay uniquely named - expose the commit as amdsmi.__commit__ so it is recoverable either way - pass --release on the published manylinux leg Signed-off-by: Maisam Arif --- .github/workflows/amdsmi-manylinux-build.yml | 4 +++- projects/amdsmi/CMakeLists.txt | 12 ++++++++++++ projects/amdsmi/py-interface/__init__.py | 2 +- projects/amdsmi/py-interface/_version.py.in | 3 ++- projects/amdsmi/py-interface/pyproject.toml.in | 2 +- projects/amdsmi/tools/build_wheel.py | 6 ++++++ 6 files changed, 25 insertions(+), 4 deletions(-) diff --git a/.github/workflows/amdsmi-manylinux-build.yml b/.github/workflows/amdsmi-manylinux-build.yml index c61973e27028..9f194c182b72 100644 --- a/.github/workflows/amdsmi-manylinux-build.yml +++ b/.github/workflows/amdsmi-manylinux-build.yml @@ -55,6 +55,7 @@ jobs: python-bin: /opt/python/cp310-cp310/bin/python3 os-variant: AlmaLinux8 repair-flag: '--repair' + release-flag: '--release' artifact-prefix: amdsmi-rpm-wheels - id: debian name: Build Debian Wheels (Ubuntu 22.04) @@ -63,6 +64,7 @@ jobs: python-bin: python3 os-variant: Ubuntu22 repair-flag: '' + release-flag: '' artifact-prefix: amdsmi-debian-wheels steps: @@ -91,7 +93,7 @@ jobs: --build-dir "$AMDSMI_WHEEL_BUILD_DIR" \ --output-dir "$GITHUB_WORKSPACE/wheels" \ --os-variant ${{matrix.target.os-variant}} \ - ${{matrix.target.repair-flag}} + ${{matrix.target.repair-flag}} ${{matrix.target.release-flag}} - name: Verify wheels exist run: | diff --git a/projects/amdsmi/CMakeLists.txt b/projects/amdsmi/CMakeLists.txt index 3f2077ea255d..7991e3175fa9 100644 --- a/projects/amdsmi/CMakeLists.txt +++ b/projects/amdsmi/CMakeLists.txt @@ -62,6 +62,18 @@ set(${AMD_SMI_LIBS_TARGET}_VERSION_STRING "${${AMD_SMI_LIBS_TARGET}_VERSION_MAJOR}.${${AMD_SMI_LIBS_TARGET}_VERSION_MINOR}.${${AMD_SMI_LIBS_TARGET}_VERSION_PATCH}+${${AMD_SMI_LIBS_TARGET}_VERSION_HASH}" ) +# PyPI rejects a PEP 440 local version (the +hash suffix), so a published wheel +# needs a clean MAJOR.MINOR.PATCH. Per-commit CI wheels keep +hash for unique +# filenames; the commit is always recoverable via amdsmi.__commit__. +option(AMDSMI_WHEEL_RELEASE "Use a clean PEP 440 version (no +hash) for the wheel" OFF) +if(AMDSMI_WHEEL_RELEASE) + set(${AMD_SMI_LIBS_TARGET}_WHEEL_VERSION + "${${AMD_SMI_LIBS_TARGET}_VERSION_MAJOR}.${${AMD_SMI_LIBS_TARGET}_VERSION_MINOR}.${${AMD_SMI_LIBS_TARGET}_VERSION_PATCH}" + ) +else() + set(${AMD_SMI_LIBS_TARGET}_WHEEL_VERSION "${${AMD_SMI_LIBS_TARGET}_VERSION_STRING}") +endif() + set(DEFAULT_VERSION "${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}") update_version_in_file("include/amd_smi/amdsmi.h" ${DEFAULT_VERSION} "#define AMDSMI_LIB_VERSION_" " *" " ") update_version_in_file("rust-interface/src/amdsmi_wrapper.rs" ${DEFAULT_VERSION} "AMDSMI_LIB_VERSION_" " *: *u32 *= *" diff --git a/projects/amdsmi/py-interface/__init__.py b/projects/amdsmi/py-interface/__init__.py index bf49a6bc4218..cd0291b52f4f 100644 --- a/projects/amdsmi/py-interface/__init__.py +++ b/projects/amdsmi/py-interface/__init__.py @@ -18,7 +18,7 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # Library Version is the tool/amdsmi_interface version -from ._version import __version__ +from ._version import __commit__, __version__ # Library Initialization from .amdsmi_interface import amdsmi_init diff --git a/projects/amdsmi/py-interface/_version.py.in b/projects/amdsmi/py-interface/_version.py.in index a8869a016581..9b72cc5848b4 100644 --- a/projects/amdsmi/py-interface/_version.py.in +++ b/projects/amdsmi/py-interface/_version.py.in @@ -1 +1,2 @@ -__version__ = "@amd_smi_lib_VERSION_STRING@" +__version__ = "@amd_smi_lib_WHEEL_VERSION@" +__commit__ = "@amd_smi_lib_VERSION_HASH@" diff --git a/projects/amdsmi/py-interface/pyproject.toml.in b/projects/amdsmi/py-interface/pyproject.toml.in index 9bbe6f8e0e50..e2682d5830bb 100644 --- a/projects/amdsmi/py-interface/pyproject.toml.in +++ b/projects/amdsmi/py-interface/pyproject.toml.in @@ -10,7 +10,7 @@ name = "amdsmi" authors = [ {name = "AMD", email = "amd-smi.support@amd.com"}, ] -version = "@amd_smi_lib_VERSION_STRING@" +version = "@amd_smi_lib_WHEEL_VERSION@" license = {file = "amdsmi/LICENSE"} readme = {file = "amdsmi/README.md", content-type = "text/markdown"} description = "AMDSMI Python LIB - AMD GPU Monitoring Library" diff --git a/projects/amdsmi/tools/build_wheel.py b/projects/amdsmi/tools/build_wheel.py index db2f0c41ee10..8aed05406407 100644 --- a/projects/amdsmi/tools/build_wheel.py +++ b/projects/amdsmi/tools/build_wheel.py @@ -258,6 +258,7 @@ def cmake_and_build(project_dir, build_dir, cmake_python, args, extra_env): "-DBUILD_TESTS=" + ("ON" if args.build_tests else "OFF"), "-DENABLE_ESMI_LIB=" + ("ON" if args.enable_esmi else "OFF"), "-DBUILD_PYTHON_WHEEL=ON", + "-DAMDSMI_WHEEL_RELEASE=" + ("ON" if args.release else "OFF"), "-DCMAKE_BUILD_TYPE=" + args.build_type, "-DPython3_EXECUTABLE=" + cmake_python, ], @@ -446,6 +447,11 @@ def parse_args(): "--no-repair", dest="repair", action="store_false", help="Skip the auditwheel repair step." ) p.add_argument("--build-tests", action="store_true", help="Build C/C++ tests [default: OFF].") + p.add_argument( + "--release", + action="store_true", + help="Use a clean PEP 440 version (no +hash) for a PyPI-publishable wheel.", + ) p.add_argument( "--clean", action="store_true", From 0cf0fae53d8382170bc41a31b9703c97ba1cd866 Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Thu, 23 Jul 2026 10:47:09 -0500 Subject: [PATCH 28/35] fix(amdsmi): make the wheel fallback-flag staging idempotent - a rebuild reuses the already-flipped staged wrapper, so the script saw zero occurrences of the True anchor and aborted the second build - treat an already-disabled flag as a no-op, while still failing when neither the anchor nor the replacement is present Signed-off-by: Maisam Arif --- projects/amdsmi/tools/disable_system_fallback.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/projects/amdsmi/tools/disable_system_fallback.py b/projects/amdsmi/tools/disable_system_fallback.py index 8c2356128a6d..fb1cf720c979 100644 --- a/projects/amdsmi/tools/disable_system_fallback.py +++ b/projects/amdsmi/tools/disable_system_fallback.py @@ -23,6 +23,14 @@ def main(argv: List[str]) -> None: sys.exit(f"usage: {argv[0]} ") path = pathlib.Path(argv[1]) text = path.read_text(encoding="utf-8") + + # A rebuild reuses the already-flipped staged copy (copy_if_different does + # not re-copy an unchanged source), so treat an already-disabled flag as a + # no-op instead of failing. + if ANCHOR not in text and text.count(REPLACEMENT) == 1: + print(f"[disable_system_fallback] {path}: already disabled, nothing to do") + return + count = text.count(ANCHOR) if count != 1: sys.exit( From 6d6ad51588321d358ec89bcaa0faefeb254f45f7 Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Fri, 24 Jul 2026 02:04:27 -0500 Subject: [PATCH 29/35] docs(amdsmi): clarify the manylinux wheel build directory - state that AMDSMI_WHEEL_BUILD_DIR mirrors build_wheel.py's default build path, so its origin is clear at the point of use Signed-off-by: Maisam Arif --- .github/workflows/amdsmi-manylinux-build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/amdsmi-manylinux-build.yml b/.github/workflows/amdsmi-manylinux-build.yml index 9f194c182b72..5b2c802f2188 100644 --- a/.github/workflows/amdsmi-manylinux-build.yml +++ b/.github/workflows/amdsmi-manylinux-build.yml @@ -37,10 +37,10 @@ jobs: name: ${{matrix.target.name}} runs-on: ${{matrix.target.runs-on}} container: ${{matrix.target.container}} - # Shared build directory: build_wheel.py writes the CMake build tree here, - # and the SONAME conflict check reads both libraries back from it. Setting - # it explicitly (rather than relying on build_wheel.py's default) keeps the - # two steps coupled to one value. + # Build directory for build_wheel.py's CMake tree. This mirrors + # build_wheel.py's own default (/tmp/amdsmi-build); it is set explicitly + # here so the build step and the SONAME conflict check, which reads both + # libraries back from it, stay coupled to one value. env: AMDSMI_WHEEL_BUILD_DIR: /tmp/amdsmi-build From a82d94783573ae3580427e2afb4b910aabc66487 Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Fri, 24 Jul 2026 02:35:11 -0500 Subject: [PATCH 30/35] ci(amdsmi): tighten the manylinux wheel job - sparse-checkout only projects/amdsmi and the workflows dir instead of the whole monorepo - assert the imported amdsmi resolves under pip's install location so the smoke test proves the built wheel is what loads, not another copy - collapse the two identical wheel uploads into one SHA-named artifact kept for manual download Signed-off-by: Maisam Arif --- .github/workflows/amdsmi-manylinux-build.yml | 22 +++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/.github/workflows/amdsmi-manylinux-build.yml b/.github/workflows/amdsmi-manylinux-build.yml index 5b2c802f2188..415ea6da0af7 100644 --- a/.github/workflows/amdsmi-manylinux-build.yml +++ b/.github/workflows/amdsmi-manylinux-build.yml @@ -68,7 +68,13 @@ jobs: artifact-prefix: amdsmi-debian-wheels steps: + # rocm-systems is a large monorepo; the wheel build only needs the amdsmi + # project, so sparse-checkout keeps the fetch cheap. - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + sparse-checkout: | + projects/amdsmi + .github/workflows - name: Install Debian build prerequisites if: matrix.target.id == 'debian' @@ -113,24 +119,20 @@ jobs: run: | ${{matrix.target.python-bin}} -m pip install --upgrade pip ${{matrix.target.python-bin}} -m pip install "$GITHUB_WORKSPACE"/wheels/*.whl - ${{matrix.target.python-bin}} -c "import amdsmi; print('amdsmi import OK')" + # Prove the installed wheel is what imports (resolves under pip's + # location), not some other amdsmi on the path. + ${{matrix.target.python-bin}} -c "import os, subprocess, amdsmi; out = subprocess.check_output(['${{matrix.target.python-bin}}', '-m', 'pip', 'show', 'amdsmi'], text=True); loc = next((l.split(':', 1)[1].strip() for l in out.splitlines() if l.startswith('Location:')), ''); p = os.path.realpath(amdsmi.__file__); print('import from', p, 'pip loc', loc); assert loc and p.startswith(os.path.realpath(loc)), 'wheel is not the imported amdsmi'; print('amdsmi import OK')" # The wheel must ship with the system-library fallback disabled, so a # build-step ordering regression that leaves it enabled fails here # rather than shipping a wheel that can silently load a system .so. ${{matrix.target.python-bin}} -c "import amdsmi.amdsmi_wrapper as w; assert not w._AMDSMI_ALLOW_SYSTEM_FALLBACK, 'wheel shipped with system fallback enabled'; print('fallback disabled OK')" - - name: Upload wheels (by SHA) + # Not consumed by any downstream job; kept so a reviewer can download the + # built wheel from a run. Named by commit SHA for traceability. + - name: Upload wheels uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: name: ${{matrix.target.artifact-prefix}}-${{github.sha}} path: ${{github.workspace}}/wheels/*.whl if-no-files-found: error retention-days: 30 - - - name: Upload wheels (downloadable) - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 - with: - name: ${{matrix.target.artifact-prefix}} - path: ${{github.workspace}}/wheels/*.whl - if-no-files-found: warn - retention-days: 90 From 2e3d5f689a00a70d2b7a363a355a6c2b0b50cfc6 Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Fri, 24 Jul 2026 10:46:57 -0500 Subject: [PATCH 31/35] docs(amdsmi): note that PYTHONPATH overrides module precedence - the coexistence table describes default sys.path ordering between install locations, but a global PYTHONPATH precedes all of them - document that some ROCm container images set PYTHONPATH=/opt/rocm/share/amd_smi, which makes the share copy win over a pip wheel, and how to restore install-location precedence Signed-off-by: Maisam Arif --- projects/amdsmi/docs/packaging.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/projects/amdsmi/docs/packaging.md b/projects/amdsmi/docs/packaging.md index 5af50cefe8d2..5bc560e7feac 100644 --- a/projects/amdsmi/docs/packaging.md +++ b/projects/amdsmi/docs/packaging.md @@ -62,6 +62,14 @@ the wheel: | rpm + pip `--user` wheel (RHEL) | wheel in `~/.local/.../site-packages` (precedes system) | No — rpm removes only its own files | | deb/rpm + venv wheel | wheel in the venv (isolated) | No — separate tree | +The precedence above is the default `sys.path` ordering between install +locations. A global `PYTHONPATH` overrides it: Python searches `PYTHONPATH` +entries before any install location, so `import amdsmi` resolves there +regardless of where the wheel or package installed. Some ROCm container images +set `PYTHONPATH=/opt/rocm/share/amd_smi`, which makes the `share/amd_smi` copy +win over a pip wheel in that environment. Unset `PYTHONPATH` (or point it at the +copy you want) to restore the install-location precedence. + ## Support matrix Legend: ✅ supported and tested · 🟡 supported, pick one recommended · ⛔ unsupported. From 862bd8465315526a11b689fb3a12cea007d76509 Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Sun, 26 Jul 2026 18:53:53 -0500 Subject: [PATCH 32/35] ci(amdsmi): set PROJECT_DIR as a job-level env var Replace the separate Set Project Directory step with a job-level env entry, since the path is a fixed constant. Signed-off-by: Maisam Arif --- .github/workflows/amdsmi-build.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/amdsmi-build.yml b/.github/workflows/amdsmi-build.yml index 3cb77bbbc267..ef393ed1143b 100644 --- a/.github/workflows/amdsmi-build.yml +++ b/.github/workflows/amdsmi-build.yml @@ -152,6 +152,9 @@ jobs: container: image: ${{ vars[format('{0}_DOCKER_IMAGE', matrix.os)] }} options: --rm --privileged --device=/dev/kfd --device=/dev/dri --group-add video --cap-add=SYS_PTRACE --security-opt seccomp=unconfined --shm-size=64G --cap-add=SYS_MODULE -v /lib/modules:/lib/modules -u root + env: + # sparse-checkout places the project at a fixed path. + PROJECT_DIR: ${{ github.workspace }}/projects/amdsmi steps: # rocm-systems is a large monorepo; only the amdsmi project and this @@ -172,12 +175,6 @@ jobs: echo "PR_NUMBER=PR${{ github.event.pull_request.number }}" >> $GITHUB_ENV echo "BUILD_DATE=$(date -u +%m%d%y-%H%M)" >> $GITHUB_ENV - - name: Set Project Directory - run: | - set -euo pipefail - # sparse-checkout places the project at a fixed path. - echo "PROJECT_DIR=$GITHUB_WORKSPACE/projects/amdsmi" >> $GITHUB_ENV - - name: Run AMDSMI Build and Install run: | set -euo pipefail From 10d1d267abec50eeaa50131fca0daf2c393614de Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Tue, 28 Jul 2026 20:04:13 -0500 Subject: [PATCH 33/35] fix(amdsmi): review fixes for the packaging loader and wheel staging - Dedupe the wrapper _libraries init: the ctypesgen preamble already initializes it, so the injected loader block no longer re-declares it (wrapper regenerated from generator.py). - Read and write the staged wrapper as UTF-8 in disable_system_fallback.py so the wheel flag flip is deterministic regardless of the build locale. - Assert the idempotent no-op in the fallback double-run guard, and add a case that the tool refuses (non-zero exit) an ambiguous wrapper. - Cover the relocatable OSError fall-through: a present-but-unloadable /lib/ must fall through to the system SONAME. - Reword a comment to drop non-standard phrasing. Signed-off-by: Maisam Arif --- projects/amdsmi/py-interface/CMakeLists.txt | 4 +- .../amdsmi/py-interface/amdsmi_wrapper.py | 2 - .../amdsmi/tests/python/test_abi_compat.py | 58 ++++++++++++++++++- .../amdsmi/tools/disable_system_fallback.py | 2 +- projects/amdsmi/tools/generator.py | 2 - 5 files changed, 60 insertions(+), 8 deletions(-) diff --git a/projects/amdsmi/py-interface/CMakeLists.txt b/projects/amdsmi/py-interface/CMakeLists.txt index d5760163d42b..7b635fc8ee5e 100644 --- a/projects/amdsmi/py-interface/CMakeLists.txt +++ b/projects/amdsmi/py-interface/CMakeLists.txt @@ -25,7 +25,7 @@ set(AMDSMI_PYTHON_LIB_PATH "${PROJECT_BINARY_DIR}/src/${AMDSMI_PYTHON_LIB_NAME}" set(GOOD_CLANG_FOUND FALSE) # only try to re-generate amdsmi_wrapper.py if BUILD_WRAPPER is on/true if(BUILD_WRAPPER) - # The committed wrapper is canonical: its loader and every ctypes binding + # The committed wrapper is authoritative: its loader and every ctypes binding # key on 'libamd_smi.so'. With BUILD_PYTHON_WHEEL=ON the generator is fed # -l libamd_smi_python.so, so ctypeslib would emit bindings keyed on # 'libamd_smi_python.so' while the loader still populates 'libamd_smi.so' -- @@ -35,7 +35,7 @@ if(BUILD_WRAPPER) if(BUILD_PYTHON_WHEEL) message( FATAL_ERROR - "BUILD_WRAPPER=ON is incompatible with BUILD_PYTHON_WHEEL=ON: regenerating the wrapper against libamd_smi_python.so produces bindings that mismatch the canonical libamd_smi.so loader. Regenerate the wrapper with BUILD_PYTHON_WHEEL=OFF, then build the wheel from the committed wrapper." + "BUILD_WRAPPER=ON is incompatible with BUILD_PYTHON_WHEEL=ON: regenerating the wrapper against libamd_smi_python.so produces bindings that mismatch the committed libamd_smi.so loader. Regenerate the wrapper with BUILD_PYTHON_WHEEL=OFF, then build the wheel from the committed wrapper." ) endif() find_program(clang NAMES clang REQUIRED) diff --git a/projects/amdsmi/py-interface/amdsmi_wrapper.py b/projects/amdsmi/py-interface/amdsmi_wrapper.py index 93340ec0ef64..67b57849effb 100644 --- a/projects/amdsmi/py-interface/amdsmi_wrapper.py +++ b/projects/amdsmi/py-interface/amdsmi_wrapper.py @@ -191,8 +191,6 @@ def char_pointer_cast(string, encoding='utf-8'): # alternate .so explicitly. # --------------------------------------------------------------------------- -_libraries = {} - # SONAME the system rpm/deb ships and the relocatable tree names. Always the # system lib, never the wheel-private libamd_smi_python.so, even when the diff --git a/projects/amdsmi/tests/python/test_abi_compat.py b/projects/amdsmi/tests/python/test_abi_compat.py index bdbac56fcbfd..617e08ef406d 100644 --- a/projects/amdsmi/tests/python/test_abi_compat.py +++ b/projects/amdsmi/tests/python/test_abi_compat.py @@ -361,6 +361,40 @@ def test_wheel_refuses_system_fallback_when_bundle_missing(self): mod._load_library() self.assertIn("refusing to fall back", str(ctx.exception)) + @unittest.skipUnless(WRAPPER_SRC.is_file(), "amdsmi_wrapper.py not found") + def test_relocatable_unloadable_lib_falls_through_to_system(self): + # TheRock relocatable layout: wrapper at /share/amd_smi/amdsmi, + # library at /lib. A present-but-unloadable relocatable .so + # (missing deps -> OSError) must fall through to the system SONAME + # rather than shadow it. + import re as _re + + soname = _re.search(r'_AMDSMI_LIB_SONAME = "([^"]+)"', WRAPPER_SRC.read_text()).group(1) + pkg = self._tmp / "share" / "amd_smi" / "amdsmi" + pkg.mkdir(parents=True) + shutil.copy(WRAPPER_SRC, pkg / "amdsmi_wrapper.py") + reloc = self._tmp / "lib" / soname + reloc.parent.mkdir() + reloc.write_bytes(b"") # present, but CDLL will raise below + attempted = [] + orig = ctypes.CDLL + + def _cdll(path, mode=0): + attempted.append(str(path)) + if str(path) == str(reloc): + raise OSError("simulated missing dependency") + return FakeCDLL(path, mode, STABLE_SYMBOLS) + + ctypes.CDLL = _cdll + try: + mod = _import_wrapper_from(str(pkg), self._modname) + finally: + ctypes.CDLL = orig + # the relocatable path was tried, then the loader fell through to the + # bare system SONAME instead of returning a broken/_MissingLibrary. + self.assertIn(str(reloc), attempted) + self.assertEqual(mod._loaded_lib_path, mod._AMDSMI_LIB_SONAME) + class DisableSystemFallbackToolTest(unittest.TestCase): """tools/disable_system_fallback.py flips the loader flag exactly once.""" @@ -379,7 +413,29 @@ def test_flag_flipped_and_double_run_guarded(self): patched = wrapper.read_text() self.assertIn("_AMDSMI_ALLOW_SYSTEM_FALLBACK = False", patched) self.assertNotIn("_AMDSMI_ALLOW_SYSTEM_FALLBACK = True", patched) - # Anchor is gone now -> a second run must fail (count != 1). + # Anchor is gone now -> a second run is a no-op (idempotent), so a + # rebuild that reuses the already-flipped staged wrapper succeeds. + rc = subprocess.call([sys.executable, str(DISABLE_SYSTEM_FALLBACK_TOOL), str(wrapper)]) + self.assertEqual(rc, 0) + self.assertIn("_AMDSMI_ALLOW_SYSTEM_FALLBACK = False", wrapper.read_text()) + finally: + shutil.rmtree(tmp, ignore_errors=True) + + @unittest.skipUnless( + WRAPPER_SRC.is_file() and DISABLE_SYSTEM_FALLBACK_TOOL.is_file(), + "wrapper or disable_system_fallback.py not found (installed layout)", + ) + def test_ambiguous_wrapper_fails_loud(self): + # Neither the True anchor nor exactly one False replacement present: + # the tool must refuse (non-zero exit) rather than stage a wheel with + # an ambiguous loader-fallback flag. + tmp = Path(tempfile.mkdtemp(prefix="amdsmi-disable-")) + try: + wrapper = tmp / "amdsmi_wrapper.py" + text = WRAPPER_SRC.read_text().replace( + "_AMDSMI_ALLOW_SYSTEM_FALLBACK = True", "_AMDSMI_ALLOW_SYSTEM_FALLBACK_UNSET = True" + ) + wrapper.write_text(text) rc = subprocess.call([sys.executable, str(DISABLE_SYSTEM_FALLBACK_TOOL), str(wrapper)]) self.assertNotEqual(rc, 0) finally: diff --git a/projects/amdsmi/tools/disable_system_fallback.py b/projects/amdsmi/tools/disable_system_fallback.py index fb1cf720c979..59bfefd084d3 100644 --- a/projects/amdsmi/tools/disable_system_fallback.py +++ b/projects/amdsmi/tools/disable_system_fallback.py @@ -37,7 +37,7 @@ def main(argv: List[str]) -> None: f"expected exactly one '{ANCHOR}' in {path}, found {count}; " "refusing to build a wheel with an ambiguous loader fallback flag" ) - path.write_text(text.replace(ANCHOR, REPLACEMENT, 1)) + path.write_text(text.replace(ANCHOR, REPLACEMENT, 1), encoding="utf-8") print(f"[disable_system_fallback] {path}: system library fallback disabled for wheel") diff --git a/projects/amdsmi/tools/generator.py b/projects/amdsmi/tools/generator.py index 1e70a5f0e5bb..0a9a93cb187a 100644 --- a/projects/amdsmi/tools/generator.py +++ b/projects/amdsmi/tools/generator.py @@ -263,8 +263,6 @@ def main(): # alternate .so explicitly. # --------------------------------------------------------------------------- -_libraries = {{}} - # SONAME the system rpm/deb ships and the relocatable tree names. Always the # system lib, never the wheel-private libamd_smi_python.so, even when the From 8418878822d34ac42c5db1e9db7748f097196e55 Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Tue, 28 Jul 2026 22:37:48 -0500 Subject: [PATCH 34/35] ci(amdsmi): set PROJECT_DIR inside the container, not from github.workspace The build/test jobs run in a container where the repo is mounted at a different path than the host runner. Job-level env PROJECT_DIR resolved ${{ github.workspace }} to the HOST path, so python3 could not open run_amdsmi_build.py inside the container (No such file or directory). Set PROJECT_DIR in a step from $GITHUB_WORKSPACE, which is the in-container mount, matching the working pattern on develop. Signed-off-by: Maisam Arif --- .github/workflows/amdsmi-build.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/amdsmi-build.yml b/.github/workflows/amdsmi-build.yml index ef393ed1143b..c172d5caaa94 100644 --- a/.github/workflows/amdsmi-build.yml +++ b/.github/workflows/amdsmi-build.yml @@ -152,9 +152,6 @@ jobs: container: image: ${{ vars[format('{0}_DOCKER_IMAGE', matrix.os)] }} options: --rm --privileged --device=/dev/kfd --device=/dev/dri --group-add video --cap-add=SYS_PTRACE --security-opt seccomp=unconfined --shm-size=64G --cap-add=SYS_MODULE -v /lib/modules:/lib/modules -u root - env: - # sparse-checkout places the project at a fixed path. - PROJECT_DIR: ${{ github.workspace }}/projects/amdsmi steps: # rocm-systems is a large monorepo; only the amdsmi project and this @@ -168,6 +165,14 @@ jobs: - name: Mark workspace safe for git run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + - name: Set Project Directory + run: | + set -euo pipefail + # Evaluate inside the container: ${{ github.workspace }} is the host + # runner path, but these steps run in a container where the repo is + # mounted elsewhere. $GITHUB_WORKSPACE is the in-container mount. + echo "PROJECT_DIR=$GITHUB_WORKSPACE/projects/amdsmi" >> $GITHUB_ENV + - name: Set Artifact Metadata if: github.event_name == 'pull_request' run: | From 8237ff3e487506a46c5430dabef38d9b538502fa Mon Sep 17 00:00:00 2001 From: Maisam Arif Date: Wed, 29 Jul 2026 02:52:39 -0500 Subject: [PATCH 35/35] docs(amdsmi): correct the PyPI wheel CLI support in the packaging matrix The standalone PyPI wheel ships only the amdsmi Python bindings; it declares no console_scripts entry point, so it does not provide the amd-smi CLI. Mark that matrix cell unsupported. Signed-off-by: Maisam Arif --- projects/amdsmi/docs/packaging.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/amdsmi/docs/packaging.md b/projects/amdsmi/docs/packaging.md index 5bc560e7feac..8ccdbe79a4f1 100644 --- a/projects/amdsmi/docs/packaging.md +++ b/projects/amdsmi/docs/packaging.md @@ -82,7 +82,7 @@ Legend: ✅ supported and tested · 🟡 supported, pick one recommended · ⛔ | tarball | ✅ | ⛔ (module not installed) | | ROCm pip | ✅ | ✅ | | ROCm pip in venv | ✅ | ✅ | -| PyPI wheel | ✅ | ✅ | +| PyPI wheel | ⛔ (Python bindings only) | ✅ | ### Two paths together