diff --git a/.github/bazel.sh b/.github/bazel.sh index 6672e4f3..1a85073f 100755 --- a/.github/bazel.sh +++ b/.github/bazel.sh @@ -5,16 +5,20 @@ set -eux export MSYS_NO_PATHCONV='1' export MSYS2_ARG_CONV_EXCL='*' +LIMITED_API="${PY_LIMITED_API:-unset}" + for compilation_mode in dbg opt do bazel clean bazel build \ --extra_toolchains=//python/toolchains:all \ + --@nanobind_bazel//:py-limited-api="${LIMITED_API}" \ --compilation_mode=${compilation_mode} -- \ //:re2 \ //python:re2 bazel test \ --extra_toolchains=//python/toolchains:all \ + --@nanobind_bazel//:py-limited-api="${LIMITED_API}" \ --compilation_mode=${compilation_mode} -- \ //:small_tests \ //python:all diff --git a/.github/workflows/ci-bazel.yml b/.github/workflows/ci-bazel.yml index ca951196..9dc949d3 100644 --- a/.github/workflows/ci-bazel.yml +++ b/.github/workflows/ci-bazel.yml @@ -14,7 +14,9 @@ jobs: matrix: os: [macos-latest, ubuntu-latest, windows-latest] # Keep in sync with python.yml. - ver: ['3.10', '3.11', '3.12', '3.13', '3.14'] + ver: ['3.10', '3.11', '3.12', '3.13', '3.14', '3.12-abi3'] + env: + PY_LIMITED_API: ${{ matrix.ver == '3.12-abi3' && 'cp312' || '' }} steps: - uses: actions/checkout@v4.2.2 - uses: bazel-contrib/setup-bazel@0.15.0 @@ -22,7 +24,7 @@ jobs: bazelisk-version: '1.x' - uses: actions/setup-python@v5.6.0 with: - python-version: ${{ matrix.ver }} + python-version: ${{ matrix.ver == '3.12-abi3' && '3.12' || matrix.ver }} - name: Prepare Python ${{ matrix.ver }} environment run: | python -m pip install --upgrade pip diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 55aee2c8..c9e23f3c 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -30,10 +30,11 @@ jobs: os: [manylinux_2_28] # Keep in sync with ci-bazel.yml and list below. # Also, when bumping the minimum version, update ../../python/setup.py. - ver: ['3.10', '3.11', '3.12', '3.13', '3.14'] + ver: ['3.10', '3.11', '3.12', '3.13', '3.14', '3.12-abi3'] env: BAZELISK_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PYTHON: /usr/local/bin/python${{ matrix.ver }} + PYTHON: /usr/local/bin/python${{ matrix.ver == '3.12-abi3' && '3.12' || matrix.ver }} + PY_LIMITED_API: ${{ matrix.ver == '3.12-abi3' && 'cp312' || '' }} # Bazel fails if the username is unknown. USER: runner steps: @@ -47,7 +48,7 @@ jobs: - name: Prepare Python ${{ matrix.ver }} environment run: | "${PYTHON}" -m pip install --upgrade pip - "${PYTHON}" -m pip install --upgrade setuptools build wheel auditwheel + "${PYTHON}" -m pip install --upgrade setuptools build wheel auditwheel abi3audit "${PYTHON}" -m pip install --upgrade absl-py mypy "${PYTHON}" python/toolchains/generate.py shell: bash @@ -59,6 +60,12 @@ jobs: "${PYTHON}" -m auditwheel repair --wheel-dir=. dist/* shell: bash working-directory: python + - if: matrix.ver == '3.12-abi3' + name: Audit stable ABI compliance + run: | + "${PYTHON}" -m abi3audit --strict google_re2-*.whl + shell: bash + working-directory: python - name: Test wheel run: | "${PYTHON}" -m pip install google_re2-*.whl @@ -86,7 +93,7 @@ jobs: # TODO(rsc): Stop cross-compiling now that we don't use macOS 12. # instead, specify `-large` suffix on X64 and `-xlarge` suffix on ARM64. os: [13, 14, 15, 26] - ver: ['3.10', '3.11', '3.12', '3.13', '3.14'] + ver: ['3.10', '3.11', '3.12', '3.13', '3.14', '3.12-abi3'] env: BAZELISK_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BAZEL_CPU: darwin_${{ matrix.arch.bazel-name }} @@ -97,6 +104,7 @@ jobs: # Stop macOS from reporting the system version as 10.x. # Otherwise, Python refuses to install the built wheel! SYSTEM_VERSION_COMPAT: 0 + PY_LIMITED_API: ${{ matrix.ver == '3.12-abi3' && 'cp312' || '' }} steps: - uses: actions/checkout@v4.1.7 # Stash the timestamp for the commit SHA that triggered the workflow. @@ -107,11 +115,11 @@ jobs: bazelisk-version: '1.x' - uses: actions/setup-python@v5.6.0 with: - python-version: ${{ matrix.ver }} + python-version: ${{ matrix.ver == '3.12-abi3' && '3.12' || matrix.ver }} - name: Prepare Python ${{ matrix.ver }} environment run: | python -m pip install --upgrade pip - python -m pip install --upgrade setuptools build wheel delocate + python -m pip install --upgrade setuptools build wheel delocate abi3audit python -m pip install --upgrade absl-py mypy python python/toolchains/generate.py shell: bash @@ -123,6 +131,12 @@ jobs: python -m delocate.cmd.delocate_wheel --wheel-dir=. dist/* shell: bash working-directory: python + - if: matrix.ver == '3.12-abi3' + name: Audit stable ABI compliance + run: | + python -m abi3audit --strict google_re2-*.whl + shell: bash + working-directory: python - if: matrix.arch.name == runner.arch name: Test wheel run: | @@ -149,7 +163,7 @@ jobs: - { name: X86, bazel-name: x64_x86, python-name: win32 } - { name: X64, bazel-name: x64, python-name: win_amd64 } - { name: ARM64, bazel-name: arm64, python-name: win_arm64 } - ver: ['3.10', '3.11', '3.12', '3.13', '3.14'] + ver: ['3.10', '3.11', '3.12', '3.13', '3.14', '3.12-abi3'] exclude: - arch: { name: ARM64, bazel-name: arm64, python-name: win_arm64 } ver: '3.9' @@ -159,6 +173,7 @@ jobs: BAZELISK_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BAZEL_CPU: ${{ matrix.arch.bazel-name }}_windows PLAT_NAME: ${{ matrix.arch.python-name }} + PY_LIMITED_API: ${{ matrix.ver == '3.12-abi3' && 'cp312' || '' }} steps: - uses: actions/checkout@v4.2.2 # Stash the timestamp for the commit SHA that triggered the workflow. @@ -174,12 +189,12 @@ jobs: shell: bash - uses: actions/setup-python@v5.6.0 with: - python-version: ${{ matrix.ver }} + python-version: ${{ matrix.ver == '3.12-abi3' && '3.12' || matrix.ver }} architecture: ${{ env.architecture }} - name: Prepare Python ${{ matrix.ver }} environment run: | python -m pip install --upgrade pip - python -m pip install --upgrade setuptools build wheel delvewheel + python -m pip install --upgrade setuptools build wheel delvewheel abi3audit python -m pip install --upgrade absl-py mypy python python/toolchains/generate.py shell: bash @@ -191,6 +206,12 @@ jobs: python -m delvewheel repair --wheel-dir=. dist/* shell: bash working-directory: python + - if: matrix.ver == '3.12-abi3' + name: Audit stable ABI compliance + run: | + python -m abi3audit --strict google_re2-*.whl + shell: bash + working-directory: python - name: Test wheel run: | python -m pip install google_re2-*.whl diff --git a/.gitignore b/.gitignore index 56f0a315..9bd53c12 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ core obj/ benchlog.* user.bazelrc +bazel-* diff --git a/MODULE.bazel b/MODULE.bazel index c2530a95..cda900ab 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -15,7 +15,7 @@ bazel_dep(name = "apple_support", version = "1.24.2") bazel_dep(name = "rules_cc", version = "0.2.14") bazel_dep(name = "abseil-cpp", version = "20250814.1") bazel_dep(name = "rules_python", version = "1.7.0") -bazel_dep(name = "pybind11_bazel", version = "3.0.0") +bazel_dep(name = "nanobind_bazel", version = "2.12.0") # This is a temporary hack for `x64_x86_windows`. # TODO(junyer): Remove whenever no longer needed. diff --git a/python/BUILD.bazel b/python/BUILD.bazel index 9484f7ee..d2da75bc 100644 --- a/python/BUILD.bazel +++ b/python/BUILD.bazel @@ -4,10 +4,10 @@ # Bazel (http://bazel.build/) BUILD file for RE2 Python. -load("@pybind11_bazel//:build_defs.bzl", "pybind_extension") +load("@nanobind_bazel//:build_defs.bzl", "nanobind_extension") load("@rules_python//python:defs.bzl", "py_library", "py_test") -pybind_extension( +nanobind_extension( name = "_re2", srcs = ["_re2.cc"], deps = [ diff --git a/python/_re2.cc b/python/_re2.cc index 22f092b2..1f980091 100644 --- a/python/_re2.cc +++ b/python/_re2.cc @@ -13,11 +13,11 @@ #include #include "absl/strings/string_view.h" -#include "pybind11/buffer_info.h" -#include "pybind11/gil.h" -#include "pybind11/pybind11.h" -#include "pybind11/pytypes.h" -#include "pybind11/stl.h" // IWYU pragma: keep +#include "nanobind/nanobind.h" +#include "nanobind/stl/pair.h" // IWYU pragma: keep +#include "nanobind/stl/tuple.h" // IWYU pragma: keep +#include "nanobind/stl/unique_ptr.h" // IWYU pragma: keep +#include "nanobind/stl/vector.h" // IWYU pragma: keep #include "re2/filtered_re2.h" #include "re2/re2.h" #include "re2/set.h" @@ -30,18 +30,33 @@ namespace re2_python { // This is conventional. -namespace py = pybind11; - -// In terms of the pybind11 API, a py::buffer is merely a py::object that -// supports the buffer interface/protocol and you must explicitly request -// a py::buffer_info in order to access the actual bytes. Under the hood, -// the py::buffer_info manages a reference count to the py::buffer, so it -// must be constructed and subsequently destructed while holding the GIL. -static inline absl::string_view FromBytes(const py::buffer_info& bytes) { - char* data = reinterpret_cast(bytes.ptr); - ssize_t size = bytes.size; - return absl::string_view(data, size); -} +namespace nb = nanobind; + +// nanobind doesn't provide a native way to access a Python buffer, so +// we extract it ourself. We use PyBUF_SIMPLE to extract a contiguous +// buffer to match the semantics of the previous pybind11-based implementation. +class BufferView { + public: + explicit BufferView(nb::handle obj) { + if (PyObject_GetBuffer(obj.ptr(), &view_, PyBUF_SIMPLE) != 0) { + throw nb::python_error(); + } + } + + ~BufferView() { PyBuffer_Release(&view_); } + + // Not copyable or movable. + BufferView(const BufferView&) = delete; + BufferView& operator=(const BufferView&) = delete; + + absl::string_view view() const { + return absl::string_view(reinterpret_cast(view_.buf), + static_cast(view_.len)); + } + + private: + Py_buffer view_; +}; static inline int OneCharLen(const char* ptr) { return "\1\1\1\1\1\1\1\1\1\1\1\1\2\2\3\4"[(*ptr & 0xFF) >> 4]; @@ -49,9 +64,9 @@ static inline int OneCharLen(const char* ptr) { // Helper function for when Python encodes str to bytes and then needs to // convert str offsets to bytes offsets. Assumes that text is valid UTF-8. -ssize_t CharLenToBytes(py::buffer buffer, ssize_t pos, ssize_t len) { - auto bytes = buffer.request(); - auto text = FromBytes(bytes); +ssize_t CharLenToBytes(nb::object buffer, ssize_t pos, ssize_t len) { + BufferView bytes(buffer); + auto text = bytes.view(); auto ptr = text.data() + pos; auto end = text.data() + text.size(); while (ptr < end && len > 0) { @@ -63,9 +78,9 @@ ssize_t CharLenToBytes(py::buffer buffer, ssize_t pos, ssize_t len) { // Helper function for when Python decodes bytes to str and then needs to // convert bytes offsets to str offsets. Assumes that text is valid UTF-8. -ssize_t BytesToCharLen(py::buffer buffer, ssize_t pos, ssize_t endpos) { - auto bytes = buffer.request(); - auto text = FromBytes(bytes); +ssize_t BytesToCharLen(nb::object buffer, ssize_t pos, ssize_t endpos) { + BufferView bytes(buffer); + auto text = bytes.view(); auto ptr = text.data() + pos; auto end = text.data() + endpos; ssize_t len = 0; @@ -76,25 +91,26 @@ ssize_t BytesToCharLen(py::buffer buffer, ssize_t pos, ssize_t endpos) { return len; } -std::unique_ptr RE2InitShim(py::buffer buffer, +std::unique_ptr RE2InitShim(nb::object buffer, const RE2::Options& options) { - auto bytes = buffer.request(); - auto pattern = FromBytes(bytes); + BufferView bytes(buffer); + auto pattern = bytes.view(); return std::make_unique(pattern, options); } -py::bytes RE2ErrorShim(const RE2& self) { +nb::bytes RE2ErrorShim(const RE2& self) { // Return std::string as bytes. That is, without decoding to str. - return self.error(); + const std::string& error = self.error(); + return nb::bytes(error.data(), error.size()); } -std::vector> RE2NamedCapturingGroupsShim( +std::vector> RE2NamedCapturingGroupsShim( const RE2& self) { const int num_groups = self.NumberOfCapturingGroups(); - std::vector> groups; + std::vector> groups; groups.reserve(num_groups); for (const auto& it : self.NamedCapturingGroups()) { - groups.emplace_back(it.first, it.second); + groups.emplace_back(nb::bytes(it.first.data(), it.first.size()), it.second); } return groups; } @@ -111,24 +127,26 @@ std::vector RE2ReverseProgramFanoutShim(const RE2& self) { return histogram; } -std::tuple RE2PossibleMatchRangeShim( +std::tuple RE2PossibleMatchRangeShim( const RE2& self, int maxlen) { std::string min, max; + bool ok = self.PossibleMatchRange(&min, &max, maxlen); // Return std::string as bytes. That is, without decoding to str. - return {self.PossibleMatchRange(&min, &max, maxlen), min, max}; + return {ok, nb::bytes(min.data(), min.size()), + nb::bytes(max.data(), max.size())}; } std::vector> RE2MatchShim(const RE2& self, RE2::Anchor anchor, - py::buffer buffer, + nb::object buffer, ssize_t pos, ssize_t endpos) { - auto bytes = buffer.request(); - auto text = FromBytes(bytes); + BufferView bytes(buffer); + auto text = bytes.view(); const int num_groups = self.NumberOfCapturingGroups() + 1; // need $0 std::vector groups; groups.resize(num_groups); - py::gil_scoped_release release_gil; + nb::gil_scoped_release release_gil; if (!self.Match(text, pos, endpos, anchor, groups.data(), groups.size())) { // Ensure that groups are null before converting to spans! for (auto& it : groups) { @@ -148,11 +166,12 @@ std::vector> RE2MatchShim(const RE2& self, return spans; } -py::bytes RE2QuoteMetaShim(py::buffer buffer) { - auto bytes = buffer.request(); - auto pattern = FromBytes(bytes); +nb::bytes RE2QuoteMetaShim(nb::object buffer) { + BufferView bytes(buffer); + auto pattern = bytes.view(); // Return std::string as bytes. That is, without decoding to str. - return RE2::QuoteMeta(pattern); + std::string quoted = RE2::QuoteMeta(pattern); + return nb::bytes(quoted.data(), quoted.size()); } class Set { @@ -166,9 +185,9 @@ class Set { Set(const Set&) = delete; Set& operator=(const Set&) = delete; - int Add(py::buffer buffer) { - auto bytes = buffer.request(); - auto pattern = FromBytes(bytes); + int Add(nb::object buffer) { + BufferView bytes(buffer); + auto pattern = bytes.view(); int index = set_.Add(pattern, /*error=*/NULL); // -1 on error return index; } @@ -178,11 +197,11 @@ class Set { return set_.Compile(); } - std::vector Match(py::buffer buffer) const { - auto bytes = buffer.request(); - auto text = FromBytes(bytes); + std::vector Match(nb::object buffer) const { + BufferView bytes(buffer); + auto text = bytes.view(); std::vector matches; - py::gil_scoped_release release_gil; + nb::gil_scoped_release release_gil; set_.Match(text, &matches); return matches; } @@ -200,9 +219,9 @@ class Filter { Filter(const Filter&) = delete; Filter& operator=(const Filter&) = delete; - int Add(py::buffer buffer, const RE2::Options& options) { - auto bytes = buffer.request(); - auto pattern = FromBytes(bytes); + int Add(nb::object buffer, const RE2::Options& options) { + BufferView bytes(buffer); + auto pattern = bytes.view(); int index = -1; // not clobbered on error filter_.Add(pattern, options, &index); return index; @@ -218,22 +237,22 @@ class Filter { for (int i = 0; i < static_cast(atoms.size()); ++i) { if (set_->Add(atoms[i], /*error=*/NULL) != i) { // Should never happen: the atom is a literal! - py::pybind11_fail("set_->Add() failed"); + throw std::runtime_error("set_->Add() failed"); } } // Compiling can fail. return set_->Compile(); } - std::vector Match(py::buffer buffer, bool potential) const { + std::vector Match(nb::object buffer, bool potential) const { if (set_ == nullptr) { - py::pybind11_fail("Match() called before compiling"); + throw std::runtime_error("Match() called before compiling"); } - auto bytes = buffer.request(); - auto text = FromBytes(bytes); + BufferView bytes(buffer); + auto text = bytes.view(); std::vector atoms; - py::gil_scoped_release release_gil; + nb::gil_scoped_release release_gil; set_->Match(text, &atoms); std::vector matches; if (potential) { @@ -253,9 +272,9 @@ class Filter { std::unique_ptr set_; }; -PYBIND11_MODULE(_re2, module) { - // Translate exceptions thrown by py::pybind11_fail() into Python. - py::register_local_exception(module, "Error"); +NB_MODULE(_re2, module) { + // Translate exceptions thrown by throw std::runtime_error() into Python. + nb::exception(module, "Error"); module.def("CharLenToBytes", &CharLenToBytes); module.def("BytesToCharLen", &BytesToCharLen); @@ -267,12 +286,12 @@ PYBIND11_MODULE(_re2, module) { // enum Encoding // class Set // class Filter - py::class_ re2(module, "RE2"); - py::enum_ anchor(re2, "Anchor"); - py::class_ options(re2, "Options"); - py::enum_ encoding(options, "Encoding"); - py::class_ set(module, "Set"); - py::class_ filter(module, "Filter"); + nb::class_ re2(module, "RE2"); + nb::enum_ anchor(re2, "Anchor"); + nb::class_ options(re2, "Options"); + nb::enum_ encoding(options, "Encoding"); + nb::class_ set(module, "Set"); + nb::class_ filter(module, "Filter"); anchor.value("UNANCHORED", RE2::Anchor::UNANCHORED); anchor.value("ANCHOR_START", RE2::Anchor::ANCHOR_START); @@ -281,48 +300,48 @@ PYBIND11_MODULE(_re2, module) { encoding.value("UTF8", RE2::Options::Encoding::EncodingUTF8); encoding.value("LATIN1", RE2::Options::Encoding::EncodingLatin1); - options.def(py::init<>()) - .def_property("max_mem", // - &RE2::Options::max_mem, // - &RE2::Options::set_max_mem) // - .def_property("encoding", // - &RE2::Options::encoding, // - &RE2::Options::set_encoding) // - .def_property("posix_syntax", // - &RE2::Options::posix_syntax, // - &RE2::Options::set_posix_syntax) // - .def_property("longest_match", // - &RE2::Options::longest_match, // - &RE2::Options::set_longest_match) // - .def_property("log_errors", // - &RE2::Options::log_errors, // - &RE2::Options::set_log_errors) // - .def_property("literal", // - &RE2::Options::literal, // - &RE2::Options::set_literal) // - .def_property("never_nl", // - &RE2::Options::never_nl, // - &RE2::Options::set_never_nl) // - .def_property("dot_nl", // - &RE2::Options::dot_nl, // - &RE2::Options::set_dot_nl) // - .def_property("never_capture", // - &RE2::Options::never_capture, // - &RE2::Options::set_never_capture) // - .def_property("case_sensitive", // - &RE2::Options::case_sensitive, // - &RE2::Options::set_case_sensitive) // - .def_property("perl_classes", // - &RE2::Options::perl_classes, // - &RE2::Options::set_perl_classes) // - .def_property("word_boundary", // - &RE2::Options::word_boundary, // - &RE2::Options::set_word_boundary) // - .def_property("one_line", // - &RE2::Options::one_line, // - &RE2::Options::set_one_line); // - - re2.def(py::init(&RE2InitShim)) + options.def(nb::init<>()) + .def_prop_rw("max_mem", // + &RE2::Options::max_mem, // + &RE2::Options::set_max_mem) // + .def_prop_rw("encoding", // + &RE2::Options::encoding, // + &RE2::Options::set_encoding) // + .def_prop_rw("posix_syntax", // + &RE2::Options::posix_syntax, // + &RE2::Options::set_posix_syntax) // + .def_prop_rw("longest_match", // + &RE2::Options::longest_match, // + &RE2::Options::set_longest_match) // + .def_prop_rw("log_errors", // + &RE2::Options::log_errors, // + &RE2::Options::set_log_errors) // + .def_prop_rw("literal", // + &RE2::Options::literal, // + &RE2::Options::set_literal) // + .def_prop_rw("never_nl", // + &RE2::Options::never_nl, // + &RE2::Options::set_never_nl) // + .def_prop_rw("dot_nl", // + &RE2::Options::dot_nl, // + &RE2::Options::set_dot_nl) // + .def_prop_rw("never_capture", // + &RE2::Options::never_capture, // + &RE2::Options::set_never_capture) // + .def_prop_rw("case_sensitive", // + &RE2::Options::case_sensitive, // + &RE2::Options::set_case_sensitive) // + .def_prop_rw("perl_classes", // + &RE2::Options::perl_classes, // + &RE2::Options::set_perl_classes) // + .def_prop_rw("word_boundary", // + &RE2::Options::word_boundary, // + &RE2::Options::set_word_boundary) // + .def_prop_rw("one_line", // + &RE2::Options::one_line, // + &RE2::Options::set_one_line); // + + re2.def(nb::new_(&RE2InitShim)) .def("ok", &RE2::ok) .def("error", &RE2ErrorShim) .def("options", &RE2::options) @@ -336,17 +355,16 @@ PYBIND11_MODULE(_re2, module) { .def("Match", &RE2MatchShim) .def_static("QuoteMeta", &RE2QuoteMetaShim); - set.def(py::init()) + set.def(nb::init()) .def("Add", &Set::Add) .def("Compile", &Set::Compile) .def("Match", &Set::Match); - filter.def(py::init<>()) + filter.def(nb::init<>()) .def("Add", &Filter::Add) .def("Compile", &Filter::Compile) .def("Match", &Filter::Match) - .def("GetRE2", &Filter::GetRE2, - py::return_value_policy::reference_internal); + .def("GetRE2", &Filter::GetRE2, nb::rv_policy::reference_internal); } } // namespace re2_python diff --git a/python/re2.py b/python/re2.py index 12c7b10f..1a7b9882 100644 --- a/python/re2.py +++ b/python/re2.py @@ -33,7 +33,7 @@ import _re2 -# pybind11 translates C++ exceptions to Python exceptions. +# nanobind translates C++ exceptions to Python exceptions. # We use that same Python exception class for consistency. error = _re2.Error diff --git a/python/setup.py b/python/setup.py index 94d5bc7a..732670d6 100644 --- a/python/setup.py +++ b/python/setup.py @@ -31,9 +31,9 @@ * Building requires RE2 to be installed on your system. On Debian, for example, install the libre2-dev package. - * Building requires pybind11 to be installed on your system OR venv. - On Debian, for example, install the pybind11-dev package. - For a venv, install the pybind11 package from PyPI. + * Building requires nanobind to be installed on your system OR venv. + On Debian, for example, install the nanobind-dev package. + For a venv, install the nanobind package from PyPI. * Building on macOS is known to work, but has been known to fail. For example, the system Python may not know which compiler flags to set when building bindings for software installed by Homebrew; @@ -65,9 +65,12 @@ def build_extension(self, ext): cmd.append(f'--macos_minimum_os={ver}') except KeyError: pass + tag = py_limited_api() + if tag: + cmd.append(f'--@nanobind_bazel//:py-limited-api={tag}') # Register the local Python toolchains with highest priority. cmd.append('--extra_toolchains=//python/toolchains:all') - cmd += ['--compilation_mode=opt', '--', ':all'] + cmd += ['--compilation_mode=opt', '--', ':_re2'] self.spawn(cmd) # This ensures that f'_re2.{importlib.machinery.EXTENSION_SUFFIXES[0]}' @@ -78,6 +81,12 @@ def build_extension(self, ext): cmd = ['bazel', 'clean', '--expunge'] self.spawn(cmd) +# When set, PY_LIMITED_API enables building an abi3 wheel. The value of the +# environment variable is its cpython floor, i.e. PY_LIMITED_API=cp312 creates +# a cp312-abi3 wheel. +def py_limited_api(): + return os.environ.get('PY_LIMITED_API') + def options(): bdist_wheel = {} @@ -85,23 +94,52 @@ def options(): bdist_wheel['plat_name'] = os.environ['PLAT_NAME'] except KeyError: pass + tag = py_limited_api() + if tag: + bdist_wheel['py_limited_api'] = tag return {'bdist_wheel': bdist_wheel} def include_dirs(): try: - import pybind11 - yield pybind11.get_include() + import nanobind except ModuleNotFoundError: - pass + return + yield nanobind.include_dir() + # include_dir() does not include nanobind's vendored robin_map dependency. + yield os.path.join( + os.path.dirname(nanobind.__file__), 'ext', 'robin_map', 'include') + + +def sources(): + yield '_re2.cc' + try: + import nanobind + except ModuleNotFoundError: + return + yield os.path.join(nanobind.source_dir(), 'nb_combined.cpp') + + +def define_macros(): + tag = py_limited_api() + if not tag: + return [] + match = re.fullmatch(r'cp3(\d+)', tag) + if not match: + raise ValueError(f'unsupported PY_LIMITED_API value: {tag!r}') + minor = int(match.group(1)) + # e.g. `cp312` -> 0x030C0000, the Py_LIMITED_API value for Python 3.12. + return [('Py_LIMITED_API', '0x03%02X0000' % minor)] ext_module = setuptools.Extension( name='_re2', - sources=['_re2.cc'], + sources=list(sources()), include_dirs=list(include_dirs()), libraries=['re2'], - extra_compile_args=['-fvisibility=hidden'], + define_macros=define_macros(), + extra_compile_args=['-fvisibility=hidden', '-std=c++17'], + py_limited_api=bool(py_limited_api()), ) # We need `re2` to be a package, not a module, because it appears that diff --git a/python/toolchains/generate.py b/python/toolchains/generate.py index 86dda142..59150cba 100644 --- a/python/toolchains/generate.py +++ b/python/toolchains/generate.py @@ -61,6 +61,7 @@ def generate(): py_cc_toolchain( name = "py_cc_toolchain", headers = ":headers", + headers_abi3 = ":headers", libs = ":libraries", python_version = "{major}.{minor}", ) @@ -75,10 +76,18 @@ def generate(): }}), ) +# Setting to switch the lib passed to Windows builds for unstable vs abi3 +config_setting( + name = "windows_unstable_abi", + constraint_values = ["@platforms//os:windows"], + flag_values = {{"@nanobind_bazel//:py-limited-api": "unset"}}, +) + cc_import( name = "interface_library", interface_library = select({{ - "@platforms//os:windows": "libs/python{major}{minor}.lib", + ":windows_unstable_abi": "libs/python{major}{minor}.lib", + "@platforms//os:windows": "libs/python3.lib", "//conditions:default": None, }}), system_provided = True,