Skip to content

Commit eaafe69

Browse files
nicholasjngdmah42
andauthored
Add Python bindings build using bzlmod (#1764)
* Add a bzlmod Python bindings build Uses the newly started `@nanobind_bazel` project to build nanobind extensions. This means that we can drop all in-tree custom build defs and build files for nanobind and the C++ Python headers. Additionally, the temporary WORKSPACE overwrite hack naturally goes away due to the WORKSPACE system being obsolete. * Bump ruff -> v0.3.1, change ruff settings The latest minor releases incurred some formatting and configuration changes, this commit rolls them out. --------- Co-authored-by: dominic <510002+dmah42@users.noreply.github.com>
1 parent c64b144 commit eaafe69

12 files changed

+92
-199
lines changed

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ repos:
1111
types_or: [ python, pyi ]
1212
args: [ "--ignore-missing-imports", "--scripts-are-modules" ]
1313
- repo: https://github.com/astral-sh/ruff-pre-commit
14-
rev: v0.1.13
14+
rev: v0.3.1
1515
hooks:
1616
- id: ruff
1717
args: [ --fix, --exit-non-zero-on-fix ]

MODULE.bazel

+20-2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ module(
44
)
55

66
bazel_dep(name = "bazel_skylib", version = "1.5.0")
7-
bazel_dep(name = "platforms", version = "0.0.7")
7+
bazel_dep(name = "platforms", version = "0.0.8")
88
bazel_dep(name = "rules_foreign_cc", version = "0.10.1")
99
bazel_dep(name = "rules_cc", version = "0.0.9")
1010

11-
bazel_dep(name = "rules_python", version = "0.27.1", dev_dependency = True)
11+
bazel_dep(name = "rules_python", version = "0.31.0", dev_dependency = True)
1212
bazel_dep(name = "googletest", version = "1.12.1", dev_dependency = True, repo_name = "com_google_googletest")
1313

1414
bazel_dep(name = "libpfm", version = "4.11.0")
@@ -19,7 +19,18 @@ bazel_dep(name = "libpfm", version = "4.11.0")
1919
# of relying on the changing default version from rules_python.
2020

2121
python = use_extension("@rules_python//python/extensions:python.bzl", "python", dev_dependency = True)
22+
python.toolchain(python_version = "3.8")
2223
python.toolchain(python_version = "3.9")
24+
python.toolchain(python_version = "3.10")
25+
python.toolchain(python_version = "3.11")
26+
python.toolchain(
27+
is_default = True,
28+
python_version = "3.12",
29+
)
30+
use_repo(
31+
python,
32+
python = "python_versions",
33+
)
2334

2435
pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip", dev_dependency = True)
2536
pip.parse(
@@ -30,3 +41,10 @@ pip.parse(
3041
use_repo(pip, "tools_pip_deps")
3142

3243
# -- bazel_dep definitions -- #
44+
45+
bazel_dep(name = "nanobind_bazel", version = "", dev_dependency = True)
46+
git_override(
47+
module_name = "nanobind_bazel",
48+
commit = "97e3db2744d3f5da244a0846a0644ffb074b4880",
49+
remote = "https://github.com/nicholasjng/nanobind-bazel",
50+
)

WORKSPACE

-6
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,3 @@ pip_parse(
2222
load("@tools_pip_deps//:requirements.bzl", "install_deps")
2323

2424
install_deps()
25-
26-
new_local_repository(
27-
name = "python_headers",
28-
build_file = "@//bindings/python:python_headers.BUILD",
29-
path = "<PYTHON_INCLUDE_PATH>", # May be overwritten by setup.py.
30-
)

bindings/python/BUILD

-3
This file was deleted.

bindings/python/build_defs.bzl

-29
This file was deleted.

bindings/python/google_benchmark/BUILD

+3-15
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
load("//bindings/python:build_defs.bzl", "py_extension")
1+
load("@nanobind_bazel//:build_defs.bzl", "nanobind_extension")
22

33
py_library(
44
name = "google_benchmark",
@@ -9,22 +9,10 @@ py_library(
99
],
1010
)
1111

12-
py_extension(
12+
nanobind_extension(
1313
name = "_benchmark",
1414
srcs = ["benchmark.cc"],
15-
copts = [
16-
"-fexceptions",
17-
"-fno-strict-aliasing",
18-
],
19-
features = [
20-
"-use_header_modules",
21-
"-parse_headers",
22-
],
23-
deps = [
24-
"//:benchmark",
25-
"@nanobind",
26-
"@python_headers",
27-
],
15+
deps = ["//:benchmark"],
2816
)
2917

3018
py_test(

bindings/python/google_benchmark/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def my_benchmark(state):
2626
if __name__ == '__main__':
2727
benchmark.main()
2828
"""
29+
2930
import atexit
3031

3132
from absl import app

bindings/python/nanobind.BUILD

-59
This file was deleted.

bindings/python/python_headers.BUILD

-10
This file was deleted.

pyproject.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,12 @@ src = ["bindings/python"]
7575
line-length = 80
7676
target-version = "py311"
7777

78+
[tool.ruff.lint]
7879
# Enable pycodestyle (`E`, `W`), Pyflakes (`F`), and isort (`I`) codes by default.
7980
select = ["E", "F", "I", "W"]
8081
ignore = [
8182
"E501", # line too long
8283
]
8384

84-
[tool.ruff.isort]
85+
[tool.ruff.lint.isort]
8586
combine-as-imports = true

setup.py

+62-70
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,27 @@
1-
import contextlib
21
import os
32
import platform
43
import shutil
5-
import sysconfig
64
from pathlib import Path
7-
from typing import Generator
5+
from typing import Any
86

97
import setuptools
108
from setuptools.command import build_ext
119

12-
PYTHON_INCLUDE_PATH_PLACEHOLDER = "<PYTHON_INCLUDE_PATH>"
13-
1410
IS_WINDOWS = platform.system() == "Windows"
1511
IS_MAC = platform.system() == "Darwin"
1612

17-
18-
@contextlib.contextmanager
19-
def temp_fill_include_path(fp: str) -> Generator[None, None, None]:
20-
"""Temporarily set the Python include path in a file."""
21-
with open(fp, "r+") as f:
22-
try:
23-
content = f.read()
24-
replaced = content.replace(
25-
PYTHON_INCLUDE_PATH_PLACEHOLDER,
26-
Path(sysconfig.get_paths()["include"]).as_posix(),
27-
)
28-
f.seek(0)
29-
f.write(replaced)
30-
f.truncate()
31-
yield
32-
finally:
33-
# revert to the original content after exit
34-
f.seek(0)
35-
f.write(content)
36-
f.truncate()
13+
# hardcoded SABI-related options. Requires that each Python interpreter
14+
# (hermetic or not) participating is of the same major-minor version.
15+
version_tuple = tuple(int(i) for i in platform.python_version_tuple())
16+
py_limited_api = version_tuple >= (3, 12)
17+
options = {"bdist_wheel": {"py_limited_api": "cp312"}} if py_limited_api else {}
3718

3819

3920
class BazelExtension(setuptools.Extension):
4021
"""A C/C++ extension that is defined as a Bazel BUILD target."""
4122

42-
def __init__(self, name: str, bazel_target: str):
43-
super().__init__(name=name, sources=[])
23+
def __init__(self, name: str, bazel_target: str, **kwargs: Any):
24+
super().__init__(name=name, sources=[], **kwargs)
4425

4526
self.bazel_target = bazel_target
4627
stripped_target = bazel_target.split("//")[-1]
@@ -67,49 +48,58 @@ def copy_extensions_to_source(self):
6748

6849
def bazel_build(self, ext: BazelExtension) -> None:
6950
"""Runs the bazel build to create the package."""
70-
with temp_fill_include_path("WORKSPACE"):
71-
temp_path = Path(self.build_temp)
72-
73-
bazel_argv = [
74-
"bazel",
75-
"build",
76-
ext.bazel_target,
77-
"--enable_bzlmod=false",
78-
f"--symlink_prefix={temp_path / 'bazel-'}",
79-
f"--compilation_mode={'dbg' if self.debug else 'opt'}",
80-
# C++17 is required by nanobind
81-
f"--cxxopt={'/std:c++17' if IS_WINDOWS else '-std=c++17'}",
82-
]
83-
84-
if IS_WINDOWS:
85-
# Link with python*.lib.
86-
for library_dir in self.library_dirs:
87-
bazel_argv.append("--linkopt=/LIBPATH:" + library_dir)
88-
elif IS_MAC:
89-
if platform.machine() == "x86_64":
90-
# C++17 needs macOS 10.14 at minimum
91-
bazel_argv.append("--macos_minimum_os=10.14")
92-
93-
# cross-compilation for Mac ARM64 on GitHub Mac x86 runners.
94-
# ARCHFLAGS is set by cibuildwheel before macOS wheel builds.
95-
archflags = os.getenv("ARCHFLAGS", "")
96-
if "arm64" in archflags:
97-
bazel_argv.append("--cpu=darwin_arm64")
98-
bazel_argv.append("--macos_cpus=arm64")
99-
100-
elif platform.machine() == "arm64":
101-
bazel_argv.append("--macos_minimum_os=11.0")
102-
103-
self.spawn(bazel_argv)
104-
105-
shared_lib_suffix = ".dll" if IS_WINDOWS else ".so"
106-
ext_name = ext.target_name + shared_lib_suffix
107-
ext_bazel_bin_path = (
108-
temp_path / "bazel-bin" / ext.relpath / ext_name
109-
)
110-
111-
ext_dest_path = Path(self.get_ext_fullpath(ext.name))
112-
shutil.copyfile(ext_bazel_bin_path, ext_dest_path)
51+
temp_path = Path(self.build_temp)
52+
# omit the patch version to avoid build errors if the toolchain is not
53+
# yet registered in the current @rules_python version.
54+
# patch version differences should be fine.
55+
python_version = ".".join(platform.python_version_tuple()[:2])
56+
57+
bazel_argv = [
58+
"bazel",
59+
"build",
60+
ext.bazel_target,
61+
f"--symlink_prefix={temp_path / 'bazel-'}",
62+
f"--compilation_mode={'dbg' if self.debug else 'opt'}",
63+
# C++17 is required by nanobind
64+
f"--cxxopt={'/std:c++17' if IS_WINDOWS else '-std=c++17'}",
65+
f"--@rules_python//python/config_settings:python_version={python_version}",
66+
]
67+
68+
if ext.py_limited_api:
69+
bazel_argv += ["--@nanobind_bazel//:py-limited-api=cp312"]
70+
71+
if IS_WINDOWS:
72+
# Link with python*.lib.
73+
for library_dir in self.library_dirs:
74+
bazel_argv.append("--linkopt=/LIBPATH:" + library_dir)
75+
elif IS_MAC:
76+
if platform.machine() == "x86_64":
77+
# C++17 needs macOS 10.14 at minimum
78+
bazel_argv.append("--macos_minimum_os=10.14")
79+
80+
# cross-compilation for Mac ARM64 on GitHub Mac x86 runners.
81+
# ARCHFLAGS is set by cibuildwheel before macOS wheel builds.
82+
archflags = os.getenv("ARCHFLAGS", "")
83+
if "arm64" in archflags:
84+
bazel_argv.append("--cpu=darwin_arm64")
85+
bazel_argv.append("--macos_cpus=arm64")
86+
87+
elif platform.machine() == "arm64":
88+
bazel_argv.append("--macos_minimum_os=11.0")
89+
90+
self.spawn(bazel_argv)
91+
92+
if IS_WINDOWS:
93+
suffix = ".pyd"
94+
else:
95+
suffix = ".abi3.so" if ext.py_limited_api else ".so"
96+
97+
ext_name = ext.target_name + suffix
98+
ext_bazel_bin_path = temp_path / "bazel-bin" / ext.relpath / ext_name
99+
ext_dest_path = Path(self.get_ext_fullpath(ext.name)).with_name(
100+
ext_name
101+
)
102+
shutil.copyfile(ext_bazel_bin_path, ext_dest_path)
113103

114104

115105
setuptools.setup(
@@ -118,6 +108,8 @@ def bazel_build(self, ext: BazelExtension) -> None:
118108
BazelExtension(
119109
name="google_benchmark._benchmark",
120110
bazel_target="//bindings/python/google_benchmark:_benchmark",
111+
py_limited_api=py_limited_api,
121112
)
122113
],
114+
options=options,
123115
)

0 commit comments

Comments
 (0)