Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Python bindings build using bzlmod #1764

Merged
merged 3 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading