From f48a9b5c46204afb5afeba42f2f85c2b7f60f180 Mon Sep 17 00:00:00 2001 From: Daniele21 Date: Thu, 3 Sep 2026 13:11:53 +0200 Subject: [PATCH 1/5] feat: add artifact-owned launcher runtime --- src/performance_lab/artifact_launcher.py | 246 +++++++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 src/performance_lab/artifact_launcher.py diff --git a/src/performance_lab/artifact_launcher.py b/src/performance_lab/artifact_launcher.py new file mode 100644 index 00000000..e98ea3c8 --- /dev/null +++ b/src/performance_lab/artifact_launcher.py @@ -0,0 +1,246 @@ +"""Standalone launcher shipped with the distributed Performance Lab artifact. + +This module intentionally depends only on the Python standard library so the same +source file can be copied to ``launch.py`` beside the packaged wheel and built web +assets. The launcher owns only its local runtime directory; model serving remains +external. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import shutil +import subprocess +import sys +import venv +from collections.abc import Sequence +from pathlib import Path +from typing import Any + +MIN_PYTHON = (3, 12) +RUNTIME_OWNER = "performance-lab-artifact-launcher-v1" +RUNTIME_MARKER = ".performance-lab-runtime.json" + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as source: + for chunk in iter(lambda: source.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _load_json(path: Path) -> dict[str, Any]: + try: + value: object = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise RuntimeError(f"cannot read launcher metadata: {path}") from exc + if not isinstance(value, dict): + raise RuntimeError(f"launcher metadata must contain an object: {path}") + return value + + +def _runtime_python(runtime_dir: Path) -> Path: + if sys.platform == "win32": + return runtime_dir / "Scripts" / "python.exe" + return runtime_dir / "bin" / "python" + + +def _packaged_wheel(root: Path) -> Path: + wheels = tuple((root / "python").glob("*.whl")) + if len(wheels) != 1: + raise RuntimeError(f"expected exactly one packaged wheel, found {len(wheels)}") + return wheels[0] + + +def runtime_identity(root: Path) -> dict[str, str]: + manifest = _load_json(root / "build-manifest.json") + artifact_stem = manifest.get("artifact_stem") + source_revision = manifest.get("source_revision") + if not isinstance(artifact_stem, str) or not artifact_stem: + raise RuntimeError("build manifest is missing artifact_stem") + if not isinstance(source_revision, str) or not source_revision: + raise RuntimeError("build manifest is missing source_revision") + + requirements = root / "runtime-requirements.txt" + if not requirements.is_file(): + raise RuntimeError("artifact is missing runtime-requirements.txt") + wheel = _packaged_wheel(root) + return { + "owner": RUNTIME_OWNER, + "artifact_stem": artifact_stem, + "source_revision": source_revision, + "requirements_sha256": _sha256(requirements), + "wheel_sha256": _sha256(wheel), + } + + +def _read_runtime_marker(runtime_dir: Path) -> dict[str, Any] | None: + marker = runtime_dir / RUNTIME_MARKER + if not marker.is_file(): + return None + try: + value: object = json.loads(marker.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return None + return value if isinstance(value, dict) else None + + +def runtime_is_current(root: Path, runtime_dir: Path) -> bool: + marker = _read_runtime_marker(runtime_dir) + if marker is None or marker.get("state") != "ready": + return False + expected = runtime_identity(root) + return all(marker.get(key) == value for key, value in expected.items()) and _runtime_python( + runtime_dir + ).is_file() + + +def _assert_runtime_owned(runtime_dir: Path) -> None: + if not runtime_dir.exists(): + return + marker = _read_runtime_marker(runtime_dir) + if marker is None or marker.get("owner") != RUNTIME_OWNER: + raise RuntimeError( + f"refusing to replace unowned runtime directory: {runtime_dir}. " + "Choose another --runtime-dir or remove it manually." + ) + + +def _write_runtime_marker(runtime_dir: Path, identity: dict[str, str], *, state: str) -> None: + payload: dict[str, str] = {**identity, "state": state} + (runtime_dir / RUNTIME_MARKER).write_text( + json.dumps(payload, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + + +def prepare_runtime(root: Path, runtime_dir: Path) -> Path: + """Create or reuse the launcher-owned isolated runtime for this artifact.""" + + root = root.resolve() + runtime_dir = runtime_dir.resolve() + if runtime_is_current(root, runtime_dir): + return _runtime_python(runtime_dir) + + identity = runtime_identity(root) + _assert_runtime_owned(runtime_dir) + if runtime_dir.exists(): + shutil.rmtree(runtime_dir) + runtime_dir.mkdir(parents=True) + _write_runtime_marker(runtime_dir, identity, state="installing") + + try: + venv.EnvBuilder(with_pip=True).create(runtime_dir) + python = _runtime_python(runtime_dir) + if not python.is_file(): + raise RuntimeError("Python venv was created without an executable") + subprocess.run( + [ + str(python), + "-m", + "pip", + "install", + "--disable-pip-version-check", + "--no-input", + "--requirement", + str(root / "runtime-requirements.txt"), + ], + check=True, + ) + subprocess.run( + [ + str(python), + "-m", + "pip", + "install", + "--disable-pip-version-check", + "--no-input", + "--no-deps", + str(_packaged_wheel(root)), + ], + check=True, + ) + except Exception: + # The marker proves ownership, so removing this partial runtime cannot delete + # unrelated user state. + shutil.rmtree(runtime_dir, ignore_errors=True) + raise + + _write_runtime_marker(runtime_dir, identity, state="ready") + return python + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(prog="performance-lab-artifact") + parser.add_argument( + "--config", + required=True, + type=Path, + help="Versioned StarterRunConfig JSON for the external inference target.", + ) + parser.add_argument( + "--runtime-dir", + type=Path, + default=None, + help="Launcher-owned runtime directory. Defaults to .runtime beside launch.py.", + ) + parser.add_argument("--port", type=int, default=8765) + parser.add_argument( + "--prepare-only", + action="store_true", + help="Prepare/reuse the isolated runtime without starting the product.", + ) + return parser + + +def main(argv: Sequence[str] | None = None) -> int: + if sys.version_info < MIN_PYTHON: + print("Performance Lab requires Python 3.12 or newer.", file=sys.stderr) + return 2 + + args = build_parser().parse_args(list(argv) if argv is not None else None) + root = Path(__file__).resolve().parent + config = args.config.expanduser().resolve() + if not config.is_file(): + print(f"error: config does not exist: {config}", file=sys.stderr) + return 2 + if not (root / "web" / "index.html").is_file(): + print("error: artifact is missing built web assets", file=sys.stderr) + return 2 + + runtime_dir = ( + args.runtime_dir.expanduser().resolve() + if args.runtime_dir is not None + else root / ".runtime" + ) + try: + python = prepare_runtime(root, runtime_dir) + except (OSError, RuntimeError, subprocess.CalledProcessError) as exc: + print(f"error: cannot prepare Performance Lab runtime: {exc}", file=sys.stderr) + return 2 + + if args.prepare_only: + print(f"Performance Lab runtime ready: {runtime_dir}") + return 0 + + command = [ + str(python), + "-m", + "performance_lab.ui_server", + "--config", + str(config), + "--assets", + str(root / "web"), + "--port", + str(args.port), + ] + os.execv(str(python), command) + raise AssertionError("os.execv returned unexpectedly") + + +if __name__ == "__main__": # pragma: no cover + raise SystemExit(main()) From 012532ba1abdbd4faa3a3ef88535126ac93f0992 Mon Sep 17 00:00:00 2001 From: Daniele21 Date: Thu, 3 Sep 2026 13:12:30 +0200 Subject: [PATCH 2/5] feat: ship launcher and locked runtime inputs --- scripts/package_release.py | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/scripts/package_release.py b/scripts/package_release.py index 3ce3a7e9..feac2c1e 100644 --- a/scripts/package_release.py +++ b/scripts/package_release.py @@ -127,17 +127,42 @@ def build_payload(staging: Path) -> Path: wheels = tuple(python_dir.glob("*.whl")) if len(wheels) != 1: raise RuntimeError(f"expected exactly one wheel, found {len(wheels)}") + + run( + [ + "uv", + "export", + "--locked", + "--extra", + "ui", + "--no-dev", + "--no-emit-project", + "--format", + "requirements-txt", + "--output-file", + str(staging / "runtime-requirements.txt"), + ] + ) + shutil.copy2(ROOT / "src" / "performance_lab" / "artifact_launcher.py", staging / "launch.py") return wheels[0] def write_run_instructions(staging: Path, wheel_name: str) -> None: (staging / "RUN.md").write_text( "# Run this artifact\n\n" - "1. Extract the ZIP.\n" - f"2. Install `python/{wheel_name}[ui]` into an isolated Python 3.12+ environment.\n" - "3. Create a versioned `StarterRunConfig` JSON for the target endpoint.\n" - "4. Run `performance-lab-ui --config --assets web`.\n\n" - "The product binds to loopback by default. Model serving remains external.\n", + "1. Extract the ZIP into a writable directory.\n" + "2. Ensure Python 3.12 or newer is available. No repository checkout, uv, Node or pnpm " + "is required.\n" + "3. Create a versioned `StarterRunConfig` JSON for the external inference target.\n" + "4. Run `python launch.py --config `.\n" + "5. Open `http://127.0.0.1:8765`. Stop the foreground process with Ctrl-C.\n\n" + "On first launch, `launch.py` creates an artifact-owned `.runtime` virtual environment " + "and installs the exact locked runtime requirements plus " + f"`python/{wheel_name}`. Dependency download therefore requires package-index access on " + "the first launch; subsequent launches reuse the matching runtime. Use `--runtime-dir` " + "when the extracted artifact directory is not writable.\n\n" + "The product binds to loopback by default. Model serving and model lifecycle remain " + "external to Performance Lab.\n", encoding="utf-8", ) @@ -236,4 +261,4 @@ def main() -> int: if __name__ == "__main__": - raise SystemExit(main()) + raise SystemExit(main()) \ No newline at end of file From e96d9bc9770ba3d7d74816872e71641d2f46b340 Mon Sep 17 00:00:00 2001 From: Daniele21 Date: Thu, 3 Sep 2026 13:12:56 +0200 Subject: [PATCH 3/5] test: cover artifact launcher runtime ownership --- tests/test_artifact_launcher.py | 103 ++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 tests/test_artifact_launcher.py diff --git a/tests/test_artifact_launcher.py b/tests/test_artifact_launcher.py new file mode 100644 index 00000000..5dfc4f61 --- /dev/null +++ b/tests/test_artifact_launcher.py @@ -0,0 +1,103 @@ +import json +from pathlib import Path + +import pytest + +from performance_lab import artifact_launcher + + +def _artifact(tmp_path: Path) -> Path: + root = tmp_path / "artifact" + (root / "python").mkdir(parents=True) + (root / "runtime-requirements.txt").write_text("pydantic==2.0\n", encoding="utf-8") + (root / "python" / "performance_lab.whl").write_bytes(b"wheel") + (root / "build-manifest.json").write_text( + json.dumps( + { + "artifact_stem": "ai-performance-lab-test", + "source_revision": "abc123", + } + ), + encoding="utf-8", + ) + return root + + +def test_runtime_identity_is_bound_to_artifact_inputs(tmp_path: Path) -> None: + root = _artifact(tmp_path) + + identity = artifact_launcher.runtime_identity(root) + + assert identity["owner"] == artifact_launcher.RUNTIME_OWNER + assert identity["artifact_stem"] == "ai-performance-lab-test" + assert identity["source_revision"] == "abc123" + assert len(identity["requirements_sha256"]) == 64 + assert len(identity["wheel_sha256"]) == 64 + + +def test_prepare_runtime_installs_once_then_reuses_matching_runtime( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + root = _artifact(tmp_path) + runtime = tmp_path / "runtime" + calls: list[list[str]] = [] + + class FakeBuilder: + def __init__(self, *, with_pip: bool) -> None: + assert with_pip is True + + def create(self, path: Path) -> None: + python = artifact_launcher._runtime_python(Path(path)) + python.parent.mkdir(parents=True, exist_ok=True) + python.write_text("fake-python", encoding="utf-8") + + def fake_run(command: list[str], *, check: bool) -> None: + assert check is True + calls.append(command) + + monkeypatch.setattr(artifact_launcher.venv, "EnvBuilder", FakeBuilder) + monkeypatch.setattr(artifact_launcher.subprocess, "run", fake_run) + + python = artifact_launcher.prepare_runtime(root, runtime) + reused = artifact_launcher.prepare_runtime(root, runtime) + + assert python == reused + assert python.is_file() + assert len(calls) == 2 + marker = json.loads((runtime / artifact_launcher.RUNTIME_MARKER).read_text(encoding="utf-8")) + assert marker["state"] == "ready" + assert marker["owner"] == artifact_launcher.RUNTIME_OWNER + assert artifact_launcher.runtime_is_current(root, runtime) is True + + +def test_prepare_runtime_refuses_to_replace_unowned_directory(tmp_path: Path) -> None: + root = _artifact(tmp_path) + runtime = tmp_path / "runtime" + runtime.mkdir() + (runtime / "user-data.txt").write_text("keep", encoding="utf-8") + + with pytest.raises(RuntimeError, match="refusing to replace unowned runtime directory"): + artifact_launcher.prepare_runtime(root, runtime) + + assert (runtime / "user-data.txt").read_text(encoding="utf-8") == "keep" + + +def test_runtime_is_invalidated_when_locked_requirements_change(tmp_path: Path) -> None: + root = _artifact(tmp_path) + runtime = tmp_path / "runtime" + runtime.mkdir() + identity = artifact_launcher.runtime_identity(root) + marker = {**identity, "state": "ready"} + (runtime / artifact_launcher.RUNTIME_MARKER).write_text( + json.dumps(marker), + encoding="utf-8", + ) + python = artifact_launcher._runtime_python(runtime) + python.parent.mkdir(parents=True) + python.write_text("fake-python", encoding="utf-8") + assert artifact_launcher.runtime_is_current(root, runtime) is True + + (root / "runtime-requirements.txt").write_text("pydantic==2.1\n", encoding="utf-8") + + assert artifact_launcher.runtime_is_current(root, runtime) is False From ea33160d1445e3c16a46560220537829d19426f0 Mon Sep 17 00:00:00 2001 From: Daniele21 Date: Thu, 3 Sep 2026 13:18:51 +0200 Subject: [PATCH 4/5] style: format artifact launcher --- src/performance_lab/artifact_launcher.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/performance_lab/artifact_launcher.py b/src/performance_lab/artifact_launcher.py index e98ea3c8..3d8a9858 100644 --- a/src/performance_lab/artifact_launcher.py +++ b/src/performance_lab/artifact_launcher.py @@ -94,9 +94,10 @@ def runtime_is_current(root: Path, runtime_dir: Path) -> bool: if marker is None or marker.get("state") != "ready": return False expected = runtime_identity(root) - return all(marker.get(key) == value for key, value in expected.items()) and _runtime_python( - runtime_dir - ).is_file() + return ( + all(marker.get(key) == value for key, value in expected.items()) + and _runtime_python(runtime_dir).is_file() + ) def _assert_runtime_owned(runtime_dir: Path) -> None: From 1aefb935be0c973027913af12ffa9fc74e0e511f Mon Sep 17 00:00:00 2001 From: Daniele21 Date: Thu, 3 Sep 2026 13:19:31 +0200 Subject: [PATCH 5/5] style: format package release script --- scripts/package_release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/package_release.py b/scripts/package_release.py index feac2c1e..06825c29 100644 --- a/scripts/package_release.py +++ b/scripts/package_release.py @@ -261,4 +261,4 @@ def main() -> int: if __name__ == "__main__": - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main())