From bb2190129a15b0aa1f22627ea40e271f0b209bf4 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Tue, 28 Oct 2025 16:53:41 -0400 Subject: [PATCH 01/58] =?UTF-8?q?Bump=20version:=200.1.7=20=E2=86=92=200.1?= =?UTF-8?q?.8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com> --- hatch_cpp/__init__.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hatch_cpp/__init__.py b/hatch_cpp/__init__.py index d3dfbab..2f17aa3 100644 --- a/hatch_cpp/__init__.py +++ b/hatch_cpp/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.1.7" +__version__ = "0.1.8" from .hooks import hatch_register_build_hook from .plugin import HatchCppBuildHook diff --git a/pyproject.toml b/pyproject.toml index e3b1ee4..70d89ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ authors = [{name = "the hatch-cpp authors", email = "t.paine154@gmail.com"}] description = "Hatch plugin for C++ builds" readme = "README.md" license = { text = "Apache-2.0" } -version = "0.1.7" +version = "0.1.8" requires-python = ">=3.9" keywords = [ "hatch", @@ -73,7 +73,7 @@ Repository = "https://github.com/python-project-templates/hatch-cpp" Homepage = "https://github.com/python-project-templates/hatch-cpp" [tool.bumpversion] -current_version = "0.1.7" +current_version = "0.1.8" commit = true tag = false commit_args = "-s" From d8371e4b04180dd1986fb9286b83c6afc89b9ea0 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 1 Nov 2025 17:54:29 +0000 Subject: [PATCH 02/58] Update from copier (2025-11-01T17:54:29) Signed-off-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .copier-answers.yaml | 2 +- pyproject.toml | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.copier-answers.yaml b/.copier-answers.yaml index e789630..026fe73 100644 --- a/.copier-answers.yaml +++ b/.copier-answers.yaml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: f812aaa +_commit: 18afe0a _src_path: https://github.com/python-project-templates/base.git add_docs: false add_extension: python diff --git a/pyproject.toml b/pyproject.toml index 70d89ac..a46bc2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,12 +45,16 @@ develop = [ "check-manifest", "codespell>=2.4,<2.5", "hatchling", +<<<<<<< before updating "hatch-build", "mdformat>=0.7.22,<0.8", +======= + "mdformat>=0.7.22,<1.1", +>>>>>>> after updating "mdformat-tables>=1", "pytest", "pytest-cov", - "ruff", + "ruff>=0.9,<0.15", "twine", "uv", "wheel", From f5c60a4ac1477f6e4485cd2f635dcfc8fcd016e2 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Sat, 1 Nov 2025 14:01:15 -0400 Subject: [PATCH 03/58] Update pyproject.toml --- pyproject.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a46bc2f..e736c34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,12 +45,8 @@ develop = [ "check-manifest", "codespell>=2.4,<2.5", "hatchling", -<<<<<<< before updating "hatch-build", - "mdformat>=0.7.22,<0.8", -======= "mdformat>=0.7.22,<1.1", ->>>>>>> after updating "mdformat-tables>=1", "pytest", "pytest-cov", From 09f727be08c6177a404e5955c776f21a0aa238db Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:49:08 -0500 Subject: [PATCH 04/58] Refactor toolchains, start on vcpkg --- hatch_cpp/__init__.py | 7 +- hatch_cpp/config.py | 80 +++++++++++ hatch_cpp/plugin.py | 2 +- hatch_cpp/tests/test_structs.py | 2 +- hatch_cpp/toolchains/__init__.py | 3 + hatch_cpp/toolchains/cmake.py | 73 ++++++++++ .../{structs.py => toolchains/common.py} | 130 ++---------------- hatch_cpp/toolchains/vcpkg.py | 30 ++++ 8 files changed, 202 insertions(+), 125 deletions(-) create mode 100644 hatch_cpp/config.py rename hatch_cpp/{structs.py => toolchains/common.py} (65%) create mode 100644 hatch_cpp/toolchains/vcpkg.py diff --git a/hatch_cpp/__init__.py b/hatch_cpp/__init__.py index 2f17aa3..d5cf19a 100644 --- a/hatch_cpp/__init__.py +++ b/hatch_cpp/__init__.py @@ -1,5 +1,6 @@ __version__ = "0.1.8" -from .hooks import hatch_register_build_hook -from .plugin import HatchCppBuildHook -from .structs import * +from .config import * +from .hooks import * +from .plugin import * +from .toolchains import * diff --git a/hatch_cpp/config.py b/hatch_cpp/config.py new file mode 100644 index 0000000..8d27fa6 --- /dev/null +++ b/hatch_cpp/config.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +from os import system as system_call +from pathlib import Path +from typing import List, Optional + +from pydantic import BaseModel, Field, model_validator + +from .toolchains import BuildType, HatchCppCmakeConfiguration, HatchCppLibrary, HatchCppPlatform, HatchCppVcpkgConfiguration + +__all__ = ( + "HatchCppBuildConfig", + "HatchCppBuildPlan", +) + + +class HatchCppBuildConfig(BaseModel): + """Build config values for Hatch C++ Builder.""" + + verbose: Optional[bool] = Field(default=False) + name: Optional[str] = Field(default=None) + libraries: List[HatchCppLibrary] = Field(default_factory=list) + cmake: Optional[HatchCppCmakeConfiguration] = Field(default=None) + platform: Optional[HatchCppPlatform] = Field(default_factory=HatchCppPlatform.default) + vcpkg: Optional[HatchCppVcpkgConfiguration] = Field(default=None) + + @model_validator(mode="wrap") + @classmethod + def validate_model(cls, data, handler): + if "toolchain" in data: + data["platform"] = HatchCppPlatform.platform_for_toolchain(data["toolchain"]) + data.pop("toolchain") + elif "platform" not in data: + data["platform"] = HatchCppPlatform.default() + if "cc" in data: + data["platform"].cc = data["cc"] + data.pop("cc") + if "cxx" in data: + data["platform"].cxx = data["cxx"] + data.pop("cxx") + if "ld" in data: + data["platform"].ld = data["ld"] + data.pop("ld") + model = handler(data) + if model.cmake and model.libraries: + raise ValueError("Must not provide libraries when using cmake toolchain.") + return model + + +class HatchCppBuildPlan(HatchCppBuildConfig): + build_type: BuildType = "release" + commands: List[str] = Field(default_factory=list) + + def generate(self): + self.commands = [] + + if self.vcpkg and Path(self.vcpkg.vcpkg).exists(): + self.commands.extend(self.vcpkg.generate(self.platform)) + + if self.libraries: + for library in self.libraries: + compile_flags = self.platform.get_compile_flags(library, self.build_type) + link_flags = self.platform.get_link_flags(library, self.build_type) + self.commands.append( + f"{self.platform.cc if library.language == 'c' else self.platform.cxx} {' '.join(library.sources)} {compile_flags} {link_flags}" + ) + elif self.cmake: + self.commands.extend(self.cmake.generate(self)) + + return self.commands + + def execute(self): + for command in self.commands: + system_call(command) + return self.commands + + def cleanup(self): + if self.platform.platform == "win32": + for temp_obj in Path(".").glob("*.obj"): + temp_obj.unlink() diff --git a/hatch_cpp/plugin.py b/hatch_cpp/plugin.py index ab6ebf8..f1a32b0 100644 --- a/hatch_cpp/plugin.py +++ b/hatch_cpp/plugin.py @@ -9,7 +9,7 @@ from hatchling.builders.hooks.plugin.interface import BuildHookInterface -from .structs import HatchCppBuildConfig, HatchCppBuildPlan +from .config import HatchCppBuildConfig, HatchCppBuildPlan from .utils import import_string __all__ = ("HatchCppBuildHook",) diff --git a/hatch_cpp/tests/test_structs.py b/hatch_cpp/tests/test_structs.py index 263b917..30815b1 100644 --- a/hatch_cpp/tests/test_structs.py +++ b/hatch_cpp/tests/test_structs.py @@ -5,7 +5,7 @@ from pydantic import ValidationError from toml import loads -from hatch_cpp.structs import HatchCppBuildConfig, HatchCppBuildPlan, HatchCppLibrary, HatchCppPlatform +from hatch_cpp import HatchCppBuildConfig, HatchCppBuildPlan, HatchCppLibrary, HatchCppPlatform class TestStructs: diff --git a/hatch_cpp/toolchains/__init__.py b/hatch_cpp/toolchains/__init__.py index e69de29..7917c7b 100644 --- a/hatch_cpp/toolchains/__init__.py +++ b/hatch_cpp/toolchains/__init__.py @@ -0,0 +1,3 @@ +from .cmake import * +from .common import * +from .vcpkg import * diff --git a/hatch_cpp/toolchains/cmake.py b/hatch_cpp/toolchains/cmake.py index e69de29..0e66dc4 100644 --- a/hatch_cpp/toolchains/cmake.py +++ b/hatch_cpp/toolchains/cmake.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from os import environ +from pathlib import Path +from sys import version_info +from typing import Any, Dict, Optional + +from pydantic import BaseModel, Field + +from .common import Platform + +__all__ = ("HatchCppCmakeConfiguration",) + + +class HatchCppCmakeConfiguration(BaseModel): + root: Path + build: Path = Field(default_factory=lambda: Path("build")) + install: Optional[Path] = Field(default=None) + + cmake_arg_prefix: Optional[str] = Field(default=None) + cmake_args: Dict[str, str] = Field(default_factory=dict) + cmake_env_args: Dict[Platform, Dict[str, str]] = Field(default_factory=dict) + + include_flags: Optional[Dict[str, Any]] = Field(default=None) + + def generate(self, config) -> Dict[str, Any]: + commands = [] + + # Derive prefix + if self.cmake_arg_prefix is None: + self.cmake_arg_prefix = f"{config.name.replace('.', '_').replace('-', '_').upper()}_" + + # Append base command + commands.append(f"cmake {Path(self.root).parent} -DCMAKE_BUILD_TYPE={config.build_type} -B {self.build}") + + # Setup install path + if self.install: + commands[-1] += f" -DCMAKE_INSTALL_PREFIX={self.install}" + else: + commands[-1] += f" -DCMAKE_INSTALL_PREFIX={Path(self.root).parent}" + + # TODO: CMAKE_CXX_COMPILER + if config.platform.platform == "win32": + # TODO: prefix? + commands[-1] += f' -G "{environ.get("GENERATOR", "Visual Studio 17 2022")}"' + + # Put in CMake flags + args = self.cmake_args.copy() + for platform, env_args in self.cmake_env_args.items(): + if platform == config.platform.platform: + for key, value in env_args.items(): + args[key] = value + for key, value in args.items(): + commands[-1] += f" -D{self.cmake_arg_prefix}{key.upper()}={value}" + + # Include customs + if self.include_flags: + if self.include_flags.get("python_version", False): + commands[-1] += f" -D{self.cmake_arg_prefix}PYTHON_VERSION={version_info.major}.{version_info.minor}" + if self.include_flags.get("manylinux", False) and config.platform.platform == "linux": + commands[-1] += f" -D{self.cmake_arg_prefix}MANYLINUX=ON" + + # Include mac deployment target + if config.platform.platform == "darwin": + commands[-1] += f" -DCMAKE_OSX_DEPLOYMENT_TARGET={environ.get('OSX_DEPLOYMENT_TARGET', '11')}" + + # Append build command + commands.append(f"cmake --build {self.build} --config {config.build_type}") + + # Append install command + commands.append(f"cmake --install {self.build} --config {config.build_type}") + + return commands diff --git a/hatch_cpp/structs.py b/hatch_cpp/toolchains/common.py similarity index 65% rename from hatch_cpp/structs.py rename to hatch_cpp/toolchains/common.py index 5884988..2ee094f 100644 --- a/hatch_cpp/structs.py +++ b/hatch_cpp/toolchains/common.py @@ -1,22 +1,27 @@ from __future__ import annotations -from os import environ, system as system_call +from os import environ from pathlib import Path from re import match from shutil import which -from sys import executable, platform as sys_platform, version_info +from sys import executable, platform as sys_platform from sysconfig import get_path -from typing import Any, Dict, List, Literal, Optional +from typing import Any, List, Literal, Optional from pydantic import AliasChoices, BaseModel, Field, field_validator, model_validator __all__ = ( - "HatchCppBuildConfig", + "BuildType", + "CompilerToolchain", + "Language", + "Binding", + "Platform", + "PlatformDefaults", "HatchCppLibrary", "HatchCppPlatform", - "HatchCppBuildPlan", ) + BuildType = Literal["debug", "release"] CompilerToolchain = Literal["gcc", "clang", "msvc"] Language = Literal["c", "c++"] @@ -231,118 +236,3 @@ def get_link_flags(self, library: HatchCppLibrary, build_type: BuildType = "rele while flags.count(" "): flags = flags.replace(" ", " ") return flags - - -class HatchCppCmakeConfiguration(BaseModel): - root: Path - build: Path = Field(default_factory=lambda: Path("build")) - install: Optional[Path] = Field(default=None) - - cmake_arg_prefix: Optional[str] = Field(default=None) - cmake_args: Dict[str, str] = Field(default_factory=dict) - cmake_env_args: Dict[Platform, Dict[str, str]] = Field(default_factory=dict) - - include_flags: Optional[Dict[str, Any]] = Field(default=None) - - -class HatchCppBuildConfig(BaseModel): - """Build config values for Hatch C++ Builder.""" - - verbose: Optional[bool] = Field(default=False) - name: Optional[str] = Field(default=None) - libraries: List[HatchCppLibrary] = Field(default_factory=list) - cmake: Optional[HatchCppCmakeConfiguration] = Field(default=None) - platform: Optional[HatchCppPlatform] = Field(default_factory=HatchCppPlatform.default) - - @model_validator(mode="wrap") - @classmethod - def validate_model(cls, data, handler): - if "toolchain" in data: - data["platform"] = HatchCppPlatform.platform_for_toolchain(data["toolchain"]) - data.pop("toolchain") - elif "platform" not in data: - data["platform"] = HatchCppPlatform.default() - if "cc" in data: - data["platform"].cc = data["cc"] - data.pop("cc") - if "cxx" in data: - data["platform"].cxx = data["cxx"] - data.pop("cxx") - if "ld" in data: - data["platform"].ld = data["ld"] - data.pop("ld") - model = handler(data) - if model.cmake and model.libraries: - raise ValueError("Must not provide libraries when using cmake toolchain.") - return model - - -class HatchCppBuildPlan(HatchCppBuildConfig): - build_type: BuildType = "release" - commands: List[str] = Field(default_factory=list) - - def generate(self): - self.commands = [] - if self.libraries: - for library in self.libraries: - compile_flags = self.platform.get_compile_flags(library, self.build_type) - link_flags = self.platform.get_link_flags(library, self.build_type) - self.commands.append( - f"{self.platform.cc if library.language == 'c' else self.platform.cxx} {' '.join(library.sources)} {compile_flags} {link_flags}" - ) - elif self.cmake: - # Derive prefix - if self.cmake.cmake_arg_prefix is None: - self.cmake.cmake_arg_prefix = f"{self.name.replace('.', '_').replace('-', '_').upper()}_" - - # Append base command - self.commands.append(f"cmake {Path(self.cmake.root).parent} -DCMAKE_BUILD_TYPE={self.build_type} -B {self.cmake.build}") - - # Setup install path - if self.cmake.install: - self.commands[-1] += f" -DCMAKE_INSTALL_PREFIX={self.cmake.install}" - else: - self.commands[-1] += f" -DCMAKE_INSTALL_PREFIX={Path(self.cmake.root).parent}" - - # TODO: CMAKE_CXX_COMPILER - if self.platform.platform == "win32": - # TODO: prefix? - self.commands[-1] += f' -G "{environ.get("GENERATOR", "Visual Studio 17 2022")}"' - - # Put in CMake flags - args = self.cmake.cmake_args.copy() - for platform, env_args in self.cmake.cmake_env_args.items(): - if platform == self.platform.platform: - for key, value in env_args.items(): - args[key] = value - for key, value in args.items(): - self.commands[-1] += f" -D{self.cmake.cmake_arg_prefix}{key.upper()}={value}" - - # Include customs - if self.cmake.include_flags: - if self.cmake.include_flags.get("python_version", False): - self.commands[-1] += f" -D{self.cmake.cmake_arg_prefix}PYTHON_VERSION={version_info.major}.{version_info.minor}" - if self.cmake.include_flags.get("manylinux", False) and self.platform.platform == "linux": - self.commands[-1] += f" -D{self.cmake.cmake_arg_prefix}MANYLINUX=ON" - - # Include mac deployment target - if self.platform.platform == "darwin": - self.commands[-1] += f" -DCMAKE_OSX_DEPLOYMENT_TARGET={environ.get('OSX_DEPLOYMENT_TARGET', '11')}" - - # Append build command - self.commands.append(f"cmake --build {self.cmake.build} --config {self.build_type}") - - # Append install command - self.commands.append(f"cmake --install {self.cmake.build} --config {self.build_type}") - - return self.commands - - def execute(self): - for command in self.commands: - system_call(command) - return self.commands - - def cleanup(self): - if self.platform.platform == "win32": - for temp_obj in Path(".").glob("*.obj"): - temp_obj.unlink() diff --git a/hatch_cpp/toolchains/vcpkg.py b/hatch_cpp/toolchains/vcpkg.py new file mode 100644 index 0000000..556f6a7 --- /dev/null +++ b/hatch_cpp/toolchains/vcpkg.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from pathlib import Path +from sys import platform as sys_platform +from typing import Optional + +from pydantic import BaseModel, Field + +__all__ = ("HatchCppVcpkgConfiguration",) + + +class HatchCppVcpkgConfiguration(BaseModel): + vcpkg: Optional[str] = Field(default="vcpkg.json") + vcpkg_root: Optional[Path] = Field(default=Path("vcpkg")) + vcpkg_repo: Optional[str] = Field(default="https://github.com/microsoft/vcpkg.git") + + def generate(self, config): + commands = [] + + if self.vcpkg and Path(self.vcpkg.vcpkg).exists(): + if not Path(self.vcpkg.vcpkg_root).exists(): + commands.append(f"git clone {self.vcpkg.vcpkg_repo} {self.vcpkg.vcpkg_root}") + commands.append( + f"./{self.vcpkg.vcpkg_root / 'bootstrap-vcpkg.sh' if sys_platform != 'win32' else self.vcpkg.vcpkg_root / 'sbootstrap-vcpkg.bat'}" + ) + commands.append( + f"./{self.vcpkg.vcpkg_root / 'vcpkg'} install --triplet {config.platform.platform}-{config.platform.toolchain} --manifest-root {Path(self.vcpkg.vcpkg).parent}" + ) + + return commands From e49505d2c8f706ffeb9e7ad1afc5ac8551b8a8cc Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Mon, 3 Nov 2025 21:04:07 -0500 Subject: [PATCH 05/58] Add vcpkg json to pybind and cmake projects --- .../test_project_cmake_vcpkg/CMakeLists.txt | 92 ++++++++++++ .../tests/test_project_cmake_vcpkg/Makefile | 140 ++++++++++++++++++ .../cpp/project/basic.cpp | 5 + .../cpp/project/basic.hpp | 17 +++ .../project/__init__.py | 0 .../project/include/project/basic.hpp | 17 +++ .../test_project_cmake_vcpkg/pyproject.toml | 39 +++++ .../tests/test_project_cmake_vcpkg/vcpkg.json | 8 + .../cpp/project/basic.cpp | 6 + .../cpp/project/basic.hpp | 9 ++ .../project/__init__.py | 0 .../test_project_pybind_vcpkg/pyproject.toml | 35 +++++ .../test_project_pybind_vcpkg/vcpkg.json | 8 + hatch_cpp/tests/test_projects.py | 2 + 14 files changed, 378 insertions(+) create mode 100644 hatch_cpp/tests/test_project_cmake_vcpkg/CMakeLists.txt create mode 100644 hatch_cpp/tests/test_project_cmake_vcpkg/Makefile create mode 100644 hatch_cpp/tests/test_project_cmake_vcpkg/cpp/project/basic.cpp create mode 100644 hatch_cpp/tests/test_project_cmake_vcpkg/cpp/project/basic.hpp create mode 100644 hatch_cpp/tests/test_project_cmake_vcpkg/project/__init__.py create mode 100644 hatch_cpp/tests/test_project_cmake_vcpkg/project/include/project/basic.hpp create mode 100644 hatch_cpp/tests/test_project_cmake_vcpkg/pyproject.toml create mode 100644 hatch_cpp/tests/test_project_cmake_vcpkg/vcpkg.json create mode 100644 hatch_cpp/tests/test_project_pybind_vcpkg/cpp/project/basic.cpp create mode 100644 hatch_cpp/tests/test_project_pybind_vcpkg/cpp/project/basic.hpp create mode 100644 hatch_cpp/tests/test_project_pybind_vcpkg/project/__init__.py create mode 100644 hatch_cpp/tests/test_project_pybind_vcpkg/pyproject.toml create mode 100644 hatch_cpp/tests/test_project_pybind_vcpkg/vcpkg.json diff --git a/hatch_cpp/tests/test_project_cmake_vcpkg/CMakeLists.txt b/hatch_cpp/tests/test_project_cmake_vcpkg/CMakeLists.txt new file mode 100644 index 0000000..6344c70 --- /dev/null +++ b/hatch_cpp/tests/test_project_cmake_vcpkg/CMakeLists.txt @@ -0,0 +1,92 @@ +cmake_minimum_required(VERSION 3.20.0) +project(hatch-cpp-test-project-basic VERSION "0.1.0") +set(CMAKE_CXX_STANDARD 20) +include(CheckCCompilerFlag) +include(CheckLinkerFlag) + +if(${CMAKE_SYSTEM_NAME} MATCHES "Windows") + set(WIN32 ON) + set(MACOS OFF) + set(LINUX OFF) +elseif(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") + set(WIN32 OFF) + set(MACOS ON) + set(LINUX OFF) +else() + set(WIN32 OFF) + set(MACOS OFF) + set(LINUX ON) +endif() + +option(CMAKE_BUILD_TYPE "Release/Debug build" RELEASE) +option(HATCH_CPP_TEST_PROJECT_BASIC_BUILD_TESTS "Build tests" OFF) +option(HATCH_CPP_TEST_PROJECT_BASIC_MANYLINUX "Build for python's manylinux setup" OFF) + +string(TOLOWER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_LOWER) + +set(BUILD_SHARED_LIBS TRUE) +set(CMAKE_MACOSX_RPATH TRUE) +set(CMAKE_SKIP_RPATH FALSE) +set(CMAKE_SKIP_BUILD_RPATH FALSE) +set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) +set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) +set(CMAKE_INSTALL_NAME_DIR "@rpath") +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +string(REGEX REPLACE "[ ]*-O[^ ]+[ ]*" " " CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") +string(REGEX REPLACE "[ ]*-Wl,-O2 -Wl,[^ ]+[ ]*" " " CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") +string(REGEX REPLACE "[ ]*-Wl,-O2 -Wl,[^ ]+[ ]*" " " CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS}") + + +if(MACOS) + set(CMAKE_THREAD_LIBS_INIT "-lpthread") + set(CMAKE_HAVE_THREADS_LIBRARY 1) + set(CMAKE_USE_WIN32_THREADS_INIT 0) + set(CMAKE_USE_PTHREADS_INIT 1) + set(THREADS_PREFER_PTHREAD_FLAG ON) + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -undefined dynamic_lookup") +endif() + + +if(MACOS) + set(CMAKE_INSTALL_RPATH "@loader_path/") +elseif(LINUX) + set(CMAKE_INSTALL_RPATH "\$ORIGIN") +endif() + +if(WIN32) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /EHsc /MP /bigobj") + foreach(warning 4244 4251 4267 4275 4290 4786 4305 4996) + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd${warning}") + endforeach(warning) +else() + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} \ + -g \ + -Wall \ + -Werror \ + -Wno-deprecated-declarations \ + -Wno-deprecated \ + ") +endif() + + +find_package(Python ${CSP_PYTHON_VERSION} EXACT REQUIRED COMPONENTS Interpreter Development.Module) +link_directories(${Python_LIBRARY_DIRS}) +include_directories(${Python_INCLUDE_DIRS}) + +set(CMAKE_SHARED_LIBRARY_PREFIX "") +if(NOT WIN32) + set(CMAKE_SHARED_LIBRARY_SUFFIX .so) +else() + set(CMAKE_SHARED_LIBRARY_SUFFIX .pyd) +endif() + +include_directories("${CMAKE_SOURCE_DIR}/cpp") + +add_library(extension SHARED cpp/project/basic.cpp) +set_target_properties(extension PROPERTIES PUBLIC_HEADER cpp/project/basic.hpp) +install(TARGETS extension + PUBLIC_HEADER DESTINATION project/include/project + RUNTIME DESTINATION project/ + LIBRARY DESTINATION project/ + ) diff --git a/hatch_cpp/tests/test_project_cmake_vcpkg/Makefile b/hatch_cpp/tests/test_project_cmake_vcpkg/Makefile new file mode 100644 index 0000000..c265da9 --- /dev/null +++ b/hatch_cpp/tests/test_project_cmake_vcpkg/Makefile @@ -0,0 +1,140 @@ +# CMAKE generated file: DO NOT EDIT! +# Generated by "Unix Makefiles" Generator, CMake Version 3.31 + +# Default target executed when no arguments are given to make. +default_target: all +.PHONY : default_target + +# Allow only one "make -f Makefile2" at a time, but pass parallelism. +.NOTPARALLEL: + +#============================================================================= +# Special targets provided by cmake. + +# Disable implicit rules so canonical targets will work. +.SUFFIXES: + +# Disable VCS-based implicit rules. +% : %,v + +# Disable VCS-based implicit rules. +% : RCS/% + +# Disable VCS-based implicit rules. +% : RCS/%,v + +# Disable VCS-based implicit rules. +% : SCCS/s.% + +# Disable VCS-based implicit rules. +% : s.% + +.SUFFIXES: .hpux_make_needs_suffix_list + +# Command-line flag to silence nested $(MAKE). +$(VERBOSE)MAKESILENT = -s + +#Suppress display of executed commands. +$(VERBOSE).SILENT: + +# A target that is always out of date. +cmake_force: +.PHONY : cmake_force + +#============================================================================= +# Set environment variables for the build. + +# The shell in which to execute make rules. +SHELL = /bin/sh + +# The CMake executable. +CMAKE_COMMAND = /opt/homebrew/bin/cmake + +# The command to remove a file. +RM = /opt/homebrew/bin/cmake -E rm -f + +# Escaping for special characters. +EQUALS = = + +# The top-level source directory on which CMake was run. +CMAKE_SOURCE_DIR = /Users/timkpaine/Developer/projects/templates/hatch-cpp/hatch_cpp/tests/test_project_cmake + +# The top-level build directory on which CMake was run. +CMAKE_BINARY_DIR = /Users/timkpaine/Developer/projects/templates/hatch-cpp/hatch_cpp/tests/test_project_cmake + +#============================================================================= +# Targets provided globally by CMake. + +# Special rule for the target edit_cache +edit_cache: + @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running CMake cache editor..." + /opt/homebrew/bin/ccmake -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) +.PHONY : edit_cache + +# Special rule for the target edit_cache +edit_cache/fast: edit_cache +.PHONY : edit_cache/fast + +# Special rule for the target rebuild_cache +rebuild_cache: + @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running CMake to regenerate build system..." + /opt/homebrew/bin/cmake --regenerate-during-build -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) +.PHONY : rebuild_cache + +# Special rule for the target rebuild_cache +rebuild_cache/fast: rebuild_cache +.PHONY : rebuild_cache/fast + +# The main all target +all: cmake_check_build_system + $(CMAKE_COMMAND) -E cmake_progress_start /Users/timkpaine/Developer/projects/templates/hatch-cpp/hatch_cpp/tests/test_project_cmake/CMakeFiles /Users/timkpaine/Developer/projects/templates/hatch-cpp/hatch_cpp/tests/test_project_cmake//CMakeFiles/progress.marks + $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 all + $(CMAKE_COMMAND) -E cmake_progress_start /Users/timkpaine/Developer/projects/templates/hatch-cpp/hatch_cpp/tests/test_project_cmake/CMakeFiles 0 +.PHONY : all + +# The main clean target +clean: + $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 clean +.PHONY : clean + +# The main clean target +clean/fast: clean +.PHONY : clean/fast + +# Prepare targets for installation. +preinstall: all + $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 preinstall +.PHONY : preinstall + +# Prepare targets for installation. +preinstall/fast: + $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 preinstall +.PHONY : preinstall/fast + +# clear depends +depend: + $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 1 +.PHONY : depend + +# Help Target +help: + @echo "The following are some of the valid targets for this Makefile:" + @echo "... all (the default if no target is provided)" + @echo "... clean" + @echo "... depend" + @echo "... edit_cache" + @echo "... rebuild_cache" +.PHONY : help + + + +#============================================================================= +# Special targets to cleanup operation of make. + +# Special rule to run CMake to check the build system integrity. +# No rule that depends on this can have commands that come from listfiles +# because they might be regenerated. +cmake_check_build_system: + $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 0 +.PHONY : cmake_check_build_system + diff --git a/hatch_cpp/tests/test_project_cmake_vcpkg/cpp/project/basic.cpp b/hatch_cpp/tests/test_project_cmake_vcpkg/cpp/project/basic.cpp new file mode 100644 index 0000000..db4432a --- /dev/null +++ b/hatch_cpp/tests/test_project_cmake_vcpkg/cpp/project/basic.cpp @@ -0,0 +1,5 @@ +#include "project/basic.hpp" + +PyObject* hello(PyObject*, PyObject*) { + return PyUnicode_FromString("A string"); +} diff --git a/hatch_cpp/tests/test_project_cmake_vcpkg/cpp/project/basic.hpp b/hatch_cpp/tests/test_project_cmake_vcpkg/cpp/project/basic.hpp new file mode 100644 index 0000000..65cb62e --- /dev/null +++ b/hatch_cpp/tests/test_project_cmake_vcpkg/cpp/project/basic.hpp @@ -0,0 +1,17 @@ +#pragma once +#include "Python.h" + +PyObject* hello(PyObject*, PyObject*); + +static PyMethodDef extension_methods[] = { + {"hello", (PyCFunction)hello, METH_NOARGS}, + {nullptr, nullptr, 0, nullptr} +}; + +static PyModuleDef extension_module = { + PyModuleDef_HEAD_INIT, "extension", "extension", -1, extension_methods}; + +PyMODINIT_FUNC PyInit_extension(void) { + Py_Initialize(); + return PyModule_Create(&extension_module); +} diff --git a/hatch_cpp/tests/test_project_cmake_vcpkg/project/__init__.py b/hatch_cpp/tests/test_project_cmake_vcpkg/project/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hatch_cpp/tests/test_project_cmake_vcpkg/project/include/project/basic.hpp b/hatch_cpp/tests/test_project_cmake_vcpkg/project/include/project/basic.hpp new file mode 100644 index 0000000..65cb62e --- /dev/null +++ b/hatch_cpp/tests/test_project_cmake_vcpkg/project/include/project/basic.hpp @@ -0,0 +1,17 @@ +#pragma once +#include "Python.h" + +PyObject* hello(PyObject*, PyObject*); + +static PyMethodDef extension_methods[] = { + {"hello", (PyCFunction)hello, METH_NOARGS}, + {nullptr, nullptr, 0, nullptr} +}; + +static PyModuleDef extension_module = { + PyModuleDef_HEAD_INIT, "extension", "extension", -1, extension_methods}; + +PyMODINIT_FUNC PyInit_extension(void) { + Py_Initialize(); + return PyModule_Create(&extension_module); +} diff --git a/hatch_cpp/tests/test_project_cmake_vcpkg/pyproject.toml b/hatch_cpp/tests/test_project_cmake_vcpkg/pyproject.toml new file mode 100644 index 0000000..8ad530e --- /dev/null +++ b/hatch_cpp/tests/test_project_cmake_vcpkg/pyproject.toml @@ -0,0 +1,39 @@ +[build-system] +requires = ["hatchling>=1.20"] +build-backend = "hatchling.build" + +[project] +name = "hatch-cpp-test-project-basic" +description = "Basic test project for hatch-cpp" +version = "0.1.0" +requires-python = ">=3.9" +dependencies = [ + "hatchling>=1.20", + "hatch-cpp", +] + +[tool.hatch.build] +artifacts = [ + "project/*.dll", + "project/*.dylib", + "project/*.so", +] + +[tool.hatch.build.sources] +src = "/" + +[tool.hatch.build.targets.sdist] +packages = ["project"] + +[tool.hatch.build.targets.wheel] +packages = ["project"] + +[tool.hatch.build.hooks.hatch-cpp] +verbose = true + +[tool.hatch.build.hooks.hatch-cpp.cmake] +root = "CMakeLists.txt" +cmake_args = {"BUILD_TESTS" = "OFF"} +include_flags = {"python_version" = true} +[tool.hatch.build.hooks.hatch-cpp.cmake.cmake_env_args] +linux = {"MANYLINUX" = "ON"} diff --git a/hatch_cpp/tests/test_project_cmake_vcpkg/vcpkg.json b/hatch_cpp/tests/test_project_cmake_vcpkg/vcpkg.json new file mode 100644 index 0000000..10c9599 --- /dev/null +++ b/hatch_cpp/tests/test_project_cmake_vcpkg/vcpkg.json @@ -0,0 +1,8 @@ +{ + "name": "main", + "version-string": "latest", + "dependencies": [ + "arrow" + ], + "builtin-baseline": "b94ade01f19e4436d8c8a16a5c52e8c802ef67dd" +} diff --git a/hatch_cpp/tests/test_project_pybind_vcpkg/cpp/project/basic.cpp b/hatch_cpp/tests/test_project_pybind_vcpkg/cpp/project/basic.cpp new file mode 100644 index 0000000..ebe96f8 --- /dev/null +++ b/hatch_cpp/tests/test_project_pybind_vcpkg/cpp/project/basic.cpp @@ -0,0 +1,6 @@ +#include "project/basic.hpp" + +std::string hello() { + return "A string"; +} + diff --git a/hatch_cpp/tests/test_project_pybind_vcpkg/cpp/project/basic.hpp b/hatch_cpp/tests/test_project_pybind_vcpkg/cpp/project/basic.hpp new file mode 100644 index 0000000..86053b2 --- /dev/null +++ b/hatch_cpp/tests/test_project_pybind_vcpkg/cpp/project/basic.hpp @@ -0,0 +1,9 @@ +#pragma once +#include +#include + +std::string hello(); + +PYBIND11_MODULE(extension, m) { + m.def("hello", &hello); +} \ No newline at end of file diff --git a/hatch_cpp/tests/test_project_pybind_vcpkg/project/__init__.py b/hatch_cpp/tests/test_project_pybind_vcpkg/project/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hatch_cpp/tests/test_project_pybind_vcpkg/pyproject.toml b/hatch_cpp/tests/test_project_pybind_vcpkg/pyproject.toml new file mode 100644 index 0000000..38e279e --- /dev/null +++ b/hatch_cpp/tests/test_project_pybind_vcpkg/pyproject.toml @@ -0,0 +1,35 @@ +[build-system] +requires = ["hatchling>=1.20"] +build-backend = "hatchling.build" + +[project] +name = "hatch-cpp-test-project-pybind" +description = "Basic test project for hatch-cpp" +version = "0.1.0" +requires-python = ">=3.9" +dependencies = [ + "hatchling>=1.20", + "hatch-cpp", +] + +[tool.hatch.build] +artifacts = [ + "project/*.dll", + "project/*.dylib", + "project/*.so", +] + +[tool.hatch.build.sources] +src = "/" + +[tool.hatch.build.targets.sdist] +packages = ["project"] + +[tool.hatch.build.targets.wheel] +packages = ["project"] + +[tool.hatch.build.hooks.hatch-cpp] +verbose = true +libraries = [ + {name = "project/extension", sources = ["cpp/project/basic.cpp"], include-dirs = ["cpp"], binding="pybind11"}, +] diff --git a/hatch_cpp/tests/test_project_pybind_vcpkg/vcpkg.json b/hatch_cpp/tests/test_project_pybind_vcpkg/vcpkg.json new file mode 100644 index 0000000..10c9599 --- /dev/null +++ b/hatch_cpp/tests/test_project_pybind_vcpkg/vcpkg.json @@ -0,0 +1,8 @@ +{ + "name": "main", + "version-string": "latest", + "dependencies": [ + "arrow" + ], + "builtin-baseline": "b94ade01f19e4436d8c8a16a5c52e8c802ef67dd" +} diff --git a/hatch_cpp/tests/test_projects.py b/hatch_cpp/tests/test_projects.py index dd4c6fc..e7b4fc6 100644 --- a/hatch_cpp/tests/test_projects.py +++ b/hatch_cpp/tests/test_projects.py @@ -16,9 +16,11 @@ class TestProject: "test_project_override_classes", "test_project_override_toolchain", "test_project_pybind", + "test_project_pybind_vcpkg", "test_project_nanobind", "test_project_limited_api", "test_project_cmake", + "test_project_cmake_vcpkg", ], ) def test_basic(self, project): From 849a02b200df2bda948902bd6acc343bd2fba74e Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Wed, 5 Nov 2025 22:08:21 -0500 Subject: [PATCH 06/58] More work on vcpkg toolchain, combining cmake and vcpkg toolchains --- .gitignore | 3 ++ hatch_cpp/config.py | 31 ++++++++++-- .../tests/test_project_cmake_vcpkg/vcpkg.json | 2 +- .../test_project_pybind_vcpkg/vcpkg.json | 2 +- hatch_cpp/toolchains/cmake.py | 16 +++++- hatch_cpp/toolchains/common.py | 2 + hatch_cpp/toolchains/vcpkg.py | 50 ++++++++++++++++--- 7 files changed, 90 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index ed2e334..9d04ba6 100644 --- a/.gitignore +++ b/.gitignore @@ -157,3 +157,6 @@ hatch_cpp/labextension # Rust target + +vcpkg +vcpkg_installed \ No newline at end of file diff --git a/hatch_cpp/config.py b/hatch_cpp/config.py index 8d27fa6..3d7ffe7 100644 --- a/hatch_cpp/config.py +++ b/hatch_cpp/config.py @@ -1,12 +1,13 @@ from __future__ import annotations +from logging import getLogger from os import system as system_call from pathlib import Path from typing import List, Optional from pydantic import BaseModel, Field, model_validator -from .toolchains import BuildType, HatchCppCmakeConfiguration, HatchCppLibrary, HatchCppPlatform, HatchCppVcpkgConfiguration +from .toolchains import BuildType, HatchCppCmakeConfiguration, HatchCppLibrary, HatchCppPlatform, HatchCppVcpkgConfiguration, Toolchain __all__ = ( "HatchCppBuildConfig", @@ -14,6 +15,9 @@ ) +_log = getLogger(__name__) + + class HatchCppBuildConfig(BaseModel): """Build config values for Hatch C++ Builder.""" @@ -22,7 +26,7 @@ class HatchCppBuildConfig(BaseModel): libraries: List[HatchCppLibrary] = Field(default_factory=list) cmake: Optional[HatchCppCmakeConfiguration] = Field(default=None) platform: Optional[HatchCppPlatform] = Field(default_factory=HatchCppPlatform.default) - vcpkg: Optional[HatchCppVcpkgConfiguration] = Field(default=None) + vcpkg: Optional[HatchCppVcpkgConfiguration] = Field(default_factory=HatchCppVcpkgConfiguration) @model_validator(mode="wrap") @classmethod @@ -41,6 +45,8 @@ def validate_model(cls, data, handler): if "ld" in data: data["platform"].ld = data["ld"] data.pop("ld") + if "vcpkg" in data and data["vcpkg"] == "false": + data["vcpkg"] = None model = handler(data) if model.cmake and model.libraries: raise ValueError("Must not provide libraries when using cmake toolchain.") @@ -51,20 +57,35 @@ class HatchCppBuildPlan(HatchCppBuildConfig): build_type: BuildType = "release" commands: List[str] = Field(default_factory=list) + _active_toolchains: List[Toolchain] = [] + def generate(self): self.commands = [] + # Evaluate toolchains if self.vcpkg and Path(self.vcpkg.vcpkg).exists(): - self.commands.extend(self.vcpkg.generate(self.platform)) - + self._active_toolchains.append("vcpkg") if self.libraries: + self._active_toolchains.append("vanilla") + elif self.cmake: + self._active_toolchains.append("cmake") + + # Collect toolchain commands + if "vcpkg" in self._active_toolchains: + self.commands.extend(self.vcpkg.generate(self)) + + if "vanilla" in self._active_toolchains: + if "vcpkg" in self._active_toolchains: + _log.warning("vcpkg toolchain is active; ensure that your compiler is configured to use vcpkg includes and libs.") + for library in self.libraries: compile_flags = self.platform.get_compile_flags(library, self.build_type) link_flags = self.platform.get_link_flags(library, self.build_type) self.commands.append( f"{self.platform.cc if library.language == 'c' else self.platform.cxx} {' '.join(library.sources)} {compile_flags} {link_flags}" ) - elif self.cmake: + + if "cmake" in self._active_toolchains: self.commands.extend(self.cmake.generate(self)) return self.commands diff --git a/hatch_cpp/tests/test_project_cmake_vcpkg/vcpkg.json b/hatch_cpp/tests/test_project_cmake_vcpkg/vcpkg.json index 10c9599..ace9c19 100644 --- a/hatch_cpp/tests/test_project_cmake_vcpkg/vcpkg.json +++ b/hatch_cpp/tests/test_project_cmake_vcpkg/vcpkg.json @@ -2,7 +2,7 @@ "name": "main", "version-string": "latest", "dependencies": [ - "arrow" + "nlohmann-json" ], "builtin-baseline": "b94ade01f19e4436d8c8a16a5c52e8c802ef67dd" } diff --git a/hatch_cpp/tests/test_project_pybind_vcpkg/vcpkg.json b/hatch_cpp/tests/test_project_pybind_vcpkg/vcpkg.json index 10c9599..ace9c19 100644 --- a/hatch_cpp/tests/test_project_pybind_vcpkg/vcpkg.json +++ b/hatch_cpp/tests/test_project_pybind_vcpkg/vcpkg.json @@ -2,7 +2,7 @@ "name": "main", "version-string": "latest", "dependencies": [ - "arrow" + "nlohmann-json" ], "builtin-baseline": "b94ade01f19e4436d8c8a16a5c52e8c802ef67dd" } diff --git a/hatch_cpp/toolchains/cmake.py b/hatch_cpp/toolchains/cmake.py index 0e66dc4..1b349af 100644 --- a/hatch_cpp/toolchains/cmake.py +++ b/hatch_cpp/toolchains/cmake.py @@ -11,6 +11,16 @@ __all__ = ("HatchCppCmakeConfiguration",) +DefaultMSVCGenerator = { + "12": "Visual Studio 12 2013", + "14": "Visual Studio 14 2015", + "14.0": "Visual Studio 14 2015", + "14.1": "Visual Studio 15 2017", + "14.2": "Visual Studio 16 2019", + "14.3": "Visual Studio 17 2022", + "14.4": "Visual Studio 17 2022", +} + class HatchCppCmakeConfiguration(BaseModel): root: Path @@ -33,6 +43,10 @@ def generate(self, config) -> Dict[str, Any]: # Append base command commands.append(f"cmake {Path(self.root).parent} -DCMAKE_BUILD_TYPE={config.build_type} -B {self.build}") + # Hook in to vcpkg if active + if "vcpkg" in config._active_toolchains: + commands[-1] += f" -DCMAKE_TOOLCHAIN_FILE={Path(config.vcpkg.vcpkg_root) / 'scripts' / 'buildsystems' / 'vcpkg.cmake'}" + # Setup install path if self.install: commands[-1] += f" -DCMAKE_INSTALL_PREFIX={self.install}" @@ -42,7 +56,7 @@ def generate(self, config) -> Dict[str, Any]: # TODO: CMAKE_CXX_COMPILER if config.platform.platform == "win32": # TODO: prefix? - commands[-1] += f' -G "{environ.get("GENERATOR", "Visual Studio 17 2022")}"' + commands[-1] += f' -G "{environ.get("CMAKE_GENERATOR", "Visual Studio 17 2022")}"' # Put in CMake flags args = self.cmake_args.copy() diff --git a/hatch_cpp/toolchains/common.py b/hatch_cpp/toolchains/common.py index 2ee094f..5922d64 100644 --- a/hatch_cpp/toolchains/common.py +++ b/hatch_cpp/toolchains/common.py @@ -13,6 +13,7 @@ __all__ = ( "BuildType", "CompilerToolchain", + "Toolchain", "Language", "Binding", "Platform", @@ -24,6 +25,7 @@ BuildType = Literal["debug", "release"] CompilerToolchain = Literal["gcc", "clang", "msvc"] +Toolchain = Literal["vcpkg", "cmake", "vanilla"] Language = Literal["c", "c++"] Binding = Literal["cpython", "pybind11", "nanobind", "generic"] Platform = Literal["linux", "darwin", "win32"] diff --git a/hatch_cpp/toolchains/vcpkg.py b/hatch_cpp/toolchains/vcpkg.py index 556f6a7..c5056dd 100644 --- a/hatch_cpp/toolchains/vcpkg.py +++ b/hatch_cpp/toolchains/vcpkg.py @@ -1,30 +1,64 @@ from __future__ import annotations from pathlib import Path +from platform import machine as platform_machine from sys import platform as sys_platform -from typing import Optional +from typing import Literal, Optional from pydantic import BaseModel, Field __all__ = ("HatchCppVcpkgConfiguration",) +VcpkgTriplet = Literal[ + "x64-android", + "x64-osx", + "x64-linux", + "x64-uwp", + "x64-windows", + "x64-windows-release", + "x64-windows-static", + "x64-windows-static-md", + "x86-windows", + "arm-neon-android", + "arm64-android", + "arm64-osx", + "arm64-uwp", + "arm64-windows", + "arm64-windows-static-md", +] +VcpkgPlatformDefaults = { + ("linux", "x86_64"): "x64-linux", + # ("linux", "arm64"): "", + ("darwin", "x86_64"): "x64-osx", + ("darwin", "arm64"): "arm64-osx", + ("win32", "x86_64"): "x64-windows-static-md", + ("win32", "arm64"): "arm64-windows-static-md", +} + + class HatchCppVcpkgConfiguration(BaseModel): vcpkg: Optional[str] = Field(default="vcpkg.json") vcpkg_root: Optional[Path] = Field(default=Path("vcpkg")) vcpkg_repo: Optional[str] = Field(default="https://github.com/microsoft/vcpkg.git") + vcpkg_triplet: Optional[VcpkgTriplet] = Field(default=None) + + # TODO: overlay def generate(self, config): commands = [] - if self.vcpkg and Path(self.vcpkg.vcpkg).exists(): - if not Path(self.vcpkg.vcpkg_root).exists(): - commands.append(f"git clone {self.vcpkg.vcpkg_repo} {self.vcpkg.vcpkg_root}") + if self.vcpkg_triplet is None: + self.vcpkg_triplet = VcpkgPlatformDefaults.get((sys_platform, platform_machine())) + if self.vcpkg_triplet is None: + raise ValueError(f"Could not determine vcpkg triplet for platform {sys_platform} and architecture {platform_machine()}") + + if self.vcpkg and Path(self.vcpkg).exists(): + if not Path(self.vcpkg_root).exists(): + commands.append(f"git clone {self.vcpkg_repo} {self.vcpkg_root}") commands.append( - f"./{self.vcpkg.vcpkg_root / 'bootstrap-vcpkg.sh' if sys_platform != 'win32' else self.vcpkg.vcpkg_root / 'sbootstrap-vcpkg.bat'}" + f"./{self.vcpkg_root / 'bootstrap-vcpkg.sh' if sys_platform != 'win32' else self.vcpkg_root / 'sbootstrap-vcpkg.bat'}" ) - commands.append( - f"./{self.vcpkg.vcpkg_root / 'vcpkg'} install --triplet {config.platform.platform}-{config.platform.toolchain} --manifest-root {Path(self.vcpkg.vcpkg).parent}" - ) + commands.append(f"./{self.vcpkg_root / 'vcpkg'} install --triplet {self.vcpkg_triplet}") return commands From 9cd1e0f4eab1829e817c0d10446c9bf7a831a556 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Wed, 5 Nov 2025 22:11:38 -0500 Subject: [PATCH 07/58] =?UTF-8?q?Bump=20version:=200.1.8=20=E2=86=92=200.1?= =?UTF-8?q?.9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com> --- hatch_cpp/__init__.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hatch_cpp/__init__.py b/hatch_cpp/__init__.py index d5cf19a..fe6e262 100644 --- a/hatch_cpp/__init__.py +++ b/hatch_cpp/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.1.8" +__version__ = "0.1.9" from .config import * from .hooks import * diff --git a/pyproject.toml b/pyproject.toml index e736c34..28b403d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ authors = [{name = "the hatch-cpp authors", email = "t.paine154@gmail.com"}] description = "Hatch plugin for C++ builds" readme = "README.md" license = { text = "Apache-2.0" } -version = "0.1.8" +version = "0.1.9" requires-python = ">=3.9" keywords = [ "hatch", @@ -73,7 +73,7 @@ Repository = "https://github.com/python-project-templates/hatch-cpp" Homepage = "https://github.com/python-project-templates/hatch-cpp" [tool.bumpversion] -current_version = "0.1.8" +current_version = "0.1.9" commit = true tag = false commit_args = "-s" From 4e91c95316a2a00c0f741d22ed37826560a32278 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 9 Nov 2025 05:15:41 +0000 Subject: [PATCH 08/58] Update from copier (2025-11-09T05:15:41) Signed-off-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .copier-answers.yaml | 2 +- pyproject.toml | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.copier-answers.yaml b/.copier-answers.yaml index 026fe73..17531dd 100644 --- a/.copier-answers.yaml +++ b/.copier-answers.yaml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 18afe0a +_commit: 973c39c _src_path: https://github.com/python-project-templates/base.git add_docs: false add_extension: python diff --git a/pyproject.toml b/pyproject.toml index 28b403d..13fc6c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ authors = [{name = "the hatch-cpp authors", email = "t.paine154@gmail.com"}] description = "Hatch plugin for C++ builds" readme = "README.md" license = { text = "Apache-2.0" } +<<<<<<< before updating version = "0.1.9" requires-python = ">=3.9" keywords = [ @@ -18,6 +19,12 @@ keywords = [ "c++", "cmake", ] +======= +version = "0.1.0" +requires-python = ">=3.10" +keywords = [] + +>>>>>>> after updating classifiers = [ "Development Status :: 3 - Alpha", "License :: OSI Approved :: Apache Software License", @@ -25,7 +32,6 @@ classifiers = [ "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", From f70e682754c026f14831dc7c9b9129f3ee231d00 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Sun, 9 Nov 2025 12:08:49 -0500 Subject: [PATCH 09/58] Update pyproject.toml --- pyproject.toml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 13fc6c5..3cc367d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,9 +8,8 @@ authors = [{name = "the hatch-cpp authors", email = "t.paine154@gmail.com"}] description = "Hatch plugin for C++ builds" readme = "README.md" license = { text = "Apache-2.0" } -<<<<<<< before updating version = "0.1.9" -requires-python = ">=3.9" +requires-python = ">=3.10" keywords = [ "hatch", "python", @@ -19,12 +18,7 @@ keywords = [ "c++", "cmake", ] -======= -version = "0.1.0" -requires-python = ">=3.10" -keywords = [] ->>>>>>> after updating classifiers = [ "Development Status :: 3 - Alpha", "License :: OSI Approved :: Apache Software License", From 6f3b89377295994944a28cf892c15a80bd909999 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Sun, 9 Nov 2025 17:46:59 -0500 Subject: [PATCH 10/58] Add support for hatch-build --- README.md | 14 ++++++ hatch_cpp/plugin.py | 4 ++ hatch_cpp/tests/test_hatch_build.py | 46 +++++++++++++++++++ .../tests/test_project_cmake/pyproject.toml | 2 +- .../cpp/project/basic.cpp | 2 + .../cpp/project/basic.hpp | 7 +++ .../project/__init__.py | 0 .../test_project_hatch_build/pyproject.toml | 36 +++++++++++++++ hatch_cpp/toolchains/cmake.py | 6 +-- pyproject.toml | 6 +-- 10 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 hatch_cpp/tests/test_hatch_build.py create mode 100644 hatch_cpp/tests/test_project_hatch_build/cpp/project/basic.cpp create mode 100644 hatch_cpp/tests/test_project_hatch_build/cpp/project/basic.hpp create mode 100644 hatch_cpp/tests/test_project_hatch_build/project/__init__.py create mode 100644 hatch_cpp/tests/test_project_hatch_build/pyproject.toml diff --git a/README.md b/README.md index 31715dd..3d69c21 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,20 @@ cmake_env_args = {} # env-specific cmake args to pass include_flags = {} # include flags to pass -D ``` +### CLI + +`hatch-cpp` is integrated with [`hatch-build`](https://github.com/python-project-templates/hatch-build) to allow easy configuration of options via command line: + +```bash +hatch-build \ + -- \ + --verbose \ + --platform linux \ + --vcpkg.vcpkg a/path/to/vcpkg.json \ + --libraries.0.binding pybind11 \ + --libraries.0.include-dirs cpp,another-dir +``` + ### Environment Variables `hatch-cpp` will respect standard environment variables for compiler control. diff --git a/hatch_cpp/plugin.py b/hatch_cpp/plugin.py index f1a32b0..3dfaec2 100644 --- a/hatch_cpp/plugin.py +++ b/hatch_cpp/plugin.py @@ -7,6 +7,7 @@ from sys import platform as sys_platform, version_info from typing import Any +from hatch_build import parse_extra_args_model from hatchling.builders.hooks.plugin.interface import BuildHookInterface from .config import HatchCppBuildConfig, HatchCppBuildPlan @@ -52,6 +53,9 @@ def initialize(self, version: str, build_data: dict[str, Any]) -> None: # Instantiate builder build_plan = build_plan_class(**config.model_dump()) + # Parse override args + parse_extra_args_model(build_plan) + # Generate commands build_plan.generate() diff --git a/hatch_cpp/tests/test_hatch_build.py b/hatch_cpp/tests/test_hatch_build.py new file mode 100644 index 0000000..b71e185 --- /dev/null +++ b/hatch_cpp/tests/test_hatch_build.py @@ -0,0 +1,46 @@ +from os import listdir +from pathlib import Path +from shutil import rmtree +from subprocess import check_call +from sys import modules, path, platform + + +class TestHatchBuild: + def test_hatch_build(self): + project = "test_project_hatch_build" + + rmtree(f"hatch_cpp/tests/{project}/project/extension.so", ignore_errors=True) + rmtree(f"hatch_cpp/tests/{project}/project/extension.pyd", ignore_errors=True) + modules.pop("project", None) + modules.pop("project.extension", None) + + # compile + check_call( + [ + "hatch-build", + "--hooks-only", + "--", + "--libraries.0.name=project/extension", + "--libraries.0.sources=cpp/project/basic.cpp", + "--libraries.0.include-dirs=cpp", + "--libraries.0.binding=nanobind", + ], + cwd=f"hatch_cpp/tests/{project}", + ) + + # assert built + + if project == "test_project_limited_api" and platform != "win32": + assert "extension.abi3.so" in listdir(f"hatch_cpp/tests/{project}/project") + else: + if platform == "win32": + assert "extension.pyd" in listdir(f"hatch_cpp/tests/{project}/project") + else: + assert "extension.so" in listdir(f"hatch_cpp/tests/{project}/project") + + # import + here = Path(__file__).parent / project + path.insert(0, str(here)) + import project.extension + + assert project.extension.hello() == "A string" diff --git a/hatch_cpp/tests/test_project_cmake/pyproject.toml b/hatch_cpp/tests/test_project_cmake/pyproject.toml index 8ad530e..51e9a66 100644 --- a/hatch_cpp/tests/test_project_cmake/pyproject.toml +++ b/hatch_cpp/tests/test_project_cmake/pyproject.toml @@ -29,7 +29,7 @@ packages = ["project"] packages = ["project"] [tool.hatch.build.hooks.hatch-cpp] -verbose = true +verbose = false [tool.hatch.build.hooks.hatch-cpp.cmake] root = "CMakeLists.txt" diff --git a/hatch_cpp/tests/test_project_hatch_build/cpp/project/basic.cpp b/hatch_cpp/tests/test_project_hatch_build/cpp/project/basic.cpp new file mode 100644 index 0000000..2ac7d56 --- /dev/null +++ b/hatch_cpp/tests/test_project_hatch_build/cpp/project/basic.cpp @@ -0,0 +1,2 @@ +#include "project/basic.hpp" + diff --git a/hatch_cpp/tests/test_project_hatch_build/cpp/project/basic.hpp b/hatch_cpp/tests/test_project_hatch_build/cpp/project/basic.hpp new file mode 100644 index 0000000..1afa022 --- /dev/null +++ b/hatch_cpp/tests/test_project_hatch_build/cpp/project/basic.hpp @@ -0,0 +1,7 @@ +#pragma once +#include +#include + +NB_MODULE(extension, m) { + m.def("hello", []() { return "A string"; }); +} diff --git a/hatch_cpp/tests/test_project_hatch_build/project/__init__.py b/hatch_cpp/tests/test_project_hatch_build/project/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hatch_cpp/tests/test_project_hatch_build/pyproject.toml b/hatch_cpp/tests/test_project_hatch_build/pyproject.toml new file mode 100644 index 0000000..e53ef12 --- /dev/null +++ b/hatch_cpp/tests/test_project_hatch_build/pyproject.toml @@ -0,0 +1,36 @@ +[build-system] +requires = ["hatchling>=1.20"] +build-backend = "hatchling.build" + +[project] +name = "hatch-cpp-test-project-nanobind" +description = "Basic test project for hatch-cpp" +version = "0.1.0" +requires-python = ">=3.9" +dependencies = [ + "hatchling>=1.20", + "hatch-cpp", +] + +[tool.hatch.build] +artifacts = [ + "project/*.dll", + "project/*.dylib", + "project/*.so", +] + +[tool.hatch.build.sources] +src = "/" + +[tool.hatch.build.targets.sdist] +packages = ["project"] + +[tool.hatch.build.targets.wheel] +packages = ["project"] + +[tool.hatch.build.hooks.hatch-cpp] +verbose = true +libraries = [ + # {name = "project/extension", sources = ["cpp/project/basic.cpp"], include-dirs = ["cpp"], binding = "nanobind"}, + {name = "wrong", sources = ["wrong"], include-dirs = ["wrong"], binding = "generic"}, +] diff --git a/hatch_cpp/toolchains/cmake.py b/hatch_cpp/toolchains/cmake.py index 1b349af..a5d2f15 100644 --- a/hatch_cpp/toolchains/cmake.py +++ b/hatch_cpp/toolchains/cmake.py @@ -3,7 +3,7 @@ from os import environ from pathlib import Path from sys import version_info -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Union from pydantic import BaseModel, Field @@ -23,7 +23,7 @@ class HatchCppCmakeConfiguration(BaseModel): - root: Path + root: Optional[Path] = None build: Path = Field(default_factory=lambda: Path("build")) install: Optional[Path] = Field(default=None) @@ -31,7 +31,7 @@ class HatchCppCmakeConfiguration(BaseModel): cmake_args: Dict[str, str] = Field(default_factory=dict) cmake_env_args: Dict[Platform, Dict[str, str]] = Field(default_factory=dict) - include_flags: Optional[Dict[str, Any]] = Field(default=None) + include_flags: Optional[Dict[str, Union[str, int, float, bool]]] = Field(default=None) def generate(self, config) -> Dict[str, Any]: commands = [] diff --git a/pyproject.toml b/pyproject.toml index 28b403d..35502e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ description = "Hatch plugin for C++ builds" readme = "README.md" license = { text = "Apache-2.0" } version = "0.1.9" -requires-python = ">=3.9" +requires-python = ">=3.10" keywords = [ "hatch", "python", @@ -25,7 +25,6 @@ classifiers = [ "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -35,6 +34,7 @@ classifiers = [ dependencies = [ "hatchling>=1.20", + "hatch-build>=0.4,<0.5", "pydantic", ] @@ -45,7 +45,7 @@ develop = [ "check-manifest", "codespell>=2.4,<2.5", "hatchling", - "hatch-build", + "hatch-build>=0.3.2", "mdformat>=0.7.22,<1.1", "mdformat-tables>=1", "pytest", From 987dff9201337280f462154e74cf51f4c20ef144 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Sun, 9 Nov 2025 17:51:18 -0500 Subject: [PATCH 11/58] =?UTF-8?q?Bump=20version:=200.1.9=20=E2=86=92=200.2?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com> --- hatch_cpp/__init__.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hatch_cpp/__init__.py b/hatch_cpp/__init__.py index fe6e262..67505b4 100644 --- a/hatch_cpp/__init__.py +++ b/hatch_cpp/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.1.9" +__version__ = "0.2.0" from .config import * from .hooks import * diff --git a/pyproject.toml b/pyproject.toml index 3eae539..d83db24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ authors = [{name = "the hatch-cpp authors", email = "t.paine154@gmail.com"}] description = "Hatch plugin for C++ builds" readme = "README.md" license = { text = "Apache-2.0" } -version = "0.1.9" +version = "0.2.0" requires-python = ">=3.10" keywords = [ "hatch", @@ -74,7 +74,7 @@ Repository = "https://github.com/python-project-templates/hatch-cpp" Homepage = "https://github.com/python-project-templates/hatch-cpp" [tool.bumpversion] -current_version = "0.1.9" +current_version = "0.2.0" commit = true tag = false commit_args = "-s" From e11c1beb26b5260a0a0c52c02dfc7b2bad4e078d Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Sun, 9 Nov 2025 20:15:53 -0500 Subject: [PATCH 12/58] Remove hatchling pin --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d83db24..76ff46c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["hatchling>=1.20"] +requires = ["hatchling"] build-backend = "hatchling.build" [project] @@ -34,7 +34,7 @@ classifiers = [ ] dependencies = [ - "hatchling>=1.20", + "hatchling", "hatch-build>=0.4,<0.5", "pydantic", ] From bc3c1eebf32156ef014c13200e02910945275361 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Sun, 9 Nov 2025 20:19:06 -0500 Subject: [PATCH 13/58] =?UTF-8?q?Bump=20version:=200.2.0=20=E2=86=92=200.2?= =?UTF-8?q?.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com> --- hatch_cpp/__init__.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hatch_cpp/__init__.py b/hatch_cpp/__init__.py index 67505b4..0bd8e49 100644 --- a/hatch_cpp/__init__.py +++ b/hatch_cpp/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.2.0" +__version__ = "0.2.1" from .config import * from .hooks import * diff --git a/pyproject.toml b/pyproject.toml index 76ff46c..b60a070 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ authors = [{name = "the hatch-cpp authors", email = "t.paine154@gmail.com"}] description = "Hatch plugin for C++ builds" readme = "README.md" license = { text = "Apache-2.0" } -version = "0.2.0" +version = "0.2.1" requires-python = ">=3.10" keywords = [ "hatch", @@ -74,7 +74,7 @@ Repository = "https://github.com/python-project-templates/hatch-cpp" Homepage = "https://github.com/python-project-templates/hatch-cpp" [tool.bumpversion] -current_version = "0.2.0" +current_version = "0.2.1" commit = true tag = false commit_args = "-s" From 9b6d2c47d4c40a4f43d5144b9d022b57ea29cb70 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:21:30 -0500 Subject: [PATCH 14/58] Update hatch-build bounds --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b60a070..7eb0080 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ classifiers = [ dependencies = [ "hatchling", - "hatch-build>=0.4,<0.5", + "hatch-build>=0.4,<0.6", "pydantic", ] From a521fb6fca76dc2e5b3078c5a322d1b752061700 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:24:16 -0500 Subject: [PATCH 15/58] =?UTF-8?q?Bump=20version:=200.2.1=20=E2=86=92=200.2?= =?UTF-8?q?.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com> --- hatch_cpp/__init__.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hatch_cpp/__init__.py b/hatch_cpp/__init__.py index 0bd8e49..17fea63 100644 --- a/hatch_cpp/__init__.py +++ b/hatch_cpp/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.2.1" +__version__ = "0.2.2" from .config import * from .hooks import * diff --git a/pyproject.toml b/pyproject.toml index 7eb0080..6aa451d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ authors = [{name = "the hatch-cpp authors", email = "t.paine154@gmail.com"}] description = "Hatch plugin for C++ builds" readme = "README.md" license = { text = "Apache-2.0" } -version = "0.2.1" +version = "0.2.2" requires-python = ">=3.10" keywords = [ "hatch", @@ -74,7 +74,7 @@ Repository = "https://github.com/python-project-templates/hatch-cpp" Homepage = "https://github.com/python-project-templates/hatch-cpp" [tool.bumpversion] -current_version = "0.2.1" +current_version = "0.2.2" commit = true tag = false commit_args = "-s" From 9acd528bed55ab153d600910ce3f380c7556a0e9 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:26:07 -0500 Subject: [PATCH 16/58] Tag on bump --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6aa451d..1520a5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ Homepage = "https://github.com/python-project-templates/hatch-cpp" [tool.bumpversion] current_version = "0.2.2" commit = true -tag = false +tag = true commit_args = "-s" [[tool.bumpversion.files]] From 0b03e0271d5d55e0f52f533d34112a2e6fd35192 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Tue, 11 Nov 2025 17:33:28 -0500 Subject: [PATCH 17/58] Add disable ccache option for CLI --- README.md | 90 ++++++++++++++++++++++++++++++---- hatch_cpp/toolchains/common.py | 34 ++++++++----- 2 files changed, 102 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 3d69c21..b68da15 100644 --- a/README.md +++ b/README.md @@ -97,17 +97,89 @@ hatch-build \ --libraries.0.include-dirs cpp,another-dir ``` -### Environment Variables +This CLI is aware of your `pyproject.toml`-configured setup. +To display help for this, run (note the passthrough `--`): + +```bash +hatch-build -- --help +``` -`hatch-cpp` will respect standard environment variables for compiler control. +For example, for the `test_project_basic` in this project's `tests` folder: + +```raw +hatch-build --hooks-only -- --help +[sdist] + +[wheel] +[2025-11-11T17:31:06-0500][p2a][WARNING]: Only dicts with str, int, float, bool, or enum values are supported - field `cmake_env_args` got value type typing.Dict[str, str] +usage: hatch-build-extras-model [-h] [--verbose] [--name NAME] [--libraries.0.name LIBRARIES.0.NAME] + [--libraries.0.sources.0 LIBRARIES.0.SOURCES.0] [--libraries.0.sources LIBRARIES.0.SOURCES] + [--libraries.0.language LIBRARIES.0.LANGUAGE] [--libraries.0.binding LIBRARIES.0.BINDING] + [--libraries.0.std LIBRARIES.0.STD] [--libraries.0.include-dirs.0 LIBRARIES.0.INCLUDE_DIRS.0] + [--libraries.0.include-dirs LIBRARIES.0.INCLUDE_DIRS] + [--libraries.0.library-dirs LIBRARIES.0.LIBRARY_DIRS] + [--libraries.0.libraries LIBRARIES.0.LIBRARIES] + [--libraries.0.extra-compile-args LIBRARIES.0.EXTRA_COMPILE_ARGS] + [--libraries.0.extra-link-args LIBRARIES.0.EXTRA_LINK_ARGS] + [--libraries.0.extra-objects LIBRARIES.0.EXTRA_OBJECTS] + [--libraries.0.define-macros LIBRARIES.0.DEFINE_MACROS] + [--libraries.0.undef-macros LIBRARIES.0.UNDEF_MACROS] + [--libraries.0.export-symbols LIBRARIES.0.EXPORT_SYMBOLS] + [--libraries.0.depends LIBRARIES.0.DEPENDS] + [--libraries.0.py-limited-api LIBRARIES.0.PY_LIMITED_API] [--cmake.root CMAKE.ROOT] + [--cmake.build CMAKE.BUILD] [--cmake.install CMAKE.INSTALL] + [--cmake.cmake-arg-prefix CMAKE.CMAKE_ARG_PREFIX] [--cmake.cmake-args CMAKE.CMAKE_ARGS] + [--cmake.include-flags CMAKE.INCLUDE_FLAGS] [--platform.cc PLATFORM.CC] + [--platform.cxx PLATFORM.CXX] [--platform.ld PLATFORM.LD] [--platform.platform PLATFORM.PLATFORM] + [--platform.toolchain PLATFORM.TOOLCHAIN] [--platform.disable-ccache] [--vcpkg.vcpkg VCPKG.VCPKG] + [--vcpkg.vcpkg-root VCPKG.VCPKG_ROOT] [--vcpkg.vcpkg-repo VCPKG.VCPKG_REPO] + [--vcpkg.vcpkg-triplet VCPKG.VCPKG_TRIPLET] [--build-type BUILD_TYPE] [--commands COMMANDS] + +options: + -h, --help show this help message and exit + --verbose + --name NAME + --libraries.0.name LIBRARIES.0.NAME + --libraries.0.sources.0 LIBRARIES.0.SOURCES.0 + --libraries.0.sources LIBRARIES.0.SOURCES + --libraries.0.language LIBRARIES.0.LANGUAGE + --libraries.0.binding LIBRARIES.0.BINDING + --libraries.0.std LIBRARIES.0.STD + --libraries.0.include-dirs.0 LIBRARIES.0.INCLUDE_DIRS.0 + --libraries.0.include-dirs LIBRARIES.0.INCLUDE_DIRS + --libraries.0.library-dirs LIBRARIES.0.LIBRARY_DIRS + --libraries.0.libraries LIBRARIES.0.LIBRARIES + --libraries.0.extra-compile-args LIBRARIES.0.EXTRA_COMPILE_ARGS + --libraries.0.extra-link-args LIBRARIES.0.EXTRA_LINK_ARGS + --libraries.0.extra-objects LIBRARIES.0.EXTRA_OBJECTS + --libraries.0.define-macros LIBRARIES.0.DEFINE_MACROS + --libraries.0.undef-macros LIBRARIES.0.UNDEF_MACROS + --libraries.0.export-symbols LIBRARIES.0.EXPORT_SYMBOLS + --libraries.0.depends LIBRARIES.0.DEPENDS + --libraries.0.py-limited-api LIBRARIES.0.PY_LIMITED_API + --cmake.root CMAKE.ROOT + --cmake.build CMAKE.BUILD + --cmake.install CMAKE.INSTALL + --cmake.cmake-arg-prefix CMAKE.CMAKE_ARG_PREFIX + --cmake.cmake-args CMAKE.CMAKE_ARGS + --cmake.include-flags CMAKE.INCLUDE_FLAGS + --platform.cc PLATFORM.CC + --platform.cxx PLATFORM.CXX + --platform.ld PLATFORM.LD + --platform.platform PLATFORM.PLATFORM + --platform.toolchain PLATFORM.TOOLCHAIN + --platform.disable-ccache + --vcpkg.vcpkg VCPKG.VCPKG + --vcpkg.vcpkg-root VCPKG.VCPKG_ROOT + --vcpkg.vcpkg-repo VCPKG.VCPKG_REPO + --vcpkg.vcpkg-triplet VCPKG.VCPKG_TRIPLET + --build-type BUILD_TYPE + --commands COMMANDS +``` + +### Environment Variables -| Name | Default | Description | -| :------------------------- | :------ | :-------------------- | -| `CC` | | C Compiler override | -| `CXX` | | C++ Compiler override | -| `LD` | | Linker override | -| `HATCH_CPP_PLATFORM` | | Platform to build | -| `HATCH_CPP_DISABLE_CCACHE` | | Disable CCache usage | +`hatch-cpp` will respect standard environment variables for compiler control, e.g. `CC`, `CXX`, `LD`, `CMAKE_GENERATOR`, `OSX_DEPLOYMENT_TARGET`, etc. > [!NOTE] > This library was generated using [copier](https://copier.readthedocs.io/en/stable/) from the [Base Python Project Template repository](https://github.com/python-project-templates/base). diff --git a/hatch_cpp/toolchains/common.py b/hatch_cpp/toolchains/common.py index 5922d64..57d65e6 100644 --- a/hatch_cpp/toolchains/common.py +++ b/hatch_cpp/toolchains/common.py @@ -96,13 +96,13 @@ class HatchCppPlatform(BaseModel): ld: str platform: Platform toolchain: CompilerToolchain + disable_ccache: bool = False @staticmethod def default() -> HatchCppPlatform: - platform = environ.get("HATCH_CPP_PLATFORM", sys_platform) - CC = environ.get("CC", PlatformDefaults[platform]["CC"]) - CXX = environ.get("CXX", PlatformDefaults[platform]["CXX"]) - LD = environ.get("LD", PlatformDefaults[platform]["LD"]) + CC = environ.get("CC", PlatformDefaults[sys_platform]["CC"]) + CXX = environ.get("CXX", PlatformDefaults[sys_platform]["CXX"]) + LD = environ.get("LD", PlatformDefaults[sys_platform]["LD"]) if "gcc" in CC and "g++" in CXX: toolchain = "gcc" elif "clang" in CC and "clang++" in CXX: @@ -110,26 +110,34 @@ def default() -> HatchCppPlatform: elif "cl" in CC and "cl" in CXX: toolchain = "msvc" # Fallback to platform defaults - elif platform == "linux": + elif sys_platform == "linux": toolchain = "gcc" - elif platform == "darwin": + elif sys_platform == "darwin": toolchain = "clang" - elif platform == "win32": + elif sys_platform == "win32": toolchain = "msvc" else: toolchain = "gcc" - # Customizations - if which("ccache") and not environ.get("HATCH_CPP_DISABLE_CCACHE"): - CC = f"ccache {CC}" - CXX = f"ccache {CXX}" - + # TODO: # https://github.com/rui314/mold/issues/647 # if which("ld.mold"): # LD = which("ld.mold") # elif which("ld.lld"): # LD = which("ld.lld") - return HatchCppPlatform(cc=CC, cxx=CXX, ld=LD, platform=platform, toolchain=toolchain) + return HatchCppPlatform(cc=CC, cxx=CXX, ld=LD, platform=sys_platform, toolchain=toolchain) + + @model_validator(mode="wrap") + @classmethod + def validate_model(cls, data, handler): + model = handler(data) + if which("ccache") and not model.disable_ccache: + if model.toolchain in ["gcc", "clang"]: + if not model.cc.startswith("ccache "): + model.cc = f"ccache {model.cc}" + if not model.cxx.startswith("ccache "): + model.cxx = f"ccache {model.cxx}" + return model @staticmethod def platform_for_toolchain(toolchain: CompilerToolchain) -> HatchCppPlatform: From d34c42423190be4c5b93621573bf3780add76ee7 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Tue, 11 Nov 2025 17:43:45 -0500 Subject: [PATCH 18/58] Bugfix for CLI overriding, add skip option --- hatch_cpp/config.py | 7 ++++--- hatch_cpp/plugin.py | 20 ++++++++------------ 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/hatch_cpp/config.py b/hatch_cpp/config.py index 3d7ffe7..9e9c4d8 100644 --- a/hatch_cpp/config.py +++ b/hatch_cpp/config.py @@ -1,10 +1,10 @@ from __future__ import annotations -from logging import getLogger from os import system as system_call from pathlib import Path from typing import List, Optional +from pkn import getSimpleLogger from pydantic import BaseModel, Field, model_validator from .toolchains import BuildType, HatchCppCmakeConfiguration, HatchCppLibrary, HatchCppPlatform, HatchCppVcpkgConfiguration, Toolchain @@ -15,13 +15,14 @@ ) -_log = getLogger(__name__) +log = getSimpleLogger("hatch_cpp") class HatchCppBuildConfig(BaseModel): """Build config values for Hatch C++ Builder.""" verbose: Optional[bool] = Field(default=False) + skip: Optional[bool] = Field(default=False) name: Optional[str] = Field(default=None) libraries: List[HatchCppLibrary] = Field(default_factory=list) cmake: Optional[HatchCppCmakeConfiguration] = Field(default=None) @@ -76,7 +77,7 @@ def generate(self): if "vanilla" in self._active_toolchains: if "vcpkg" in self._active_toolchains: - _log.warning("vcpkg toolchain is active; ensure that your compiler is configured to use vcpkg includes and libs.") + log.warning("vcpkg toolchain is active; ensure that your compiler is configured to use vcpkg includes and libs.") for library in self.libraries: compile_flags = self.platform.get_compile_flags(library, self.build_type) diff --git a/hatch_cpp/plugin.py b/hatch_cpp/plugin.py index 3dfaec2..76914ac 100644 --- a/hatch_cpp/plugin.py +++ b/hatch_cpp/plugin.py @@ -1,7 +1,5 @@ from __future__ import annotations -from logging import getLogger -from os import getenv from pathlib import Path from platform import machine as platform_machine from sys import platform as sys_platform, version_info @@ -10,7 +8,7 @@ from hatch_build import parse_extra_args_model from hatchling.builders.hooks.plugin.interface import BuildHookInterface -from .config import HatchCppBuildConfig, HatchCppBuildPlan +from .config import HatchCppBuildConfig, HatchCppBuildPlan, log from .utils import import_string __all__ = ("HatchCppBuildHook",) @@ -20,7 +18,7 @@ class HatchCppBuildHook(BuildHookInterface[HatchCppBuildConfig]): """The hatch-cpp build hook.""" PLUGIN_NAME = "hatch-cpp" - _logger = getLogger(__name__) + _logger = log def initialize(self, version: str, build_data: dict[str, Any]) -> None: """Initialize the plugin.""" @@ -35,12 +33,6 @@ def initialize(self, version: str, build_data: dict[str, Any]) -> None: self._logger.info("ignoring target name %s", self.target_name) return - # Skip if SKIP_HATCH_CPP is set - # TODO: Support CLI once https://github.com/pypa/hatch/pull/1743 - if getenv("SKIP_HATCH_CPP"): - self._logger.info("Skipping the build hook since SKIP_HATCH_CPP was set") - return - # Get build config class or use default build_config_class = import_string(self.config["build-config-class"]) if "build-config-class" in self.config else HatchCppBuildConfig @@ -60,10 +52,14 @@ def initialize(self, version: str, build_data: dict[str, Any]) -> None: build_plan.generate() # Log commands if in verbose mode - if config.verbose: + if build_plan.verbose: for command in build_plan.commands: self._logger.warning(command) + if build_plan.skip: + self._logger.warning("Skipping build") + return + # Execute build plan build_plan.execute() @@ -114,4 +110,4 @@ def initialize(self, version: str, build_data: dict[str, Any]) -> None: build_data["force_include"][str(path)] = str(path) for path in build_data["force_include"]: - self._logger.warning(f"Force include: {path}") + self._logger.info(f"Force include: {path}") From f18423eb20a831972a3874aa8f4e3e08dede124a Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Tue, 11 Nov 2025 17:49:06 -0500 Subject: [PATCH 19/58] Bump minimum hatch-build --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1520a5a..12c906b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ classifiers = [ dependencies = [ "hatchling", - "hatch-build>=0.4,<0.6", + "hatch-build>=0.5,<0.6", "pydantic", ] From 657e9a9eceefb9e44e628256edc28a7fb3013fcb Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Tue, 11 Nov 2025 17:49:10 -0500 Subject: [PATCH 20/58] =?UTF-8?q?Bump=20version:=200.2.2=20=E2=86=92=200.3?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com> --- hatch_cpp/__init__.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hatch_cpp/__init__.py b/hatch_cpp/__init__.py index 17fea63..03c6186 100644 --- a/hatch_cpp/__init__.py +++ b/hatch_cpp/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.2.2" +__version__ = "0.3.0" from .config import * from .hooks import * diff --git a/pyproject.toml b/pyproject.toml index 12c906b..fe48ba1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ authors = [{name = "the hatch-cpp authors", email = "t.paine154@gmail.com"}] description = "Hatch plugin for C++ builds" readme = "README.md" license = { text = "Apache-2.0" } -version = "0.2.2" +version = "0.3.0" requires-python = ">=3.10" keywords = [ "hatch", @@ -74,7 +74,7 @@ Repository = "https://github.com/python-project-templates/hatch-cpp" Homepage = "https://github.com/python-project-templates/hatch-cpp" [tool.bumpversion] -current_version = "0.2.2" +current_version = "0.3.0" commit = true tag = true commit_args = "-s" From a07f3ddca7962399459d2d0f72a29f75e97fa7fe Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Tue, 11 Nov 2025 17:50:10 -0500 Subject: [PATCH 21/58] Add pkn dep explicitl --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index fe48ba1..3e17220 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ classifiers = [ dependencies = [ "hatchling", "hatch-build>=0.5,<0.6", + "pkn", "pydantic", ] From e1431b5117a90e7d5a995be4a732e19f2fd5f31b Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 15:43:06 +0000 Subject: [PATCH 22/58] Update from copier (2025-11-21T15:43:06) Signed-off-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .copier-answers.yaml | 2 +- .github/workflows/build.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.copier-answers.yaml b/.copier-answers.yaml index 17531dd..ad6688c 100644 --- a/.copier-answers.yaml +++ b/.copier-answers.yaml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 973c39c +_commit: 55f9353 _src_path: https://github.com/python-project-templates/base.git add_docs: false add_extension: python diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 6c3e975..38db1b8 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -33,7 +33,7 @@ jobs: python-version: ["3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions-ext/python/setup@main with: From b28b28dc290d265b4b0cb21f5259ea20345f2049 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 14 Dec 2025 00:44:05 +0000 Subject: [PATCH 23/58] Update from copier (2025-12-14T00:44:05) Signed-off-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .copier-answers.yaml | 2 +- .github/workflows/build.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.copier-answers.yaml b/.copier-answers.yaml index ad6688c..937124f 100644 --- a/.copier-answers.yaml +++ b/.copier-answers.yaml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 55f9353 +_commit: b74d698 _src_path: https://github.com/python-project-templates/base.git add_docs: false add_extension: python diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 38db1b8..4ada2ac 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -55,7 +55,7 @@ jobs: run: make coverage - name: Upload test results (Python) - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: test-results-${{ matrix.os }}-${{ matrix.python-version }} path: junit.xml @@ -73,7 +73,7 @@ jobs: - name: Make dist run: make dist - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 with: name: dist-${{matrix.os}} path: dist From 7b6a2cb9c0b4343dae7aea87f13787980b45aee3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Jan 2026 07:08:11 +0000 Subject: [PATCH 24/58] Update nanobind requirement from <2.10.0 to <2.11.0 Updates the requirements on [nanobind](https://github.com/wjakob/nanobind) to permit the latest version. - [Changelog](https://github.com/wjakob/nanobind/blob/master/docs/changelog.rst) - [Commits](https://github.com/wjakob/nanobind/compare/v0.0.1...v2.10.2) --- updated-dependencies: - dependency-name: nanobind dependency-version: 2.10.2 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3e17220..73418ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ develop = [ "uv", "wheel", # test - "nanobind<2.10.0", # https://github.com/wjakob/nanobind/commit/abd27e3b5565bc95f5091321f0f863fce8b5b95b + "nanobind<2.11.0", # https://github.com/wjakob/nanobind/commit/abd27e3b5565bc95f5091321f0f863fce8b5b95b "pybind11", "pytest", "pytest-cov", From b7a591e6bfb3d593bccd22ff5c28596ffcf2c465 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 07:54:16 +0000 Subject: [PATCH 25/58] Update nanobind requirement from <2.11.0 to <2.12.0 Updates the requirements on [nanobind](https://github.com/wjakob/nanobind) to permit the latest version. - [Changelog](https://github.com/wjakob/nanobind/blob/master/docs/changelog.rst) - [Commits](https://github.com/wjakob/nanobind/compare/v0.0.1...v2.11.0) --- updated-dependencies: - dependency-name: nanobind dependency-version: 2.11.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 73418ab..31a1420 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ develop = [ "uv", "wheel", # test - "nanobind<2.11.0", # https://github.com/wjakob/nanobind/commit/abd27e3b5565bc95f5091321f0f863fce8b5b95b + "nanobind<2.12.0", # https://github.com/wjakob/nanobind/commit/abd27e3b5565bc95f5091321f0f863fce8b5b95b "pybind11", "pytest", "pytest-cov", From 1ce494c61e52a068de7f1cfd860641271eb291c5 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Mon, 2 Feb 2026 12:26:17 -0500 Subject: [PATCH 26/58] Add AMD64 to valid windows platforms --- hatch_cpp/toolchains/vcpkg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hatch_cpp/toolchains/vcpkg.py b/hatch_cpp/toolchains/vcpkg.py index c5056dd..db8d2f0 100644 --- a/hatch_cpp/toolchains/vcpkg.py +++ b/hatch_cpp/toolchains/vcpkg.py @@ -33,6 +33,7 @@ ("darwin", "x86_64"): "x64-osx", ("darwin", "arm64"): "arm64-osx", ("win32", "x86_64"): "x64-windows-static-md", + ("win32", "AMD64"): "x64-windows-static-md", ("win32", "arm64"): "arm64-windows-static-md", } From 17e2307ea6a5f0bc0431752695118c7afc3ee59f Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Tue, 3 Feb 2026 12:59:37 -0500 Subject: [PATCH 27/58] Add more platform-specific overrides --- hatch_cpp/tests/test_platform_specific.py | 456 ++++++++++++++++++++++ hatch_cpp/toolchains/common.py | 192 +++++++-- 2 files changed, 617 insertions(+), 31 deletions(-) create mode 100644 hatch_cpp/tests/test_platform_specific.py diff --git a/hatch_cpp/tests/test_platform_specific.py b/hatch_cpp/tests/test_platform_specific.py new file mode 100644 index 0000000..162c5d6 --- /dev/null +++ b/hatch_cpp/tests/test_platform_specific.py @@ -0,0 +1,456 @@ +"""Tests for platform-specific library configuration fields.""" + +from hatch_cpp import HatchCppLibrary, HatchCppPlatform + + +class TestPlatformSpecificFields: + """Test suite for platform-specific field handling in HatchCppLibrary.""" + + def test_effective_include_dirs(self): + """Test that include_dirs are properly merged with platform-specific dirs.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + include_dirs=["common/include"], + include_dirs_linux=["linux/include"], + include_dirs_darwin=["darwin/include"], + include_dirs_win32=["win32/include"], + ) + + linux_dirs = library.get_effective_include_dirs("linux") + assert "common/include" in linux_dirs + assert "linux/include" in linux_dirs + assert "darwin/include" not in linux_dirs + assert "win32/include" not in linux_dirs + + darwin_dirs = library.get_effective_include_dirs("darwin") + assert "common/include" in darwin_dirs + assert "darwin/include" in darwin_dirs + assert "linux/include" not in darwin_dirs + + win32_dirs = library.get_effective_include_dirs("win32") + assert "common/include" in win32_dirs + assert "win32/include" in win32_dirs + assert "linux/include" not in win32_dirs + + def test_effective_library_dirs(self): + """Test that library_dirs are properly merged with platform-specific dirs.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + library_dirs=["common/lib"], + library_dirs_linux=["linux/lib"], + library_dirs_darwin=["darwin/lib"], + library_dirs_win32=["win32/lib"], + ) + + linux_dirs = library.get_effective_library_dirs("linux") + assert "common/lib" in linux_dirs + assert "linux/lib" in linux_dirs + assert "darwin/lib" not in linux_dirs + + darwin_dirs = library.get_effective_library_dirs("darwin") + assert "common/lib" in darwin_dirs + assert "darwin/lib" in darwin_dirs + + win32_dirs = library.get_effective_library_dirs("win32") + assert "common/lib" in win32_dirs + assert "win32/lib" in win32_dirs + + def test_effective_libraries(self): + """Test that libraries are properly merged with platform-specific libraries.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + libraries=["common"], + libraries_linux=["pthread", "dl"], + libraries_darwin=["objc"], + libraries_win32=["kernel32", "user32"], + ) + + linux_libs = library.get_effective_libraries("linux") + assert "common" in linux_libs + assert "pthread" in linux_libs + assert "dl" in linux_libs + assert "objc" not in linux_libs + + darwin_libs = library.get_effective_libraries("darwin") + assert "common" in darwin_libs + assert "objc" in darwin_libs + assert "pthread" not in darwin_libs + + win32_libs = library.get_effective_libraries("win32") + assert "common" in win32_libs + assert "kernel32" in win32_libs + assert "user32" in win32_libs + assert "pthread" not in win32_libs + + def test_effective_compile_args(self): + """Test that extra_compile_args are properly merged with platform-specific args.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + extra_compile_args=["-O2"], + extra_compile_args_linux=["-march=native"], + extra_compile_args_darwin=["-mmacosx-version-min=11"], + extra_compile_args_win32=["/O2"], + ) + + linux_args = library.get_effective_compile_args("linux") + assert "-O2" in linux_args + assert "-march=native" in linux_args + assert "-mmacosx-version-min=11" not in linux_args + + darwin_args = library.get_effective_compile_args("darwin") + assert "-O2" in darwin_args + assert "-mmacosx-version-min=11" in darwin_args + + win32_args = library.get_effective_compile_args("win32") + assert "-O2" in win32_args + assert "/O2" in win32_args + + def test_effective_link_args(self): + """Test that extra_link_args are properly merged with platform-specific args.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + extra_link_args=["-shared"], + extra_link_args_linux=["-Wl,-rpath,$ORIGIN/lib"], + extra_link_args_darwin=["-Wl,-rpath,@loader_path/lib"], + extra_link_args_win32=["/NODEFAULTLIB"], + ) + + linux_args = library.get_effective_link_args("linux") + assert "-shared" in linux_args + assert "-Wl,-rpath,$ORIGIN/lib" in linux_args + assert "-Wl,-rpath,@loader_path/lib" not in linux_args + + darwin_args = library.get_effective_link_args("darwin") + assert "-shared" in darwin_args + assert "-Wl,-rpath,@loader_path/lib" in darwin_args + + win32_args = library.get_effective_link_args("win32") + assert "-shared" in win32_args + assert "/NODEFAULTLIB" in win32_args + + def test_effective_extra_objects(self): + """Test that extra_objects are properly merged with platform-specific objects.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + extra_objects=["common.o"], + extra_objects_linux=["linux.o"], + extra_objects_darwin=["darwin.o"], + extra_objects_win32=["win32.obj"], + ) + + linux_objs = library.get_effective_extra_objects("linux") + assert "common.o" in linux_objs + assert "linux.o" in linux_objs + assert "darwin.o" not in linux_objs + + darwin_objs = library.get_effective_extra_objects("darwin") + assert "common.o" in darwin_objs + assert "darwin.o" in darwin_objs + + win32_objs = library.get_effective_extra_objects("win32") + assert "common.o" in win32_objs + assert "win32.obj" in win32_objs + + def test_effective_define_macros(self): + """Test that define_macros are properly merged with platform-specific macros.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + define_macros=["COMMON=1"], + define_macros_linux=["LINUX=1", "_GNU_SOURCE"], + define_macros_darwin=["DARWIN=1", "__APPLE__"], + define_macros_win32=["WIN32=1", "_WINDOWS"], + ) + + linux_macros = library.get_effective_define_macros("linux") + assert "COMMON=1" in linux_macros + assert "LINUX=1" in linux_macros + assert "_GNU_SOURCE" in linux_macros + assert "DARWIN=1" not in linux_macros + + darwin_macros = library.get_effective_define_macros("darwin") + assert "COMMON=1" in darwin_macros + assert "DARWIN=1" in darwin_macros + assert "__APPLE__" in darwin_macros + + win32_macros = library.get_effective_define_macros("win32") + assert "COMMON=1" in win32_macros + assert "WIN32=1" in win32_macros + assert "_WINDOWS" in win32_macros + + def test_effective_undef_macros(self): + """Test that undef_macros are properly merged with platform-specific macros.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + undef_macros=["COMMON_UNDEF"], + undef_macros_linux=["LINUX_UNDEF"], + undef_macros_darwin=["DARWIN_UNDEF"], + undef_macros_win32=["WIN32_UNDEF"], + ) + + linux_macros = library.get_effective_undef_macros("linux") + assert "COMMON_UNDEF" in linux_macros + assert "LINUX_UNDEF" in linux_macros + assert "DARWIN_UNDEF" not in linux_macros + + darwin_macros = library.get_effective_undef_macros("darwin") + assert "COMMON_UNDEF" in darwin_macros + assert "DARWIN_UNDEF" in darwin_macros + + win32_macros = library.get_effective_undef_macros("win32") + assert "COMMON_UNDEF" in win32_macros + assert "WIN32_UNDEF" in win32_macros + + def test_empty_platform_specific_fields(self): + """Test that empty platform-specific fields don't cause issues.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + include_dirs=["common/include"], + # No platform-specific fields set + ) + + linux_dirs = library.get_effective_include_dirs("linux") + assert linux_dirs == ["common/include"] + + darwin_dirs = library.get_effective_include_dirs("darwin") + assert darwin_dirs == ["common/include"] + + win32_dirs = library.get_effective_include_dirs("win32") + assert win32_dirs == ["common/include"] + + def test_only_platform_specific_fields(self): + """Test that only platform-specific fields work without common fields.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + # No common include_dirs + include_dirs_linux=["linux/include"], + include_dirs_darwin=["darwin/include"], + ) + + linux_dirs = library.get_effective_include_dirs("linux") + assert linux_dirs == ["linux/include"] + + darwin_dirs = library.get_effective_include_dirs("darwin") + assert darwin_dirs == ["darwin/include"] + + win32_dirs = library.get_effective_include_dirs("win32") + assert win32_dirs == [] + + def test_alias_hyphenated_names(self): + """Test that hyphenated field names work as aliases.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + **{ + "include-dirs": ["common/include"], + "include-dirs-linux": ["linux/include"], + "library-dirs-darwin": ["darwin/lib"], + "extra-compile-args-win32": ["/O2"], + "extra-link-args-linux": ["-Wl,-rpath,$ORIGIN"], + "define-macros-darwin": ["DARWIN=1"], + "undef-macros-win32": ["NDEBUG"], + }, + ) + + assert library.include_dirs == ["common/include"] + assert library.include_dirs_linux == ["linux/include"] + assert library.library_dirs_darwin == ["darwin/lib"] + assert library.extra_compile_args_win32 == ["/O2"] + assert library.extra_link_args_linux == ["-Wl,-rpath,$ORIGIN"] + assert library.define_macros_darwin == ["DARWIN=1"] + assert library.undef_macros_win32 == ["NDEBUG"] + + +class TestPlatformFlagsIntegration: + """Integration tests for platform-specific fields in compile/link flags.""" + + def test_compile_flags_include_platform_specific_include_dirs(self): + """Test that get_compile_flags includes platform-specific include dirs.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + binding="generic", + include_dirs=["common/include"], + include_dirs_linux=["linux/include"], + ) + + # Create a mock linux platform + platform = HatchCppPlatform(cc="gcc", cxx="g++", ld="ld", platform="linux", toolchain="gcc", disable_ccache=True) + + flags = platform.get_compile_flags(library) + assert "-Icommon/include" in flags + assert "-Ilinux/include" in flags + + def test_compile_flags_include_platform_specific_macros(self): + """Test that get_compile_flags includes platform-specific define/undef macros.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + binding="generic", + define_macros=["COMMON=1"], + define_macros_linux=["LINUX_SPECIFIC=1"], + undef_macros=["OLD_MACRO"], + undef_macros_linux=["LINUX_OLD"], + ) + + platform = HatchCppPlatform(cc="gcc", cxx="g++", ld="ld", platform="linux", toolchain="gcc", disable_ccache=True) + + flags = platform.get_compile_flags(library) + assert "-DCOMMON=1" in flags + assert "-DLINUX_SPECIFIC=1" in flags + assert "-UOLD_MACRO" in flags + assert "-ULINUX_OLD" in flags + + def test_link_flags_include_platform_specific_libraries(self): + """Test that get_link_flags includes platform-specific libraries.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + binding="generic", + libraries=["common"], + libraries_linux=["pthread", "dl"], + library_dirs=["common/lib"], + library_dirs_linux=["linux/lib"], + ) + + platform = HatchCppPlatform(cc="gcc", cxx="g++", ld="ld", platform="linux", toolchain="gcc", disable_ccache=True) + + flags = platform.get_link_flags(library) + assert "-lcommon" in flags + assert "-lpthread" in flags + assert "-ldl" in flags + assert "-Lcommon/lib" in flags + assert "-Llinux/lib" in flags + + def test_link_flags_include_platform_specific_link_args(self): + """Test that get_link_flags includes platform-specific extra_link_args.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + binding="generic", + extra_link_args=["-shared"], + extra_link_args_linux=["-Wl,-rpath,$ORIGIN/lib"], + ) + + platform = HatchCppPlatform(cc="gcc", cxx="g++", ld="ld", platform="linux", toolchain="gcc", disable_ccache=True) + + flags = platform.get_link_flags(library) + assert "-shared" in flags + assert "-Wl,-rpath,$ORIGIN/lib" in flags + + def test_darwin_platform_uses_darwin_specific_fields(self): + """Test that darwin platform uses darwin-specific fields.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + binding="generic", + libraries_linux=["pthread"], + libraries_darwin=["objc"], + extra_link_args_darwin=["-Wl,-rpath,@loader_path/lib"], + ) + + platform = HatchCppPlatform(cc="clang", cxx="clang++", ld="ld", platform="darwin", toolchain="clang", disable_ccache=True) + + flags = platform.get_link_flags(library) + assert "-lobjc" in flags + assert "-lpthread" not in flags + assert "-Wl,-rpath,@loader_path/lib" in flags + + def test_win32_platform_uses_win32_specific_fields(self): + """Test that win32 platform uses win32-specific fields.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + binding="generic", + libraries=["common"], + libraries_win32=["kernel32"], + library_dirs=["common/lib"], + library_dirs_win32=["win32/lib"], + ) + + platform = HatchCppPlatform(cc="cl", cxx="cl", ld="link", platform="win32", toolchain="msvc", disable_ccache=True) + + flags = platform.get_link_flags(library) + assert "common.lib" in flags + assert "kernel32.lib" in flags + assert "/LIBPATH:common/lib" in flags + assert "/LIBPATH:win32/lib" in flags + + def test_msvc_compile_flags_use_platform_specific_fields(self): + """Test that MSVC compile flags include platform-specific fields.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + binding="generic", + include_dirs=["common/include"], + include_dirs_win32=["win32/include"], + define_macros=["COMMON=1"], + define_macros_win32=["_WINDOWS"], + extra_compile_args_win32=["/W4"], + ) + + platform = HatchCppPlatform(cc="cl", cxx="cl", ld="link", platform="win32", toolchain="msvc", disable_ccache=True) + + flags = platform.get_compile_flags(library) + assert "/Icommon/include" in flags + assert "/Iwin32/include" in flags + assert "/DCOMMON=1" in flags + assert "/D_WINDOWS" in flags + assert "/W4" in flags + + +class TestPlatformFieldOrdering: + """Test that platform-specific fields are appended after common fields.""" + + def test_include_dirs_ordering(self): + """Test that platform-specific include dirs come after common dirs.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + include_dirs=["first", "second"], + include_dirs_linux=["third", "fourth"], + ) + + dirs = library.get_effective_include_dirs("linux") + assert dirs == ["first", "second", "third", "fourth"] + + def test_libraries_ordering(self): + """Test that platform-specific libraries come after common libraries.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + libraries=["common1", "common2"], + libraries_linux=["linux1", "linux2"], + ) + + libs = library.get_effective_libraries("linux") + assert libs == ["common1", "common2", "linux1", "linux2"] + + def test_base_list_not_mutated(self): + """Test that the base lists are not mutated when getting effective values.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + include_dirs=["common"], + include_dirs_linux=["linux"], + ) + + # Get effective dirs multiple times + dirs1 = library.get_effective_include_dirs("linux") + dirs2 = library.get_effective_include_dirs("linux") + + # Both should be equal + assert dirs1 == dirs2 + + # Base list should not be modified + assert library.include_dirs == ["common"] + assert library.include_dirs_linux == ["linux"] diff --git a/hatch_cpp/toolchains/common.py b/hatch_cpp/toolchains/common.py index 57d65e6..3abcde7 100644 --- a/hatch_cpp/toolchains/common.py +++ b/hatch_cpp/toolchains/common.py @@ -47,15 +47,44 @@ class HatchCppLibrary(BaseModel, validate_assignment=True): std: Optional[str] = None include_dirs: List[str] = Field(default_factory=list, alias=AliasChoices("include_dirs", "include-dirs")) + include_dirs_linux: List[str] = Field(default_factory=list, alias=AliasChoices("include_dirs_linux", "include-dirs-linux")) + include_dirs_darwin: List[str] = Field(default_factory=list, alias=AliasChoices("include_dirs_darwin", "include-dirs-darwin")) + include_dirs_win32: List[str] = Field(default_factory=list, alias=AliasChoices("include_dirs_win32", "include-dirs-win32")) + library_dirs: List[str] = Field(default_factory=list, alias=AliasChoices("library_dirs", "library-dirs")) + library_dirs_linux: List[str] = Field(default_factory=list, alias=AliasChoices("library_dirs_linux", "library-dirs-linux")) + library_dirs_darwin: List[str] = Field(default_factory=list, alias=AliasChoices("library_dirs_darwin", "library-dirs-darwin")) + library_dirs_win32: List[str] = Field(default_factory=list, alias=AliasChoices("library_dirs_win32", "library-dirs-win32")) + libraries: List[str] = Field(default_factory=list) + libraries_linux: List[str] = Field(default_factory=list, alias=AliasChoices("libraries_linux", "libraries-linux")) + libraries_darwin: List[str] = Field(default_factory=list, alias=AliasChoices("libraries_darwin", "libraries-darwin")) + libraries_win32: List[str] = Field(default_factory=list, alias=AliasChoices("libraries_win32", "libraries-win32")) extra_compile_args: List[str] = Field(default_factory=list, alias=AliasChoices("extra_compile_args", "extra-compile-args")) + extra_compile_args_linux: List[str] = Field(default_factory=list, alias=AliasChoices("extra_compile_args_linux", "extra-compile-args-linux")) + extra_compile_args_darwin: List[str] = Field(default_factory=list, alias=AliasChoices("extra_compile_args_darwin", "extra-compile-args-darwin")) + extra_compile_args_win32: List[str] = Field(default_factory=list, alias=AliasChoices("extra_compile_args_win32", "extra-compile-args-win32")) + extra_link_args: List[str] = Field(default_factory=list, alias=AliasChoices("extra_link_args", "extra-link-args")) + extra_link_args_linux: List[str] = Field(default_factory=list, alias=AliasChoices("extra_link_args_linux", "extra-link-args-linux")) + extra_link_args_darwin: List[str] = Field(default_factory=list, alias=AliasChoices("extra_link_args_darwin", "extra-link-args-darwin")) + extra_link_args_win32: List[str] = Field(default_factory=list, alias=AliasChoices("extra_link_args_win32", "extra-link-args-win32")) + extra_objects: List[str] = Field(default_factory=list, alias=AliasChoices("extra_objects", "extra-objects")) + extra_objects_linux: List[str] = Field(default_factory=list, alias=AliasChoices("extra_objects_linux", "extra-objects-linux")) + extra_objects_darwin: List[str] = Field(default_factory=list, alias=AliasChoices("extra_objects_darwin", "extra-objects-darwin")) + extra_objects_win32: List[str] = Field(default_factory=list, alias=AliasChoices("extra_objects_win32", "extra-objects-win32")) define_macros: List[str] = Field(default_factory=list, alias=AliasChoices("define_macros", "define-macros")) + define_macros_linux: List[str] = Field(default_factory=list, alias=AliasChoices("define_macros_linux", "define-macros-linux")) + define_macros_darwin: List[str] = Field(default_factory=list, alias=AliasChoices("define_macros_darwin", "define-macros-darwin")) + define_macros_win32: List[str] = Field(default_factory=list, alias=AliasChoices("define_macros_win32", "define-macros-win32")) + undef_macros: List[str] = Field(default_factory=list, alias=AliasChoices("undef_macros", "undef-macros")) + undef_macros_linux: List[str] = Field(default_factory=list, alias=AliasChoices("undef_macros_linux", "undef-macros-linux")) + undef_macros_darwin: List[str] = Field(default_factory=list, alias=AliasChoices("undef_macros_darwin", "undef-macros-darwin")) + undef_macros_win32: List[str] = Field(default_factory=list, alias=AliasChoices("undef_macros_win32", "undef-macros-win32")) export_symbols: List[str] = Field(default_factory=list, alias=AliasChoices("export_symbols", "export-symbols")) depends: List[str] = Field(default_factory=list) @@ -89,6 +118,94 @@ def check_binding_and_py_limited_api(self): raise ValueError("Generic binding can not support Py_LIMITED_API") return self + def get_effective_link_args(self, platform: Platform) -> List[str]: + """Get link args merged with platform-specific link args.""" + args = list(self.extra_link_args) + if platform == "linux": + args.extend(self.extra_link_args_linux) + elif platform == "darwin": + args.extend(self.extra_link_args_darwin) + elif platform == "win32": + args.extend(self.extra_link_args_win32) + return args + + def get_effective_include_dirs(self, platform: Platform) -> List[str]: + """Get include dirs merged with platform-specific include dirs.""" + dirs = list(self.include_dirs) + if platform == "linux": + dirs.extend(self.include_dirs_linux) + elif platform == "darwin": + dirs.extend(self.include_dirs_darwin) + elif platform == "win32": + dirs.extend(self.include_dirs_win32) + return dirs + + def get_effective_library_dirs(self, platform: Platform) -> List[str]: + """Get library dirs merged with platform-specific library dirs.""" + dirs = list(self.library_dirs) + if platform == "linux": + dirs.extend(self.library_dirs_linux) + elif platform == "darwin": + dirs.extend(self.library_dirs_darwin) + elif platform == "win32": + dirs.extend(self.library_dirs_win32) + return dirs + + def get_effective_libraries(self, platform: Platform) -> List[str]: + """Get libraries merged with platform-specific libraries.""" + libs = list(self.libraries) + if platform == "linux": + libs.extend(self.libraries_linux) + elif platform == "darwin": + libs.extend(self.libraries_darwin) + elif platform == "win32": + libs.extend(self.libraries_win32) + return libs + + def get_effective_compile_args(self, platform: Platform) -> List[str]: + """Get compile args merged with platform-specific compile args.""" + args = list(self.extra_compile_args) + if platform == "linux": + args.extend(self.extra_compile_args_linux) + elif platform == "darwin": + args.extend(self.extra_compile_args_darwin) + elif platform == "win32": + args.extend(self.extra_compile_args_win32) + return args + + def get_effective_extra_objects(self, platform: Platform) -> List[str]: + """Get extra objects merged with platform-specific extra objects.""" + objs = list(self.extra_objects) + if platform == "linux": + objs.extend(self.extra_objects_linux) + elif platform == "darwin": + objs.extend(self.extra_objects_darwin) + elif platform == "win32": + objs.extend(self.extra_objects_win32) + return objs + + def get_effective_define_macros(self, platform: Platform) -> List[str]: + """Get define macros merged with platform-specific define macros.""" + macros = list(self.define_macros) + if platform == "linux": + macros.extend(self.define_macros_linux) + elif platform == "darwin": + macros.extend(self.define_macros_darwin) + elif platform == "win32": + macros.extend(self.define_macros_win32) + return macros + + def get_effective_undef_macros(self, platform: Platform) -> List[str]: + """Get undef macros merged with platform-specific undef macros.""" + macros = list(self.undef_macros) + if platform == "linux": + macros.extend(self.undef_macros_linux) + elif platform == "darwin": + macros.extend(self.undef_macros_darwin) + elif platform == "win32": + macros.extend(self.undef_macros_win32) + return macros + class HatchCppPlatform(BaseModel): cc: str @@ -148,54 +265,62 @@ def platform_for_toolchain(toolchain: CompilerToolchain) -> HatchCppPlatform: def get_compile_flags(self, library: HatchCppLibrary, build_type: BuildType = "release") -> str: flags = "" + # Get effective platform-specific values + effective_include_dirs = library.get_effective_include_dirs(self.platform) + effective_compile_args = library.get_effective_compile_args(self.platform) + effective_define_macros = library.get_effective_define_macros(self.platform) + effective_undef_macros = library.get_effective_undef_macros(self.platform) + effective_extra_objects = library.get_effective_extra_objects(self.platform) + effective_link_args = library.get_effective_link_args(self.platform) + # Python.h if library.binding != "generic": - library.include_dirs.append(get_path("include")) + effective_include_dirs.append(get_path("include")) if library.binding == "pybind11": import pybind11 - library.include_dirs.append(pybind11.get_include()) + effective_include_dirs.append(pybind11.get_include()) if not library.std: library.std = "c++11" elif library.binding == "nanobind": import nanobind - library.include_dirs.append(nanobind.include_dir()) + effective_include_dirs.append(nanobind.include_dir()) if not library.std: library.std = "c++17" library.sources.append(str(Path(nanobind.include_dir()).parent / "src" / "nb_combined.cpp")) - library.include_dirs.append(str((Path(nanobind.include_dir()).parent / "ext" / "robin_map" / "include"))) + effective_include_dirs.append(str((Path(nanobind.include_dir()).parent / "ext" / "robin_map" / "include"))) if library.py_limited_api: if library.binding == "pybind11": raise ValueError("pybind11 does not support Py_LIMITED_API") - library.define_macros.append(f"Py_LIMITED_API=0x0{library.py_limited_api[2]}0{hex(int(library.py_limited_api[3:]))[2:]}00f0") + effective_define_macros.append(f"Py_LIMITED_API=0x0{library.py_limited_api[2]}0{hex(int(library.py_limited_api[3:]))[2:]}00f0") # Toolchain-specific flags if self.toolchain == "gcc": - flags += " " + " ".join(f"-I{d}" for d in library.include_dirs) + flags += " " + " ".join(f"-I{d}" for d in effective_include_dirs) flags += " -fPIC" - flags += " " + " ".join(library.extra_compile_args) - flags += " " + " ".join(f"-D{macro}" for macro in library.define_macros) - flags += " " + " ".join(f"-U{macro}" for macro in library.undef_macros) + flags += " " + " ".join(effective_compile_args) + flags += " " + " ".join(f"-D{macro}" for macro in effective_define_macros) + flags += " " + " ".join(f"-U{macro}" for macro in effective_undef_macros) if library.std: flags += f" -std={library.std}" elif self.toolchain == "clang": - flags += " ".join(f"-I{d}" for d in library.include_dirs) + flags += " ".join(f"-I{d}" for d in effective_include_dirs) flags += " -fPIC" - flags += " " + " ".join(library.extra_compile_args) - flags += " " + " ".join(f"-D{macro}" for macro in library.define_macros) - flags += " " + " ".join(f"-U{macro}" for macro in library.undef_macros) + flags += " " + " ".join(effective_compile_args) + flags += " " + " ".join(f"-D{macro}" for macro in effective_define_macros) + flags += " " + " ".join(f"-U{macro}" for macro in effective_undef_macros) if library.std: flags += f" -std={library.std}" elif self.toolchain == "msvc": - flags += " ".join(f"/I{d}" for d in library.include_dirs) - flags += " " + " ".join(library.extra_compile_args) - flags += " " + " ".join(library.extra_link_args) - flags += " " + " ".join(library.extra_objects) - flags += " " + " ".join(f"/D{macro}" for macro in library.define_macros) - flags += " " + " ".join(f"/U{macro}" for macro in library.undef_macros) + flags += " ".join(f"/I{d}" for d in effective_include_dirs) + flags += " " + " ".join(effective_compile_args) + flags += " " + " ".join(effective_link_args) + flags += " " + " ".join(effective_extra_objects) + flags += " " + " ".join(f"/D{macro}" for macro in effective_define_macros) + flags += " " + " ".join(f"/U{macro}" for macro in effective_undef_macros) flags += " /EHsc /DWIN32" if library.std: flags += f" /std:{library.std}" @@ -206,12 +331,17 @@ def get_compile_flags(self, library: HatchCppLibrary, build_type: BuildType = "r def get_link_flags(self, library: HatchCppLibrary, build_type: BuildType = "release") -> str: flags = "" + effective_link_args = library.get_effective_link_args(self.platform) + effective_extra_objects = library.get_effective_extra_objects(self.platform) + effective_libraries = library.get_effective_libraries(self.platform) + effective_library_dirs = library.get_effective_library_dirs(self.platform) + if self.toolchain == "gcc": flags += " -shared" - flags += " " + " ".join(library.extra_link_args) - flags += " " + " ".join(library.extra_objects) - flags += " " + " ".join(f"-l{lib}" for lib in library.libraries) - flags += " " + " ".join(f"-L{lib}" for lib in library.library_dirs) + flags += " " + " ".join(effective_link_args) + flags += " " + " ".join(effective_extra_objects) + flags += " " + " ".join(f"-l{lib}" for lib in effective_libraries) + flags += " " + " ".join(f"-L{lib}" for lib in effective_library_dirs) flags += f" -o {library.get_qualified_name(self.platform)}" if self.platform == "darwin": flags += " -undefined dynamic_lookup" @@ -221,10 +351,10 @@ def get_link_flags(self, library: HatchCppLibrary, build_type: BuildType = "rele flags += " -fuse-ld=lld" elif self.toolchain == "clang": flags += " -shared" - flags += " " + " ".join(library.extra_link_args) - flags += " " + " ".join(library.extra_objects) - flags += " " + " ".join(f"-l{lib}" for lib in library.libraries) - flags += " " + " ".join(f"-L{lib}" for lib in library.library_dirs) + flags += " " + " ".join(effective_link_args) + flags += " " + " ".join(effective_extra_objects) + flags += " " + " ".join(f"-l{lib}" for lib in effective_libraries) + flags += " " + " ".join(f"-L{lib}" for lib in effective_library_dirs) flags += f" -o {library.get_qualified_name(self.platform)}" if self.platform == "darwin": flags += " -undefined dynamic_lookup" @@ -233,15 +363,15 @@ def get_link_flags(self, library: HatchCppLibrary, build_type: BuildType = "rele elif "lld" in self.ld: flags += " -fuse-ld=lld" elif self.toolchain == "msvc": - flags += " " + " ".join(library.extra_link_args) - flags += " " + " ".join(library.extra_objects) + flags += " " + " ".join(effective_link_args) + flags += " " + " ".join(effective_extra_objects) flags += " /LD" flags += f" /Fe:{library.get_qualified_name(self.platform)}" flags += " /link /DLL" if (Path(executable).parent / "libs").exists(): flags += f" /LIBPATH:{str(Path(executable).parent / 'libs')}" - flags += " " + " ".join(f"{lib}.lib" for lib in library.libraries) - flags += " " + " ".join(f"/LIBPATH:{lib}" for lib in library.library_dirs) + flags += " " + " ".join(f"{lib}.lib" for lib in effective_libraries) + flags += " " + " ".join(f"/LIBPATH:{lib}" for lib in effective_library_dirs) # clean while flags.count(" "): flags = flags.replace(" ", " ") From 3ad3652fc4e3f608fb392c124e907bf91c5d4724 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Tue, 3 Feb 2026 13:40:40 -0500 Subject: [PATCH 28/58] =?UTF-8?q?Bump=20version:=200.3.0=20=E2=86=92=200.3?= =?UTF-8?q?.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com> --- hatch_cpp/__init__.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hatch_cpp/__init__.py b/hatch_cpp/__init__.py index 03c6186..3489ebe 100644 --- a/hatch_cpp/__init__.py +++ b/hatch_cpp/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.3.0" +__version__ = "0.3.1" from .config import * from .hooks import * diff --git a/pyproject.toml b/pyproject.toml index 31a1420..bf87911 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ authors = [{name = "the hatch-cpp authors", email = "t.paine154@gmail.com"}] description = "Hatch plugin for C++ builds" readme = "README.md" license = { text = "Apache-2.0" } -version = "0.3.0" +version = "0.3.1" requires-python = ">=3.10" keywords = [ "hatch", @@ -75,7 +75,7 @@ Repository = "https://github.com/python-project-templates/hatch-cpp" Homepage = "https://github.com/python-project-templates/hatch-cpp" [tool.bumpversion] -current_version = "0.3.0" +current_version = "0.3.1" commit = true tag = true commit_args = "-s" From 1f210fa3bf5864f0d081e41849312c3f71e690da Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:53:55 -0500 Subject: [PATCH 29/58] Tweak paths --- hatch_cpp/tests/test_platform_specific.py | 79 +++++++++++++++++++++++ hatch_cpp/toolchains/common.py | 14 +++- 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/hatch_cpp/tests/test_platform_specific.py b/hatch_cpp/tests/test_platform_specific.py index 162c5d6..34778bd 100644 --- a/hatch_cpp/tests/test_platform_specific.py +++ b/hatch_cpp/tests/test_platform_specific.py @@ -454,3 +454,82 @@ def test_base_list_not_mutated(self): # Base list should not be modified assert library.include_dirs == ["common"] assert library.include_dirs_linux == ["linux"] + + +class TestMSVCPythonLibsPath: + """Tests for MSVC Python libs path discovery.""" + + def test_msvc_link_flags_include_libpath(self): + """Test that MSVC link flags include /LIBPATH for Python libs.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + binding="generic", # Skip Python.h include + ) + + platform = HatchCppPlatform( + cc="cl", + cxx="cl", + ld="link", + platform="win32", + toolchain="msvc", + disable_ccache=True, + ) + + flags = platform.get_link_flags(library) + # Should have /link /DLL flags + assert "/link" in flags + assert "/DLL" in flags + # Should have output file + assert "/Fe:" in flags + + def test_msvc_link_flags_with_libraries(self): + """Test that MSVC link flags properly format library names.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + binding="generic", + libraries=["mylib"], + library_dirs=["path/to/libs"], + ) + + platform = HatchCppPlatform( + cc="cl", + cxx="cl", + ld="link", + platform="win32", + toolchain="msvc", + disable_ccache=True, + ) + + flags = platform.get_link_flags(library) + # Libraries should have .lib suffix on Windows + assert "mylib.lib" in flags + # Library dirs should use /LIBPATH: + assert "/LIBPATH:path/to/libs" in flags + + def test_msvc_link_flags_with_platform_specific_libraries(self): + """Test that MSVC uses win32-specific libraries.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + binding="generic", + libraries=["common"], + libraries_win32=["kernel32", "user32"], + library_dirs_win32=["C:/Windows/System32"], + ) + + platform = HatchCppPlatform( + cc="cl", + cxx="cl", + ld="link", + platform="win32", + toolchain="msvc", + disable_ccache=True, + ) + + flags = platform.get_link_flags(library) + assert "common.lib" in flags + assert "kernel32.lib" in flags + assert "user32.lib" in flags + assert "/LIBPATH:C:/Windows/System32" in flags diff --git a/hatch_cpp/toolchains/common.py b/hatch_cpp/toolchains/common.py index 3abcde7..120ed4a 100644 --- a/hatch_cpp/toolchains/common.py +++ b/hatch_cpp/toolchains/common.py @@ -5,7 +5,7 @@ from re import match from shutil import which from sys import executable, platform as sys_platform -from sysconfig import get_path +from sysconfig import get_config_var, get_path from typing import Any, List, Literal, Optional from pydantic import AliasChoices, BaseModel, Field, field_validator, model_validator @@ -368,8 +368,16 @@ def get_link_flags(self, library: HatchCppLibrary, build_type: BuildType = "rele flags += " /LD" flags += f" /Fe:{library.get_qualified_name(self.platform)}" flags += " /link /DLL" - if (Path(executable).parent / "libs").exists(): - flags += f" /LIBPATH:{str(Path(executable).parent / 'libs')}" + # Add Python libs directory - check multiple possible locations + python_libs_paths = [ + Path(executable).parent / "libs", # Standard Python install + Path(executable).parent.parent / "libs", # Some virtualenv layouts + Path(get_config_var("installed_base") or "") / "libs", # sysconfig approach + ] + for libs_path in python_libs_paths: + if libs_path.exists(): + flags += f" /LIBPATH:{str(libs_path)}" + break flags += " " + " ".join(f"{lib}.lib" for lib in effective_libraries) flags += " " + " ".join(f"/LIBPATH:{lib}" for lib in effective_library_dirs) # clean From 1f498bfc87426a0e2f10b7de18f3ca0c87308f3c Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:57:09 -0500 Subject: [PATCH 30/58] =?UTF-8?q?Bump=20version:=200.3.1=20=E2=86=92=200.3?= =?UTF-8?q?.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com> --- hatch_cpp/__init__.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hatch_cpp/__init__.py b/hatch_cpp/__init__.py index 3489ebe..53547ad 100644 --- a/hatch_cpp/__init__.py +++ b/hatch_cpp/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.3.1" +__version__ = "0.3.2" from .config import * from .hooks import * diff --git a/pyproject.toml b/pyproject.toml index bf87911..6ad85dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ authors = [{name = "the hatch-cpp authors", email = "t.paine154@gmail.com"}] description = "Hatch plugin for C++ builds" readme = "README.md" license = { text = "Apache-2.0" } -version = "0.3.1" +version = "0.3.2" requires-python = ">=3.10" keywords = [ "hatch", @@ -75,7 +75,7 @@ Repository = "https://github.com/python-project-templates/hatch-cpp" Homepage = "https://github.com/python-project-templates/hatch-cpp" [tool.bumpversion] -current_version = "0.3.1" +current_version = "0.3.2" commit = true tag = true commit_args = "-s" From bf6ebb3f50b8ca56d4c80817f7dab12c62cc5946 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 15 Feb 2026 05:53:50 +0000 Subject: [PATCH 31/58] Update from copier (2026-02-15T05:53:50) Signed-off-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .copier-answers.yaml | 2 +- pyproject.toml | 48 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/.copier-answers.yaml b/.copier-answers.yaml index 937124f..6c7aaa8 100644 --- a/.copier-answers.yaml +++ b/.copier-answers.yaml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: b74d698 +_commit: 37f89c1 _src_path: https://github.com/python-project-templates/base.git add_docs: false add_extension: python diff --git a/pyproject.toml b/pyproject.toml index 6ad85dd..46ed51c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,19 @@ [build-system] +<<<<<<< before updating requires = ["hatchling"] build-backend = "hatchling.build" +======= +requires = [ + "hatchling", +] +build-backend="hatchling.build" +>>>>>>> after updating [project] name = "hatch-cpp" -authors = [{name = "the hatch-cpp authors", email = "t.paine154@gmail.com"}] +authors = [ + {name = "the hatch-cpp authors", email = "t.paine154@gmail.com"}, +] description = "Hatch plugin for C++ builds" readme = "README.md" license = { text = "Apache-2.0" } @@ -54,6 +63,7 @@ develop = [ "pytest-cov", "ruff>=0.9,<0.15", "twine", + "ty", "uv", "wheel", # test @@ -118,26 +128,50 @@ artifacts = [] src = "/" [tool.hatch.build.targets.sdist] -packages = ["hatch_cpp"] +packages = [ + "hatch_cpp", +] [tool.hatch.build.targets.wheel] -packages = ["hatch_cpp"] +packages = [ + "hatch_cpp", +] [tool.pytest.ini_options] -addopts = ["-vvv", "--junitxml=junit.xml"] +addopts = [ + "-vvv", + "--junitxml=junit.xml", +] testpaths = "hatch_cpp/tests" [tool.ruff] line-length = 150 [tool.ruff.lint] -extend-select = ["I"] +extend-select = [ + "I", +] [tool.ruff.lint.isort] combine-as-imports = true default-section = "third-party" -known-first-party = ["hatch_cpp"] -section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"] +known-first-party = [ + "hatch_cpp", +] +section-order = [ + "future", + "standard-library", + "third-party", + "first-party", + "local-folder", +] [tool.ruff.lint.per-file-ignores] +<<<<<<< before updating "__init__.py" = ["F401", "F403"] +======= +"__init__.py" = [ + "F401", + "F403", +] +>>>>>>> after updating From e711561daeb017abdbebda1721b00ed297e96a1f Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Sun, 15 Feb 2026 01:04:38 -0500 Subject: [PATCH 32/58] Clean up pyproject.toml formatting --- pyproject.toml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 46ed51c..2e974a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,8 @@ [build-system] -<<<<<<< before updating -requires = ["hatchling"] -build-backend = "hatchling.build" -======= requires = [ "hatchling", ] build-backend="hatchling.build" ->>>>>>> after updating [project] name = "hatch-cpp" @@ -167,11 +162,7 @@ section-order = [ ] [tool.ruff.lint.per-file-ignores] -<<<<<<< before updating -"__init__.py" = ["F401", "F403"] -======= "__init__.py" = [ "F401", "F403", ] ->>>>>>> after updating From 87f04804e12386f585abaa0685e8521f8250efe3 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 05:49:16 +0000 Subject: [PATCH 33/58] Update from copier (2026-02-22T05:49:16) Signed-off-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .copier-answers.yaml | 2 +- .gitignore | 12 ++++++++---- Makefile | 11 +++++++---- pyproject.toml | 19 ++++++++----------- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/.copier-answers.yaml b/.copier-answers.yaml index 6c7aaa8..a4cd5cd 100644 --- a/.copier-answers.yaml +++ b/.copier-answers.yaml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 37f89c1 +_commit: 08a244d _src_path: https://github.com/python-project-templates/base.git add_docs: false add_extension: python diff --git a/.gitignore b/.gitignore index 9d04ba6..7885aeb 100644 --- a/.gitignore +++ b/.gitignore @@ -141,14 +141,12 @@ js/node_modules js/test-results js/playwright-report js/*.tgz -hatch_cpp/extension # Jupyter .ipynb_checkpoints .autoversion Untitled*.ipynb -!hatch_cpp/extension/hatch_cpp.json -!hatch_cpp/extension/install.json +hatch_cpp/extension hatch_cpp/nbextension hatch_cpp/labextension @@ -158,5 +156,11 @@ hatch_cpp/labextension # Rust target +<<<<<<< before updating vcpkg -vcpkg_installed \ No newline at end of file +vcpkg_installed +======= +# Hydra +outputs/ +multirun/ +>>>>>>> after updating diff --git a/Makefile b/Makefile index e55cbef..abd4aba 100644 --- a/Makefile +++ b/Makefile @@ -46,12 +46,15 @@ format: fix ################ # Other Checks # ################ -.PHONY: check-manifest checks check +.PHONY: check-dist check-types checks check -check-manifest: ## check python sdist manifest with check-manifest - check-manifest -v +check-dist: ## check python sdist and wheel with check-dist + check-dist -v -checks: check-manifest +check-types: ## check python types with ty + ty check --python $$(which python) + +checks: check-dist # Alias check: checks diff --git a/pyproject.toml b/pyproject.toml index 2e974a7..1c87a8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ requires = [ "hatchling", ] -build-backend="hatchling.build" +build-backend = "hatchling.build" [project] name = "hatch-cpp" @@ -48,15 +48,19 @@ dependencies = [ develop = [ "build", "bump-my-version", - "check-manifest", - "codespell>=2.4,<2.5", + "check-dist", + "codespell", "hatchling", +<<<<<<< before updating "hatch-build>=0.3.2", "mdformat>=0.7.22,<1.1", +======= + "mdformat", +>>>>>>> after updating "mdformat-tables>=1", "pytest", "pytest-cov", - "ruff>=0.9,<0.15", + "ruff", "twine", "ty", "uv", @@ -95,13 +99,6 @@ filename = "pyproject.toml" search = 'version = "{current_version}"' replace = 'version = "{new_version}"' -[tool.check-manifest] -ignore = [ - ".copier-answers.yaml", - "Makefile", - "docs/**/*", -] - [tool.coverage.run] branch = false omit = [ From e3e0767973737c13d0ccb680c0d11b371c502e80 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Sun, 22 Feb 2026 08:12:15 -0500 Subject: [PATCH 34/58] Update .gitignore --- .gitignore | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 7885aeb..2bddce6 100644 --- a/.gitignore +++ b/.gitignore @@ -156,11 +156,9 @@ hatch_cpp/labextension # Rust target -<<<<<<< before updating -vcpkg -vcpkg_installed -======= # Hydra outputs/ multirun/ ->>>>>>> after updating + +vcpkg +vcpkg_installed From 3dd1fae81714d13e5d9a13f09ec21a1432f43f46 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Sun, 22 Feb 2026 08:12:27 -0500 Subject: [PATCH 35/58] Update pyproject.toml --- pyproject.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1c87a8a..f5428e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,12 +51,8 @@ develop = [ "check-dist", "codespell", "hatchling", -<<<<<<< before updating "hatch-build>=0.3.2", - "mdformat>=0.7.22,<1.1", -======= "mdformat", ->>>>>>> after updating "mdformat-tables>=1", "pytest", "pytest-cov", From 337c351d14848f75a55bbf60063ce9820951f28e Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:24:22 -0500 Subject: [PATCH 36/58] Small hotfix for venvs --- hatch_cpp/toolchains/common.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/hatch_cpp/toolchains/common.py b/hatch_cpp/toolchains/common.py index 120ed4a..4a42b27 100644 --- a/hatch_cpp/toolchains/common.py +++ b/hatch_cpp/toolchains/common.py @@ -4,7 +4,7 @@ from pathlib import Path from re import match from shutil import which -from sys import executable, platform as sys_platform +from sys import base_exec_prefix, exec_prefix, executable, platform as sys_platform from sysconfig import get_config_var, get_path from typing import Any, List, Literal, Optional @@ -369,10 +369,14 @@ def get_link_flags(self, library: HatchCppLibrary, build_type: BuildType = "rele flags += f" /Fe:{library.get_qualified_name(self.platform)}" flags += " /link /DLL" # Add Python libs directory - check multiple possible locations + # In virtual environments, sys.executable is in the venv, but pythonXX.lib + # lives under the base Python installation's 'libs' directory. python_libs_paths = [ Path(executable).parent / "libs", # Standard Python install Path(executable).parent.parent / "libs", # Some virtualenv layouts Path(get_config_var("installed_base") or "") / "libs", # sysconfig approach + Path(exec_prefix) / "libs", # exec_prefix approach + Path(base_exec_prefix) / "libs", # base_exec_prefix approach ] for libs_path in python_libs_paths: if libs_path.exists(): From f2bb298555b9ecd56b4c7422677203d2abe6b967 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Tue, 24 Feb 2026 21:56:41 -0500 Subject: [PATCH 37/58] =?UTF-8?q?Bump=20version:=200.3.2=20=E2=86=92=200.3?= =?UTF-8?q?.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com> --- hatch_cpp/__init__.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hatch_cpp/__init__.py b/hatch_cpp/__init__.py index 53547ad..54d0b37 100644 --- a/hatch_cpp/__init__.py +++ b/hatch_cpp/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.3.2" +__version__ = "0.3.3" from .config import * from .hooks import * diff --git a/pyproject.toml b/pyproject.toml index f5428e0..bc6f119 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ authors = [ description = "Hatch plugin for C++ builds" readme = "README.md" license = { text = "Apache-2.0" } -version = "0.3.2" +version = "0.3.3" requires-python = ">=3.10" keywords = [ "hatch", @@ -80,7 +80,7 @@ Repository = "https://github.com/python-project-templates/hatch-cpp" Homepage = "https://github.com/python-project-templates/hatch-cpp" [tool.bumpversion] -current_version = "0.3.2" +current_version = "0.3.3" commit = true tag = true commit_args = "-s" From 3136a3d236d22588f812fb3c26056a42dd32f05a Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:59:10 -0500 Subject: [PATCH 38/58] raise if system calls fail, don't pass C++11 on msvc --- hatch_cpp/config.py | 4 +++- hatch_cpp/toolchains/common.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/hatch_cpp/config.py b/hatch_cpp/config.py index 9e9c4d8..1b53dff 100644 --- a/hatch_cpp/config.py +++ b/hatch_cpp/config.py @@ -93,7 +93,9 @@ def generate(self): def execute(self): for command in self.commands: - system_call(command) + ret = system_call(command) + if ret != 0: + raise RuntimeError(f"hatch-cpp build command failed with exit code {ret}: {command}") return self.commands def cleanup(self): diff --git a/hatch_cpp/toolchains/common.py b/hatch_cpp/toolchains/common.py index 4a42b27..133728a 100644 --- a/hatch_cpp/toolchains/common.py +++ b/hatch_cpp/toolchains/common.py @@ -323,7 +323,9 @@ def get_compile_flags(self, library: HatchCppLibrary, build_type: BuildType = "r flags += " " + " ".join(f"/U{macro}" for macro in effective_undef_macros) flags += " /EHsc /DWIN32" if library.std: - flags += f" /std:{library.std}" + # MSVC minimum is c++14; clamp older standards + std = library.std if library.std not in ("c++11", "c++0x") else "c++14" + flags += f" /std:{std}" # clean while flags.count(" "): flags = flags.replace(" ", " ") From 46431f8e30067e004042a4cf55b1795b2e2152a5 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Wed, 25 Feb 2026 00:03:12 -0500 Subject: [PATCH 39/58] =?UTF-8?q?Bump=20version:=200.3.3=20=E2=86=92=200.3?= =?UTF-8?q?.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com> --- hatch_cpp/__init__.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hatch_cpp/__init__.py b/hatch_cpp/__init__.py index 54d0b37..3562226 100644 --- a/hatch_cpp/__init__.py +++ b/hatch_cpp/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.3.3" +__version__ = "0.3.4" from .config import * from .hooks import * diff --git a/pyproject.toml b/pyproject.toml index bc6f119..c686d2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ authors = [ description = "Hatch plugin for C++ builds" readme = "README.md" license = { text = "Apache-2.0" } -version = "0.3.3" +version = "0.3.4" requires-python = ">=3.10" keywords = [ "hatch", @@ -80,7 +80,7 @@ Repository = "https://github.com/python-project-templates/hatch-cpp" Homepage = "https://github.com/python-project-templates/hatch-cpp" [tool.bumpversion] -current_version = "0.3.3" +current_version = "0.3.4" commit = true tag = true commit_args = "-s" From b4882a26b75ae1cd95e2db06043f7a189a1ded3b Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Thu, 26 Feb 2026 12:39:09 -0500 Subject: [PATCH 40/58] Enable specific vcpkg hash checkout --- hatch_cpp/tests/test_vcpkg_ref.py | 174 ++++++++++++++++++++++++++++++ hatch_cpp/toolchains/vcpkg.py | 41 ++++++- 2 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 hatch_cpp/tests/test_vcpkg_ref.py diff --git a/hatch_cpp/tests/test_vcpkg_ref.py b/hatch_cpp/tests/test_vcpkg_ref.py new file mode 100644 index 0000000..02d862e --- /dev/null +++ b/hatch_cpp/tests/test_vcpkg_ref.py @@ -0,0 +1,174 @@ +"""Tests for vcpkg ref/branch checkout support.""" + +from __future__ import annotations + +from pathlib import Path + +from hatch_cpp.toolchains.vcpkg import ( + HatchCppVcpkgConfiguration, + _read_vcpkg_ref_from_gitmodules, +) + + +class TestReadVcpkgRefFromGitmodules: + """Tests for the _read_vcpkg_ref_from_gitmodules helper.""" + + def test_no_gitmodules_file(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + assert _read_vcpkg_ref_from_gitmodules(Path("vcpkg")) is None + + def test_gitmodules_without_vcpkg_submodule(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + (tmp_path / ".gitmodules").write_text('[submodule "other"]\n\tpath = other\n\turl = https://github.com/example/other.git\n') + assert _read_vcpkg_ref_from_gitmodules(Path("vcpkg")) is None + + def test_gitmodules_vcpkg_without_branch(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + (tmp_path / ".gitmodules").write_text('[submodule "vcpkg"]\n\tpath = vcpkg\n\turl = https://github.com/microsoft/vcpkg.git\n') + assert _read_vcpkg_ref_from_gitmodules(Path("vcpkg")) is None + + def test_gitmodules_vcpkg_with_branch(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + (tmp_path / ".gitmodules").write_text( + '[submodule "vcpkg"]\n\tpath = vcpkg\n\turl = https://github.com/microsoft/vcpkg.git\n\tbranch = 2024.01.12\n' + ) + assert _read_vcpkg_ref_from_gitmodules(Path("vcpkg")) == "2024.01.12" + + def test_gitmodules_vcpkg_with_commit_sha_branch(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + sha = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2" + (tmp_path / ".gitmodules").write_text( + f'[submodule "vcpkg"]\n\tpath = vcpkg\n\turl = https://github.com/microsoft/vcpkg.git\n\tbranch = {sha}\n' + ) + assert _read_vcpkg_ref_from_gitmodules(Path("vcpkg")) == sha + + def test_gitmodules_custom_vcpkg_root(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + (tmp_path / ".gitmodules").write_text( + '[submodule "deps/vcpkg"]\n\tpath = deps/vcpkg\n\turl = https://github.com/microsoft/vcpkg.git\n\tbranch = 2024.06.15\n' + ) + # Default vcpkg root won't match + assert _read_vcpkg_ref_from_gitmodules(Path("vcpkg")) is None + # Custom root matches + assert _read_vcpkg_ref_from_gitmodules(Path("deps/vcpkg")) == "2024.06.15" + + def test_gitmodules_multiple_submodules(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + (tmp_path / ".gitmodules").write_text( + '[submodule "other"]\n' + "\tpath = other\n" + "\turl = https://github.com/example/other.git\n" + "\tbranch = main\n" + '[submodule "vcpkg"]\n' + "\tpath = vcpkg\n" + "\turl = https://github.com/microsoft/vcpkg.git\n" + "\tbranch = 2024.01.12\n" + ) + assert _read_vcpkg_ref_from_gitmodules(Path("vcpkg")) == "2024.01.12" + + +class TestVcpkgRefConfig: + """Tests for vcpkg_ref configuration field.""" + + def test_default_vcpkg_ref_is_none(self): + cfg = HatchCppVcpkgConfiguration() + assert cfg.vcpkg_ref is None + + def test_explicit_vcpkg_ref(self): + cfg = HatchCppVcpkgConfiguration(vcpkg_ref="2024.01.12") + assert cfg.vcpkg_ref == "2024.01.12" + + def test_explicit_vcpkg_ref_commit_sha(self): + sha = "a1b2c3d4e5f6" + cfg = HatchCppVcpkgConfiguration(vcpkg_ref=sha) + assert cfg.vcpkg_ref == sha + + +class TestResolveVcpkgRef: + """Tests for _resolve_vcpkg_ref priority logic.""" + + def test_explicit_ref_takes_priority_over_gitmodules(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + (tmp_path / ".gitmodules").write_text( + '[submodule "vcpkg"]\n\tpath = vcpkg\n\turl = https://github.com/microsoft/vcpkg.git\n\tbranch = 2024.01.12\n' + ) + cfg = HatchCppVcpkgConfiguration(vcpkg_ref="my-custom-tag") + assert cfg._resolve_vcpkg_ref() == "my-custom-tag" + + def test_falls_back_to_gitmodules(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + (tmp_path / ".gitmodules").write_text( + '[submodule "vcpkg"]\n\tpath = vcpkg\n\turl = https://github.com/microsoft/vcpkg.git\n\tbranch = 2024.01.12\n' + ) + cfg = HatchCppVcpkgConfiguration() + assert cfg._resolve_vcpkg_ref() == "2024.01.12" + + def test_returns_none_when_no_ref(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + cfg = HatchCppVcpkgConfiguration() + assert cfg._resolve_vcpkg_ref() is None + + +class TestVcpkgGenerate: + """Tests that generate() includes the checkout command when a ref is set.""" + + def _make_vcpkg_env(self, tmp_path): + """Create a minimal vcpkg.json so generate() produces commands.""" + (tmp_path / "vcpkg.json").write_text("{}") + + def test_generate_with_explicit_ref(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + self._make_vcpkg_env(tmp_path) + + cfg = HatchCppVcpkgConfiguration(vcpkg_ref="2024.01.12") + commands = cfg.generate(None) + + assert any("git clone" in cmd for cmd in commands) + assert any("git -C vcpkg checkout 2024.01.12" in cmd for cmd in commands) + # checkout must come after clone but before bootstrap + clone_idx = next(i for i, c in enumerate(commands) if "git clone" in c) + checkout_idx = next(i for i, c in enumerate(commands) if "checkout" in c) + bootstrap_idx = next(i for i, c in enumerate(commands) if "bootstrap" in c) + assert clone_idx < checkout_idx < bootstrap_idx + + def test_generate_with_gitmodules_ref(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + self._make_vcpkg_env(tmp_path) + (tmp_path / ".gitmodules").write_text( + '[submodule "vcpkg"]\n\tpath = vcpkg\n\turl = https://github.com/microsoft/vcpkg.git\n\tbranch = 2024.06.15\n' + ) + + cfg = HatchCppVcpkgConfiguration() + commands = cfg.generate(None) + + assert any("git -C vcpkg checkout 2024.06.15" in cmd for cmd in commands) + + def test_generate_without_ref(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + self._make_vcpkg_env(tmp_path) + + cfg = HatchCppVcpkgConfiguration() + commands = cfg.generate(None) + + assert not any("checkout" in cmd for cmd in commands) + assert any("git clone" in cmd for cmd in commands) + + def test_generate_skips_clone_when_vcpkg_root_exists(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + self._make_vcpkg_env(tmp_path) + (tmp_path / "vcpkg").mkdir() + + cfg = HatchCppVcpkgConfiguration(vcpkg_ref="2024.01.12") + commands = cfg.generate(None) + + # When vcpkg_root already exists, no clone or checkout happens + assert not any("git clone" in cmd for cmd in commands) + assert not any("checkout" in cmd for cmd in commands) + assert any("vcpkg" in cmd and "install" in cmd for cmd in commands) + + def test_generate_no_vcpkg_json(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + # No vcpkg.json => no commands at all + cfg = HatchCppVcpkgConfiguration(vcpkg_ref="2024.01.12") + commands = cfg.generate(None) + assert commands == [] diff --git a/hatch_cpp/toolchains/vcpkg.py b/hatch_cpp/toolchains/vcpkg.py index db8d2f0..a93be72 100644 --- a/hatch_cpp/toolchains/vcpkg.py +++ b/hatch_cpp/toolchains/vcpkg.py @@ -1,5 +1,6 @@ from __future__ import annotations +import configparser from pathlib import Path from platform import machine as platform_machine from sys import platform as sys_platform @@ -38,14 +39,45 @@ } +def _read_vcpkg_ref_from_gitmodules(vcpkg_root: Path) -> Optional[str]: + """Read the branch/ref for vcpkg from .gitmodules if it exists. + + Looks for a submodule whose path matches ``vcpkg_root`` and returns + its ``branch`` value when present. + """ + gitmodules_path = Path(".gitmodules") + if not gitmodules_path.exists(): + return None + + parser = configparser.ConfigParser() + parser.read(str(gitmodules_path)) + + for section in parser.sections(): + if parser.get(section, "path", fallback=None) == str(vcpkg_root): + return parser.get(section, "branch", fallback=None) + + return None + + class HatchCppVcpkgConfiguration(BaseModel): vcpkg: Optional[str] = Field(default="vcpkg.json") vcpkg_root: Optional[Path] = Field(default=Path("vcpkg")) vcpkg_repo: Optional[str] = Field(default="https://github.com/microsoft/vcpkg.git") vcpkg_triplet: Optional[VcpkgTriplet] = Field(default=None) + vcpkg_ref: Optional[str] = Field( + default=None, + description="Branch, tag, or commit SHA to checkout after cloning vcpkg. " + "If not set, falls back to the branch specified in .gitmodules for the vcpkg submodule.", + ) # TODO: overlay + def _resolve_vcpkg_ref(self) -> Optional[str]: + """Return the ref to checkout: explicit config takes priority, then .gitmodules.""" + if self.vcpkg_ref is not None: + return self.vcpkg_ref + return _read_vcpkg_ref_from_gitmodules(self.vcpkg_root) + def generate(self, config): commands = [] @@ -57,9 +89,12 @@ def generate(self, config): if self.vcpkg and Path(self.vcpkg).exists(): if not Path(self.vcpkg_root).exists(): commands.append(f"git clone {self.vcpkg_repo} {self.vcpkg_root}") - commands.append( - f"./{self.vcpkg_root / 'bootstrap-vcpkg.sh' if sys_platform != 'win32' else self.vcpkg_root / 'sbootstrap-vcpkg.bat'}" - ) + + ref = self._resolve_vcpkg_ref() + if ref is not None: + commands.append(f"git -C {self.vcpkg_root} checkout {ref}") + + commands.append(f"./{self.vcpkg_root / 'bootstrap-vcpkg.sh' if sys_platform != 'win32' else self.vcpkg_root / 'bootstrap-vcpkg.bat'}") commands.append(f"./{self.vcpkg_root / 'vcpkg'} install --triplet {self.vcpkg_triplet}") return commands From 01b55aadf7d4f1f6c2e950e25ed4bfb9d5d5cf55 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Thu, 26 Feb 2026 23:49:33 -0500 Subject: [PATCH 41/58] =?UTF-8?q?Bump=20version:=200.3.4=20=E2=86=92=200.3?= =?UTF-8?q?.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com> --- hatch_cpp/__init__.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hatch_cpp/__init__.py b/hatch_cpp/__init__.py index 3562226..d8f7710 100644 --- a/hatch_cpp/__init__.py +++ b/hatch_cpp/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.3.4" +__version__ = "0.3.5" from .config import * from .hooks import * diff --git a/pyproject.toml b/pyproject.toml index c686d2f..0665a19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ authors = [ description = "Hatch plugin for C++ builds" readme = "README.md" license = { text = "Apache-2.0" } -version = "0.3.4" +version = "0.3.5" requires-python = ">=3.10" keywords = [ "hatch", @@ -80,7 +80,7 @@ Repository = "https://github.com/python-project-templates/hatch-cpp" Homepage = "https://github.com/python-project-templates/hatch-cpp" [tool.bumpversion] -current_version = "0.3.4" +current_version = "0.3.5" commit = true tag = true commit_args = "-s" From f45e3a6ef7f580b76e2931eeddc51a7217eb6dc1 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 05:49:34 +0000 Subject: [PATCH 42/58] Update from copier (2026-03-01T05:49:34) Signed-off-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .copier-answers.yaml | 2 +- .github/workflows/build.yaml | 4 ++-- pyproject.toml | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.copier-answers.yaml b/.copier-answers.yaml index a4cd5cd..0f85d64 100644 --- a/.copier-answers.yaml +++ b/.copier-answers.yaml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 08a244d +_commit: 4d4d95a _src_path: https://github.com/python-project-templates/base.git add_docs: false add_extension: python diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 4ada2ac..7899eb5 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -55,7 +55,7 @@ jobs: run: make coverage - name: Upload test results (Python) - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: test-results-${{ matrix.os }}-${{ matrix.python-version }} path: junit.xml @@ -73,7 +73,7 @@ jobs: - name: Make dist run: make dist - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 with: name: dist-${{matrix.os}} path: dist diff --git a/pyproject.toml b/pyproject.toml index 0665a19..a128b16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,6 +100,7 @@ branch = false omit = [ "hatch_cpp/tests/integration/", ] + [tool.coverage.report] exclude_also = [ "raise NotImplementedError", From a6f2e67d60c3c62c4c3cf1d35524733149289dca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 07:54:39 +0000 Subject: [PATCH 43/58] Update nanobind requirement from <2.12.0 to <2.13.0 Updates the requirements on [nanobind](https://github.com/wjakob/nanobind) to permit the latest version. - [Changelog](https://github.com/wjakob/nanobind/blob/master/docs/changelog.rst) - [Commits](https://github.com/wjakob/nanobind/compare/v0.0.1...v2.12.0) --- updated-dependencies: - dependency-name: nanobind dependency-version: 2.12.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0665a19..2b51723 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ develop = [ "uv", "wheel", # test - "nanobind<2.12.0", # https://github.com/wjakob/nanobind/commit/abd27e3b5565bc95f5091321f0f863fce8b5b95b + "nanobind<2.13.0", # https://github.com/wjakob/nanobind/commit/abd27e3b5565bc95f5091321f0f863fce8b5b95b "pybind11", "pytest", "pytest-cov", 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 44/58] 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}") From 1b1c06c7e655d82469e1d9d9a075ea1ae26311d8 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:23:31 -0500 Subject: [PATCH 45/58] Fix and normalize loader args, fixes #80 --- hatch_cpp/tests/test_platform_specific.py | 2 +- hatch_cpp/tests/test_structs.py | 70 +++++++++++++++++++++++ hatch_cpp/toolchains/common.py | 26 +++++++++ 3 files changed, 97 insertions(+), 1 deletion(-) diff --git a/hatch_cpp/tests/test_platform_specific.py b/hatch_cpp/tests/test_platform_specific.py index 34778bd..455d7e1 100644 --- a/hatch_cpp/tests/test_platform_specific.py +++ b/hatch_cpp/tests/test_platform_specific.py @@ -345,7 +345,7 @@ def test_link_flags_include_platform_specific_link_args(self): flags = platform.get_link_flags(library) assert "-shared" in flags - assert "-Wl,-rpath,$ORIGIN/lib" in flags + assert r"-Wl,-rpath,\$ORIGIN/lib" in flags def test_darwin_platform_uses_darwin_specific_fields(self): """Test that darwin platform uses darwin-specific fields.""" diff --git a/hatch_cpp/tests/test_structs.py b/hatch_cpp/tests/test_structs.py index afa0c25..fa22880 100644 --- a/hatch_cpp/tests/test_structs.py +++ b/hatch_cpp/tests/test_structs.py @@ -8,6 +8,7 @@ from toml import loads from hatch_cpp import HatchCppBuildConfig, HatchCppBuildPlan, HatchCppLibrary, HatchCppPlatform +from hatch_cpp.toolchains.common import _normalize_rpath class TestStructs: @@ -168,3 +169,72 @@ def test_hatch_cpp_vcpkg_env_force_on(self): with patch.dict(environ, {"HATCH_CPP_VCPKG": "1"}): hatch_build_plan.generate() assert "vcpkg" in hatch_build_plan._active_toolchains + + +class TestNormalizeRpath: + def test_origin_to_loader_path_on_darwin(self): + """$ORIGIN should be translated to @loader_path on macOS.""" + assert _normalize_rpath("-Wl,-rpath,$ORIGIN", "darwin") == "-Wl,-rpath,@loader_path" + + def test_loader_path_to_origin_on_linux(self): + """@loader_path should be translated to (escaped) $ORIGIN on Linux.""" + result = _normalize_rpath("-Wl,-rpath,@loader_path", "linux") + assert result == r"-Wl,-rpath,\$ORIGIN" + + def test_origin_escaped_on_linux(self): + """$ORIGIN should be escaped as \\$ORIGIN on Linux for shell safety.""" + result = _normalize_rpath("-Wl,-rpath,$ORIGIN", "linux") + assert result == r"-Wl,-rpath,\$ORIGIN" + + def test_already_escaped_origin_on_darwin(self): + """Already-escaped \\$ORIGIN should still translate to @loader_path on macOS.""" + assert _normalize_rpath(r"-Wl,-rpath,\$ORIGIN", "darwin") == "-Wl,-rpath,@loader_path" + + def test_no_rpath_unchanged(self): + """Args without rpath values should pass through unchanged.""" + assert _normalize_rpath("-lfoo", "linux") == "-lfoo" + assert _normalize_rpath("-lfoo", "darwin") == "-lfoo" + + def test_win32_no_transform(self): + """Windows should not transform rpath values.""" + assert _normalize_rpath("$ORIGIN", "win32") == "$ORIGIN" + assert _normalize_rpath("@loader_path", "win32") == "@loader_path" + + def test_link_flags_rpath_translation_darwin(self): + """Full integration: extra_link_args with $ORIGIN produce @loader_path on macOS.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + binding="generic", + extra_link_args=["-Wl,-rpath,$ORIGIN"], + ) + platform = HatchCppPlatform( + cc="clang", + cxx="clang++", + ld="ld", + platform="darwin", + toolchain="clang", + disable_ccache=True, + ) + flags = platform.get_link_flags(library) + assert "@loader_path" in flags + assert "$ORIGIN" not in flags + + def test_link_flags_rpath_escaped_linux(self): + """Full integration: extra_link_args with $ORIGIN are shell-escaped on Linux.""" + library = HatchCppLibrary( + name="test", + sources=["test.cpp"], + binding="generic", + extra_link_args=["-Wl,-rpath,$ORIGIN"], + ) + platform = HatchCppPlatform( + cc="gcc", + cxx="g++", + ld="ld", + platform="linux", + toolchain="gcc", + disable_ccache=True, + ) + flags = platform.get_link_flags(library) + assert r"\$ORIGIN" in flags diff --git a/hatch_cpp/toolchains/common.py b/hatch_cpp/toolchains/common.py index 120ed4a..300f4e8 100644 --- a/hatch_cpp/toolchains/common.py +++ b/hatch_cpp/toolchains/common.py @@ -20,6 +20,7 @@ "PlatformDefaults", "HatchCppLibrary", "HatchCppPlatform", + "_normalize_rpath", ) @@ -207,6 +208,28 @@ def get_effective_undef_macros(self, platform: Platform) -> List[str]: return macros +def _normalize_rpath(value: str, platform: Platform) -> str: + r"""Translate and escape rpath values for the target platform. + + - On macOS (darwin): ``$ORIGIN`` is replaced with ``@loader_path``. + - On Linux: ``@loader_path`` is replaced with ``$ORIGIN``, and + ``$ORIGIN`` is escaped as ``\$ORIGIN`` so that ``os.system()`` + (which invokes a shell) passes it through literally. + - On Windows: no transformation is applied (Windows does not use + rpath). + """ + if platform == "darwin": + # Handle already-escaped \$ORIGIN first, then plain $ORIGIN + value = value.replace(r"\$ORIGIN", "@loader_path") + value = value.replace("$ORIGIN", "@loader_path") + elif platform == "linux": + # Translate macOS rpath to Linux equivalent + value = value.replace("@loader_path", "$ORIGIN") + # Escape $ORIGIN for shell safety (os.system runs through bash) + value = value.replace("$ORIGIN", r"\$ORIGIN") + return value + + class HatchCppPlatform(BaseModel): cc: str cxx: str @@ -336,6 +359,9 @@ def get_link_flags(self, library: HatchCppLibrary, build_type: BuildType = "rele effective_libraries = library.get_effective_libraries(self.platform) effective_library_dirs = library.get_effective_library_dirs(self.platform) + # Normalize rpath values ($ORIGIN <-> @loader_path) and escape for shell + effective_link_args = [_normalize_rpath(arg, self.platform) for arg in effective_link_args] + if self.toolchain == "gcc": flags += " -shared" flags += " " + " ".join(effective_link_args) From 29035fe3be4bb635027a23fbb16e1f615017534e Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Mon, 2 Mar 2026 23:38:51 -0500 Subject: [PATCH 46/58] =?UTF-8?q?Bump=20version:=200.3.5=20=E2=86=92=200.3?= =?UTF-8?q?.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com> --- hatch_cpp/__init__.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hatch_cpp/__init__.py b/hatch_cpp/__init__.py index d8f7710..2274de6 100644 --- a/hatch_cpp/__init__.py +++ b/hatch_cpp/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.3.5" +__version__ = "0.3.6" from .config import * from .hooks import * diff --git a/pyproject.toml b/pyproject.toml index af8fa15..52c054e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ authors = [ description = "Hatch plugin for C++ builds" readme = "README.md" license = { text = "Apache-2.0" } -version = "0.3.5" +version = "0.3.6" requires-python = ">=3.10" keywords = [ "hatch", @@ -80,7 +80,7 @@ Repository = "https://github.com/python-project-templates/hatch-cpp" Homepage = "https://github.com/python-project-templates/hatch-cpp" [tool.bumpversion] -current_version = "0.3.5" +current_version = "0.3.6" commit = true tag = true commit_args = "-s" From 19fdb0c1d8a1f5a2f738a50be7384ff2844bb3d6 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 05:40:44 +0000 Subject: [PATCH 47/58] Update from copier (2026-03-08T05:40:44) Signed-off-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .copier-answers.yaml | 2 +- .gitignore | 28 +++++++--------------------- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/.copier-answers.yaml b/.copier-answers.yaml index 0f85d64..39b4d5e 100644 --- a/.copier-answers.yaml +++ b/.copier-answers.yaml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 4d4d95a +_commit: 3160d4c _src_path: https://github.com/python-project-templates/base.git add_docs: false add_extension: python diff --git a/.gitignore b/.gitignore index 2bddce6..e4ed094 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ __pycache__/ *.exp *.lib +# Rust +target + # Distribution / packaging .Python build/ @@ -55,26 +58,12 @@ junit.xml .hypothesis/ .pytest_cache/ -# Translations -*.mo -*.pot - -# Django stuff: +# Django *.log local_settings.py db.sqlite3 db.sqlite3-journal -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# PyBuilder -target/ - # IPython profile_default/ ipython_config.py @@ -85,15 +74,12 @@ ipython_config.py # pipenv Pipfile.lock -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff +# Celery celerybeat-schedule celerybeat.pid -# SageMath parsed files -*.sage.py +# Airspeed Velocity +.asv # Environments .env From 22995edd69f12d20bab224223e109e049be008af Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 22 Mar 2026 05:51:23 +0000 Subject: [PATCH 48/58] Update from copier (2026-03-22T05:51:23) Signed-off-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .copier-answers.yaml | 2 +- .gitignore | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.copier-answers.yaml b/.copier-answers.yaml index 39b4d5e..63bcdad 100644 --- a/.copier-answers.yaml +++ b/.copier-answers.yaml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 3160d4c +_commit: 9be318c _src_path: https://github.com/python-project-templates/base.git add_docs: false add_extension: python diff --git a/.gitignore b/.gitignore index e4ed094..062343d 100644 --- a/.gitignore +++ b/.gitignore @@ -146,5 +146,12 @@ target outputs/ multirun/ +<<<<<<< before updating vcpkg vcpkg_installed +======= +# AI +ROADMAP.md +AGENTS.md +.github/hooks/sdlc.json +>>>>>>> after updating From 95ae86b60072686df3d79f219d14ee969c902b32 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Sun, 22 Mar 2026 13:07:41 -0400 Subject: [PATCH 49/58] Update .gitignore --- .gitignore | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 062343d..72e3e8d 100644 --- a/.gitignore +++ b/.gitignore @@ -146,12 +146,10 @@ target outputs/ multirun/ -<<<<<<< before updating -vcpkg -vcpkg_installed -======= # AI ROADMAP.md AGENTS.md .github/hooks/sdlc.json ->>>>>>> after updating + +vcpkg +vcpkg_installed From 732ec84bb18c60dc80462e20f5b63e802fb61bc3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 07:53:30 +0000 Subject: [PATCH 50/58] Bump codecov/codecov-action from 5 to 6 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5 to 6. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v5...v6) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 7899eb5..1110e6f 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -66,7 +66,7 @@ jobs: files: '**/junit.xml' - name: Upload coverage - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} From 99a6db4ea0ad94f2a7eafb43a6f7bad90b87767a Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Wed, 1 Apr 2026 12:21:09 -0400 Subject: [PATCH 51/58] Be more durable to partial vcpkg checkouts or vcpkg submodules --- hatch_cpp/tests/test_vcpkg_ref.py | 56 +++++++++++++++++++++++++++-- hatch_cpp/toolchains/vcpkg.py | 58 +++++++++++++++++++++++++++---- 2 files changed, 105 insertions(+), 9 deletions(-) diff --git a/hatch_cpp/tests/test_vcpkg_ref.py b/hatch_cpp/tests/test_vcpkg_ref.py index 02d862e..f300f31 100644 --- a/hatch_cpp/tests/test_vcpkg_ref.py +++ b/hatch_cpp/tests/test_vcpkg_ref.py @@ -156,14 +156,66 @@ def test_generate_without_ref(self, tmp_path, monkeypatch): def test_generate_skips_clone_when_vcpkg_root_exists(self, tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) self._make_vcpkg_env(tmp_path) - (tmp_path / "vcpkg").mkdir() + vcpkg_root = tmp_path / "vcpkg" + vcpkg_root.mkdir() + # Existing bootstrap script and executable mean no clone/bootstrap is needed. + (vcpkg_root / "bootstrap-vcpkg.sh").write_text("#!/bin/sh\nexit 0\n") + (vcpkg_root / "vcpkg").write_text("#!/bin/sh\nexit 0\n") cfg = HatchCppVcpkgConfiguration(vcpkg_ref="2024.01.12") + monkeypatch.setattr(cfg, "_is_vcpkg_working", lambda: True) commands = cfg.generate(None) - # When vcpkg_root already exists, no clone or checkout happens + # When vcpkg_root already exists with a working executable, no clone or bootstrap happens. assert not any("git clone" in cmd for cmd in commands) assert not any("checkout" in cmd for cmd in commands) + assert not any("bootstrap-vcpkg" in cmd for cmd in commands) + assert any("vcpkg" in cmd and "install" in cmd for cmd in commands) + + def test_generate_reclones_when_vcpkg_root_exists_but_empty(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + self._make_vcpkg_env(tmp_path) + (tmp_path / "vcpkg").mkdir() + + cfg = HatchCppVcpkgConfiguration(vcpkg_ref="2024.01.12") + commands = cfg.generate(None) + + assert any(cmd.startswith('rm -rf "vcpkg"') for cmd in commands) + assert any("git clone" in cmd for cmd in commands) + assert any("checkout 2024.01.12" in cmd for cmd in commands) + assert any("bootstrap-vcpkg" in cmd for cmd in commands) + assert any("vcpkg" in cmd and "install" in cmd for cmd in commands) + + def test_generate_bootstraps_when_vcpkg_executable_missing(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + self._make_vcpkg_env(tmp_path) + vcpkg_root = tmp_path / "vcpkg" + vcpkg_root.mkdir() + (vcpkg_root / "bootstrap-vcpkg.sh").write_text("#!/bin/sh\nexit 0\n") + + cfg = HatchCppVcpkgConfiguration(vcpkg_ref="2024.01.12") + commands = cfg.generate(None) + + assert not any("git clone" in cmd for cmd in commands) + assert any("bootstrap-vcpkg" in cmd for cmd in commands) + assert any("vcpkg" in cmd and "install" in cmd for cmd in commands) + + def test_generate_reclones_when_vcpkg_exists_but_not_working(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + self._make_vcpkg_env(tmp_path) + vcpkg_root = tmp_path / "vcpkg" + vcpkg_root.mkdir() + (vcpkg_root / "bootstrap-vcpkg.sh").write_text("#!/bin/sh\nexit 0\n") + (vcpkg_root / "vcpkg").write_text("#!/bin/sh\nexit 1\n") + + cfg = HatchCppVcpkgConfiguration(vcpkg_ref="2024.01.12") + monkeypatch.setattr(cfg, "_is_vcpkg_working", lambda: False) + commands = cfg.generate(None) + + assert any(cmd.startswith('rm -rf "vcpkg"') for cmd in commands) + assert any("git clone" in cmd for cmd in commands) + assert any("checkout 2024.01.12" in cmd for cmd in commands) + assert any("bootstrap-vcpkg" in cmd for cmd in commands) assert any("vcpkg" in cmd and "install" in cmd for cmd in commands) def test_generate_no_vcpkg_json(self, tmp_path, monkeypatch): diff --git a/hatch_cpp/toolchains/vcpkg.py b/hatch_cpp/toolchains/vcpkg.py index a93be72..530df98 100644 --- a/hatch_cpp/toolchains/vcpkg.py +++ b/hatch_cpp/toolchains/vcpkg.py @@ -1,6 +1,7 @@ from __future__ import annotations import configparser +import subprocess from pathlib import Path from platform import machine as platform_machine from sys import platform as sys_platform @@ -78,6 +79,39 @@ def _resolve_vcpkg_ref(self) -> Optional[str]: return self.vcpkg_ref return _read_vcpkg_ref_from_gitmodules(self.vcpkg_root) + def _bootstrap_script_path(self) -> Path: + return self.vcpkg_root / ("bootstrap-vcpkg.bat" if sys_platform == "win32" else "bootstrap-vcpkg.sh") + + def _vcpkg_executable_path(self) -> Path: + if sys_platform == "win32": + return self.vcpkg_root / "vcpkg.exe" + return self.vcpkg_root / "vcpkg" + + def _delete_dir_command(self, path: Path) -> str: + if sys_platform == "win32": + return f'rmdir /s /q "{path}"' + return f'rm -rf "{path}"' + + def _is_vcpkg_working(self) -> bool: + vcpkg_executable = self._vcpkg_executable_path() + if not vcpkg_executable.exists(): + return False + try: + result = subprocess.run([str(vcpkg_executable), "version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False) + return result.returncode == 0 + except OSError: + return False + + def _clone_checkout_bootstrap_commands(self) -> list[str]: + commands = [f"git clone {self.vcpkg_repo} {self.vcpkg_root}"] + + ref = self._resolve_vcpkg_ref() + if ref is not None: + commands.append(f"git -C {self.vcpkg_root} checkout {ref}") + + commands.append(f"./{self._bootstrap_script_path()}") + return commands + def generate(self, config): commands = [] @@ -87,14 +121,24 @@ def generate(self, config): raise ValueError(f"Could not determine vcpkg triplet for platform {sys_platform} and architecture {platform_machine()}") if self.vcpkg and Path(self.vcpkg).exists(): - if not Path(self.vcpkg_root).exists(): - commands.append(f"git clone {self.vcpkg_repo} {self.vcpkg_root}") - - ref = self._resolve_vcpkg_ref() - if ref is not None: - commands.append(f"git -C {self.vcpkg_root} checkout {ref}") + vcpkg_root = Path(self.vcpkg_root) + bootstrap_script = self._bootstrap_script_path() + + if not vcpkg_root.exists(): + commands.extend(self._clone_checkout_bootstrap_commands()) + else: + is_empty_dir = vcpkg_root.is_dir() and not any(vcpkg_root.iterdir()) + if is_empty_dir: + commands.append(self._delete_dir_command(vcpkg_root)) + commands.extend(self._clone_checkout_bootstrap_commands()) + else: + vcpkg_executable = self._vcpkg_executable_path() + if not vcpkg_executable.exists(): + commands.append(f"./{bootstrap_script}") + elif not self._is_vcpkg_working(): + commands.append(self._delete_dir_command(vcpkg_root)) + commands.extend(self._clone_checkout_bootstrap_commands()) - commands.append(f"./{self.vcpkg_root / 'bootstrap-vcpkg.sh' if sys_platform != 'win32' else self.vcpkg_root / 'bootstrap-vcpkg.bat'}") commands.append(f"./{self.vcpkg_root / 'vcpkg'} install --triplet {self.vcpkg_triplet}") return commands From 44c264c10beeb6f82580de6c3943500aa9d088e5 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Thu, 2 Apr 2026 18:26:56 -0400 Subject: [PATCH 52/58] =?UTF-8?q?Bump=20version:=200.3.6=20=E2=86=92=200.3?= =?UTF-8?q?.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com> --- hatch_cpp/__init__.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hatch_cpp/__init__.py b/hatch_cpp/__init__.py index 2274de6..804e24a 100644 --- a/hatch_cpp/__init__.py +++ b/hatch_cpp/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.3.6" +__version__ = "0.3.7" from .config import * from .hooks import * diff --git a/pyproject.toml b/pyproject.toml index 52c054e..efe73e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ authors = [ description = "Hatch plugin for C++ builds" readme = "README.md" license = { text = "Apache-2.0" } -version = "0.3.6" +version = "0.3.7" requires-python = ">=3.10" keywords = [ "hatch", @@ -80,7 +80,7 @@ Repository = "https://github.com/python-project-templates/hatch-cpp" Homepage = "https://github.com/python-project-templates/hatch-cpp" [tool.bumpversion] -current_version = "0.3.6" +current_version = "0.3.7" commit = true tag = true commit_args = "-s" From fc5d6f6cb464bc9e447cf7afe5e1d4b8e5703ed2 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 5 Apr 2026 06:13:06 +0000 Subject: [PATCH 53/58] Update from copier (2026-04-05T06:13:06) Signed-off-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .copier-answers.yaml | 2 +- .gitignore | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.copier-answers.yaml b/.copier-answers.yaml index 63bcdad..6acc7d3 100644 --- a/.copier-answers.yaml +++ b/.copier-answers.yaml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 9be318c +_commit: 9b579a3 _src_path: https://github.com/python-project-templates/base.git add_docs: false add_extension: python diff --git a/.gitignore b/.gitignore index 72e3e8d..52465be 100644 --- a/.gitignore +++ b/.gitignore @@ -112,11 +112,12 @@ dmypy.json /site index.md docs/_build/ -docs/src/_build/ docs/api -docs/index.md docs/html +docs/index.md docs/jupyter_execute +docs/src/_build/ +docs/superpowers index.md # JS From 854ea05a2f74e559b03fa20fdac4f943d3076ee3 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 06:22:17 +0000 Subject: [PATCH 54/58] Update from copier (2026-04-12T06:22:17) Signed-off-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .copier-answers.yaml | 2 +- .github/ISSUE_TEMPLATE/bug_report.md | 49 +++++++++-------------- .github/ISSUE_TEMPLATE/feature_request.md | 25 ++++++------ .github/ISSUE_TEMPLATE/question.md | 16 ++++++++ .github/pull_request_template.md | 20 +++++++++ .github/workflows/build.yaml | 12 ++++++ 6 files changed, 81 insertions(+), 43 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/question.md create mode 100644 .github/pull_request_template.md diff --git a/.copier-answers.yaml b/.copier-answers.yaml index 6acc7d3..2e49079 100644 --- a/.copier-answers.yaml +++ b/.copier-answers.yaml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 9b579a3 +_commit: fdea3fe _src_path: https://github.com/python-project-templates/base.git add_docs: false add_extension: python diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index dd84ea7..fab4a9f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,38 +1,29 @@ --- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' +name: Bug Report +about: Report a bug to help us improve +title: '[BUG] ' +labels: bug assignees: '' - --- -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error +**Description** +A clear and concise description of the bug. -**Expected behavior** -A clear and concise description of what you expected to happen. +**Steps to Reproduce** +1. +2. +3. -**Screenshots** -If applicable, add screenshots to help explain your problem. +**Expected Behavior** +What you expected to happen. -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] +**Actual Behavior** +What actually happened. Include full error messages or tracebacks if available. -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] +**Environment** +- OS: [e.g. Ubuntu 22.04, macOS 14.0, Windows 11] +- Python version: [e.g. 3.11.5] (`python --version`) +- Package version: (`pip show hatch-cpp | grep Version`) -**Additional context** -Add any other context about the problem here. +**Additional Context** +Add any other relevant context, logs, or screenshots. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index bbcbbe7..7af06ac 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,20 +1,19 @@ --- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: '' +name: Feature Request +about: Suggest a new feature or improvement +title: '[FEATURE] ' +labels: enhancement assignees: '' - --- -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] +**Problem Statement** +A clear description of the problem this feature would solve. Ex. "I'm always frustrated when [...]" -**Describe the solution you'd like** -A clear and concise description of what you want to happen. +**Proposed Solution** +A clear description of the desired behavior or feature. -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. +**Alternatives Considered** +Any alternative solutions or workarounds you've considered. -**Additional context** -Add any other context or screenshots about the feature request here. +**Additional Context** +Add any other context, mockups, or examples. diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..0e4f9cf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,16 @@ +--- +name: Question +about: Ask a question about usage or behavior +title: '[QUESTION] ' +labels: question +assignees: '' +--- + +**Question** +A clear and concise description of your question. + +**Context** +What are you trying to accomplish? Include relevant code snippets, configuration, or links to documentation you've already consulted. + +**Environment** +If relevant, include your environment details (OS, language versions, package version). diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..35c3de1 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,20 @@ +## Description + +Brief description of the changes in this PR. + +## Type of Change + +- [ ] Bug fix +- [ ] New feature +- [ ] Documentation update +- [ ] Refactor / code cleanup +- [ ] CI / build configuration +- [ ] Other (describe below) + +## Checklist + +- [ ] Linting passes (`make lint`) +- [ ] Tests pass (`make test`) +- [ ] New tests added for new functionality +- [ ] Documentation updated (if applicable) +- [ ] Changelog / version bump (if applicable) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 1110e6f..c587c3b 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -73,6 +73,18 @@ jobs: - name: Make dist run: make dist + - name: Test wheel install + run: | + python -m venv /tmp/test-wheel + /tmp/test-wheel/bin/pip install dist/*.whl + /tmp/test-wheel/bin/python -c "import hatch_cpp" + + - name: Test sdist install + run: | + python -m venv /tmp/test-sdist + /tmp/test-sdist/bin/pip install dist/*.tar.gz + /tmp/test-sdist/bin/python -c "import hatch_cpp" + - uses: actions/upload-artifact@v7 with: name: dist-${{matrix.os}} From b184534479b412bc3854d8001838fb0fcc4d4802 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 19:42:14 +0000 Subject: [PATCH 55/58] Update from copier (2026-04-12T19:42:14) Signed-off-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .copier-answers.yaml | 2 +- .github/workflows/build.yaml | 18 +++++++----------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/.copier-answers.yaml b/.copier-answers.yaml index 2e49079..cbda04e 100644 --- a/.copier-answers.yaml +++ b/.copier-answers.yaml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: fdea3fe +_commit: 41c2f2c _src_path: https://github.com/python-project-templates/base.git add_docs: false add_extension: python diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index c587c3b..eca457f 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -73,17 +73,13 @@ jobs: - name: Make dist run: make dist - - name: Test wheel install - run: | - python -m venv /tmp/test-wheel - /tmp/test-wheel/bin/pip install dist/*.whl - /tmp/test-wheel/bin/python -c "import hatch_cpp" - - - name: Test sdist install - run: | - python -m venv /tmp/test-sdist - /tmp/test-sdist/bin/pip install dist/*.tar.gz - /tmp/test-sdist/bin/python -c "import hatch_cpp" + - uses: actions-ext/python/test-wheel@main + with: + module: hatch_cpp + + - uses: actions-ext/python/test-sdist@main + with: + module: hatch_cpp - uses: actions/upload-artifact@v7 with: From c195a528c7f67337f80fd32865b9a1c146a90389 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 00:01:02 +0000 Subject: [PATCH 56/58] Update from copier (2026-04-13T00:01:02) Signed-off-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .copier-answers.yaml | 2 +- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- .github/ISSUE_TEMPLATE/question.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.copier-answers.yaml b/.copier-answers.yaml index cbda04e..c9d6446 100644 --- a/.copier-answers.yaml +++ b/.copier-answers.yaml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 41c2f2c +_commit: 9232d1f _src_path: https://github.com/python-project-templates/base.git add_docs: false add_extension: python diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index fab4a9f..0027d64 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,7 +2,7 @@ name: Bug Report about: Report a bug to help us improve title: '[BUG] ' -labels: bug +labels: 'type: bug' assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 7af06ac..c3e422e 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -2,7 +2,7 @@ name: Feature Request about: Suggest a new feature or improvement title: '[FEATURE] ' -labels: enhancement +labels: 'type: enhancement' assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 0e4f9cf..ca52f7e 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -2,7 +2,7 @@ name: Question about: Ask a question about usage or behavior title: '[QUESTION] ' -labels: question +labels: 'tag: question' assignees: '' --- From 89f304d922f260c600361c3578ae1fc673ab4061 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 06:26:53 +0000 Subject: [PATCH 57/58] Update from copier (2026-04-19T06:26:53) Signed-off-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .copier-answers.yaml | 2 +- .gitignore | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.copier-answers.yaml b/.copier-answers.yaml index c9d6446..e277451 100644 --- a/.copier-answers.yaml +++ b/.copier-answers.yaml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 9232d1f +_commit: 9a153d8 _src_path: https://github.com/python-project-templates/base.git add_docs: false add_extension: python diff --git a/.gitignore b/.gitignore index 52465be..059c013 100644 --- a/.gitignore +++ b/.gitignore @@ -151,6 +151,10 @@ multirun/ ROADMAP.md AGENTS.md .github/hooks/sdlc.json +<<<<<<< before updating vcpkg vcpkg_installed +======= +.superpowers +>>>>>>> after updating From 2149e66f133c977a1e71df4e8f0264a393c2e309 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Sun, 19 Apr 2026 13:53:09 -0400 Subject: [PATCH 58/58] Update .gitignore --- .gitignore | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 059c013..704ae1b 100644 --- a/.gitignore +++ b/.gitignore @@ -151,10 +151,7 @@ multirun/ ROADMAP.md AGENTS.md .github/hooks/sdlc.json -<<<<<<< before updating +.superpowers vcpkg vcpkg_installed -======= -.superpowers ->>>>>>> after updating