1+ import contextlib
12import os
2- import posixpath
33import platform
4- import re
54import shutil
6- import sys
5+ import sysconfig
6+ from pathlib import Path
7+ from typing import List
78
8- from distutils import sysconfig
99import setuptools
1010from setuptools .command import build_ext
1111
1212
13- HERE = os . path . dirname ( os . path . abspath ( __file__ ))
13+ PYTHON_INCLUDE_PATH_PLACEHOLDER = "<PYTHON_INCLUDE_PATH>"
1414
15+ IS_WINDOWS = platform .system () == "Windows"
16+ IS_MAC = platform .system () == "Darwin"
1517
16- IS_WINDOWS = sys .platform .startswith ("win" )
1718
19+ def _get_long_description (fp : str ) -> str :
20+ with open (fp , "r" , encoding = "utf-8" ) as f :
21+ return f .read ()
1822
19- with open ("README.md" , "r" , encoding = "utf-8" ) as fp :
20- long_description = fp .read ()
2123
22-
23- def _get_version ():
24- """Parse the version string from __init__.py."""
25- with open (
26- os .path .join (HERE , "bindings" , "python" , "google_benchmark" , "__init__.py" )
27- ) as init_file :
28- try :
29- version_line = next (
30- line for line in init_file if line .startswith ("__version__" )
31- )
32- except StopIteration :
33- raise ValueError ("__version__ not defined in __init__.py" )
34- else :
35- namespace = {}
36- exec (version_line , namespace ) # pylint: disable=exec-used
37- return namespace ["__version__" ]
24+ def _get_version (fp : str ) -> str :
25+ """Parse a version string from a file."""
26+ with open (fp , "r" ) as f :
27+ for line in f :
28+ if "__version__" in line :
29+ delim = '"'
30+ return line .split (delim )[1 ]
31+ raise RuntimeError (f"could not find a version string in file { fp !r} ." )
3832
3933
40- def _parse_requirements (path ) :
41- with open (os . path . join ( HERE , path ) ) as requirements :
34+ def _parse_requirements (fp : str ) -> List [ str ] :
35+ with open (fp ) as requirements :
4236 return [
4337 line .rstrip ()
4438 for line in requirements
4539 if not (line .isspace () or line .startswith ("#" ))
4640 ]
4741
4842
43+ @contextlib .contextmanager
44+ def temp_fill_include_path (fp : str ):
45+ """Temporarily set the Python include path in a file."""
46+ with open (fp , "r+" ) as f :
47+ try :
48+ content = f .read ()
49+ replaced = content .replace (
50+ PYTHON_INCLUDE_PATH_PLACEHOLDER ,
51+ Path (sysconfig .get_paths ()['include' ]).as_posix (),
52+ )
53+ f .seek (0 )
54+ f .write (replaced )
55+ f .truncate ()
56+ yield
57+ finally :
58+ # revert to the original content after exit
59+ f .seek (0 )
60+ f .write (content )
61+ f .truncate ()
62+
63+
4964class BazelExtension (setuptools .Extension ):
5065 """A C/C++ extension that is defined as a Bazel BUILD target."""
5166
52- def __init__ (self , name , bazel_target ):
67+ def __init__ (self , name : str , bazel_target : str ):
68+ super ().__init__ (name = name , sources = [])
69+
5370 self .bazel_target = bazel_target
54- self .relpath , self .target_name = posixpath .relpath (bazel_target , "//" ).split (
55- ":"
56- )
57- setuptools .Extension .__init__ (self , name , sources = [])
71+ stripped_target = bazel_target .split ("//" )[- 1 ]
72+ self .relpath , self .target_name = stripped_target .split (":" )
5873
5974
6075class BuildBazelExtension (build_ext .build_ext ):
@@ -65,74 +80,58 @@ def run(self):
6580 self .bazel_build (ext )
6681 build_ext .build_ext .run (self )
6782
68- def bazel_build (self , ext ):
83+ def bazel_build (self , ext : BazelExtension ):
6984 """Runs the bazel build to create the package."""
70- with open ("WORKSPACE" , "r" ) as workspace :
71- workspace_contents = workspace .read ()
72-
73- with open ("WORKSPACE" , "w" ) as workspace :
74- workspace .write (
75- re .sub (
76- r'(?<=path = ").*(?=", # May be overwritten by setup\.py\.)' ,
77- sysconfig .get_python_inc ().replace (os .path .sep , posixpath .sep ),
78- workspace_contents ,
79- )
80- )
81-
82- if not os .path .exists (self .build_temp ):
83- os .makedirs (self .build_temp )
84-
85- bazel_argv = [
86- "bazel" ,
87- "build" ,
88- ext .bazel_target ,
89- "--symlink_prefix=" + os .path .join (self .build_temp , "bazel-" ),
90- "--compilation_mode=" + ("dbg" if self .debug else "opt" ),
91- "--cxxopt=-std=c++17" , # required by nanobind
92- ]
93-
94- if IS_WINDOWS :
95- # Link with python*.lib.
96- for library_dir in self .library_dirs :
97- bazel_argv .append ("--linkopt=/LIBPATH:" + library_dir )
98- elif sys .platform == "darwin" :
99- if platform .machine () == "x86_64" :
100- # C++17 needs macOS 10.14 at minimum
101- bazel_argv .append ("--macos_minimum_os=10.14" )
102-
103- # cross-compilation for Mac ARM64 on GitHub Mac x86 runners.
104- # ARCHFLAGS is set by cibuildwheel before macOS wheel builds.
105- archflags = os .getenv ("ARCHFLAGS" , "" )
106- if "arm64" in archflags :
107- bazel_argv .append ("--cpu=darwin_arm64" )
108- bazel_argv .append ("--macos_cpus=arm64" )
109-
110- elif platform .machine () == "arm64" :
111- bazel_argv .append ("--macos_minimum_os=11.0" )
112-
113- self .spawn (bazel_argv )
114-
115- shared_lib_suffix = '.dll' if IS_WINDOWS else '.so'
116- ext_bazel_bin_path = os .path .join (
117- self .build_temp , 'bazel-bin' ,
118- ext .relpath , ext .target_name + shared_lib_suffix )
119-
120- ext_dest_path = self .get_ext_fullpath (ext .name )
121- ext_dest_dir = os .path .dirname (ext_dest_path )
122- if not os .path .exists (ext_dest_dir ):
123- os .makedirs (ext_dest_dir )
124- shutil .copyfile (ext_bazel_bin_path , ext_dest_path )
125-
126- # explicitly call `bazel shutdown` for graceful exit
127- self .spawn (["bazel" , "shutdown" ])
85+ with temp_fill_include_path ("WORKSPACE" ):
86+ temp_path = Path (self .build_temp )
87+
88+ bazel_argv = [
89+ "bazel" ,
90+ "build" ,
91+ ext .bazel_target ,
92+ f"--symlink_prefix={ temp_path / 'bazel-' } " ,
93+ f"--compilation_mode={ 'dbg' if self .debug else 'opt' } " ,
94+ "--cxxopt=-std=c++17" , # required by nanobind
95+ ]
96+
97+ if IS_WINDOWS :
98+ # Link with python*.lib.
99+ for library_dir in self .library_dirs :
100+ bazel_argv .append ("--linkopt=/LIBPATH:" + library_dir )
101+ elif IS_MAC :
102+ if platform .machine () == "x86_64" :
103+ # C++17 needs macOS 10.14 at minimum
104+ bazel_argv .append ("--macos_minimum_os=10.14" )
105+
106+ # cross-compilation for Mac ARM64 on GitHub Mac x86 runners.
107+ # ARCHFLAGS is set by cibuildwheel before macOS wheel builds.
108+ archflags = os .getenv ("ARCHFLAGS" , "" )
109+ if "arm64" in archflags :
110+ bazel_argv .append ("--cpu=darwin_arm64" )
111+ bazel_argv .append ("--macos_cpus=arm64" )
112+
113+ elif platform .machine () == "arm64" :
114+ bazel_argv .append ("--macos_minimum_os=11.0" )
115+
116+ self .spawn (bazel_argv )
117+
118+ shared_lib_suffix = '.dll' if IS_WINDOWS else '.so'
119+ ext_name = ext .target_name + shared_lib_suffix
120+ ext_bazel_bin_path = temp_path / 'bazel-bin' / ext .relpath / ext_name
121+
122+ ext_dest_path = Path (self .get_ext_fullpath (ext .name ))
123+ shutil .copyfile (ext_bazel_bin_path , ext_dest_path )
124+
125+ # explicitly call `bazel shutdown` for graceful exit
126+ self .spawn (["bazel" , "shutdown" ])
128127
129128
130129setuptools .setup (
131130 name = "google_benchmark" ,
132- version = _get_version (),
131+ version = _get_version ("bindings/python/google_benchmark/__init__.py" ),
133132 url = "https://github.com/google/benchmark" ,
134133 description = "A library to benchmark code snippets." ,
135- long_description = long_description ,
134+ long_description = _get_long_description ( "README.md" ) ,
136135 long_description_content_type = "text/markdown" ,
137136 author = "Google" ,
138137 author_email = "benchmark-py@google.com" ,
0 commit comments