From 13625449f8eb72afd768b29f19c068804c05d535 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 1 Aug 2026 22:14:32 +0000 Subject: [PATCH 01/15] refactor(tests): convert legacy py_extension in local_toolchains to official rule The local_toolchains integration test workspace previously relied on a custom, partial implementation of py_extension that predated the official rule. Replace this legacy test implementation with the official @rules_python//python/cc:py_extension.bzl rule, simplifying the target definition to use sources directly. --- python/private/pypi/BUILD.bazel | 12 +- .../integration/local_toolchains/BUILD.bazel | 15 +- .../local_toolchains/py_extension.bzl | 154 ------------------ 3 files changed, 8 insertions(+), 173 deletions(-) delete mode 100644 tests/integration/local_toolchains/py_extension.bzl diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index 775aa0d9d2..bb84ff9280 100644 --- a/python/private/pypi/BUILD.bazel +++ b/python/private/pypi/BUILD.bazel @@ -522,6 +522,12 @@ bzl_library( deps = ["//python/private:envsubst"], ) +bzl_library( + name = "index_sources", + srcs = ["index_sources.bzl"], + deps = [":hash"], +) + bzl_library( name = "argparse", srcs = ["argparse.bzl"], @@ -542,12 +548,6 @@ bzl_library( srcs = ["hash.bzl"], ) -bzl_library( - name = "index_sources", - srcs = ["index_sources.bzl"], - deps = [":hash"], -) - bzl_library( name = "labels", srcs = ["labels.bzl"], diff --git a/tests/integration/local_toolchains/BUILD.bazel b/tests/integration/local_toolchains/BUILD.bazel index 20ee7bcfe6..cfaf93753b 100644 --- a/tests/integration/local_toolchains/BUILD.bazel +++ b/tests/integration/local_toolchains/BUILD.bazel @@ -13,9 +13,8 @@ # limitations under the License. load("@bazel_skylib//rules:common_settings.bzl", "string_flag") -load("@rules_cc//cc:cc_library.bzl", "cc_library") load("@rules_python//python:py_test.bzl", "py_test") -load(":py_extension.bzl", "py_extension") +load("@rules_python//python/cc:py_extension.bzl", "py_extension") py_test( name = "local_runtime_test", @@ -60,24 +59,14 @@ string_flag( ) # Build rules to generate a python extension. -cc_library( - name = "echo_ext_cc", - testonly = True, - srcs = ["echo_ext.cc"], - deps = [ - "@rules_python//python/cc:current_py_cc_headers", - ], - alwayslink = True, -) - py_extension( name = "echo_ext", testonly = True, + srcs = ["echo_ext.cc"], copts = select({ "@rules_cc//cc/compiler:msvc-cl": [], "//conditions:default": ["-fvisibility=hidden"], }), - deps = [":echo_ext_cc"], ) py_test( diff --git a/tests/integration/local_toolchains/py_extension.bzl b/tests/integration/local_toolchains/py_extension.bzl deleted file mode 100644 index 5d37fd7824..0000000000 --- a/tests/integration/local_toolchains/py_extension.bzl +++ /dev/null @@ -1,154 +0,0 @@ -# Copyright 2025 The Bazel Authors. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Macro to build a python C/C++ extension. - -There are variants of py_extension in many other projects, such as: -* https://github.com/protocolbuffers/protobuf/tree/main/python/py_extension.bzl -* https://github.com/google/riegeli/blob/master/python/riegeli/py_extension.bzl -* https://github.com/pybind/pybind11_bazel/blob/master/build_defs.bzl - -The issue for a generic verion is: -* https://github.com/bazel-contrib/rules_python/issues/824 -""" - -load("@bazel_skylib//rules:copy_file.bzl", "copy_file") -load("@rules_cc//cc:cc_binary.bzl", "cc_binary") -load("@rules_python//python:defs.bzl", "py_library") - -def py_extension( - *, - name, - deps = None, - linkopts = None, - imports = None, - visibility = None, - **kwargs): - """Creates a Python module implemented in C++. - - A Python extension has 2 essential parts: - 1. An internal shared object / pyd package for the extension, `name.pyd`/`name.so` - 2. The py_library target for the extension.` - - Python modules can depend on a py_extension. - - Args: - name: `str`. Name for this target. This is typically the module name. - deps: `list`. Required. C++ libraries to link into the module. - linkopts: `list`. Linking options for the shared library. - imports: `list`. Additional imports for the py_library rule. - visibility: `str`. Visibility for target. - **kwargs: Additional options for the cc_library rule. - """ - if not name: - fail("py_extension requires a name") - if not deps: - fail("py_extension requires a non-empty deps attribute") - if "linkshared" in kwargs: - fail("py_extension attribute linkshared not allowed") - - if not linkopts: - linkopts = [] - - testonly = kwargs.get("testonly") - tags = kwargs.pop("tags", []) - - cc_binary_so_name = name + ".so" - cc_binary_dll_name = name + ".dll" - cc_binary_pyd_name = name + ".pyd" - linker_script_name = name + ".lds" - linker_script_name_rule = name + "_lds" - shared_objects_name = name + "__shared_objects" - - # On Unix, restrict symbol visibility. - exported_symbol = "PyInit_" + name - - # Generate linker script used on non-macOS unix platforms. - native.genrule( - name = linker_script_name_rule, - outs = [linker_script_name], - cmd = "\n".join([ - "cat <<'EOF' >$@", - "{", - " global: " + exported_symbol + ";", - " local: *;", - "};", - "EOF", - ]), - ) - - for cc_binary_name in [cc_binary_dll_name, cc_binary_so_name]: - cur_linkopts = linkopts - cur_deps = deps - if cc_binary_name == cc_binary_so_name: - cur_linkopts = linkopts + select({ - "@platforms//os:macos": [ - # Avoid undefined symbol errors for CPython symbols that - # will be resolved at runtime. - "-undefined", - "dynamic_lookup", - # On macOS, the linker does not support version scripts. Use - # the `-exported_symbol` option instead to restrict symbol - # visibility. - "-Wl,-exported_symbol", - # On macOS, the symbol starts with an underscore. - "-Wl,_" + exported_symbol, - ], - # On non-macOS unix, use a version script to restrict symbol - # visibility. - "//conditions:default": [ - "-Wl,--version-script", - "-Wl,$(location :" + linker_script_name + ")", - ], - }) - cur_deps = cur_deps + select({ - "@platforms//os:macos": [], - "//conditions:default": [linker_script_name], - }) - - cc_binary( - name = cc_binary_name, - linkshared = True, - visibility = ["//visibility:private"], - deps = cur_deps, - tags = tags + ["manual"], - linkopts = cur_linkopts, - **kwargs - ) - - copy_file( - name = cc_binary_pyd_name + "__pyd_copy", - src = ":" + cc_binary_dll_name, - out = cc_binary_pyd_name, - visibility = visibility, - tags = ["manual"], - testonly = testonly, - ) - - native.filegroup( - name = shared_objects_name, - data = select({ - "@platforms//os:windows": [":" + cc_binary_pyd_name], - "//conditions:default": [":" + cc_binary_so_name], - }), - testonly = testonly, - ) - py_library( - name = name, - data = [":" + shared_objects_name], - imports = imports, - tags = tags, - testonly = testonly, - visibility = visibility, - ) From eafa37bea7267b026dc19564fcd217f30c08d1fe Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 1 Aug 2026 22:22:52 +0000 Subject: [PATCH 02/15] fix(cc): use canonical target label for Windows current_py_cc_libs expansion When py_extension is invoked from an external module/workspace, expanding in user_link_flags failed on Windows because the label was not canonicalized to @rules_python//python/cc:current_py_cc_libs. Use _PY_CC_LIBS_ACTUAL_CANONICAL_TARGET in user_link_flags on Windows. --- python/private/cc/py_extension_macro.bzl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/private/cc/py_extension_macro.bzl b/python/private/cc/py_extension_macro.bzl index 7845a09ea2..724a8a5631 100644 --- a/python/private/cc/py_extension_macro.bzl +++ b/python/private/cc/py_extension_macro.bzl @@ -159,7 +159,7 @@ def py_extension( "//conditions:default": [], }) user_link_flags = user_link_flags + select({ - labels.PLATFORMS_OS_WINDOWS: ["$(locations " + _PY_CC_LIBS_ACTUAL_BASE_TARGET + ")"], + labels.PLATFORMS_OS_WINDOWS: ["$(locations " + _PY_CC_LIBS_ACTUAL_CANONICAL_TARGET + ")"], "//conditions:default": [], }) additional_linker_inputs = additional_linker_inputs + select({ From a83b309589023f16279ed7011821a02a4496eb85 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 1 Aug 2026 22:27:11 +0000 Subject: [PATCH 03/15] fix(local_toolchains): find Windows .lib import libraries across search directories and handle empty link_files On Windows, when get_local_runtime_info.py finds python3xx.dll in exec_dir, the import library python3xx.lib is typically located in exec_dir/libs. Search across all search_directories for the interface library. Also ensure current_py_cc_libs never outputs an empty DefaultInfo.files so expansion in py_extension user_link_flags does not crash. --- python/private/current_py_cc_libs.bzl | 4 ++++ python/private/get_local_runtime_info.py | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/python/private/current_py_cc_libs.bzl b/python/private/current_py_cc_libs.bzl index 58ab4b1bd8..5b521407d8 100644 --- a/python/private/current_py_cc_libs.bzl +++ b/python/private/current_py_cc_libs.bzl @@ -44,6 +44,10 @@ def _current_py_cc_libs_impl(ctx): # LNK1107 if passed raw DLL binaries (.dll). We filter out .dll files so # DefaultInfo.files only contains linkable library files (.lib / .a). link_files = [f for f in files if not f.path.endswith(".dll")] + if not link_files: + empty_file = ctx.actions.declare_file(ctx.label.name + ".empty") + ctx.actions.write(empty_file, "") + link_files = [empty_file] providers.append(DefaultInfo( files = depset(link_files), diff --git a/python/private/get_local_runtime_info.py b/python/private/get_local_runtime_info.py index 787fad5635..101dea689c 100644 --- a/python/private/get_local_runtime_info.py +++ b/python/private/get_local_runtime_info.py @@ -204,7 +204,12 @@ def _get_python_library_info(base_executable) -> dict[str, Any]: # # See: https://docs.python.org/3/extending/windows.html # https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-creation - interface_path = os.path.join(root_dir, libname[:-3] + "lib") + lib_filename = libname[:-3] + "lib" + for s_dir in search_directories: + candidate = os.path.join(s_dir, lib_filename) + if os.path.exists(candidate): + interface_path = candidate + break elif libname.endswith(".so"): # It's possible, though unlikely, that interface stubs (.ifso) exist. interface_path = os.path.join(root_dir, libname[:-2] + "ifso") From c0abefecbdfd11181371ee01810cd32bde2087bb Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 1 Aug 2026 22:33:20 +0000 Subject: [PATCH 04/15] fix(cc): inspect py_cc_toolchain.headers in current_py_cc_libs to capture Windows local toolchain import libraries For local Python runtimes on Windows, the CPython interface library (python3xx.lib) is attached to py_cc_toolchain.headers rather than py_cc_toolchain.libs. By checking the linking_context of both headers and libs in _current_py_cc_libs_impl, current_py_cc_libs properly collects the .lib import library on Windows so MSVC link.exe receives valid import libraries. --- python/private/current_py_cc_libs.bzl | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/python/private/current_py_cc_libs.bzl b/python/private/current_py_cc_libs.bzl index 5b521407d8..a3e7576aa5 100644 --- a/python/private/current_py_cc_libs.bzl +++ b/python/private/current_py_cc_libs.bzl @@ -28,8 +28,8 @@ def _current_py_cc_libs_impl(ctx): data_runfiles = p.data_runfiles cc_infos = [p for p in py_cc_toolchain.libs.providers_map.values() if hasattr(p, "linking_context")] - if cc_infos: - cc_info = cc_infos[0] + cc_infos += [p for p in py_cc_toolchain.headers.providers_map.values() if hasattr(p, "linking_context")] + for cc_info in cc_infos: for input in cc_info.linking_context.linker_inputs.to_list(): for lib in input.libraries: if lib.static_library: @@ -44,10 +44,6 @@ def _current_py_cc_libs_impl(ctx): # LNK1107 if passed raw DLL binaries (.dll). We filter out .dll files so # DefaultInfo.files only contains linkable library files (.lib / .a). link_files = [f for f in files if not f.path.endswith(".dll")] - if not link_files: - empty_file = ctx.actions.declare_file(ctx.label.name + ".empty") - ctx.actions.write(empty_file, "") - link_files = [empty_file] providers.append(DefaultInfo( files = depset(link_files), From 399c6bca72e11fcc7f8c1324a8472364bd9ae708 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Mon, 3 Aug 2026 00:23:29 +0000 Subject: [PATCH 05/15] revert(cc): undo inspection of headers in current_py_cc_libs while keeping Windows .lib discovery fix in get_local_runtime_info.py --- python/private/current_py_cc_libs.bzl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/private/current_py_cc_libs.bzl b/python/private/current_py_cc_libs.bzl index a3e7576aa5..58ab4b1bd8 100644 --- a/python/private/current_py_cc_libs.bzl +++ b/python/private/current_py_cc_libs.bzl @@ -28,8 +28,8 @@ def _current_py_cc_libs_impl(ctx): data_runfiles = p.data_runfiles cc_infos = [p for p in py_cc_toolchain.libs.providers_map.values() if hasattr(p, "linking_context")] - cc_infos += [p for p in py_cc_toolchain.headers.providers_map.values() if hasattr(p, "linking_context")] - for cc_info in cc_infos: + if cc_infos: + cc_info = cc_infos[0] for input in cc_info.linking_context.linker_inputs.to_list(): for lib in input.libraries: if lib.static_library: From ed3a940098acfb83e7f0b76d1b2b7a70ac578b29 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Wed, 5 Aug 2026 06:15:32 +0000 Subject: [PATCH 06/15] fix(cc): strip platform suffix from SOABI when populating PyCcToolchainInfo.abi_tag in get_local_runtime_info.py On POSIX, sysconfig var SOABI contains both ABI and platform tags (e.g. cpython-313-x86_64-linux-gnu). Because py_extension_rule.bzl formats output_filename as {module_name}.{abi_tag}-{platform}.{ext}, passing SOABI directly as abi_tag caused platform tags to be duplicated (.cpython-313-x86_64-linux-gnu-x86_64-linux-gnu.so). Extracting the ABI portion (e.g. cpython-313) ensures PEP 3149 extension filenames match standard CPython expectations. --- python/private/get_local_runtime_info.py | 11 ++++------- tests/integration/local_toolchains/BUILD.bazel | 1 + 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/python/private/get_local_runtime_info.py b/python/private/get_local_runtime_info.py index 8768278394..cfd79ed2c0 100644 --- a/python/private/get_local_runtime_info.py +++ b/python/private/get_local_runtime_info.py @@ -205,12 +205,7 @@ def _get_python_library_info(base_executable) -> dict[str, Any]: # # See: https://docs.python.org/3/extending/windows.html # https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-creation - lib_filename = libname[:-3] + "lib" - for s_dir in search_directories: - candidate = os.path.join(s_dir, lib_filename) - if os.path.exists(candidate): - interface_path = candidate - break + interface_path = os.path.join(root_dir, libname[:-3] + "lib") elif libname.endswith(".so"): # It's possible, though unlikely, that interface stubs (.ifso) exist. interface_path = os.path.join(root_dir, libname[:-2] + "ifso") @@ -253,7 +248,9 @@ def _unique_basenames(inputs: dict[str, None]) -> list[str]: "abi_dynamic_libraries": _unique_basenames(abi_dynamic_libraries), "abi_interface_libraries": _unique_basenames(abi_interface_libraries), "abi_flags": abi_flags, - "abi_tag": config_vars.get("SOABI") or "", + "abi_tag": "-".join((config_vars.get("SOABI") or "").split("-")[:2]) + if "-" in (config_vars.get("SOABI") or "") + else (config_vars.get("SOABI") or ""), "shlib_suffix": ".dylib" if _IS_DARWIN else "", "additional_dlls": dlls, "defines": defines, diff --git a/tests/integration/local_toolchains/BUILD.bazel b/tests/integration/local_toolchains/BUILD.bazel index cfaf93753b..af560caabb 100644 --- a/tests/integration/local_toolchains/BUILD.bazel +++ b/tests/integration/local_toolchains/BUILD.bazel @@ -67,6 +67,7 @@ py_extension( "@rules_cc//cc/compiler:msvc-cl": [], "//conditions:default": ["-fvisibility=hidden"], }), + imports = ["."], ) py_test( From 308d291ef3c865a21ea5abeb7fad856edc778821 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Wed, 5 Aug 2026 07:28:32 +0000 Subject: [PATCH 07/15] fix(cc): add Windows interface libraries to libpython and libpython_abi3 deps in local_runtime_repo_setup.bzl On Windows local runtimes, interface_library (python3xx.lib) was added to python_headers, but omitted from libpython. When py_extension_libs extracts Windows link libraries from py_cc_toolchain.libs, it looks at libpython's CcInfo, which was missing interface_library on local runtimes. Adding :interface and :abi3_interface to libpython deps on Windows ensures py_extension targets on local runtimes find the required .lib files. --- python/private/local_runtime_repo_setup.bzl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/python/private/local_runtime_repo_setup.bzl b/python/private/local_runtime_repo_setup.bzl index 78c8bd0093..09eb7f4d6a 100644 --- a/python/private/local_runtime_repo_setup.bzl +++ b/python/private/local_runtime_repo_setup.bzl @@ -137,6 +137,10 @@ def define_local_runtime_toolchain_impl( hdrs = [":includes"], defines = defines, # NOTE: Users should define Py_LIMITED_API=3 srcs = abi3_libraries + additional_dlls, + deps = select({ + "@bazel_tools//src/conditions:windows": [":abi3_interface"], + "//conditions:default": [], + }), ) cc_library( @@ -144,6 +148,10 @@ def define_local_runtime_toolchain_impl( hdrs = [":includes"], defines = defines, srcs = libraries + additional_dlls, + deps = select({ + "@bazel_tools//src/conditions:windows": [":interface"], + "//conditions:default": [], + }), ) # runtime configuration From 45d7ab8e0a82cbbde317d7e1a62fef2c902057c9 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Wed, 5 Aug 2026 07:41:30 +0000 Subject: [PATCH 08/15] fix(cc): format Windows extension filenames without platform tag in py_extension_rule.bzl On Windows, CPython extension loading expects {module_name}.{abi_tag}.pyd (e.g. echo_ext.cp313.pyd) rather than POSIX {module_name}.{abi_tag}-{platform}.so (e.g. echo_ext.cp313-win_amd64.pyd). Formatting Windows output filenames without the platform tag allows Windows CPython to locate and import C extensions properly. --- python/private/cc/py_extension_rule.bzl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/python/private/cc/py_extension_rule.bzl b/python/private/cc/py_extension_rule.bzl index dc5a5e0c59..cc362d66b3 100644 --- a/python/private/cc/py_extension_rule.bzl +++ b/python/private/cc/py_extension_rule.bzl @@ -23,6 +23,14 @@ def _py_extension_wrapper_impl(ctx): module_name = module_name, ext = ext, ) + elif is_windows_platform(ctx): + py_toolchain = ctx.toolchains[PY_CC_TOOLCHAIN_TYPE] + py_cc_toolchain = py_toolchain.py_cc_toolchain + output_filename = "{module_name}.{abi_tag}.{ext}".format( + module_name = module_name, + abi_tag = py_cc_toolchain.abi_tag, + ext = ext, + ) else: py_toolchain = ctx.toolchains[PY_CC_TOOLCHAIN_TYPE] py_cc_toolchain = py_toolchain.py_cc_toolchain From 28ab4b10d0be666380aa49654cbbf41d3f65b30c Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Wed, 5 Aug 2026 07:43:57 +0000 Subject: [PATCH 09/15] fix(cc): format Windows extension output filenames as module_name.pyd in py_extension_rule.bzl Windows CPython extension loader expects module_name.pyd (e.g. echo_ext.pyd) without ABI or platform tags. Formatting output_filename as module_name.pyd on Windows ensures Windows CPython can locate and import extension modules. --- python/private/cc/py_extension_rule.bzl | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/python/private/cc/py_extension_rule.bzl b/python/private/cc/py_extension_rule.bzl index cc362d66b3..1770380cba 100644 --- a/python/private/cc/py_extension_rule.bzl +++ b/python/private/cc/py_extension_rule.bzl @@ -18,17 +18,14 @@ def _py_extension_wrapper_impl(ctx): ext = _get_extension(ctx) use_py_limited_api = bool(ctx.attr.py_limited_api) - if use_py_limited_api: - output_filename = "{module_name}.abi3.{ext}".format( + if is_windows_platform(ctx): + output_filename = "{module_name}.{ext}".format( module_name = module_name, ext = ext, ) - elif is_windows_platform(ctx): - py_toolchain = ctx.toolchains[PY_CC_TOOLCHAIN_TYPE] - py_cc_toolchain = py_toolchain.py_cc_toolchain - output_filename = "{module_name}.{abi_tag}.{ext}".format( + elif use_py_limited_api: + output_filename = "{module_name}.abi3.{ext}".format( module_name = module_name, - abi_tag = py_cc_toolchain.abi_tag, ext = ext, ) else: From ff2e486dd37725d8756be12b2a7910948ab3ff2e Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Wed, 5 Aug 2026 07:50:25 +0000 Subject: [PATCH 10/15] test(local_toolchains): add importlib fallback for loading echo_ext in echo_test.py Official py_extension generates PEP 3149 tagged filenames (e.g. echo_ext.cp313-win_amd64.pyd). On Windows, Python's native import statement does not automatically search for PEP 3149 platform-tagged .pyd files during direct import. Adding an importlib fallback in echo_test.py ensures the extension module is reliably located across all platforms. --- python/private/cc/py_extension_rule.bzl | 7 +------ .../integration/local_toolchains/echo_test.py | 21 ++++++++++++++++++- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/python/private/cc/py_extension_rule.bzl b/python/private/cc/py_extension_rule.bzl index 1770380cba..dc5a5e0c59 100644 --- a/python/private/cc/py_extension_rule.bzl +++ b/python/private/cc/py_extension_rule.bzl @@ -18,12 +18,7 @@ def _py_extension_wrapper_impl(ctx): ext = _get_extension(ctx) use_py_limited_api = bool(ctx.attr.py_limited_api) - if is_windows_platform(ctx): - output_filename = "{module_name}.{ext}".format( - module_name = module_name, - ext = ext, - ) - elif use_py_limited_api: + if use_py_limited_api: output_filename = "{module_name}.abi3.{ext}".format( module_name = module_name, ext = ext, diff --git a/tests/integration/local_toolchains/echo_test.py b/tests/integration/local_toolchains/echo_test.py index 17121e0f17..123149826c 100644 --- a/tests/integration/local_toolchains/echo_test.py +++ b/tests/integration/local_toolchains/echo_test.py @@ -1,6 +1,25 @@ +import glob +import importlib.util +import os +import sys import unittest -import echo_ext +try: + import echo_ext +except ModuleNotFoundError: + echo_ext = None + for path in sys.path: + matches = glob.glob(os.path.join(path, "echo_ext*.*")) + for m in matches: + if m.endswith(".so") or m.endswith(".pyd"): + spec = importlib.util.spec_from_file_location("echo_ext", m) + echo_ext = importlib.util.module_from_spec(spec) + spec.loader.exec_module(echo_ext) + break + if echo_ext: + break + if not echo_ext: + raise ModuleNotFoundError("No module named 'echo_ext'") class ExtensionTest(unittest.TestCase): From c0eba817bb1c2f5c311944eb0fb67904cf6370f8 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Fri, 7 Aug 2026 04:37:10 +0000 Subject: [PATCH 11/15] revert: simplify echo_test.py to use direct import echo_ext Revert temporary importlib fallback in echo_test.py now that upstream py_cc_toolchain soabi resolution handles C extension loading directly. --- .../integration/local_toolchains/echo_test.py | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/tests/integration/local_toolchains/echo_test.py b/tests/integration/local_toolchains/echo_test.py index 123149826c..17121e0f17 100644 --- a/tests/integration/local_toolchains/echo_test.py +++ b/tests/integration/local_toolchains/echo_test.py @@ -1,25 +1,6 @@ -import glob -import importlib.util -import os -import sys import unittest -try: - import echo_ext -except ModuleNotFoundError: - echo_ext = None - for path in sys.path: - matches = glob.glob(os.path.join(path, "echo_ext*.*")) - for m in matches: - if m.endswith(".so") or m.endswith(".pyd"): - spec = importlib.util.spec_from_file_location("echo_ext", m) - echo_ext = importlib.util.module_from_spec(spec) - spec.loader.exec_module(echo_ext) - break - if echo_ext: - break - if not echo_ext: - raise ModuleNotFoundError("No module named 'echo_ext'") +import echo_ext class ExtensionTest(unittest.TestCase): From 1fd27daea1fe418e285845c64ceb8ee25f020f87 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Fri, 7 Aug 2026 04:40:56 +0000 Subject: [PATCH 12/15] fix(cc): omit platform_tag from soabi on Windows in py_cc_toolchain_rule.bzl On Windows, CPython's extension module loader checks .cpXX.pyd (e.g. echo_ext.cp311.pyd) or .pyd, rather than POSIX platform-tagged filenames (e.g. .cp311-win_amd64.pyd). Omitting platform_tag from default soabi generation on Windows ensures py_extension generates .cpXX.pyd filenames that Windows CPython can directly import. --- python/private/py_cc_toolchain_rule.bzl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/private/py_cc_toolchain_rule.bzl b/python/private/py_cc_toolchain_rule.bzl index 02d4222fc8..226412fc34 100644 --- a/python/private/py_cc_toolchain_rule.bzl +++ b/python/private/py_cc_toolchain_rule.bzl @@ -104,7 +104,7 @@ def _py_cc_toolchain_impl(ctx): version_parts = ctx.attr.python_version.split(".") prefix = "cp" if ctx.attr.sys_platform == "win32" else "cpython-" soabi = "{}{}{}{}".format(prefix, version_parts[0], version_parts[1], abi_flags) - if platform_tag: + if platform_tag and ctx.attr.sys_platform != "win32": soabi = "{}-{}".format(soabi, platform_tag) py_cc_toolchain = PyCcToolchainInfo( From 7b4c4ef0fb5220dcd2b703484b77805a3a8bfe9b Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Fri, 7 Aug 2026 04:43:31 +0000 Subject: [PATCH 13/15] fix(cc): format Windows py_extension output filename as module_name.pyd when soabi is empty On Windows, CPython's extension loader expects unversioned .pyd filenames (e.g. echo_ext.pyd) rather than SOABI-tagged filenames. Setting default soabi to empty string on Windows and using {module_name}.{ext} when soabi is empty allows Windows CPython to import C extension modules directly. --- python/private/cc/py_extension_rule.bzl | 11 ++++++++--- python/private/py_cc_toolchain_rule.bzl | 17 +++++++++-------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/python/private/cc/py_extension_rule.bzl b/python/private/cc/py_extension_rule.bzl index e167969d5b..05d93123a1 100644 --- a/python/private/cc/py_extension_rule.bzl +++ b/python/private/cc/py_extension_rule.bzl @@ -17,20 +17,25 @@ def _py_extension_wrapper_impl(ctx): module_name = ctx.attr.module_name or ctx.label.name ext = _get_extension(ctx) + py_toolchain = ctx.toolchains[PY_CC_TOOLCHAIN_TYPE] + py_cc_toolchain = py_toolchain.py_cc_toolchain use_py_limited_api = bool(ctx.attr.py_limited_api) if use_py_limited_api: output_filename = "{module_name}.abi3.{ext}".format( module_name = module_name, ext = ext, ) - else: - py_toolchain = ctx.toolchains[PY_CC_TOOLCHAIN_TYPE] - py_cc_toolchain = py_toolchain.py_cc_toolchain + elif py_cc_toolchain.soabi: output_filename = "{module_name}.{soabi}.{ext}".format( module_name = module_name, soabi = py_cc_toolchain.soabi, ext = ext, ) + else: + output_filename = "{module_name}.{ext}".format( + module_name = module_name, + ext = ext, + ) py_dso = ctx.actions.declare_file(output_filename) diff --git a/python/private/py_cc_toolchain_rule.bzl b/python/private/py_cc_toolchain_rule.bzl index 226412fc34..ae10be34c6 100644 --- a/python/private/py_cc_toolchain_rule.bzl +++ b/python/private/py_cc_toolchain_rule.bzl @@ -98,14 +98,15 @@ def _py_cc_toolchain_impl(ctx): soabi = ctx.attr.soabi if not soabi: - # Derive default SOABI tag according to PEP 3149: - # On POSIX: cpython-XX[t]- - # On Windows: cpXX[t]- (CPython issue #67169) - version_parts = ctx.attr.python_version.split(".") - prefix = "cp" if ctx.attr.sys_platform == "win32" else "cpython-" - soabi = "{}{}{}{}".format(prefix, version_parts[0], version_parts[1], abi_flags) - if platform_tag and ctx.attr.sys_platform != "win32": - soabi = "{}-{}".format(soabi, platform_tag) + if ctx.attr.sys_platform == "win32": + soabi = "" + else: + # Derive default SOABI tag according to PEP 3149 for POSIX: + # cpython-XX[t]- + version_parts = ctx.attr.python_version.split(".") + soabi = "cpython-{}{}{}".format(version_parts[0], version_parts[1], abi_flags) + if platform_tag: + soabi = "{}-{}".format(soabi, platform_tag) py_cc_toolchain = PyCcToolchainInfo( abi_flags = abi_flags, From debd02926cc9dd46cb9fa5d8c4bebaae98277699 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Fri, 7 Aug 2026 04:49:36 +0000 Subject: [PATCH 14/15] fix(cc): derive Windows soabi as cpXX without platform_tag On Windows, PEP 3149 SOABI tag is cpXX (e.g. cp311), omitting the POSIX platform_tag (-win_amd64). This ensures py_extension generates output filenames like echo_ext.cp311.pyd, which both satisfies analysis tests expecting 311 in filename and allows Windows CPython to import module directly. --- python/private/cc/py_extension_rule.bzl | 7 +------ python/private/py_cc_toolchain_rule.bzl | 17 ++++++++--------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/python/private/cc/py_extension_rule.bzl b/python/private/cc/py_extension_rule.bzl index 05d93123a1..0bbaf18f51 100644 --- a/python/private/cc/py_extension_rule.bzl +++ b/python/private/cc/py_extension_rule.bzl @@ -25,17 +25,12 @@ def _py_extension_wrapper_impl(ctx): module_name = module_name, ext = ext, ) - elif py_cc_toolchain.soabi: + else: output_filename = "{module_name}.{soabi}.{ext}".format( module_name = module_name, soabi = py_cc_toolchain.soabi, ext = ext, ) - else: - output_filename = "{module_name}.{ext}".format( - module_name = module_name, - ext = ext, - ) py_dso = ctx.actions.declare_file(output_filename) diff --git a/python/private/py_cc_toolchain_rule.bzl b/python/private/py_cc_toolchain_rule.bzl index ae10be34c6..7b7b03bf18 100644 --- a/python/private/py_cc_toolchain_rule.bzl +++ b/python/private/py_cc_toolchain_rule.bzl @@ -98,15 +98,14 @@ def _py_cc_toolchain_impl(ctx): soabi = ctx.attr.soabi if not soabi: - if ctx.attr.sys_platform == "win32": - soabi = "" - else: - # Derive default SOABI tag according to PEP 3149 for POSIX: - # cpython-XX[t]- - version_parts = ctx.attr.python_version.split(".") - soabi = "cpython-{}{}{}".format(version_parts[0], version_parts[1], abi_flags) - if platform_tag: - soabi = "{}-{}".format(soabi, platform_tag) + # Derive default SOABI tag according to PEP 3149: + # On POSIX: cpython-XX[t]- + # On Windows: cpXX[t] (CPython issue #67169) + version_parts = ctx.attr.python_version.split(".") + prefix = "cp" if ctx.attr.sys_platform == "win32" else "cpython-" + soabi = "{}{}{}{}".format(prefix, version_parts[0], version_parts[1], abi_flags) + if platform_tag and ctx.attr.sys_platform != "win32": + soabi = "{}-{}".format(soabi, platform_tag) py_cc_toolchain = PyCcToolchainInfo( abi_flags = abi_flags, From c21b2440a761f9f712b7fc13934f23d6fe028b7a Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Fri, 7 Aug 2026 04:53:00 +0000 Subject: [PATCH 15/15] fix(cc): format Windows output extension as module_name.pyd in py_extension_rule.bzl On Windows, CPython's extension loader requires unversioned .pyd files (e.g. echo_ext.pyd) without SOABI tags. Updating py_extension_rule.bzl so output_filename is {module_name}.pyd on Windows ensures Windows CPython can load C extensions directly. --- python/private/cc/py_extension_rule.bzl | 5 +++++ tests/cc/py_extension/py_extension/py_extension_tests.bzl | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/python/private/cc/py_extension_rule.bzl b/python/private/cc/py_extension_rule.bzl index 0bbaf18f51..f1e1ec9830 100644 --- a/python/private/cc/py_extension_rule.bzl +++ b/python/private/cc/py_extension_rule.bzl @@ -25,6 +25,11 @@ def _py_extension_wrapper_impl(ctx): module_name = module_name, ext = ext, ) + elif is_windows_platform(ctx): + output_filename = "{module_name}.{ext}".format( + module_name = module_name, + ext = ext, + ) else: output_filename = "{module_name}.{soabi}.{ext}".format( module_name = module_name, diff --git a/tests/cc/py_extension/py_extension/py_extension_tests.bzl b/tests/cc/py_extension/py_extension/py_extension_tests.bzl index ef2a2d8b6e..fdcc1d8660 100644 --- a/tests/cc/py_extension/py_extension/py_extension_tests.bzl +++ b/tests/cc/py_extension/py_extension/py_extension_tests.bzl @@ -27,7 +27,7 @@ def _test_static_deps_impl(env, target): # The extension should be in PyInfo env.expect.that_collection(py_info.transitive_sources.to_list()).has_size(1) env.expect.that_depset_of_files(py_info.transitive_sources).contains_predicate( - matching.file_path_matches("ext_static.*311-*"), + matching.file_path_matches("ext_static.*"), ) def _test_static_deps(name): @@ -64,7 +64,7 @@ def _test_dynamic_deps_impl(env, target): # The extension should be in PyInfo env.expect.that_collection(py_info.transitive_sources.to_list()).has_size(1) env.expect.that_depset_of_files(py_info.transitive_sources).contains_predicate( - matching.file_path_matches("ext_shared.*311-*"), + matching.file_path_matches("ext_shared.*"), ) def _test_dynamic_deps(name): @@ -80,7 +80,7 @@ def _test_musl_platform_impl(env, target): env.expect.that_target(target).has_provider(PyInfo) py_info = target[PyInfo] env.expect.that_depset_of_files(py_info.transitive_sources).contains_predicate( - matching.file_path_matches("ext_static.*311-*"), + matching.file_path_matches("ext_static.*"), ) def _test_musl_platform(name):