Skip to content

Commit

Permalink
Add Python bindings build using bzlmod (#1764)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
nicholasjng and dmah42 committed Mar 7, 2024
1 parent c64b144 commit eaafe69
Show file tree
Hide file tree
Showing 12 changed files with 92 additions and 199 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ repos:
types_or: [ python, pyi ]
args: [ "--ignore-missing-imports", "--scripts-are-modules" ]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.13
rev: v0.3.1
hooks:
- id: ruff
args: [ --fix, --exit-non-zero-on-fix ]
Expand Down
22 changes: 20 additions & 2 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ module(
)

bazel_dep(name = "bazel_skylib", version = "1.5.0")
bazel_dep(name = "platforms", version = "0.0.7")
bazel_dep(name = "platforms", version = "0.0.8")
bazel_dep(name = "rules_foreign_cc", version = "0.10.1")
bazel_dep(name = "rules_cc", version = "0.0.9")

bazel_dep(name = "rules_python", version = "0.27.1", dev_dependency = True)
bazel_dep(name = "rules_python", version = "0.31.0", dev_dependency = True)
bazel_dep(name = "googletest", version = "1.12.1", dev_dependency = True, repo_name = "com_google_googletest")

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

python = use_extension("@rules_python//python/extensions:python.bzl", "python", dev_dependency = True)
python.toolchain(python_version = "3.8")
python.toolchain(python_version = "3.9")
python.toolchain(python_version = "3.10")
python.toolchain(python_version = "3.11")
python.toolchain(
is_default = True,
python_version = "3.12",
)
use_repo(
python,
python = "python_versions",
)

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

# -- bazel_dep definitions -- #

bazel_dep(name = "nanobind_bazel", version = "", dev_dependency = True)
git_override(
module_name = "nanobind_bazel",
commit = "97e3db2744d3f5da244a0846a0644ffb074b4880",
remote = "https://github.com/nicholasjng/nanobind-bazel",
)
6 changes: 0 additions & 6 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,3 @@ pip_parse(
load("@tools_pip_deps//:requirements.bzl", "install_deps")

install_deps()

new_local_repository(
name = "python_headers",
build_file = "@//bindings/python:python_headers.BUILD",
path = "<PYTHON_INCLUDE_PATH>", # May be overwritten by setup.py.
)
3 changes: 0 additions & 3 deletions bindings/python/BUILD

This file was deleted.

29 changes: 0 additions & 29 deletions bindings/python/build_defs.bzl

This file was deleted.

18 changes: 3 additions & 15 deletions bindings/python/google_benchmark/BUILD
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
load("//bindings/python:build_defs.bzl", "py_extension")
load("@nanobind_bazel//:build_defs.bzl", "nanobind_extension")

py_library(
name = "google_benchmark",
Expand All @@ -9,22 +9,10 @@ py_library(
],
)

py_extension(
nanobind_extension(
name = "_benchmark",
srcs = ["benchmark.cc"],
copts = [
"-fexceptions",
"-fno-strict-aliasing",
],
features = [
"-use_header_modules",
"-parse_headers",
],
deps = [
"//:benchmark",
"@nanobind",
"@python_headers",
],
deps = ["//:benchmark"],
)

py_test(
Expand Down
1 change: 1 addition & 0 deletions bindings/python/google_benchmark/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def my_benchmark(state):
if __name__ == '__main__':
benchmark.main()
"""

import atexit

from absl import app
Expand Down
59 changes: 0 additions & 59 deletions bindings/python/nanobind.BUILD

This file was deleted.

10 changes: 0 additions & 10 deletions bindings/python/python_headers.BUILD

This file was deleted.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,12 @@ src = ["bindings/python"]
line-length = 80
target-version = "py311"

[tool.ruff.lint]
# Enable pycodestyle (`E`, `W`), Pyflakes (`F`), and isort (`I`) codes by default.
select = ["E", "F", "I", "W"]
ignore = [
"E501", # line too long
]

[tool.ruff.isort]
[tool.ruff.lint.isort]
combine-as-imports = true
132 changes: 62 additions & 70 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,27 @@
import contextlib
import os
import platform
import shutil
import sysconfig
from pathlib import Path
from typing import Generator
from typing import Any

import setuptools
from setuptools.command import build_ext

PYTHON_INCLUDE_PATH_PLACEHOLDER = "<PYTHON_INCLUDE_PATH>"

IS_WINDOWS = platform.system() == "Windows"
IS_MAC = platform.system() == "Darwin"


@contextlib.contextmanager
def temp_fill_include_path(fp: str) -> Generator[None, None, None]:
"""Temporarily set the Python include path in a file."""
with open(fp, "r+") as f:
try:
content = f.read()
replaced = content.replace(
PYTHON_INCLUDE_PATH_PLACEHOLDER,
Path(sysconfig.get_paths()["include"]).as_posix(),
)
f.seek(0)
f.write(replaced)
f.truncate()
yield
finally:
# revert to the original content after exit
f.seek(0)
f.write(content)
f.truncate()
# hardcoded SABI-related options. Requires that each Python interpreter
# (hermetic or not) participating is of the same major-minor version.
version_tuple = tuple(int(i) for i in platform.python_version_tuple())
py_limited_api = version_tuple >= (3, 12)
options = {"bdist_wheel": {"py_limited_api": "cp312"}} if py_limited_api else {}


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

def __init__(self, name: str, bazel_target: str):
super().__init__(name=name, sources=[])
def __init__(self, name: str, bazel_target: str, **kwargs: Any):
super().__init__(name=name, sources=[], **kwargs)

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

def bazel_build(self, ext: BazelExtension) -> None:
"""Runs the bazel build to create the package."""
with temp_fill_include_path("WORKSPACE"):
temp_path = Path(self.build_temp)

bazel_argv = [
"bazel",
"build",
ext.bazel_target,
"--enable_bzlmod=false",
f"--symlink_prefix={temp_path / 'bazel-'}",
f"--compilation_mode={'dbg' if self.debug else 'opt'}",
# C++17 is required by nanobind
f"--cxxopt={'/std:c++17' if IS_WINDOWS else '-std=c++17'}",
]

if IS_WINDOWS:
# Link with python*.lib.
for library_dir in self.library_dirs:
bazel_argv.append("--linkopt=/LIBPATH:" + library_dir)
elif IS_MAC:
if platform.machine() == "x86_64":
# C++17 needs macOS 10.14 at minimum
bazel_argv.append("--macos_minimum_os=10.14")

# cross-compilation for Mac ARM64 on GitHub Mac x86 runners.
# ARCHFLAGS is set by cibuildwheel before macOS wheel builds.
archflags = os.getenv("ARCHFLAGS", "")
if "arm64" in archflags:
bazel_argv.append("--cpu=darwin_arm64")
bazel_argv.append("--macos_cpus=arm64")

elif platform.machine() == "arm64":
bazel_argv.append("--macos_minimum_os=11.0")

self.spawn(bazel_argv)

shared_lib_suffix = ".dll" if IS_WINDOWS else ".so"
ext_name = ext.target_name + shared_lib_suffix
ext_bazel_bin_path = (
temp_path / "bazel-bin" / ext.relpath / ext_name
)

ext_dest_path = Path(self.get_ext_fullpath(ext.name))
shutil.copyfile(ext_bazel_bin_path, ext_dest_path)
temp_path = Path(self.build_temp)
# omit the patch version to avoid build errors if the toolchain is not
# yet registered in the current @rules_python version.
# patch version differences should be fine.
python_version = ".".join(platform.python_version_tuple()[:2])

bazel_argv = [
"bazel",
"build",
ext.bazel_target,
f"--symlink_prefix={temp_path / 'bazel-'}",
f"--compilation_mode={'dbg' if self.debug else 'opt'}",
# C++17 is required by nanobind
f"--cxxopt={'/std:c++17' if IS_WINDOWS else '-std=c++17'}",
f"--@rules_python//python/config_settings:python_version={python_version}",
]

if ext.py_limited_api:
bazel_argv += ["--@nanobind_bazel//:py-limited-api=cp312"]

if IS_WINDOWS:
# Link with python*.lib.
for library_dir in self.library_dirs:
bazel_argv.append("--linkopt=/LIBPATH:" + library_dir)
elif IS_MAC:
if platform.machine() == "x86_64":
# C++17 needs macOS 10.14 at minimum
bazel_argv.append("--macos_minimum_os=10.14")

# cross-compilation for Mac ARM64 on GitHub Mac x86 runners.
# ARCHFLAGS is set by cibuildwheel before macOS wheel builds.
archflags = os.getenv("ARCHFLAGS", "")
if "arm64" in archflags:
bazel_argv.append("--cpu=darwin_arm64")
bazel_argv.append("--macos_cpus=arm64")

elif platform.machine() == "arm64":
bazel_argv.append("--macos_minimum_os=11.0")

self.spawn(bazel_argv)

if IS_WINDOWS:
suffix = ".pyd"
else:
suffix = ".abi3.so" if ext.py_limited_api else ".so"

ext_name = ext.target_name + suffix
ext_bazel_bin_path = temp_path / "bazel-bin" / ext.relpath / ext_name
ext_dest_path = Path(self.get_ext_fullpath(ext.name)).with_name(
ext_name
)
shutil.copyfile(ext_bazel_bin_path, ext_dest_path)


setuptools.setup(
Expand All @@ -118,6 +108,8 @@ def bazel_build(self, ext: BazelExtension) -> None:
BazelExtension(
name="google_benchmark._benchmark",
bazel_target="//bindings/python/google_benchmark:_benchmark",
py_limited_api=py_limited_api,
)
],
options=options,
)
Loading

0 comments on commit eaafe69

Please sign in to comment.