CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
479 lines
18 KiB
Python
479 lines
18 KiB
Python
"""Setuptools shim — runs `scripts/build-bundled-opencode.sh` automatically
|
|
during `pip install` (wheel + editable) so the patched opencode binary is
|
|
bundled into ``perfxpert/_bundled/opencode`` as part of install.
|
|
|
|
Project metadata lives in ``pyproject.toml``. This file only exists to
|
|
inject a pre-build hook. Deleting it would leave users needing to run
|
|
the build script manually.
|
|
|
|
Behavior:
|
|
* Pre-build (``build_py``), pre-editable-install (``develop`` /
|
|
``editable_wheel``): invoke the build script if the bundled binary
|
|
is missing OR older than the patch series.
|
|
* If ``bun`` is not on PATH: fail with distro-specific package-manager
|
|
guidance by default. Set ``PERFXPERT_AUTO_INSTALL_BUN=1`` only when you
|
|
explicitly want setup.py to bootstrap bun into the user's home directory
|
|
for this build.
|
|
* Opt-out via ``PERFXPERT_SKIP_BUNDLED_BUILD=1`` — useful in
|
|
tightly-sandboxed CI where network isn't available and the interactive
|
|
``perfxpert-code`` TUI is intentionally not being built.
|
|
* The setup hook will initialize the repo-pinned opencode submodule
|
|
when possible. If the source tree is missing that submodule, it fails
|
|
with an actionable message rather than cloning a mutable upstream tag
|
|
during install.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from setuptools import setup
|
|
from setuptools.command.build_py import build_py as _build_py
|
|
from setuptools.command.develop import develop as _develop
|
|
|
|
|
|
# -------------------------------------------------------------------
|
|
# pip 22 / setuptools <61 bootstrap guard (Phase 8 Blocker 4).
|
|
#
|
|
# Stock ``rocm/dev-ubuntu-22.04:latest`` ships pip 22.0.2 + setuptools
|
|
# 59.x. Even though ``pyproject.toml`` declares
|
|
# ``[build-system] requires = ["setuptools>=61"]``, pre-PEP-517 pip
|
|
# invokes ``setup.py`` directly and the old setuptools writes
|
|
# ``UNKNOWN`` as the wheel metadata name — pip then rejects the wheel
|
|
# with the misleading error ``filename has 'perfxpert', but metadata
|
|
# has 'unknown'``. Refuse with a CLEAR, actionable message instead.
|
|
# -------------------------------------------------------------------
|
|
def _guard_old_setuptools() -> None:
|
|
try:
|
|
import setuptools # noqa: F401 — we just need __version__
|
|
except ImportError:
|
|
return
|
|
ver_str = getattr(setuptools, "__version__", "") or ""
|
|
try:
|
|
# Parse the first dotted segment as major; forgiving of pre-release
|
|
# suffixes ("61.0.0rc1" → 61).
|
|
major = int(ver_str.split(".")[0].split("-")[0])
|
|
except (ValueError, IndexError):
|
|
return
|
|
if major < 61:
|
|
msg = (
|
|
"\n\n"
|
|
"[perfxpert/setup.py] setuptools {v} is too old (requires >= 61).\n"
|
|
" Old setuptools writes 'UNKNOWN' into the wheel metadata and\n"
|
|
" pip rejects the build with 'filename has \"perfxpert\", but\n"
|
|
" metadata has \"unknown\"'. Fix:\n\n"
|
|
" pip install -U pip setuptools wheel\n\n"
|
|
" Then re-run your pip install command.\n"
|
|
).format(v=ver_str or "<unknown>")
|
|
print(msg, file=sys.stderr)
|
|
raise SystemExit(1)
|
|
|
|
|
|
_guard_old_setuptools()
|
|
|
|
try: # PEP 660 — available on setuptools 64+
|
|
from setuptools.command.editable_wheel import editable_wheel as _editable_wheel
|
|
except ImportError: # older setuptools
|
|
_editable_wheel = None # type: ignore[assignment]
|
|
|
|
|
|
_HERE = Path(__file__).resolve().parent
|
|
_BUILD_SCRIPT = _HERE / "scripts" / "build-bundled-opencode.sh"
|
|
_BUNDLE_PATH = _HERE / "perfxpert" / "_bundled" / "opencode"
|
|
_PATCHES_DIR = _HERE / ".patches"
|
|
_OPENCODE_DIR = _HERE / "opencode"
|
|
_SKIP_ENV = "PERFXPERT_SKIP_BUNDLED_BUILD"
|
|
_SKIP_OPENCODE_FETCH_ENV = "PERFXPERT_SKIP_OPENCODE_FETCH"
|
|
_AUTO_INSTALL_BUN_ENV = "PERFXPERT_AUTO_INSTALL_BUN"
|
|
_DEFAULT_BUN_VERSION = "1.2.23"
|
|
_DEFAULT_BUN_INSTALL_URL = "https://bun.sh/install"
|
|
|
|
|
|
def _package_manager_prereq_hint() -> str:
|
|
return (
|
|
"Install the OS prerequisites first:\n"
|
|
" Ubuntu 22.04 / 24.04:\n"
|
|
" apt install -y curl git unzip python3-venv python3-pip\n"
|
|
" RHEL 9:\n"
|
|
" command -v curl >/dev/null || dnf install -y curl\n"
|
|
" dnf install -y git unzip python3.11 python3.11-pip\n"
|
|
" RHEL 10:\n"
|
|
" command -v curl >/dev/null || dnf install -y curl\n"
|
|
" dnf install -y git unzip python3 python3-pip\n"
|
|
" SLES 15.6:\n"
|
|
" zypper install -y curl git unzip python311 python311-pip\n"
|
|
)
|
|
|
|
|
|
def _missing_tools(tools: tuple[str, ...]) -> list[str]:
|
|
missing: list[str] = []
|
|
for tool in tools:
|
|
if shutil.which(tool) is None:
|
|
missing.append(tool)
|
|
return missing
|
|
|
|
|
|
def _missing_build_prereqs() -> list[str]:
|
|
return _missing_tools(("bash", "git"))
|
|
|
|
|
|
def _missing_bun_bootstrap_prereqs() -> list[str]:
|
|
return _missing_tools(("bash", "curl", "unzip"))
|
|
|
|
|
|
def _env_truthy(name: str) -> bool:
|
|
return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"}
|
|
|
|
|
|
def _fail_build(message: str) -> None:
|
|
print(
|
|
f"[perfxpert/setup.py] ERROR: {message}\n\n{_package_manager_prereq_hint()}",
|
|
file=sys.stderr,
|
|
)
|
|
raise SystemExit(1)
|
|
|
|
|
|
def _run_bun_install_script(env: dict[str, str]) -> int:
|
|
# Internal mirrors may override the installer URL, but only after the
|
|
# caller has explicitly opted into automatic bun bootstrap.
|
|
url = os.environ.get("PERFXPERT_BUN_INSTALL_URL", _DEFAULT_BUN_INSTALL_URL)
|
|
missing = _missing_bun_bootstrap_prereqs()
|
|
if missing:
|
|
_fail_build(
|
|
"missing OS prerequisites for bun bootstrap: "
|
|
+ ", ".join(missing)
|
|
+ "."
|
|
)
|
|
curl = shutil.which("curl")
|
|
bash = shutil.which("bash")
|
|
assert curl is not None
|
|
assert bash is not None
|
|
|
|
curl_result = subprocess.run(
|
|
[curl, "-fsSL", url],
|
|
capture_output=True,
|
|
env=env,
|
|
check=False,
|
|
)
|
|
if curl_result.returncode != 0:
|
|
sys.stderr.write(curl_result.stderr.decode("utf-8", errors="replace"))
|
|
return curl_result.returncode
|
|
|
|
bash_result = subprocess.run(
|
|
[bash],
|
|
input=curl_result.stdout,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
env=env,
|
|
check=False,
|
|
)
|
|
if bash_result.returncode != 0:
|
|
sys.stderr.write(bash_result.stdout.decode("utf-8", errors="replace"))
|
|
sys.stderr.write(bash_result.stderr.decode("utf-8", errors="replace"))
|
|
return bash_result.returncode
|
|
|
|
|
|
def _ensure_bun_on_path() -> str | None:
|
|
"""Return a PATH env string that includes an available bun binary.
|
|
|
|
Automatic bootstrap is opt-in because it downloads and executes an
|
|
external installer. Default installs fail closed with an actionable hint.
|
|
"""
|
|
existing = shutil.which("bun")
|
|
if existing:
|
|
return os.environ.get("PATH", "")
|
|
if not _env_truthy(_AUTO_INSTALL_BUN_ENV):
|
|
_fail_build(
|
|
"bun is not on PATH. Install bun from an approved package source, "
|
|
f"or set {_AUTO_INSTALL_BUN_ENV}=1 to explicitly allow setup.py "
|
|
f"to bootstrap bun {_DEFAULT_BUN_VERSION} into $BUN_INSTALL "
|
|
"(defaults to ~/.bun) for this build."
|
|
)
|
|
missing = _missing_bun_bootstrap_prereqs()
|
|
if missing:
|
|
_fail_build(
|
|
"missing OS prerequisites for bun bootstrap: "
|
|
+ ", ".join(missing)
|
|
+ "."
|
|
)
|
|
|
|
bun_install = os.environ.get("BUN_INSTALL") or str(Path.home() / ".bun")
|
|
env = os.environ.copy()
|
|
env["BUN_INSTALL"] = bun_install
|
|
env.setdefault("BUN_VERSION", _DEFAULT_BUN_VERSION)
|
|
|
|
print(
|
|
f"[perfxpert/setup.py] {_AUTO_INSTALL_BUN_ENV}=1 — bootstrapping "
|
|
f"bun {env['BUN_VERSION']} so the bundled patched opencode binary is "
|
|
"built during pip install.",
|
|
file=sys.stderr,
|
|
)
|
|
rc = _run_bun_install_script(env)
|
|
if rc != 0:
|
|
_fail_build(f"bun bootstrap failed with exit code {rc}.")
|
|
|
|
bun_path = str(Path(bun_install) / "bin")
|
|
path = os.environ.get("PATH", "")
|
|
if bun_path not in path.split(os.pathsep):
|
|
path = bun_path + os.pathsep + path if path else bun_path
|
|
if shutil.which("bun", path=path) is None:
|
|
_fail_build(f"bun bootstrap finished but bun is unavailable under {bun_path}.")
|
|
return path
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# opencode source checkout (scoped submodule init / no-network fallback).
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _opencode_dir_is_populated() -> bool:
|
|
"""Return True if the vendored opencode/ source tree is populated.
|
|
|
|
``package.json`` is the load-bearing file the build script checks
|
|
(``build-bundled-opencode.sh`` step 1), but the patch/build pipeline
|
|
also needs git metadata because ``apply-opencode-patches.sh`` uses
|
|
``git apply`` + ``git checkout`` for sequencing and rollback. Accept
|
|
only the repo-pinned git checkout here; tarball/direct-clone layouts
|
|
without ``.git`` are intentionally rejected.
|
|
"""
|
|
return (_OPENCODE_DIR / "package.json").is_file() and (_OPENCODE_DIR / ".git").exists()
|
|
|
|
|
|
def _run_git(args: list[str], cwd: Path | None = None, timeout: int = 300) -> bool:
|
|
"""Run a git command silently. Return True on exit-0, False otherwise.
|
|
|
|
Never raises — callers treat a False return as "try the next
|
|
fallback" so a transient git failure never aborts `pip install`.
|
|
"""
|
|
try:
|
|
result = subprocess.run(
|
|
["git", *args],
|
|
cwd=str(cwd) if cwd is not None else None,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout,
|
|
check=False,
|
|
)
|
|
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
print(
|
|
f"[perfxpert/setup.py] git {' '.join(args)} failed: {exc}",
|
|
file=sys.stderr,
|
|
)
|
|
return False
|
|
if result.returncode != 0:
|
|
print(
|
|
f"[perfxpert/setup.py] git {' '.join(args)} exited "
|
|
f"{result.returncode}: {result.stderr.strip()[:400]}",
|
|
file=sys.stderr,
|
|
)
|
|
return False
|
|
return True
|
|
|
|
|
|
def _ensure_opencode_checkout() -> bool:
|
|
"""Populate ``opencode/`` if empty, using the cheapest strategy that works.
|
|
|
|
The build hook requires the opencode source tree. Normally it's
|
|
present because either (a) the user ran ``git submodule update
|
|
--init`` on the rocm-systems checkout, or (b) pip's built-in
|
|
recursive submodule init populated it during the VCS install.
|
|
|
|
When a user invokes ``scripts/pip-install-from-git.sh`` (or sets
|
|
``GIT_CONFIG_COUNT``/``submodule.active`` env vars manually to skip
|
|
pip's expensive all-submodule init), opencode is intentionally left
|
|
un-initialised — scoped down to just the opencode path. This
|
|
function handles that scoped case plus the pathological case where
|
|
opencode didn't get populated at all.
|
|
|
|
Strategy, in order (stops at the first one that succeeds):
|
|
|
|
1. ``opencode/package.json`` already exists — nothing to do.
|
|
2. ``_HERE`` sits inside a git work-tree and
|
|
``experimental/python/perfxpert/opencode`` is a registered
|
|
submodule of that tree — run a scoped
|
|
``git submodule update --init --depth 1 -- <path>`` so only
|
|
opencode gets initialised.
|
|
3. Refuse to clone from the network during install. If the source
|
|
tree does not contain the repo-pinned submodule, warn and skip.
|
|
|
|
Opt-out via ``PERFXPERT_SKIP_OPENCODE_FETCH=1`` for air-gap CI.
|
|
"""
|
|
if _opencode_dir_is_populated():
|
|
return True
|
|
|
|
if os.environ.get(_SKIP_OPENCODE_FETCH_ENV, "").strip() in {"1", "true", "yes"}:
|
|
print(
|
|
f"[perfxpert/setup.py] {_SKIP_OPENCODE_FETCH_ENV}=1 — "
|
|
"not fetching opencode source.",
|
|
file=sys.stderr,
|
|
)
|
|
return False
|
|
|
|
# Strategy 2: scoped submodule init in the enclosing git work-tree.
|
|
# We look upward from _HERE for the repo root. If opencode is
|
|
# registered as a submodule relative to that root, init only that
|
|
# one path — cheap, honors the pinned SHA recorded in the tree.
|
|
repo_root = None
|
|
for parent in (_HERE, *_HERE.parents):
|
|
if (parent / ".git").exists():
|
|
repo_root = parent
|
|
break
|
|
if repo_root is not None:
|
|
rel_opencode = str(_OPENCODE_DIR.relative_to(repo_root))
|
|
gitmodules = repo_root / ".gitmodules"
|
|
if gitmodules.is_file():
|
|
# Confirm the submodule is actually registered at the
|
|
# expected path — ``git config -f .gitmodules --get-regexp``
|
|
# is the portable query. Exit-0 = registered, non-zero =
|
|
# not registered (fall through to the warning path).
|
|
probed = subprocess.run(
|
|
[
|
|
"git",
|
|
"config",
|
|
"-f",
|
|
str(gitmodules),
|
|
"--get-regexp",
|
|
rf"^submodule\..*\.path$",
|
|
rf"^{rel_opencode}$",
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
if probed.returncode == 0 and rel_opencode in probed.stdout:
|
|
print(
|
|
f"[perfxpert/setup.py] opencode/ is empty — running scoped "
|
|
f"`git submodule update --init --depth 1 -- {rel_opencode}`",
|
|
file=sys.stderr,
|
|
)
|
|
if _run_git(
|
|
[
|
|
"submodule",
|
|
"update",
|
|
"--init",
|
|
"--depth",
|
|
"1",
|
|
"--",
|
|
rel_opencode,
|
|
],
|
|
cwd=repo_root,
|
|
):
|
|
if _opencode_dir_is_populated():
|
|
return True
|
|
|
|
rel_hint = (
|
|
str(_OPENCODE_DIR.relative_to(repo_root))
|
|
if repo_root is not None
|
|
else "experimental/python/perfxpert/opencode"
|
|
)
|
|
print(
|
|
"[perfxpert/setup.py] WARNING: opencode/ is empty and setup.py will "
|
|
"not clone from the network during install. Initialize the pinned "
|
|
f"submodule first (`git submodule update --init --depth 1 -- {rel_hint}`) "
|
|
"or use scripts/pip-install-from-git.sh so pip scopes submodule init "
|
|
"to the PerfXpert opencode submodule.",
|
|
file=sys.stderr,
|
|
)
|
|
return False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Build-hook entry points.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _opencode_build_needed() -> tuple[bool, str]:
|
|
"""Return (should_build, reason). ``should_build=False`` stops here."""
|
|
if os.environ.get(_SKIP_ENV, "").strip() in {"1", "true", "yes"}:
|
|
return False, f"{_SKIP_ENV}=1 — skipping bundled opencode build"
|
|
if not _BUILD_SCRIPT.is_file():
|
|
return True, f"build script missing ({_BUILD_SCRIPT})"
|
|
# Rebuild when the binary is missing OR older than the newest patch.
|
|
if not _BUNDLE_PATH.is_file():
|
|
return True, "bundled opencode binary missing — building"
|
|
binary_mtime = _BUNDLE_PATH.stat().st_mtime
|
|
if _PATCHES_DIR.is_dir():
|
|
for patch in _PATCHES_DIR.glob("*.patch"):
|
|
if patch.stat().st_mtime > binary_mtime:
|
|
return True, f"patch {patch.name} is newer than binary — rebuilding"
|
|
return False, "bundled opencode binary up-to-date"
|
|
|
|
|
|
def _run_opencode_build() -> None:
|
|
should_build, reason = _opencode_build_needed()
|
|
print(f"[perfxpert/setup.py] opencode build: {reason}", file=sys.stderr)
|
|
if not should_build:
|
|
return
|
|
if not _BUILD_SCRIPT.is_file():
|
|
_fail_build(
|
|
f"required bundled opencode build script is missing: {_BUILD_SCRIPT}. "
|
|
f"Set {_SKIP_ENV}=1 only if this build intentionally excludes perfxpert-code."
|
|
)
|
|
missing = _missing_build_prereqs()
|
|
if missing:
|
|
_fail_build(
|
|
"missing OS prerequisites for bundled perfxpert-code build: "
|
|
+ ", ".join(missing)
|
|
+ "."
|
|
)
|
|
# Belt-and-suspenders: the opencode source tree is normally populated
|
|
# by the enclosing git submodule init, but when the user invokes
|
|
# `scripts/pip-install-from-git.sh` (or sets `submodule.active`
|
|
# themselves) to skip pip's slow all-submodule init, the source can
|
|
# be missing here. Check + fetch on-demand before the build script
|
|
# tries to find package.json.
|
|
if not _ensure_opencode_checkout():
|
|
_fail_build(
|
|
"opencode source tree unavailable. The default perfxpert-code "
|
|
"path requires the repo-pinned opencode submodule."
|
|
)
|
|
build_path = _ensure_bun_on_path()
|
|
if build_path is None:
|
|
_fail_build("bun is unavailable after bootstrap.")
|
|
env = os.environ.copy()
|
|
env["PATH"] = build_path
|
|
result = subprocess.run(
|
|
["bash", str(_BUILD_SCRIPT)],
|
|
cwd=str(_HERE),
|
|
env=env,
|
|
check=False,
|
|
)
|
|
if result.returncode != 0:
|
|
_fail_build(
|
|
f"build-bundled-opencode.sh exited {result.returncode}; "
|
|
"bundled perfxpert-code was not produced."
|
|
)
|
|
|
|
|
|
class _BuildPyWithOpencode(_build_py):
|
|
def run(self) -> None:
|
|
_run_opencode_build()
|
|
super().run()
|
|
|
|
|
|
class _DevelopWithOpencode(_develop):
|
|
def run(self) -> None:
|
|
_run_opencode_build()
|
|
super().run()
|
|
|
|
|
|
_cmdclass: dict = {
|
|
"build_py": _BuildPyWithOpencode,
|
|
"develop": _DevelopWithOpencode,
|
|
}
|
|
|
|
if _editable_wheel is not None:
|
|
class _EditableWheelWithOpencode(_editable_wheel):
|
|
def run(self) -> None:
|
|
_run_opencode_build()
|
|
super().run()
|
|
|
|
_cmdclass["editable_wheel"] = _EditableWheelWithOpencode
|
|
|
|
|
|
setup(cmdclass=_cmdclass)
|