From 5339f62741b3e588d9039b657bc367addfab5865 Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Tue, 4 Aug 2026 10:27:21 +0100 Subject: [PATCH 1/2] feat: add --prefix to install for install-time destination selection `holoscan install` emitted a bare `cmake --install `, so the only way to pick a destination was `--configure-args="-DCMAKE_INSTALL_PREFIX=..."`. That bakes the path into the build tree's CMake cache: a configured tree can only ever install to one location, and changing it forces a reconfigure. Add `--prefix`, forwarded to `cmake --install --prefix`. It is resolved at install time and leaves the cache untouched, so one built tree can be installed to several destinations. The flag is spelled to match CMake's own option; internally it is `install_prefix` so it cannot be confused with the unrelated `cli.prefix` metadata placeholder prefix used in the same module. Both paths are covered: the local branch appends the option to the cmake invocation, and the container branch forwards it (shell-quoted) into the recursive in-container `holoscan install ... --local` command. Signed-off-by: Wenqi Li Co-Authored-By: Claude Opus 5 (1M context) --- src/holoscan_cli/commands/install.py | 16 +++++++++++++++- tests/unit/test_lifecycle_commands.py | 25 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/holoscan_cli/commands/install.py b/src/holoscan_cli/commands/install.py index c684195..6a6e13b 100644 --- a/src/holoscan_cli/commands/install.py +++ b/src/holoscan_cli/commands/install.py @@ -102,6 +102,15 @@ def register_install_parser( help="Additional configuration arguments for cmake " "example: --configure-args='-DCUSTOM_OPTION=ON' --configure-args='-Dtest=ON'", ) + parser.add_argument( + "--prefix", + dest="install_prefix", + type=Path, + default=None, + help="Install destination directory, forwarded to `cmake --install --prefix`. " + "Applied at install time, so the build tree's CMake cache is left untouched. " + "Without --local, the path is resolved inside the container.", + ) parser.set_defaults(func=lambda args: handle_install(cli, args)) return parser @@ -173,7 +182,10 @@ def handle_install(cli, args: argparse.Namespace) -> None: ) # Install the project - run_command(["cmake", "--install", str(build_dir)], dry_run=args.dryrun, env=install_env) + cmake_install_cmd = ["cmake", "--install", str(build_dir)] + if getattr(args, "install_prefix", None): + cmake_install_cmd += ["--prefix", str(args.install_prefix)] + run_command(cmake_install_cmd, dry_run=args.dryrun, env=install_env) if not args.dryrun: print(f"{Color.green('Successfully installed')} {args.project}") else: @@ -212,6 +224,8 @@ def handle_install(cli, args: argparse.Namespace) -> None: if getattr(args, "configure_args", None): for configure_arg in args.configure_args: install_cmd += f" --configure-args={shlex.quote(configure_arg)}" + if getattr(args, "install_prefix", None): + install_cmd += f" --prefix {shlex.quote(str(args.install_prefix))}" img = getattr(args, "img", None) or container.image_name docker_opts = build_args.get("docker_opts", "") diff --git a/tests/unit/test_lifecycle_commands.py b/tests/unit/test_lifecycle_commands.py index c863bda..760a0e0 100644 --- a/tests/unit/test_lifecycle_commands.py +++ b/tests/unit/test_lifecycle_commands.py @@ -4,6 +4,7 @@ from __future__ import annotations import json +import shlex from argparse import Namespace from holoscan_cli.commands import build as build_cmd @@ -143,6 +144,7 @@ def _project_args(**overrides): "uninstall": False, "build_dir": None, "site_dir": None, + "install_prefix": None, } ) defaults.update(overrides) @@ -491,6 +493,27 @@ def test_handle_install_local_installs_built_project(tmp_path, monkeypatch): assert calls == [["cmake", "--install", str(build_dir)]] +def test_handle_install_local_forwards_prefix_at_install_time(tmp_path, monkeypatch): + """`--prefix` reaches `cmake --install` without touching the configure step.""" + cli = RecordingCLI(tmp_path) + build_dir = tmp_path / "build" / "smoke_app" + install_dir = tmp_path / "opt" / "smoke_app" + calls = [] + build_kwargs = {} + + def record_build(*args, **kwargs): + build_kwargs.update(kwargs) + return build_dir, cli.project_data + + monkeypatch.setattr(install_cmd, "build_project_locally", record_build) + monkeypatch.setattr(install_cmd, "run_command", lambda cmd, **kwargs: calls.append(cmd)) + + install_cmd.handle_install(cli, _project_args(local=True, install_prefix=install_dir)) + + assert calls == [["cmake", "--install", str(build_dir), "--prefix", str(install_dir)]] + assert build_kwargs["configure_args"] is None + + def test_handle_install_container_branch_passes_recursive_local_command(tmp_path, monkeypatch): cli = RecordingCLI(tmp_path) captured = {} @@ -509,6 +532,7 @@ def capture_entrypoint(img, cmd, opts, dry_run=False): with_operators="op_a", parallel="4", configure_args=["-DDEV=ON"], + install_prefix=tmp_path / "opt" / "smoke app", docker_opts="--ipc=host", verbose=True, ), @@ -524,6 +548,7 @@ def capture_entrypoint(img, cmd, opts, dry_run=False): assert '--build-with "op_a"' in command assert "--parallel 4" in command assert "--configure-args=-DDEV=ON" in command + assert f"--prefix {shlex.quote(str(tmp_path / 'opt' / 'smoke app'))}" in command assert cli.container.run_calls[0]["extra_args"] == ["-c", command] From 8b5d42e3e5c4730736e7798509d904bada482de6 Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Tue, 4 Aug 2026 14:22:31 +0100 Subject: [PATCH 2/2] update Signed-off-by: Wenqi Li --- src/holoscan_cli/commands/install.py | 21 ++++++++++----------- tests/unit/test_lifecycle_commands.py | 24 +++++++++++++++--------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/holoscan_cli/commands/install.py b/src/holoscan_cli/commands/install.py index 0a8448d..a514e11 100644 --- a/src/holoscan_cli/commands/install.py +++ b/src/holoscan_cli/commands/install.py @@ -103,13 +103,12 @@ def register_install_parser( "example: --configure-args='-DCUSTOM_OPTION=ON' --configure-args='-Dtest=ON'", ) parser.add_argument( - "--prefix", - dest="install_prefix", - type=Path, - default=None, - help="Install destination directory, forwarded to `cmake --install --prefix`. " - "Applied at install time, so the build tree's CMake cache is left untouched. " - "Without --local, the path is resolved inside the container.", + "--install-args", + action="append", + help="Additional arguments for `cmake --install`, applied at install time so the " + "build tree's CMake cache is left untouched " + "example: --install-args='--prefix /opt/holohub' --install-args=--strip. " + "When installation runs in a container, paths are resolved inside the container.", ) parser.set_defaults(func=lambda args: handle_install(cli, args)) return parser @@ -184,8 +183,8 @@ def handle_install(cli, args: argparse.Namespace) -> None: # Install the project cmake_install_cmd = ["cmake", "--install", str(build_dir)] - if getattr(args, "install_prefix", None): - cmake_install_cmd += ["--prefix", str(args.install_prefix)] + for install_arg in getattr(args, "install_args", None) or []: + cmake_install_cmd += shlex.split(os.path.expandvars(install_arg)) run_command(cmake_install_cmd, dry_run=args.dryrun, env=install_env) if not args.dryrun: print(f"{Color.green('Successfully installed')} {args.project}") @@ -225,8 +224,8 @@ def handle_install(cli, args: argparse.Namespace) -> None: if getattr(args, "configure_args", None): for configure_arg in args.configure_args: install_cmd += f" --configure-args={shlex.quote(configure_arg)}" - if getattr(args, "install_prefix", None): - install_cmd += f" --prefix {shlex.quote(str(args.install_prefix))}" + for install_arg in getattr(args, "install_args", None) or []: + install_cmd += f" --install-args={shlex.quote(install_arg)}" img = getattr(args, "img", None) or container.image_name docker_opts = build_args.get("docker_opts", "") diff --git a/tests/unit/test_lifecycle_commands.py b/tests/unit/test_lifecycle_commands.py index c3ad54e..2b3ac55 100644 --- a/tests/unit/test_lifecycle_commands.py +++ b/tests/unit/test_lifecycle_commands.py @@ -4,7 +4,6 @@ from __future__ import annotations import json -import shlex from argparse import Namespace from holoscan_cli.commands import build as build_cmd @@ -144,7 +143,7 @@ def _project_args(**overrides): "uninstall": False, "build_dir": None, "site_dir": None, - "install_prefix": None, + "install_args": None, } ) defaults.update(overrides) @@ -506,11 +505,10 @@ def test_handle_install_local_installs_built_project(tmp_path, monkeypatch): assert calls == [["cmake", "--install", str(build_dir)]] -def test_handle_install_local_forwards_prefix_at_install_time(tmp_path, monkeypatch): - """`--prefix` reaches `cmake --install` without touching the configure step.""" +def test_handle_install_local_forwards_install_args_at_install_time(tmp_path, monkeypatch): + """`--install-args` reaches `cmake --install` without touching the configure step.""" cli = RecordingCLI(tmp_path) build_dir = tmp_path / "build" / "smoke_app" - install_dir = tmp_path / "opt" / "smoke_app" calls = [] build_kwargs = {} @@ -521,9 +519,16 @@ def record_build(*args, **kwargs): monkeypatch.setattr(install_cmd, "build_project_locally", record_build) monkeypatch.setattr(install_cmd, "run_command", lambda cmd, **kwargs: calls.append(cmd)) - install_cmd.handle_install(cli, _project_args(local=True, install_prefix=install_dir)) + install_cmd.handle_install( + cli, + _project_args( + local=True, + install_args=["--prefix /opt/holohub", "--strip"], + ), + ) - assert calls == [["cmake", "--install", str(build_dir), "--prefix", str(install_dir)]] + # Each value is split into cmake tokens; nothing leaks into the configure step. + assert calls == [["cmake", "--install", str(build_dir), "--prefix", "/opt/holohub", "--strip"]] assert build_kwargs["configure_args"] is None @@ -545,7 +550,7 @@ def capture_entrypoint(img, cmd, opts, dry_run=False): with_operators="op_a", parallel="4", configure_args=["-DDEV=ON"], - install_prefix=tmp_path / "opt" / "smoke app", + install_args=["--prefix /opt/smoke app", "--component dev"], docker_opts="--ipc=host", verbose=True, ), @@ -561,7 +566,8 @@ def capture_entrypoint(img, cmd, opts, dry_run=False): assert '--build-with "op_a"' in command assert "--parallel 4" in command assert "--configure-args=-DDEV=ON" in command - assert f"--prefix {shlex.quote(str(tmp_path / 'opt' / 'smoke app'))}" in command + assert "--install-args='--prefix /opt/smoke app'" in command + assert "--install-args='--component dev'" in command assert cli.container.run_calls[0]["extra_args"] == ["-c", command]