xref: /aosp_15_r20/external/google-benchmark/setup.py (revision dbb99499c3810fa1611fa2242a2fc446be01a57c)
1import contextlib
2import os
3import platform
4import re
5import shutil
6from pathlib import Path
7from typing import Any, Generator
8
9import setuptools
10from setuptools.command import build_ext
11
12IS_WINDOWS = platform.system() == "Windows"
13IS_MAC = platform.system() == "Darwin"
14IS_LINUX = platform.system() == "Linux"
15
16# hardcoded SABI-related options. Requires that each Python interpreter
17# (hermetic or not) participating is of the same major-minor version.
18version_tuple = tuple(int(i) for i in platform.python_version_tuple())
19py_limited_api = version_tuple >= (3, 12)
20options = {"bdist_wheel": {"py_limited_api": "cp312"}} if py_limited_api else {}
21
22
23def is_cibuildwheel() -> bool:
24    return os.getenv("CIBUILDWHEEL") is not None
25
26
27@contextlib.contextmanager
28def _maybe_patch_toolchains() -> Generator[None, None, None]:
29    """
30    Patch rules_python toolchains to ignore root user error
31    when run in a Docker container on Linux in cibuildwheel.
32    """
33
34    def fmt_toolchain_args(matchobj):
35        suffix = "ignore_root_user_error = True"
36        callargs = matchobj.group(1)
37        # toolchain def is broken over multiple lines
38        if callargs.endswith("\n"):
39            callargs = callargs + "    " + suffix + ",\n"
40        # toolchain def is on one line.
41        else:
42            callargs = callargs + ", " + suffix
43        return "python.toolchain(" + callargs + ")"
44
45    CIBW_LINUX = is_cibuildwheel() and IS_LINUX
46    try:
47        if CIBW_LINUX:
48            module_bazel = Path("MODULE.bazel")
49            content: str = module_bazel.read_text()
50            module_bazel.write_text(
51                re.sub(
52                    r"python.toolchain\(([\w\"\s,.=]*)\)",
53                    fmt_toolchain_args,
54                    content,
55                )
56            )
57        yield
58    finally:
59        if CIBW_LINUX:
60            module_bazel.write_text(content)
61
62
63class BazelExtension(setuptools.Extension):
64    """A C/C++ extension that is defined as a Bazel BUILD target."""
65
66    def __init__(self, name: str, bazel_target: str, **kwargs: Any):
67        super().__init__(name=name, sources=[], **kwargs)
68
69        self.bazel_target = bazel_target
70        stripped_target = bazel_target.split("//")[-1]
71        self.relpath, self.target_name = stripped_target.split(":")
72
73
74class BuildBazelExtension(build_ext.build_ext):
75    """A command that runs Bazel to build a C/C++ extension."""
76
77    def run(self):
78        for ext in self.extensions:
79            self.bazel_build(ext)
80        super().run()
81        # explicitly call `bazel shutdown` for graceful exit
82        self.spawn(["bazel", "shutdown"])
83
84    def copy_extensions_to_source(self):
85        """
86        Copy generated extensions into the source tree.
87        This is done in the ``bazel_build`` method, so it's not necessary to
88        do again in the `build_ext` base class.
89        """
90        pass
91
92    def bazel_build(self, ext: BazelExtension) -> None:
93        """Runs the bazel build to create the package."""
94        temp_path = Path(self.build_temp)
95        # omit the patch version to avoid build errors if the toolchain is not
96        # yet registered in the current @rules_python version.
97        # patch version differences should be fine.
98        python_version = ".".join(platform.python_version_tuple()[:2])
99
100        bazel_argv = [
101            "bazel",
102            "run",
103            ext.bazel_target,
104            f"--symlink_prefix={temp_path / 'bazel-'}",
105            f"--compilation_mode={'dbg' if self.debug else 'opt'}",
106            # C++17 is required by nanobind
107            f"--cxxopt={'/std:c++17' if IS_WINDOWS else '-std=c++17'}",
108            f"--@rules_python//python/config_settings:python_version={python_version}",
109        ]
110
111        if ext.py_limited_api:
112            bazel_argv += ["--@nanobind_bazel//:py-limited-api=cp312"]
113
114        if IS_WINDOWS:
115            # Link with python*.lib.
116            for library_dir in self.library_dirs:
117                bazel_argv.append("--linkopt=/LIBPATH:" + library_dir)
118        elif IS_MAC:
119            # C++17 needs macOS 10.14 at minimum
120            bazel_argv.append("--macos_minimum_os=10.14")
121
122        with _maybe_patch_toolchains():
123            self.spawn(bazel_argv)
124
125        if IS_WINDOWS:
126            suffix = ".pyd"
127        else:
128            suffix = ".abi3.so" if ext.py_limited_api else ".so"
129
130        # copy the Bazel build artifacts into setuptools' libdir,
131        # from where the wheel is built.
132        pkgname = "google_benchmark"
133        pythonroot = Path("bindings") / "python" / "google_benchmark"
134        srcdir = temp_path / "bazel-bin" / pythonroot
135        libdir = Path(self.build_lib) / pkgname
136        for root, dirs, files in os.walk(srcdir, topdown=True):
137            # exclude runfiles directories and children.
138            dirs[:] = [d for d in dirs if "runfiles" not in d]
139
140            for f in files:
141                print(f)
142                fp = Path(f)
143                should_copy = False
144                # we do not want the bare .so file included
145                # when building for ABI3, so we require a
146                # full and exact match on the file extension.
147                if "".join(fp.suffixes) == suffix:
148                    should_copy = True
149                elif fp.suffix == ".pyi":
150                    should_copy = True
151                elif Path(root) == srcdir and f == "py.typed":
152                    # copy py.typed, but only at the package root.
153                    should_copy = True
154
155                if should_copy:
156                    shutil.copyfile(root / fp, libdir / fp)
157
158
159setuptools.setup(
160    cmdclass=dict(build_ext=BuildBazelExtension),
161    package_data={"google_benchmark": ["py.typed", "*.pyi"]},
162    ext_modules=[
163        BazelExtension(
164            name="google_benchmark._benchmark",
165            bazel_target="//bindings/python/google_benchmark:benchmark_stubgen",
166            py_limited_api=py_limited_api,
167        )
168    ],
169    options=options,
170)
171