From d9b1cfbd630284356ead4825ec1dc3a7da763dfa Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Fri, 14 Aug 2026 21:54:06 +0100 Subject: [PATCH] feat: add --configure-only to build Stop after the CMake configure step instead of compiling, so a project can be validated as configurable without paying for a full build. In container mode the image is still built as usual; the flag only affects the CMake steps that run inside it. Any flow-benchmarking patch applied before configure is still reverted. Co-Authored-By: Claude Opus 5 (1M context) --- src/holoscan_cli/commands/build.py | 36 ++++++++++++---- tests/unit/test_lifecycle_commands.py | 60 +++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 7 deletions(-) diff --git a/src/holoscan_cli/commands/build.py b/src/holoscan_cli/commands/build.py index 1e36d913..f83ae41d 100644 --- a/src/holoscan_cli/commands/build.py +++ b/src/holoscan_cli/commands/build.py @@ -74,6 +74,8 @@ def make_local_build_command( command += f" --parallel {args.parallel}" if args.verbose: command += " --verbose" + if getattr(args, "configure_only", False): + command += " --configure-only" if getattr(args, "benchmark", False): command += " --benchmark" for configure_arg in getattr(args, "configure_args", None) or []: @@ -104,6 +106,12 @@ def register_build_parser( dest="with_operators", help="Optional operators that should be built, separated by semicolons (;)", ) + parser.add_argument( + "--configure-only", + action="store_true", + help="Stop after the CMake configure step instead of compiling. Validates that a " + "project configures without paying for a full build", + ) parser.add_argument( "--dryrun", action="store_true", help="Print commands without executing them" ) @@ -176,6 +184,7 @@ def handle_build(cli, args: argparse.Namespace) -> None: benchmark=getattr(args, "benchmark", False), configure_args=build_args.get("configure_args"), extra_env=build_mode_env, + configure_only=getattr(args, "configure_only", False), ) else: # Build in container @@ -230,6 +239,15 @@ def handle_build(cli, args: argparse.Namespace) -> None: ) +def _restore_benchmark_patch(cli, app_source_path, project_type: str, dryrun: bool) -> None: + """Revert the flow-benchmarking patch applied before configure, if there was one.""" + if app_source_path and project_type in ["application", "benchmark"]: + restore_script = ( + cli.HOLOHUB_ROOT / "benchmarks/holoscan_flow_benchmarking/restore_application.sh" + ) + run_command([str(restore_script), str(app_source_path)], dry_run=dryrun) + + def build_project_locally( cli, project_name: str, @@ -243,8 +261,13 @@ def build_project_locally( benchmark: bool = False, configure_args: Optional[list[str]] = None, extra_env: Optional[dict] = None, + configure_only: bool = False, ) -> tuple[Path, dict]: - """Helper to build a project locally (cmake + cmake --build).""" + """Helper to build a project locally (cmake + cmake --build). + + With *configure_only*, stop after the configure step. Any benchmark patch applied + beforehand is still reverted, so the source tree is left as it was found. + """ project_data = cli.find_project(project_name=project_name, language=language) project_type = project_data.get("project_type", "application") @@ -396,6 +419,10 @@ def build_project_locally( run_command(cmake_args, dry_run=dryrun, env=build_env) + if configure_only: + _restore_benchmark_patch(cli, app_source_path, project_type, dryrun) + return build_dir, project_data + # Build the project with optional parallel jobs build_cmd = ["cmake", "--build", str(build_dir), "--config", build_type] # Determine the number of parallel jobs (user input > env var > CPU count): @@ -439,11 +466,6 @@ def build_project_locally( env=build_env, ) - # Handle benchmark restoration after building - if benchmark and app_source_path and project_type in ["application", "benchmark"]: - restore_script = ( - cli.HOLOHUB_ROOT / "benchmarks/holoscan_flow_benchmarking/restore_application.sh" - ) - run_command([str(restore_script), str(app_source_path)], dry_run=dryrun) + _restore_benchmark_patch(cli, app_source_path, project_type, dryrun) return build_dir, project_data diff --git a/tests/unit/test_lifecycle_commands.py b/tests/unit/test_lifecycle_commands.py index f8b9f9ab..46cfb0f5 100644 --- a/tests/unit/test_lifecycle_commands.py +++ b/tests/unit/test_lifecycle_commands.py @@ -608,3 +608,63 @@ def test_handle_test_local_runs_ctest_in_repo_with_environment(tmp_path, monkeyp assert "-S local.ctest" in command[2] assert kwargs["dry_run"] is True assert str(cli.HOLOHUB_ROOT) in kwargs["env"]["PYTHONPATH"] + + +def test_build_project_locally_configure_only_skips_the_compile(tmp_path, monkeypatch): + cli = RecordingCLI(tmp_path) + calls = [] + monkeypatch.setattr(build_cmd, "run_command", lambda cmd, **kwargs: calls.append(cmd)) + monkeypatch.setattr(build_cmd.shutil, "which", lambda name: None) + + build_dir, _ = build_cmd.build_project_locally( + cli, + "smoke_app", + dryrun=True, + configure_only=True, + ) + + assert len(calls) == 1, f"expected only the configure step, got {calls}" + assert calls[0][0] == "cmake" + assert "--build" not in calls[0] + assert build_dir == tmp_path / "build" / "smoke_app" + + +def test_build_project_locally_configure_only_still_restores_benchmark_patch(tmp_path, monkeypatch): + """The patch is applied before configure, so stopping early must not leave it behind.""" + cli = RecordingCLI(tmp_path) + calls = [] + monkeypatch.setattr(build_cmd, "run_command", lambda cmd, **kwargs: calls.append(cmd)) + monkeypatch.setattr(build_cmd.shutil, "which", lambda name: None) + + build_cmd.build_project_locally( + cli, + "smoke_app", + dryrun=True, + benchmark=True, + configure_only=True, + ) + + rendered = [" ".join(str(part) for part in cmd) for cmd in calls] + assert any("patch_application.sh" in cmd for cmd in rendered) + assert any("restore_application.sh" in cmd for cmd in rendered) + assert not any("--build" in cmd for cmd in rendered) + + +def test_make_local_build_command_forwards_configure_only(): + args = Namespace( + project="smoke_app", + mode=None, + build_type=None, + with_operators=None, + pkg_generator=None, + parallel=None, + verbose=False, + configure_only=True, + benchmark=False, + configure_args=None, + ) + + command = build_cmd.make_local_build_command("holoscan", args, None, None) + + assert "--configure-only" in command + assert command.startswith("holoscan build smoke_app --local")