From 8a88c3b9e2f330777f7bdcfa75ccdbd8458db398 Mon Sep 17 00:00:00 2001 From: Nicholas Junge Date: Tue, 5 Nov 2024 16:57:22 +0100 Subject: [PATCH 1/5] 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 | 17 +++++++ .gitignore | 4 ++ MODULE.bazel | 35 +++++++++++++++ pyproject.toml | 6 +-- setup.py | 119 +++++++++++++++++++++++++++++++++++++++++++++++++ src/BUILD | 23 ++++++++++ 6 files changed, 201 insertions(+), 3 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..5478346 --- /dev/null +++ b/.bazelrc @@ -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 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..c85fe5b --- /dev/null +++ b/MODULE.bazel @@ -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", +) diff --git a/pyproject.toml b/pyproject.toml index a116039..9ca08af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = [ diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..8794eca --- /dev/null +++ b/setup.py @@ -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, +) 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", +) From 78b924e66a946f7af0cf5347d5e9fcf850c5361a Mon Sep 17 00:00:00 2001 From: Cemlyn Waters Date: Sun, 17 Nov 2024 21:14:51 +0000 Subject: [PATCH 2/5] feat: copy preserving relative file structure --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8794eca..19c85ad 100644 --- a/setup.py +++ b/setup.py @@ -102,7 +102,10 @@ def bazel_build(self, ext: BazelExtension) -> None: should_copy = True if should_copy: - shutil.copyfile(root / fp, libdir / fp) + new_directory = libdir / os.path.relpath(root, srcdir) + if not os.path.exists(new_directory): + os.mkdir(new_directory) + shutil.copyfile(root / fp, libdir / new_directory / fp) setuptools.setup( From 847d700290d6d36201dfa7e722fcccd5c9ac98cb Mon Sep 17 00:00:00 2001 From: Cemlyn Waters Date: Sun, 17 Nov 2024 21:35:44 +0000 Subject: [PATCH 3/5] feat: recursive example --- setup.py | 8 ++++---- src/BUILD | 4 +++- src/nanobind_example/sub_ext/__init__.py | 1 + src/nanobind_example_ext.cpp | 4 ++++ 4 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 src/nanobind_example/sub_ext/__init__.py diff --git a/setup.py b/setup.py index 19c85ad..501a5b5 100644 --- a/setup.py +++ b/setup.py @@ -102,10 +102,10 @@ def bazel_build(self, ext: BazelExtension) -> None: should_copy = True if should_copy: - new_directory = libdir / os.path.relpath(root, srcdir) - if not os.path.exists(new_directory): - os.mkdir(new_directory) - shutil.copyfile(root / fp, libdir / new_directory / fp) + dstdir = libdir / os.path.relpath(root, srcdir) + if not os.path.exists(dstdir): + os.mkdir(dstdir) + shutil.copyfile(root / fp, dstdir / fp) setuptools.setup( diff --git a/src/BUILD b/src/BUILD index c896138..5562dca 100644 --- a/src/BUILD +++ b/src/BUILD @@ -6,7 +6,7 @@ load( py_library( name = "nanobind_example", - srcs = ["nanobind_example/__init__.py"], + srcs = ["nanobind_example/__init__.py", "nanobind_example/sub_ext/__init__.py"], data = [":nanobind_example_ext"], visibility = ["//visibility:public"], ) @@ -20,4 +20,6 @@ nanobind_stubgen( name = "nanobind_example_ext_stubgen", module = ":nanobind_example_ext", marker_file = "src/py.typed", + output_directory = "src", + recursive = True, ) diff --git a/src/nanobind_example/sub_ext/__init__.py b/src/nanobind_example/sub_ext/__init__.py new file mode 100644 index 0000000..17fab73 --- /dev/null +++ b/src/nanobind_example/sub_ext/__init__.py @@ -0,0 +1 @@ +from ..nanobind_example_ext.sub_ext import sub diff --git a/src/nanobind_example_ext.cpp b/src/nanobind_example_ext.cpp index 6d30dbf..3c4f541 100644 --- a/src/nanobind_example_ext.cpp +++ b/src/nanobind_example_ext.cpp @@ -7,4 +7,8 @@ using namespace nb::literals; NB_MODULE(nanobind_example_ext, m) { m.doc() = "This is a \"hello world\" example with nanobind"; m.def("add", [](int a, int b) { return a + b; }, "a"_a, "b"_a); + + nb::module_ sm = m.def_submodule("sub_ext", "A submodule of 'nanobind_example_ext'"); + + sm.def("sub", [](int a, int b) { return a - b; }, "a"_a, "b"_a); } From 47dbfd8b6cc13d7abc1a803005b77c31f39bb2b0 Mon Sep 17 00:00:00 2001 From: Nicholas Junge Date: Tue, 5 Nov 2024 16:57:22 +0100 Subject: [PATCH 4/5] 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 | 17 ++++++++ .gitignore | 4 ++ MODULE.bazel | 34 +++++++++++++++ pyproject.toml | 27 +++--------- setup.py | 110 +++++++++++++++++++++++++++++++++++++++++++++++++ src/BUILD | 23 +++++++++++ 6 files changed, 194 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..5478346 --- /dev/null +++ b/.bazelrc @@ -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 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..3e82e63 --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,34 @@ +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 = "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", +) + +use_repo( + python, + python = "python_versions", +) diff --git a/pyproject.toml b/pyproject.toml index a116039..c2eb6b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,34 +1,19 @@ [build-system] -requires = ["scikit-build-core >=0.4.3", "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 = "0.4" - -# 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..16cbb65 --- /dev/null +++ b/setup.py @@ -0,0 +1,110 @@ +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) + # 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"] + + 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, +) 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", +) From 11633aa7849f0606066361423dc6b0b31ba70637 Mon Sep 17 00:00:00 2001 From: Nicholas Junge Date: Tue, 5 Nov 2024 16:57:22 +0100 Subject: [PATCH 5/5] 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 | 17 ++++++++ .gitignore | 4 ++ MODULE.bazel | 16 +++++++ pyproject.toml | 27 +++--------- setup.py | 110 +++++++++++++++++++++++++++++++++++++++++++++++++ src/BUILD | 23 +++++++++++ 6 files changed, 176 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..5478346 --- /dev/null +++ b/.bazelrc @@ -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 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..ca77080 --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,16 @@ +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", +) + +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..16cbb65 --- /dev/null +++ b/setup.py @@ -0,0 +1,110 @@ +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) + # 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"] + + 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, +) 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", +)