From b7ab9017b0e89c448ee52a09a9014749ff0b2fc2 Mon Sep 17 00:00:00 2001 From: Nicholas Junge Date: Thu, 12 Dec 2024 01:54:03 +0100 Subject: [PATCH 1/3] Add note and link referring to the `bazel` branch for Bazel builds (#56) Mentions nanobind-bazel as the method of choice for building extensions. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 56a5755..fc81f0a 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,9 @@ was derived from the corresponding _pybind11_ [example project](https://github.com/pybind/scikit_build_example/) developed by [@henryiii](https://github.com/henryiii). +Furthermore, the [bazel](https://github.com/wjakob/nanobind_example/tree/bazel) branch contains an example +on how to build nanobind bindings extensions with Bazel using the [nanobind-bazel](https://github.com/nicholasjng/nanobind-bazel/) project. + Installation ------------ From 5ce5ce57143a469e92d9ebaee9c47949ee77caa8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:59:02 +0900 Subject: [PATCH 2/3] Bump pypa/cibuildwheel in the actions group across 1 directory (#55) Bumps the actions group with 1 update in the / directory: [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel). Updates `pypa/cibuildwheel` from 2.19 to 2.22 - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.19...v2.22) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 67f320b..6b44870 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -44,7 +44,7 @@ jobs: with: submodules: true - - uses: pypa/cibuildwheel@v2.19 + - uses: pypa/cibuildwheel@v2.22 - name: Verify clean directory run: git diff --exit-code From 5dadbc5156917645b767198bca7c9c0f52bf4689 Mon Sep 17 00:00:00 2001 From: Nicholas Junge Date: Tue, 5 Nov 2024 16:57:22 +0100 Subject: [PATCH 3/3] Add Bazel build system configuration 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. --- .bazelrc | 20 ++++++++ .gitignore | 4 ++ MODULE.bazel | 17 +++++++ pyproject.toml | 27 +++-------- setup.py | 121 +++++++++++++++++++++++++++++++++++++++++++++++++ src/BUILD | 23 ++++++++++ 6 files changed, 191 insertions(+), 21 deletions(-) create mode 100644 .bazelrc create mode 100644 MODULE.bazel create mode 100644 setup.py create mode 100644 src/BUILD diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 0000000..9d0099d --- /dev/null +++ b/.bazelrc @@ -0,0 +1,20 @@ +# 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 +# rules_python's indicator to only use free-threaded toolchains for CPython 3.13+. +# Needs to be given _together with_ nanobind-bazel's `free_threading` flag. +build --flag_alias=free_threaded=@rules_python//python/config_settings:py_freethreaded diff --git a/.gitignore b/.gitignore index 29f2c4e..9bee2da 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ build *.egg-info .vscode .vs + +# Bazel-specific. +!src/BUILD +MODULE.bazel.lock \ No newline at end of file diff --git a/MODULE.bazel b/MODULE.bazel new file mode 100644 index 0000000..e1f5903 --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,17 @@ +module(name = "nanobind_example", version = "0.1.0") + +bazel_dep(name = "nanobind_bazel", version = "2.2.0") +bazel_dep(name = "rules_python", version = "1.0.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") diff --git a/pyproject.toml b/pyproject.toml index e49c78d..c2eb6b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,34 +1,19 @@ [build-system] -requires = ["scikit-build-core >=0.10", "nanobind >=1.3.2"] -build-backend = "scikit_build_core.build" +requires = ["setuptools"] +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 = [ - { name = "Wenzel Jakob", email = "wenzel.jakob@epfl.ch" }, -] -classifiers = [ - "License :: OSI Approved :: BSD License", -] +authors = [{ name = "Wenzel Jakob", email = "wenzel.jakob@epfl.ch" }] +classifiers = ["License :: OSI Approved :: BSD License"] [project.urls] Homepage = "https://github.com/wjakob/nanobind_example" - -[tool.scikit-build] -# Protect the configuration against future changes in scikit-build-core -minimum-version = "build-system.requires" - -# Setuptools-style build caching in a local directory -build-dir = "build/{wheel_tag}" - -# Build stable ABI wheels for CPython 3.12+ -wheel.py-api = "cp312" - [tool.cibuildwheel] # Necessary to see build output from the actual compilation build-verbosity = 1 @@ -38,7 +23,7 @@ test-command = "pytest {project}/tests" test-requires = "pytest" # Don't test Python 3.8 wheels on macOS/arm64 -test-skip="cp38-macosx_*:arm64" +test-skip = "cp38-macosx_*:arm64" # Needed for full C++17 support [tool.cibuildwheel.macos.environment] diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..d7f5ebe --- /dev/null +++ b/setup.py @@ -0,0 +1,121 @@ +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" + +# free-threaded build option, requires Python 3.13+. +# Source: https://docs.python.org/3/howto/free-threading-python.html#identifying-free-threaded-python +free_threaded = "experimental free-threading build" in sys.version +# hardcoded SABI-related options. Requires that each Python interpreter +# (hermetic or not) participating is of the same major-minor version. +# Cannot be used together with free-threading. +py_limited_api = sys.version_info >= (3, 12) and not free_threaded +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.free_threaded = free_threaded + 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) + # 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 ext.free_threaded: + bazel_argv += [ + "--@nanobind_bazel//:free_threading", + "--free_threaded=yes", + ] + + 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", + free_threaded=free_threaded, + py_limited_api=py_limited_api, + ) + ], + options=options, +) diff --git a/src/BUILD b/src/BUILD new file mode 100644 index 0000000..c896138 --- /dev/null +++ b/src/BUILD @@ -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", +)