From c5b83de20a66403fecfc460a748d4afdafe28c2b Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Wed, 10 Jun 2026 10:32:47 +0100 Subject: [PATCH 1/3] Fix bundled setup script fallback Bundle template setup requirements with the setup scripts and allow container extra-script layers to use setup script directories outside the active source project. This keeps the packaged fallback usable for downstream projects that do not vendor utilities/setup. Co-authored-by: Codex Signed-off-by: Wenqi Li --- .github/scripts/assert_wheel_contents.sh | 1 + src/holoscan_cli/container/core.py | 16 ++++---- .../setup_scripts/Dockerfile.util | 6 +-- .../setup_scripts/requirements.template.txt | 3 ++ src/holoscan_cli/setup_scripts/template.sh | 14 +++++-- tests/unit/test_container_core.py | 40 +++++++++++++++++++ tests/unit/test_package_data.py | 36 +++++++++++++++++ 7 files changed, 102 insertions(+), 14 deletions(-) create mode 100644 src/holoscan_cli/setup_scripts/requirements.template.txt diff --git a/.github/scripts/assert_wheel_contents.sh b/.github/scripts/assert_wheel_contents.sh index 2ab49776..0751376d 100755 --- a/.github/scripts/assert_wheel_contents.sh +++ b/.github/scripts/assert_wheel_contents.sh @@ -24,6 +24,7 @@ required=( 'holoscan_cli/py\.typed$' 'holoscan_cli/metadata/.+\.schema\.json$' 'holoscan_cli/setup_scripts/.+' + 'holoscan_cli/setup_scripts/requirements\.template\.txt$' 'holoscan_cli/testing/' ) for pattern in "${required[@]}"; do diff --git a/src/holoscan_cli/container/core.py b/src/holoscan_cli/container/core.py index 985b6b21..1bdc9c65 100644 --- a/src/holoscan_cli/container/core.py +++ b/src/holoscan_cli/container/core.py @@ -501,17 +501,17 @@ def build( run_command(cmd, dry_run=self.dryrun) if extra_scripts: + setup_scripts_dir = get_holohub_setup_scripts_dir() for script in extra_scripts: - script_path = get_holohub_setup_scripts_dir() / f"{script}.sh" + script_path = setup_scripts_dir / f"{script}.sh" if not script_path.exists(): - fatal(f"Script {script}.sh not found in {get_holohub_setup_scripts_dir()}") + fatal(f"Script {script}.sh not found in {setup_scripts_dir}") try: relative_script_path = script_path.relative_to(HoloscanContainer.HOLOHUB_ROOT) + script_build_context = HoloscanContainer.HOLOHUB_ROOT except ValueError: - fatal( - f"Script {script}.sh at {script_path} is not within {HoloscanContainer.HOLOHUB_ROOT}. " - f"The HOLOSCAN_CLI_SETUP_SCRIPTS_DIR environment variable must resolve to a subdirectory within the project scope." - ) + relative_script_path = script_path.relative_to(setup_scripts_dir) + script_build_context = setup_scripts_dir cmd = [ self.DOCKER_EXE, "build", @@ -523,8 +523,8 @@ def build( "--build-arg", f"SCRIPT={relative_script_path}", "-f", - str(get_holohub_setup_scripts_dir() / "Dockerfile.util"), - str(HoloscanContainer.HOLOHUB_ROOT), + str(setup_scripts_dir / "Dockerfile.util"), + str(script_build_context), ] for tag_name in tags: # We override the default tag so we can add the next scripts on top of this. diff --git a/src/holoscan_cli/setup_scripts/Dockerfile.util b/src/holoscan_cli/setup_scripts/Dockerfile.util index 4d97b9d2..569cdc1d 100644 --- a/src/holoscan_cli/setup_scripts/Dockerfile.util +++ b/src/holoscan_cli/setup_scripts/Dockerfile.util @@ -22,6 +22,6 @@ ARG BASE_IMAGE FROM ${BASE_IMAGE} AS base ARG SCRIPT -COPY ${SCRIPT} ${SCRIPT} -RUN chmod +x ${SCRIPT} -RUN ${SCRIPT} +COPY . /tmp/holoscan-cli-setup/ +RUN chmod +x /tmp/holoscan-cli-setup/${SCRIPT} +RUN /tmp/holoscan-cli-setup/${SCRIPT} diff --git a/src/holoscan_cli/setup_scripts/requirements.template.txt b/src/holoscan_cli/setup_scripts/requirements.template.txt new file mode 100644 index 00000000..d4489751 --- /dev/null +++ b/src/holoscan_cli/setup_scripts/requirements.template.txt @@ -0,0 +1,3 @@ +cookiecutter>=2.7.1 +jsonschema>=4.26.0 +referencing>=0.36.2 diff --git a/src/holoscan_cli/setup_scripts/template.sh b/src/holoscan_cli/setup_scripts/template.sh index 9739116e..edd43d1c 100644 --- a/src/holoscan_cli/setup_scripts/template.sh +++ b/src/holoscan_cli/setup_scripts/template.sh @@ -19,10 +19,18 @@ set -e # Install dependencies used by project templates and metadata validation SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REQUIREMENTS_FILE="${SCRIPT_DIR}/../requirements.template.txt" +REQUIREMENTS_FILE="" +for candidate in \ + "${SCRIPT_DIR}/requirements.template.txt" \ + "${SCRIPT_DIR}/../requirements.template.txt"; do + if [[ -f "${candidate}" ]]; then + REQUIREMENTS_FILE="${candidate}" + break + fi +done -if [[ ! -f "${REQUIREMENTS_FILE}" ]]; then - echo "requirements.template.txt not found at ${REQUIREMENTS_FILE}" >&2 +if [[ -z "${REQUIREMENTS_FILE}" ]]; then + echo "requirements.template.txt not found next to or above ${SCRIPT_DIR}" >&2 exit 1 fi diff --git a/tests/unit/test_container_core.py b/tests/unit/test_container_core.py index f5d9fef9..1d49b8c0 100644 --- a/tests/unit/test_container_core.py +++ b/tests/unit/test_container_core.py @@ -422,6 +422,46 @@ def test_build_dryrun_emits_base_and_extra_script_layers(tmp_path, monkeypatch): assert "holohub-my_app:feature-x-coverage" in layer +def test_build_dryrun_allows_bundled_extra_script_dir(tmp_path, monkeypatch): + """Bundled setup scripts live outside the source project but can still + serve as the Docker build context for extra-script layers.""" + root = tmp_path / "project" + project_dir = root / "applications" / "my_app" + project_dir.mkdir(parents=True) + dockerfile = project_dir / "Dockerfile" + dockerfile.write_text("FROM scratch\n", encoding="utf-8") + setup_dir = tmp_path / "package_setup" + setup_dir.mkdir() + (setup_dir / "coverage.sh").write_text("#!/bin/sh\n", encoding="utf-8") + (setup_dir / "Dockerfile.util").write_text("FROM scratch\n", encoding="utf-8") + + calls = [] + monkeypatch.setenv("HOLOSCAN_CLI_SETUP_SCRIPTS_DIR", str(setup_dir)) + monkeypatch.setattr(container_core, "get_host_gpu", lambda: "dgpu") + monkeypatch.setattr(container_core, "get_compute_capacity", lambda: "90") + monkeypatch.setattr(container_core, "get_default_cuda_version", lambda: "13") + monkeypatch.setattr(container_core, "get_current_branch_slug", lambda: "feature-x") + monkeypatch.setattr(container_core, "get_git_short_sha", lambda: "abcdef0") + monkeypatch.setattr(container_core, "run_command", lambda cmd, **kwargs: calls.append(cmd)) + + c = _stub_container( + root, + project_metadata={ + "project_name": "my_app", + "source_folder": str(project_dir), + "metadata": {"language": "python"}, + }, + ) + c.dryrun = True + + c.build(extra_scripts=["coverage"]) + + layer = calls[1] + assert "SCRIPT=coverage.sh" in layer + assert str(setup_dir / "Dockerfile.util") in layer + assert str(setup_dir) in layer + + def test_build_dryrun_omits_base_sdk_version_when_not_configured(tmp_path, monkeypatch): project_dir = tmp_path / "applications" / "my_app" project_dir.mkdir(parents=True) diff --git a/tests/unit/test_package_data.py b/tests/unit/test_package_data.py index b9e21613..40f0cc05 100644 --- a/tests/unit/test_package_data.py +++ b/tests/unit/test_package_data.py @@ -32,6 +32,8 @@ import importlib.metadata import importlib.resources +import os +import subprocess import sys from pathlib import Path @@ -67,6 +69,7 @@ "sccache.sh", "template.sh", "xvfb.sh", + "requirements.template.txt", } @@ -122,6 +125,39 @@ def test_setup_scripts_are_packaged(): assert not missing, f"missing bundled setup scripts: {missing}" +def test_bundled_template_script_uses_bundled_requirements(tmp_path): + """The fallback template setup script must not depend on HoloHub's + ``utilities/requirements.template.txt`` being present.""" + setup_dir = importlib.resources.files("holoscan_cli.setup_scripts") + script = setup_dir.joinpath("template.sh") + requirements = setup_dir.joinpath("requirements.template.txt") + assert script.is_file() + assert requirements.is_file() + + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + args_file = tmp_path / "python-args.txt" + fake_python = bin_dir / "python3" + fake_python.write_text( + "#!/usr/bin/env bash\n" 'printf \'%s\\n\' "$@" > "${PYTHON_ARGS_FILE}"\n', + encoding="utf-8", + ) + fake_python.chmod(0o755) + + env = os.environ.copy() + env["PATH"] = f"{bin_dir}:{env['PATH']}" + env["PYTHON_ARGS_FILE"] = str(args_file) + subprocess.run(["bash", str(script)], check=True, env=env) + + assert args_file.read_text(encoding="utf-8").splitlines() == [ + "-m", + "pip", + "install", + "-r", + str(requirements), + ] + + # ---- pyproject.toml entry-point declarations -------------------------------- From caa72ee2387bab722c4338d4918fcc48c240a2f7 Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Wed, 10 Jun 2026 10:45:45 +0100 Subject: [PATCH 2/3] Document PR-first RC fix flow Clarify that release-candidate fixes must merge to main before being cherry-picked to the release branch and used for a new RC workflow or Kitmaker release. Co-authored-by: Codex Signed-off-by: Wenqi Li --- .github/CI.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/CI.md b/.github/CI.md index 2aa86372..f003652e 100644 --- a/.github/CI.md +++ b/.github/CI.md @@ -130,6 +130,13 @@ temporary `vX.Y.Z` tag; a **GA** dispatch keeps the `vX.Y.Z` tag and is the one K2 Kitmaker promotes to the release registry (Artifactory). The cutover to public PyPI happens out of band — until then, installs use the TestPyPI index. +Release-candidate fixes follow the normal review path first: create a PR +against `main`, wait for it to merge, cherry-pick the merged commit onto +`release/X.Y.0`, and only then dispatch the next RC from the release branch. +Do not direct-push unreviewed fixes to `release/X.Y.0`, and do not submit an RC +workflow or Kitmaker release before the fix has merged to `main` and has been +picked onto the release branch. + ### Steps 1. **Land everything on `main`** and confirm it is green. @@ -158,8 +165,9 @@ public PyPI happens out of band — until then, installs use the TestPyPI index. --extra-index-url https://pypi.org/simple/ "holoscan-cli==X.Y.Zrc1" ``` -5. **Iterate** if fixes are needed: cherry-pick onto `release/X.Y.0` (or merge - from `main`), then dispatch with `-f rc=2`, `-f rc=3`, … (bump each time). +5. **Iterate** if fixes are needed: merge the fix to `main`, cherry-pick the + merged commit onto `release/X.Y.0`, then dispatch with `-f rc=2`, + `-f rc=3`, … (bump each time). 6. **Cut GA** once an RC is accepted: ```bash From 44d5bdea463f938c151da9196b927bda59845930 Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Wed, 10 Jun 2026 10:52:26 +0100 Subject: [PATCH 3/3] Simplify bundled template requirements lookup Use the requirements.template.txt file colocated with the bundled setup scripts as the only supported lookup path. Downstream projects that override setup scripts can copy the setup-script directory as one self-contained unit. Co-authored-by: Codex Signed-off-by: Wenqi Li --- src/holoscan_cli/setup_scripts/template.sh | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/holoscan_cli/setup_scripts/template.sh b/src/holoscan_cli/setup_scripts/template.sh index edd43d1c..863e0ca8 100644 --- a/src/holoscan_cli/setup_scripts/template.sh +++ b/src/holoscan_cli/setup_scripts/template.sh @@ -19,18 +19,10 @@ set -e # Install dependencies used by project templates and metadata validation SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REQUIREMENTS_FILE="" -for candidate in \ - "${SCRIPT_DIR}/requirements.template.txt" \ - "${SCRIPT_DIR}/../requirements.template.txt"; do - if [[ -f "${candidate}" ]]; then - REQUIREMENTS_FILE="${candidate}" - break - fi -done +REQUIREMENTS_FILE="${SCRIPT_DIR}/requirements.template.txt" -if [[ -z "${REQUIREMENTS_FILE}" ]]; then - echo "requirements.template.txt not found next to or above ${SCRIPT_DIR}" >&2 +if [[ ! -f "${REQUIREMENTS_FILE}" ]]; then + echo "requirements.template.txt not found at ${REQUIREMENTS_FILE}" >&2 exit 1 fi