From b7bc6512565e702c47fb6e309317a74d9cee19f2 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:04:18 -0500 Subject: [PATCH] Respect CMake args, enable forcing toolchains on/off --- hatch_cpp/config.py | 21 +++++- hatch_cpp/tests/test_structs.py | 114 ++++++++++++++++++++++++++++++++ hatch_cpp/toolchains/cmake.py | 14 +++- 3 files changed, 144 insertions(+), 5 deletions(-) diff --git a/hatch_cpp/config.py b/hatch_cpp/config.py index 9e9c4d8..4165c43 100644 --- a/hatch_cpp/config.py +++ b/hatch_cpp/config.py @@ -1,6 +1,6 @@ from __future__ import annotations -from os import system as system_call +from os import environ, system as system_call from pathlib import Path from typing import List, Optional @@ -63,12 +63,27 @@ class HatchCppBuildPlan(HatchCppBuildConfig): def generate(self): self.commands = [] + # Check for env var overrides + vcpkg_override = environ.get("HATCH_CPP_VCPKG") + cmake_override = environ.get("HATCH_CPP_CMAKE") + # Evaluate toolchains - if self.vcpkg and Path(self.vcpkg.vcpkg).exists(): + if vcpkg_override == "1": + if self.vcpkg: + self._active_toolchains.append("vcpkg") + else: + log.warning("HATCH_CPP_VCPKG=1 set but no vcpkg configuration found; ignoring.") + elif vcpkg_override != "0" and self.vcpkg and Path(self.vcpkg.vcpkg).exists(): self._active_toolchains.append("vcpkg") + if self.libraries: self._active_toolchains.append("vanilla") - elif self.cmake: + elif cmake_override == "1": + if self.cmake: + self._active_toolchains.append("cmake") + else: + log.warning("HATCH_CPP_CMAKE=1 set but no cmake configuration found; ignoring.") + elif cmake_override != "0" and self.cmake: self._active_toolchains.append("cmake") # Collect toolchain commands diff --git a/hatch_cpp/tests/test_structs.py b/hatch_cpp/tests/test_structs.py index 30815b1..afa0c25 100644 --- a/hatch_cpp/tests/test_structs.py +++ b/hatch_cpp/tests/test_structs.py @@ -1,5 +1,7 @@ +from os import environ from pathlib import Path from sys import version_info +from unittest.mock import patch import pytest from pydantic import ValidationError @@ -54,3 +56,115 @@ def test_platform_toolchain_override(self): assert "clang" in hatch_build_config.platform.cc assert "clang++" in hatch_build_config.platform.cxx assert hatch_build_config.platform.toolchain == "gcc" + + def test_cmake_args_env_variable(self): + """Test that CMAKE_ARGS environment variable is respected.""" + txt = (Path(__file__).parent / "test_project_cmake" / "pyproject.toml").read_text() + toml_data = loads(txt) + hatch_build_config = HatchCppBuildConfig(name=toml_data["project"]["name"], **toml_data["tool"]["hatch"]["build"]["hooks"]["hatch-cpp"]) + hatch_build_plan = HatchCppBuildPlan(**hatch_build_config.model_dump()) + + with patch.dict(environ, {"CMAKE_ARGS": "-DFOO=bar -DBAZ=qux"}): + hatch_build_plan.generate() + assert "-DFOO=bar" in hatch_build_plan.commands[0] + assert "-DBAZ=qux" in hatch_build_plan.commands[0] + + def test_cmake_args_env_variable_empty(self): + """Test that an empty CMAKE_ARGS does not add extra whitespace.""" + txt = (Path(__file__).parent / "test_project_cmake" / "pyproject.toml").read_text() + toml_data = loads(txt) + hatch_build_config = HatchCppBuildConfig(name=toml_data["project"]["name"], **toml_data["tool"]["hatch"]["build"]["hooks"]["hatch-cpp"]) + hatch_build_plan = HatchCppBuildPlan(**hatch_build_config.model_dump()) + + with patch.dict(environ, {"CMAKE_ARGS": ""}): + hatch_build_plan.generate() + # Should not have trailing whitespace from empty CMAKE_ARGS + assert not hatch_build_plan.commands[0].endswith(" ") + + def test_cmake_generator_env_variable(self): + """Test that CMAKE_GENERATOR environment variable is respected on non-Windows platforms.""" + txt = (Path(__file__).parent / "test_project_cmake" / "pyproject.toml").read_text() + toml_data = loads(txt) + hatch_build_config = HatchCppBuildConfig(name=toml_data["project"]["name"], **toml_data["tool"]["hatch"]["build"]["hooks"]["hatch-cpp"]) + hatch_build_plan = HatchCppBuildPlan(**hatch_build_config.model_dump()) + + with patch.dict(environ, {"CMAKE_GENERATOR": "Ninja"}): + hatch_build_plan.generate() + assert '-G "Ninja"' in hatch_build_plan.commands[0] + + def test_cmake_generator_env_variable_unset(self): + """Test that no -G flag is added on non-Windows when CMAKE_GENERATOR is not set.""" + txt = (Path(__file__).parent / "test_project_cmake" / "pyproject.toml").read_text() + toml_data = loads(txt) + hatch_build_config = HatchCppBuildConfig(name=toml_data["project"]["name"], **toml_data["tool"]["hatch"]["build"]["hooks"]["hatch-cpp"]) + hatch_build_plan = HatchCppBuildPlan(**hatch_build_config.model_dump()) + + with patch.dict(environ, {}, clear=False): + # Remove CMAKE_GENERATOR if present + environ.pop("CMAKE_GENERATOR", None) + hatch_build_plan.generate() + if hatch_build_plan.platform.platform != "win32": + assert "-G " not in hatch_build_plan.commands[0] + + def test_hatch_cpp_cmake_env_force_off(self): + """Test that HATCH_CPP_CMAKE=0 disables cmake even when cmake config is present.""" + txt = (Path(__file__).parent / "test_project_cmake" / "pyproject.toml").read_text() + toml_data = loads(txt) + hatch_build_config = HatchCppBuildConfig(name=toml_data["project"]["name"], **toml_data["tool"]["hatch"]["build"]["hooks"]["hatch-cpp"]) + hatch_build_plan = HatchCppBuildPlan(**hatch_build_config.model_dump()) + + assert hatch_build_plan.cmake is not None + with patch.dict(environ, {"HATCH_CPP_CMAKE": "0"}): + hatch_build_plan.generate() + # cmake should not be active, so no cmake commands generated + assert len(hatch_build_plan.commands) == 0 + assert "cmake" not in hatch_build_plan._active_toolchains + + def test_hatch_cpp_cmake_env_force_on(self): + """Test that HATCH_CPP_CMAKE=1 enables cmake when cmake config is present.""" + txt = (Path(__file__).parent / "test_project_cmake" / "pyproject.toml").read_text() + toml_data = loads(txt) + hatch_build_config = HatchCppBuildConfig(name=toml_data["project"]["name"], **toml_data["tool"]["hatch"]["build"]["hooks"]["hatch-cpp"]) + hatch_build_plan = HatchCppBuildPlan(**hatch_build_config.model_dump()) + + assert hatch_build_plan.cmake is not None + with patch.dict(environ, {"HATCH_CPP_CMAKE": "1"}): + hatch_build_plan.generate() + assert "cmake" in hatch_build_plan._active_toolchains + + def test_hatch_cpp_cmake_env_force_on_no_config(self): + """Test that HATCH_CPP_CMAKE=1 warns and skips when no cmake config exists.""" + txt = (Path(__file__).parent / "test_project_cmake" / "pyproject.toml").read_text() + toml_data = loads(txt) + config_data = toml_data["tool"]["hatch"]["build"]["hooks"]["hatch-cpp"].copy() + config_data.pop("cmake", None) + hatch_build_config = HatchCppBuildConfig(name=toml_data["project"]["name"], **config_data) + hatch_build_plan = HatchCppBuildPlan(**hatch_build_config.model_dump()) + + assert hatch_build_plan.cmake is None + with patch.dict(environ, {"HATCH_CPP_CMAKE": "1"}): + hatch_build_plan.generate() + # cmake should NOT be activated when there's no config + assert "cmake" not in hatch_build_plan._active_toolchains + + def test_hatch_cpp_vcpkg_env_force_off(self): + """Test that HATCH_CPP_VCPKG=0 disables vcpkg even when vcpkg.json exists.""" + txt = (Path(__file__).parent / "test_project_cmake_vcpkg" / "pyproject.toml").read_text() + toml_data = loads(txt) + hatch_build_config = HatchCppBuildConfig(name=toml_data["project"]["name"], **toml_data["tool"]["hatch"]["build"]["hooks"]["hatch-cpp"]) + hatch_build_plan = HatchCppBuildPlan(**hatch_build_config.model_dump()) + + with patch.dict(environ, {"HATCH_CPP_VCPKG": "0"}): + hatch_build_plan.generate() + assert "vcpkg" not in hatch_build_plan._active_toolchains + + def test_hatch_cpp_vcpkg_env_force_on(self): + """Test that HATCH_CPP_VCPKG=1 enables vcpkg even when vcpkg.json doesn't exist.""" + txt = (Path(__file__).parent / "test_project_cmake" / "pyproject.toml").read_text() + toml_data = loads(txt) + hatch_build_config = HatchCppBuildConfig(name=toml_data["project"]["name"], **toml_data["tool"]["hatch"]["build"]["hooks"]["hatch-cpp"]) + hatch_build_plan = HatchCppBuildPlan(**hatch_build_config.model_dump()) + + with patch.dict(environ, {"HATCH_CPP_VCPKG": "1"}): + hatch_build_plan.generate() + assert "vcpkg" in hatch_build_plan._active_toolchains diff --git a/hatch_cpp/toolchains/cmake.py b/hatch_cpp/toolchains/cmake.py index a5d2f15..01e1f53 100644 --- a/hatch_cpp/toolchains/cmake.py +++ b/hatch_cpp/toolchains/cmake.py @@ -54,9 +54,14 @@ def generate(self, config) -> Dict[str, Any]: commands[-1] += f" -DCMAKE_INSTALL_PREFIX={Path(self.root).parent}" # TODO: CMAKE_CXX_COMPILER + # Respect CMAKE_GENERATOR environment variable + cmake_generator = environ.get("CMAKE_GENERATOR", "") if config.platform.platform == "win32": - # TODO: prefix? - commands[-1] += f' -G "{environ.get("CMAKE_GENERATOR", "Visual Studio 17 2022")}"' + if not cmake_generator: + cmake_generator = "Visual Studio 17 2022" + commands[-1] += f' -G "{cmake_generator}"' + elif cmake_generator: + commands[-1] += f' -G "{cmake_generator}"' # Put in CMake flags args = self.cmake_args.copy() @@ -78,6 +83,11 @@ def generate(self, config) -> Dict[str, Any]: if config.platform.platform == "darwin": commands[-1] += f" -DCMAKE_OSX_DEPLOYMENT_TARGET={environ.get('OSX_DEPLOYMENT_TARGET', '11')}" + # Respect CMAKE_ARGS environment variable + cmake_args_env = environ.get("CMAKE_ARGS", "").strip() + if cmake_args_env: + commands[-1] += " " + cmake_args_env + # Append build command commands.append(f"cmake --build {self.build} --config {config.build_type}")