Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions hatch_cpp/config.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand Down
114 changes: 114 additions & 0 deletions hatch_cpp/tests/test_structs.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
14 changes: 12 additions & 2 deletions hatch_cpp/toolchains/cmake.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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}")

Expand Down