Skip to content

Commit

Permalink
Add Bazel build system configuration
Browse files Browse the repository at this point in the history
Provides a Bazel build configuration using bzlmod.

Currently, the required `nanobind_bazel` dep is sourced from a local
directory. This needs to be adjusted to either reflect the setup in CI,
or point to a `nanobind-bazel` tag on BCR once it's released.

Uses legacy setup.py / setuptools machinery since there's currently no
build backend support for Bazel available.

Builds a stable ABI wheel for CPython 3.12+ on all architectures.
  • Loading branch information
nicholasjng committed Nov 9, 2024
1 parent 12778d3 commit 8a88c3b
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 3 deletions.
17 changes: 17 additions & 0 deletions .bazelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Enable automatic configs based on platform
common --enable_platform_specific_config

# Set minimum supported C++ version
build:macos --host_cxxopt=-std=c++17 --cxxopt=-std=c++17
build:linux --host_cxxopt=-std=c++17 --cxxopt=-std=c++17
build:windows --host_cxxopt=/std:c++17 --cxxopt=/std:c++17

# Set minimum supported MacOS version to 10.14 for C++17.
build:macos --macos_minimum_os=10.14

# nanobind's minsize.
build --flag_alias=minsize=@nanobind_bazel//:minsize
# nanobind's py-limited-api.
build --flag_alias=py_limited_api=@nanobind_bazel//:py-limited-api
# rules_python's Python version, should not collide with builtin --python_version.
build --flag_alias=target_python_version=@rules_python//python/config_settings:python_version
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ build
*.egg-info
.vscode
.vs

# Bazel-specific.
!src/BUILD
MODULE.bazel.lock
35 changes: 35 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
module(
name = "nanobind_example",
version = "0.1.0",
)

bazel_dep(name = "nanobind_bazel", version = "")
local_path_override(
module_name = "nanobind_bazel",
path = "../nanobind-bazel",
)

bazel_dep(name = "hedron_compile_commands", dev_dependency = True)
git_override(
module_name = "hedron_compile_commands",
commit = "204aa593e002cbd177d30f11f54cff3559110bb9",
remote = "https://github.com/hedronvision/bazel-compile-commands-extractor.git",
)

bazel_dep(name = "rules_python", version = "0.31.0")

python = use_extension("@rules_python//python/extensions:python.bzl", "python")
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",
)
python.toolchain(python_version = "3.13")

use_repo(
python,
python = "python_versions",
)
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
[build-system]
requires = ["scikit-build-core >=0.4.3", "nanobind >=1.3.2"]
build-backend = "scikit_build_core.build"
requires = ["setuptools<73"]
build-backend = "setuptools.build_meta"

[project]
name = "nanobind-example"
version = "0.0.1"
description = "An example minimal project that compiles bindings using nanobind and scikit-build"
description = "An example minimal project that compiles bindings using nanobind and Bazel"
readme = "README.md"
requires-python = ">=3.8"
authors = [
Expand Down
119 changes: 119 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import os
import platform
import shutil
import sys
from pathlib import Path

import setuptools
from setuptools.command import build_ext

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

# hardcoded SABI-related options. Requires that each Python interpreter
# (hermetic or not) participating is of the same major-minor version.
py_limited_api = sys.version_info >= (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, **kwargs):
super().__init__(name=name, sources=[], **kwargs)

self.bazel_target = bazel_target
stripped_target = bazel_target.split("//")[-1]
self.relpath, self.target_name = stripped_target.split(":")


class BuildBazelExtension(build_ext.build_ext):
"""A command that runs Bazel to build a C/C++ extension."""

def run(self):
for ext in self.extensions:
self.bazel_build(ext)
super().run()
# explicitly call `bazel shutdown` for graceful exit
self.spawn(["bazel", "shutdown"])

def copy_extensions_to_source(self):
"""
Copy generated extensions into the source tree.
This is done in the ``bazel_build`` method, so it's not necessary to
do again in the `build_ext` base class.
"""
pass

def bazel_build(self, ext: BazelExtension) -> None:
"""Runs the bazel build to create a nanobind extension."""
temp_path = Path(self.build_temp)

# Specifying only MAJOR.MINOR makes rules_python do an internal
# lookup selecting the newest patch version.
python_version = "{0}.{1}".format(*sys.version_info[:2])

bazel_argv = [
"bazel",
"run",
ext.bazel_target,
f"--symlink_prefix={temp_path / 'bazel-'}",
f"--compilation_mode={'dbg' if self.debug else 'opt'}",
f"--target_python_version={python_version}",
]

if ext.py_limited_api:
bazel_argv += ["--py_limited_api=cp312"]

if IS_WINDOWS:
# Link with python*.lib.
# This technically breaks the hermeticity of rules_python,
# but its library target does not contain libs/python3.lib for SABI builds,
# so we source it from the build interpreter instead.
for library_dir in self.library_dirs:
bazel_argv.append("--linkopt=/LIBPATH:" + library_dir)

self.spawn(bazel_argv)

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

# copy the Bazel build artifacts into setuptools' libdir,
# from where the wheel is built.
srcdir = temp_path / "bazel-bin" / "src"
libdir = Path(self.build_lib) / "nanobind_example"
for root, dirs, files in os.walk(srcdir, topdown=True):
# exclude runfiles directories and children.
dirs[:] = [d for d in dirs if "runfiles" not in d]

for f in files:
fp = Path(f)
should_copy = False
# we do not want the bare .so file included
# when building for ABI3, so we require a
# full and exact match on the file extension.
if "".join(fp.suffixes) == suffix:
should_copy = True
elif fp.suffix == ".pyi":
should_copy = True
elif Path(root) == srcdir and f == "py.typed":
# copy py.typed, but only at the package root.
should_copy = True

if should_copy:
shutil.copyfile(root / fp, libdir / fp)


setuptools.setup(
cmdclass=dict(build_ext=BuildBazelExtension),
package_data={'nanobind_example': ["py.typed", "*.pyi", "**/*.pyi"]},
ext_modules=[
BazelExtension(
name="nanobind_example.nanobind_example_ext",
bazel_target="//src:nanobind_example_ext_stubgen",
py_limited_api=py_limited_api,
)
],
options=options,
)
23 changes: 23 additions & 0 deletions src/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
load(
"@nanobind_bazel//:build_defs.bzl",
"nanobind_extension",
"nanobind_stubgen",
)

py_library(
name = "nanobind_example",
srcs = ["nanobind_example/__init__.py"],
data = [":nanobind_example_ext"],
visibility = ["//visibility:public"],
)

nanobind_extension(
name = "nanobind_example_ext",
srcs = ["nanobind_example_ext.cpp"],
)

nanobind_stubgen(
name = "nanobind_example_ext_stubgen",
module = ":nanobind_example_ext",
marker_file = "src/py.typed",
)

0 comments on commit 8a88c3b

Please sign in to comment.