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", +)