11from __future__ import annotations
22
3- from os import environ , system
3+ from os import environ , system as system_call
44from pathlib import Path
55from re import match
66from shutil import which
7- from sys import executable , platform as sys_platform
7+ from sys import executable , platform as sys_platform , version_info
88from sysconfig import get_path
9- from typing import Any , List , Literal , Optional
9+ from typing import Any , Dict , List , Literal , Optional
1010
1111from pydantic import AliasChoices , BaseModel , Field , field_validator , model_validator
1212
2020BuildType = Literal ["debug" , "release" ]
2121CompilerToolchain = Literal ["gcc" , "clang" , "msvc" ]
2222Language = Literal ["c" , "c++" ]
23- Binding = Literal ["cpython" , "pybind11" , "nanobind" ]
23+ Binding = Literal ["cpython" , "pybind11" , "nanobind" , "generic" ]
2424Platform = Literal ["linux" , "darwin" , "win32" ]
2525PlatformDefaults = {
2626 "linux" : {"CC" : "gcc" , "CXX" : "g++" , "LD" : "ld" },
@@ -65,9 +65,9 @@ def check_py_limited_api(cls, value: Any) -> Any:
6565
6666 def get_qualified_name (self , platform ):
6767 if platform == "win32" :
68- suffix = "dll" if self .binding == "none " else "pyd"
68+ suffix = "dll" if self .binding == "generic " else "pyd"
6969 elif platform == "darwin" :
70- suffix = "dylib" if self .binding == "none " else "so"
70+ suffix = "dylib" if self .binding == "generic " else "so"
7171 else :
7272 suffix = "so"
7373 if self .py_limited_api and platform != "win32" :
@@ -78,6 +78,8 @@ def get_qualified_name(self, platform):
7878 def check_binding_and_py_limited_api (self ):
7979 if self .binding == "pybind11" and self .py_limited_api :
8080 raise ValueError ("pybind11 does not support Py_LIMITED_API" )
81+ if self .binding == "generic" and self .py_limited_api :
82+ raise ValueError ("Generic binding can not support Py_LIMITED_API" )
8183 return self
8284
8385
@@ -119,7 +121,8 @@ def get_compile_flags(self, library: HatchCppLibrary, build_type: BuildType = "r
119121 flags = ""
120122
121123 # Python.h
122- library .include_dirs .append (get_path ("include" ))
124+ if library .binding != "generic" :
125+ library .include_dirs .append (get_path ("include" ))
123126
124127 if library .binding == "pybind11" :
125128 import pybind11
@@ -217,36 +220,100 @@ def get_link_flags(self, library: HatchCppLibrary, build_type: BuildType = "rele
217220 return flags
218221
219222
220- class HatchCppBuildPlan (BaseModel ):
221- build_type : BuildType = "release"
223+ class HatchCppCmakeConfiguration (BaseModel ):
224+ root : Path
225+ build : Path = Field (default_factory = lambda : Path ("build" ))
226+ install : Optional [Path ] = Field (default = None )
227+
228+ cmake_arg_prefix : Optional [str ] = Field (default = None )
229+ cmake_args : Dict [str , str ] = Field (default_factory = dict )
230+ cmake_env_args : Dict [Platform , Dict [str , str ]] = Field (default_factory = dict )
231+
232+ include_flags : Optional [Dict [str , Any ]] = Field (default = None )
233+
234+
235+ class HatchCppBuildConfig (BaseModel ):
236+ """Build config values for Hatch C++ Builder."""
237+
238+ verbose : Optional [bool ] = Field (default = False )
239+ name : Optional [str ] = Field (default = None )
222240 libraries : List [HatchCppLibrary ] = Field (default_factory = list )
223- platform : HatchCppPlatform = Field (default_factory = HatchCppPlatform .default )
241+ cmake : Optional [HatchCppCmakeConfiguration ] = Field (default = None )
242+ platform : Optional [HatchCppPlatform ] = Field (default_factory = HatchCppPlatform .default )
243+
244+ @model_validator (mode = "after" )
245+ def check_toolchain_matches_args (self ):
246+ if self .cmake and self .libraries :
247+ raise ValueError ("Must not provide libraries when using cmake toolchain." )
248+ return self
249+
250+
251+ class HatchCppBuildPlan (HatchCppBuildConfig ):
252+ build_type : BuildType = "release"
224253 commands : List [str ] = Field (default_factory = list )
225254
226255 def generate (self ):
227256 self .commands = []
228- for library in self .libraries :
229- compile_flags = self .platform .get_compile_flags (library , self .build_type )
230- link_flags = self .platform .get_link_flags (library , self .build_type )
231- self .commands .append (
232- f"{ self .platform .cc if library .language == 'c' else self .platform .cxx } { ' ' .join (library .sources )} { compile_flags } { link_flags } "
233- )
257+ if self .libraries :
258+ for library in self .libraries :
259+ compile_flags = self .platform .get_compile_flags (library , self .build_type )
260+ link_flags = self .platform .get_link_flags (library , self .build_type )
261+ self .commands .append (
262+ f"{ self .platform .cc if library .language == 'c' else self .platform .cxx } { ' ' .join (library .sources )} { compile_flags } { link_flags } "
263+ )
264+ elif self .cmake :
265+ # Derive prefix
266+ if self .cmake .cmake_arg_prefix is None :
267+ self .cmake .cmake_arg_prefix = f"{ self .name .replace ('.' , '_' ).replace ('-' , '_' ).upper ()} _"
268+
269+ # Append base command
270+ self .commands .append (f"cmake { Path (self .cmake .root ).parent } -DCMAKE_BUILD_TYPE={ self .build_type } -B { self .cmake .build } " )
271+
272+ # Setup install path
273+ if self .cmake .install :
274+ self .commands [- 1 ] += f" -DCMAKE_INSTALL_PREFIX={ self .cmake .install } "
275+ else :
276+ self .commands [- 1 ] += f" -DCMAKE_INSTALL_PREFIX={ Path (self .cmake .root ).parent } "
277+
278+ # TODO: CMAKE_CXX_COMPILER
279+ if self .platform .platform == "win32" :
280+ # TODO: prefix?
281+ self .commands [- 1 ] += f' -G "{ environ .get ("GENERATOR" , "Visual Studio 17 2022" )} "'
282+
283+ # Put in CMake flags
284+ args = self .cmake .cmake_args .copy ()
285+ for platform , env_args in self .cmake .cmake_env_args .items ():
286+ if platform == self .platform .platform :
287+ for key , value in env_args .items ():
288+ args [key ] = value
289+ for key , value in args .items ():
290+ self .commands [- 1 ] += f" -D{ self .cmake .cmake_arg_prefix } { key .upper ()} ={ value } "
291+
292+ # Include customs
293+ if self .cmake .include_flags :
294+ if self .cmake .include_flags .get ("python_version" , False ):
295+ self .commands [- 1 ] += f" -D{ self .cmake .cmake_arg_prefix } PYTHON_VERSION={ version_info .major } .{ version_info .minor } "
296+ if self .cmake .include_flags .get ("manylinux" , False ) and self .platform .platform == "linux" :
297+ self .commands [- 1 ] += f" -D{ self .cmake .cmake_arg_prefix } MANYLINUX=ON"
298+
299+ # Include mac deployment target
300+ if self .platform .platform == "darwin" :
301+ self .commands [- 1 ] += f" -DCMAKE_OSX_DEPLOYMENT_TARGET={ environ .get ('OSX_DEPLOYMENT_TARGET' , '11' )} "
302+
303+ # Append build command
304+ self .commands .append (f"cmake --build { self .cmake .build } --config { self .build_type } " )
305+
306+ # Append install command
307+ self .commands .append (f"cmake --install { self .cmake .build } --config { self .build_type } " )
308+
234309 return self .commands
235310
236311 def execute (self ):
237312 for command in self .commands :
238- system (command )
313+ system_call (command )
239314 return self .commands
240315
241316 def cleanup (self ):
242317 if self .platform .platform == "win32" :
243318 for temp_obj in Path ("." ).glob ("*.obj" ):
244319 temp_obj .unlink ()
245-
246-
247- class HatchCppBuildConfig (BaseModel ):
248- """Build config values for Hatch C++ Builder."""
249-
250- verbose : Optional [bool ] = Field (default = False )
251- libraries : List [HatchCppLibrary ] = Field (default_factory = list )
252- platform : Optional [HatchCppPlatform ] = Field (default_factory = HatchCppPlatform .default )
0 commit comments