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] 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