diff --git a/src/holoscan_cli/commands/package.py b/src/holoscan_cli/commands/package.py index e73a8e7..220ca61 100644 --- a/src/holoscan_cli/commands/package.py +++ b/src/holoscan_cli/commands/package.py @@ -61,7 +61,7 @@ def register_package_parser( type=str, default="DEB", dest="pkg_generator", - help="Comma-separated package generators: DEB, WHEEL (default: DEB)", + help="Comma-separated package generators: DEB, TGZ, WHEEL (default: DEB)", ) parser.add_argument("--language", choices=["cpp", "python"], default=None) parser.add_argument("--verbose", action="store_true") @@ -236,6 +236,8 @@ def _package_locally(cli, args: argparse.Namespace, project_data: dict) -> None: f"-DMODULE_{package_slug}=ON", f"-DPKG_{package_slug}=ON", ] + if "TGZ" in cpack_generators: + cmake_args.append("-DHOLOHUB_PKG_TGZ=ON") if shutil.which("ninja"): cmake_args.extend(["-G", "Ninja"]) run_command(cmake_args, dry_run=dryrun, env=build_env) @@ -252,18 +254,46 @@ def _package_locally(cli, args: argparse.Namespace, project_data: dict) -> None: run_command(build_cmd, dry_run=dryrun, env=build_env) pkg_config_dir = build_dir / "pkg" - cpack_configs = ( + all_cpack_configs = ( list(pkg_config_dir.glob("CPackConfig-*.cmake")) if pkg_config_dir.exists() else [] ) - if not cpack_configs and dryrun: + if not all_cpack_configs and dryrun: bare = project_name.replace("_", "-") if bare.startswith("holoscan-"): bare = bare[len("holoscan-") :] - cpack_configs = [pkg_config_dir / f"CPackConfig-holoscan-{bare}.cmake"] - for cpack_config in cpack_configs: - for generator in cpack_generators: + base_name = f"CPackConfig-holoscan-{bare}" + all_cpack_configs = [pkg_config_dir / f"{base_name}.cmake"] + all_cpack_configs += [ + pkg_config_dir / f"{base_name}-{g}.cmake" for g in cpack_generators + ] + elif not all_cpack_configs: + fatal( + f"No CPack config files were generated in {pkg_config_dir}. " + "Check module packaging configuration." + ) + + _KNOWN_GEN_SUFFIXES = ("TGZ", "DEB", "RPM", "ZIP") + gen_specific_configs: dict = {} + base_configs = [] + for c in all_cpack_configs: + stem_upper = c.stem.upper() + matched = next((g for g in _KNOWN_GEN_SUFFIXES if stem_upper.endswith(f"-{g}")), None) + if matched: + gen_specific_configs.setdefault(matched, []).append(c) + else: + base_configs.append(c) + + for gen in cpack_generators: + configs_for_gen = gen_specific_configs.get(gen) or base_configs + if not configs_for_gen: + available = ", ".join(sorted(gen_specific_configs.keys())) or "none" + fatal( + f"No CPack config found for generator '{gen}' in {pkg_config_dir}. " + f"Available generator-specific configs: {available}." + ) + for cpack_config in configs_for_gen: run_command( - ["cpack", "--config", str(cpack_config), "-G", generator], + ["cpack", "--config", str(cpack_config), "-G", gen], dry_run=dryrun, env=build_env, ) diff --git a/tests/unit/test_package_cmd.py b/tests/unit/test_package_cmd.py index 0d06221..ab36d55 100644 --- a/tests/unit/test_package_cmd.py +++ b/tests/unit/test_package_cmd.py @@ -77,6 +77,7 @@ def test_package_deb_emits_module_cmake_flag_for_in_tree_module(tmp_path, monkey assert "-DMODULE_test_module_fixture=ON" in cmake_args assert "-DPKG_test_module_fixture=ON" in cmake_args assert "-DBUILD_ALL=OFF" in cmake_args + assert "-DHOLOHUB_PKG_TGZ=ON" not in cmake_args assert calls[2][0] == "cpack" @@ -233,3 +234,138 @@ def test_resolve_module_project_falls_back_to_source_tree_when_cwd_metadata_inva ) assert project_data["project_name"] == "test-module-fixture" + + +def _tgz_project_data(tmp_path): + return { + "project_name": "test-module-fixture", + "project_type": "module", + "source_folder": tmp_path / "repo" / "modules" / "test-module-fixture", + "metadata": {"language": ["C++"]}, + } + + +def test_package_tgz_sets_cmake_flag(tmp_path, monkeypatch): + """TGZ generator adds -DHOLOHUB_PKG_TGZ=ON to the cmake configure call.""" + cli = _cli(tmp_path, _tgz_project_data(tmp_path)) + calls = [] + monkeypatch.setattr(package_cmd, "run_command", lambda cmd, **kwargs: calls.append(cmd)) + monkeypatch.setattr(package_cmd.shutil, "which", lambda _: None) + + package_cmd.handle_package(cli, _args(project="test-module-fixture", pkg_generator="TGZ")) + + cmake_args = " ".join(str(a) for a in calls[0]) + assert "-DHOLOHUB_PKG_TGZ=ON" in cmake_args + + +def test_package_tgz_invokes_cpack_tgz(tmp_path, monkeypatch): + """TGZ generator calls cpack with -G TGZ using a generator-specific config.""" + cli = _cli(tmp_path, _tgz_project_data(tmp_path)) + pkg_dir = cli.DEFAULT_BUILD_PARENT_DIR / "test_module_fixture" / "package" / "pkg" + pkg_dir.mkdir(parents=True) + (pkg_dir / "CPackConfig-test-module-fixture-TGZ.cmake").touch() + + calls = [] + monkeypatch.setattr(package_cmd, "run_command", lambda cmd, **kwargs: calls.append(cmd)) + monkeypatch.setattr(package_cmd.shutil, "which", lambda _: None) + + package_cmd.handle_package( + cli, _args(project="test-module-fixture", pkg_generator="TGZ", dryrun=False) + ) + + assert len(calls) == 3 # cmake configure, cmake build, cpack + cmake_args = " ".join(str(a) for a in calls[0]) + assert "-DHOLOHUB_PKG_TGZ=ON" in cmake_args + cpack_args = calls[2] + assert cpack_args[0] == "cpack" + assert "-G" in cpack_args + assert "TGZ" in cpack_args + + +def test_package_multi_generator_deb_tgz(tmp_path, monkeypatch): + """DEB,TGZ produces two cpack calls, one per generator.""" + cli = _cli(tmp_path, _tgz_project_data(tmp_path)) + pkg_dir = cli.DEFAULT_BUILD_PARENT_DIR / "test_module_fixture" / "package" / "pkg" + pkg_dir.mkdir(parents=True) + (pkg_dir / "CPackConfig-test-module-fixture.cmake").touch() + (pkg_dir / "CPackConfig-test-module-fixture-TGZ.cmake").touch() + + calls = [] + monkeypatch.setattr(package_cmd, "run_command", lambda cmd, **kwargs: calls.append(cmd)) + monkeypatch.setattr(package_cmd.shutil, "which", lambda _: None) + + package_cmd.handle_package( + cli, _args(project="test-module-fixture", pkg_generator="DEB,TGZ", dryrun=False) + ) + + assert len(calls) == 4 # cmake configure, cmake build, cpack DEB, cpack TGZ + cmake_args = " ".join(str(a) for a in calls[0]) + assert "-DHOLOHUB_PKG_TGZ=ON" in cmake_args + assert "DEB" in calls[2] + assert "TGZ" in calls[3] + + +def test_package_tgz_routes_to_generator_specific_config(tmp_path, monkeypatch): + """Generator-specific config is used for TGZ; base config is used for DEB.""" + cli = _cli(tmp_path, _tgz_project_data(tmp_path)) + pkg_dir = cli.DEFAULT_BUILD_PARENT_DIR / "test_module_fixture" / "package" / "pkg" + pkg_dir.mkdir(parents=True) + base_cfg = pkg_dir / "CPackConfig-test-module-fixture.cmake" + tgz_cfg = pkg_dir / "CPackConfig-test-module-fixture-TGZ.cmake" + base_cfg.touch() + tgz_cfg.touch() + + calls = [] + monkeypatch.setattr(package_cmd, "run_command", lambda cmd, **kwargs: calls.append(cmd)) + monkeypatch.setattr(package_cmd.shutil, "which", lambda _: None) + + package_cmd.handle_package( + cli, _args(project="test-module-fixture", pkg_generator="DEB,TGZ", dryrun=False) + ) + + deb_args = " ".join(str(a) for a in calls[2]) + assert str(base_cfg) in deb_args + assert str(tgz_cfg) not in deb_args + + tgz_args = " ".join(str(a) for a in calls[3]) + assert str(tgz_cfg) in tgz_args + assert str(base_cfg) not in tgz_args + + +def test_package_missing_cpack_configs_fatal(tmp_path, monkeypatch, capsys): + """When the build produces no CPack configs, fatal is called with a clear message.""" + cli = _cli(tmp_path, _tgz_project_data(tmp_path)) + pkg_dir = cli.DEFAULT_BUILD_PARENT_DIR / "test_module_fixture" / "package" / "pkg" + pkg_dir.mkdir(parents=True) + + monkeypatch.setattr(package_cmd, "run_command", lambda cmd, **kwargs: None) + monkeypatch.setattr(package_cmd.shutil, "which", lambda _: None) + + with pytest.raises(SystemExit) as excinfo: + package_cmd.handle_package( + cli, _args(project="test-module-fixture", pkg_generator="TGZ", dryrun=False) + ) + + assert excinfo.value.code == 1 + assert "No CPack config files" in capsys.readouterr().err + + +def test_package_missing_generator_config_fatal(tmp_path, monkeypatch, capsys): + """When no config exists for the requested generator, fatal lists available generators.""" + cli = _cli(tmp_path, _tgz_project_data(tmp_path)) + pkg_dir = cli.DEFAULT_BUILD_PARENT_DIR / "test_module_fixture" / "package" / "pkg" + pkg_dir.mkdir(parents=True) + (pkg_dir / "CPackConfig-test-module-fixture-DEB.cmake").touch() + + monkeypatch.setattr(package_cmd, "run_command", lambda cmd, **kwargs: None) + monkeypatch.setattr(package_cmd.shutil, "which", lambda _: None) + + with pytest.raises(SystemExit) as excinfo: + package_cmd.handle_package( + cli, _args(project="test-module-fixture", pkg_generator="TGZ", dryrun=False) + ) + + assert excinfo.value.code == 1 + err = capsys.readouterr().err + assert "TGZ" in err + assert "DEB" in err