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/.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/__init__.py b/hatch_cpp/__init__.py index 2f17aa3..fe6e262 100644 --- a/hatch_cpp/__init__.py +++ b/hatch_cpp/__init__.py @@ -1,5 +1,6 @@ -__version__ = "0.1.8" +__version__ = "0.1.9" -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..3d7ffe7 --- /dev/null +++ b/hatch_cpp/config.py @@ -0,0 +1,101 @@ +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, Toolchain + +__all__ = ( + "HatchCppBuildConfig", + "HatchCppBuildPlan", +) + + +_log = getLogger(__name__) + + +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_factory=HatchCppVcpkgConfiguration) + + @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") + 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.") + return model + + +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._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}" + ) + + if "cmake" in self._active_toolchains: + 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_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..ace9c19 --- /dev/null +++ b/hatch_cpp/tests/test_project_cmake_vcpkg/vcpkg.json @@ -0,0 +1,8 @@ +{ + "name": "main", + "version-string": "latest", + "dependencies": [ + "nlohmann-json" + ], + "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..ace9c19 --- /dev/null +++ b/hatch_cpp/tests/test_project_pybind_vcpkg/vcpkg.json @@ -0,0 +1,8 @@ +{ + "name": "main", + "version-string": "latest", + "dependencies": [ + "nlohmann-json" + ], + "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): 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..1b349af 100644 --- a/hatch_cpp/toolchains/cmake.py +++ b/hatch_cpp/toolchains/cmake.py @@ -0,0 +1,87 @@ +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",) + +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 + 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}") + + # 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}" + 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("CMAKE_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..5922d64 100644 --- a/hatch_cpp/structs.py +++ b/hatch_cpp/toolchains/common.py @@ -1,24 +1,31 @@ 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", + "Toolchain", + "Language", + "Binding", + "Platform", + "PlatformDefaults", "HatchCppLibrary", "HatchCppPlatform", - "HatchCppBuildPlan", ) + 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"] @@ -231,118 +238,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..c5056dd --- /dev/null +++ b/hatch_cpp/toolchains/vcpkg.py @@ -0,0 +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 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_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_root / 'bootstrap-vcpkg.sh' if sys_platform != 'win32' else self.vcpkg_root / 'sbootstrap-vcpkg.bat'}" + ) + commands.append(f"./{self.vcpkg_root / 'vcpkg'} install --triplet {self.vcpkg_triplet}") + + return commands diff --git a/pyproject.toml b/pyproject.toml index 70d89ac..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", @@ -46,11 +46,11 @@ develop = [ "codespell>=2.4,<2.5", "hatchling", "hatch-build", - "mdformat>=0.7.22,<0.8", + "mdformat>=0.7.22,<1.1", "mdformat-tables>=1", "pytest", "pytest-cov", - "ruff", + "ruff>=0.9,<0.15", "twine", "uv", "wheel", @@ -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"