Skip to content

feat(cc): introduce py_extension rule for C/C++ Python extension modules - #3973

Merged
rickeylev merged 99 commits into
mainfrom
py-extension
Aug 1, 2026
Merged

feat(cc): introduce py_extension rule for C/C++ Python extension modules#3973
rickeylev merged 99 commits into
mainfrom
py-extension

Conversation

@rickeylev

@rickeylev rickeylev commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Building C/C++ Python extension modules currently requires manually
configuring complex cc_binary or cc_shared_library targets and
setting up platform, ABI3, and extension tags. The new
py_extension rule provides a high-level Starlark API for compiling
native extensions that integrate seamlessly with rules_python
toolchains and py_library dependencies.

At a high level, the implementation is fairly simple:
Users can provide raw C/C++ srcs, which are put into a cc_library.
That cc_library is then given to cc_shared_library, which performs
the linking to create e.g. libfoo.so. That artifact is then given
to a custom rule that handles rename it according to Python needs,
e.g. foo.abi3.so.

cc_shared_library is used because it provides various facilities
to control creation of a shared library, namely, control over what
is and isn't linked into the shared library.

Credit to @rsartor-cmd for finishing the prototype, expanding test
coverage, and verifying behavior.

Closes #3283.

rickeylev and others added 17 commits September 22, 2025 19:54
...and sycophantic toasters are bad collaborators
This builds on the existing `py-extension` branch. It adds support for
platform and abi tags in the resulting library filename. The
`:py_extension_test`, `:py_extension_analysis_tests`, and
`:py_limited_api_tests` test targets in `//tests/cc/py_extension` now
build and pass.

Relates to #3283
…tension (#3875)

Per feedback from #3851, this PR re-works the platform detection logic
to rely on platform constraints instead of the Python runtime/toolchain.

### Description

This PR contains updates to the experimental `py_extension`
implementation. It re-works the platform tag detection to rely on modern
platform constraints, and refactors the compilation and linking
mechanism
to delegate to Bazel's native `cc_shared_library` and `cc_library`
rules.

This PR targets the main repository's `py-extension` branch (not
`main`).

### Key Changes

#### 1. Platform Tag & ABI Tag Derivation
* **Introduced `abi_tag` to `PyCcToolchainInfo`**: Added the `abi_tag`
field to the `PyCcToolchainInfo` provider, populated by the
`py_cc_toolchain` rule. It defaults to deriving the tag from
`python_version` (e.g. `cpython-311`) for backward compatibility.
* **Constraint-Based Detection**: Replaced legacy parsing of
`cc_toolchain` CPU names with direct lookup in rules_python's central
`PLATFORMS` registry using platform constraints.
* **Limited API Configuration**: Updated how limited API (`.abi3.so`)
suffixes are appended based on the toolchain configuration.

#### 2. Compilation & Linking Delegation (Refactor to
`cc_shared_library`)
* **Macro Fusion**: Replaced the custom linking logic in the
`py_extension` rule with a macro of the same name. It now accepts C/C++
source/header files directly (`srcs`, `hdrs`, `copts`, `defines`),
implicitly wrapping them in a private `cc_library` under the hood.
* **Consolidated Attributes**: Consolidated static linkage under
standard `deps` (removing the redundant `static_deps` attribute) and
aligned linker arguments with `cc_shared_library`'s `user_link_flags`.
* **Wrapper Rule (`_py_extension_wrapper`)**: Added a lightweight,
private rule that wraps the `cc_shared_library` output to:
* Query the Python toolchain for platform/ABI tags to resolve PEP 3149
compliant filenames (e.g. `.cpython-311-x86_64-linux-gnu.so` or
`.abi3.so`).
*   Create a cheap `symlink` from the CSL output to the PEP 3149 name.
* Propagate `PyInfo` (for python rules) and `CcSharedLibraryInfo` (for
dynamic C++ dependencies).
* **ODR Validation**: Leveraging native `cc_shared_library` ensures
strict analysis-time validation against One Definition Rule (ODR)
violations (e.g., duplicate static linkage in dynamic chains).

#### 3. Runfiles and Data Support
* **Runfiles Propagation**: Re-implemented runfiles collection using the
repository's standard `builders.RunfilesBuilder` to support runtime
assets (`data` attribute) and dynamic library dependency
propagation.

### Verification & Testing
* Added parity test cases in `dependency_graph_tests.bzl` to verify
dynamic dependency chains and static sharing behavior.
* Added runfiles validation in `py_extension_tests.bzl` to verify data
asset propagation.
*   Updated limited API tests to match CSL output structure.
* All 14 tests in `//tests/cc/py_extension/...` are compiling and
passing.
──────
TAG=agy
CONV=23f8a5e8-2d99-401a-9903-1256b7d42b0e

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Why:
Merge the latest upstream changes into py-extension to keep the branch up to date and compatible with Starlarkified rules_cc in Bazel 9.

How:
- Resolve conflict in dev/pyproject.toml and remove stale docs/requirements.txt.
- Explicitly load CcSharedLibraryInfo from rules_cc.
- Use cc_common.link without main_output to avoid private API restrictions, naming the intermediate library with a leading underscore and .pyextimpl suffix.
- Add missing bzl_library targets and fix header includes in tests.
Why:
Integrate upstream/py-extension (#3875) platform detection logic and cc_shared_library macro wrapper architecture into our local branch.

How:
- Resolve conflict in py_extension_rule.bzl in favor of upstream wrapper implementation.
- Preserve runfiles dynamic lookup test in py_extension_test.py.
- Fix function docstring header formatting in py_cc_toolchain_rule.bzl.
…e type annotations

Why:
Mark py_extension as a public but volatile API in documentation and improve docstring parameter type readability.

How:
- Add volatile API inclusion block to py_extension macro docstring.
- Cross-reference py_library.imports using {attr}.
- Add {type} annotations for all parameters and remove redundant Optional wording.
…3971)

Per feedback from #3875 , this PR makes a few changes:
* Removed an unnecessary hasattr check on py_cc_toolchain
* Renamed the arbitrary "os" and "cpu" identifiers to terms standardized
in PEP 508.

Also marks `py_extension` as experimental.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Richard Levasseur <richardlev@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Why:
The py_cc_toolchain_rule.bzl loads flags.bzl, but :flags was missing from the deps attribute of //python/private:py_cc_toolchain_rule, breaking starlark doc extraction in docs build test.

How:
Add :flags to deps of //python/private:py_cc_toolchain_rule in python/private/BUILD.bazel.
…brary

Why:
py_cc_toolchain_macro.bzl loads //python/private/pypi:pep508_env.bzl, but the dependency was missing from the py_cc_toolchain_macro target in //python/private:BUILD.bazel.

How:
Add //python/private/pypi:pep508_env to deps of py_cc_toolchain_macro in python/private/BUILD.bazel.
@rickeylev
rickeylev requested a review from aignas as a code owner July 29, 2026 03:46
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@rickeylev rickeylev added the do not merge Tag that prevents merging label Jul 29, 2026
@rickeylev
rickeylev marked this pull request as draft July 29, 2026 03:48
…D file

Why:
CI failed because tests/cc/py_extension/BUILD.bazel failed Buildifier checks and python/private/cc/BUILD.bazel referenced invalid bzl_library target names from @rules_cc.

How:
- Run buildifier to format tests/cc/py_extension/BUILD.bazel.
- Update py_extension_macro and py_extension_rule bzl_library deps in python/private/cc/BUILD.bazel to use correct target names.
Why:
WORKSPACE mode builds failed on Buildkite because bzlmod-only target names were used for rules_cc bzl_library dependencies.

How:
Use @rules_cc//cc:core_rules and @rules_cc//cc/common in python/private/cc/BUILD.bazel, which are supported in both WORKSPACE mode and bzlmod mode.
Why:
Linking C extension shared libraries (.dylib) on macOS failed with undefined Python C symbols because ld requires -undefined dynamic_lookup when symbols are resolved at runtime.

How:
Add default user_link_flags with -undefined dynamic_lookup for @platforms//os:osx and @platforms//os:macos in py_extension_macro.bzl.
Why:
GitHub actions CI failed on the ruff format check due to an extra blank line in py_extension_pkg_test.py.

How:
Reformat tests/cc/py_extension/py_extension_pkg_test.py using ruff format.
@rickeylev

Copy link
Copy Markdown
Collaborator Author

cc @rsartor-cmd : something that you might run into integrating on the protobuf side: 65886db tldr, some flags necessary for building on mac are missing (that commit adds them)

Why:
py_extension_bzl is superseded by the py_extension bzl_library target in //python/cc:BUILD.bazel.

How:
Remove the py_extension_bzl bzl_library target definition from python/cc/BUILD.bazel.
Why:
pyelftools is ELF-specific and fails when parsing Mach-O (.dylib on macOS) or PE (.dll on Windows) dynamic libraries.

How:
Decorate test_inspect_elf with @unittest.skipIf(sys.platform != 'linux', ...) in tests/cc/py_extension/py_extension_test.py.
…ames

Why:
Analysis tests in py_extension_tests.bzl and py_cc_toolchain_tests.bzl failed on macOS because extension file basenames and toolchain target platform tags vary across operating systems.

How:
Set explicit platform_machine and sys_platform on fake_py_cc_toolchain_impl in tests/support/cc_toolchains/BUILD.bazel, and use file_path_matches pattern matching for extension filenames in tests/cc/py_extension/py_extension_tests.bzl.
rickeylev added 19 commits July 31, 2026 20:02
Document LNK1104, Bzlmod exports_filter, PyMODINIT_FUNC dllexport, LNK1107, and current_py_cc_libs propagation solutions.
…ion_macro

Why:
- Avoid duplicate label errors in cc_shared_library deps when srcs/hdrs are omitted.
- Ensure exports_filter uses canonical Bzlmod Label strings so current_py_cc_libs is matched and retained by link.exe on Windows.

How:
- Wrap alias target labels with str(Label(...)) for canonical string expansion.
- Only append py_cc_libs_win_alias to csl_deps_with_win when srcs or hdrs are provided.
…dows

Why:
- Bazel dereferences alias rules during cc_shared_library dependency processing.
- exports_filter matching failed because it contained the alias target label rather than the dereferenced actual target label (//python/cc:current_py_cc_libs), causing current_py_cc_libs to be pruned and link.exe to fail with LNK1104.

How:
- Add str(Label("//python/cc:current_py_cc_libs")) to win_exports_filter in py_extension_macro.bzl.
Define _PY_CC_HEADERS_ALIAS, _PY_CC_LIBS_ALIAS, and _PY_CC_LIBS_TARGET as uppercase module constants.
Evaluating str(Label(...)) at top-level module load time bakes in @@//... instead of caller's canonical Bzlmod repository context (e.g. @@rules_python+...). Evaluating Label inside def py_extension(...) ensures exports_filter matches cc_shared_library canonical target labels on Windows.
Why:
- Matching exact target labels in exports_filter across Bzlmod repository canonical names can fail if the dereferenced target label format differs from the macro stringification.

How:
- Use str(Label("//python/cc:__subpackages__")) and str(Label("//python/private/cc:__subpackages__")) in win_exports_filter.
Why:
- Adding restrictive package label strings to exports_filter prevented matching external toolchain targets (e.g. @@+python+python_3_11_15...//:python_lib).

How:
- Pass csl_deps_with_win directly to exports_filter when exports_filter is None.
…ilter

Why:
- Passing str(Label(...)) pre-evaluated the label string as @@//... instead of letting rules_cc resolve string literals inside the target caller's Bzlmod context (@@rules_python+...).

How:
- Use plain string literals "//python/cc:current_py_cc_libs" and "//python/private/cc:current_py_cc_libs_private_alias" in exports_filter.
Why:
- CPython import libraries (python3xx.lib) are imported with system_provided = True in cc_import, which causes Bazel's CcInfo provider to suppress propagating the .lib path to link.exe.

How:
- Pass  in user_link_flags and Label("//python/cc:current_py_cc_libs") in additional_linker_inputs on Windows.
Why:
- Clean up macro attribute normalization, use canonical platform labels from common_labels.bzl, and organize Windows-specific linking requirements in a contiguous block.

How:
- Add PLATFORMS_OS_MACOS to common_labels.bzl.
- Define top-level target constants (_BASE_TARGET and _CANONICAL_LABEL).
- Normalize list attributes at start of py_extension.
- Group Windows platform select statements before cc_shared_library.
Why:
- Adding PLATFORMS_OS_MACOS to common_labels.bzl updated transitive module extension hashes loaded by uv.bzl in bzlmod_lockfile test workspace.

How:
- Ran bazel mod deps --lockfile_mode=update in tests/integration/bzlmod_lockfile.
…o.bzl

Why:
- final_csl_deps can be a select() expression, which cannot be iterated over via list comprehension at macro expansion time.
- Using target strings in deps ensures final_csl_deps elements are strings, allowing direct assignment to exports_filter.

How:
- Use _PY_CC_HEADERS_ALIAS_BASE_TARGET and _PY_CC_LIBS_ALIAS_BASE_TARGET strings in deps.
- Set csl_kwargs["exports_filter"] = final_csl_deps directly.
…cro.bzl

Why:
- Ensure dependencies use rules-python resolved canonical target strings while maintaining string type compatibility for exports_filter in cc_shared_library.

How:
- Define _CANONICAL_TARGET constants using str(Label(...)) at top-level.
- Append _CANONICAL_TARGET constants to deps.
Why:
- Simplify csl_deps fallbacks by reassigning the list directly and removing final_csl_deps.
- Remove redundant exports_filter default assignment since cc_shared_library exports deps by default.
- Inline _EMPTY_BASE_TARGET into _EMPTY_CANONICAL_TARGET.

How:
- Define _EMPTY_CANONICAL_TARGET = str(Label("//python/private/cc:empty")).
- Fallback csl_deps to deps and then _EMPTY_CANONICAL_TARGET if empty.
- Only set exports_filter when non-None.
Why:
- Document the addition of the experimental py_extension macro for release changelogs.

How:
- Create news/3973.added.md noting the addition of experimental py_extension.
…utes

Why:
- Expose new public //python/cc targets in python/features.bzl for feature detection.
- Document new py_cc_toolchain attributes in news entry.

How:
- Add //python/cc:current_py_cc_headers, //python/cc:current_py_cc_libs, and //python/cc:py_extension to _TARGETS in python/features.bzl.
- Update news/3973.added.md with py_cc_toolchain attribute documentation.
Why:
- Ensure news entry symbols use Sphinx MyST {obj} cross-references.
- Clean up features.bzl target entries.

How:
- Add {obj} xrefs to py_extension, py_cc_toolchain, and PyCcToolchainInfo in news/3973.added.md.
- Remove current_py_cc_headers and current_py_cc_libs from python/features.bzl.
Why:
- Cross-reference issue #3283 in the release news entry.

How:
- Add ([#3283](#3283)) to news/3973.added.md.
@rickeylev rickeylev removed the do not merge Tag that prevents merging label Aug 1, 2026
Why:
- Remove temporary plan file from tracked repository sources.

How:
- Git rm .agents/plans/windows_build_issues.md.
@rickeylev
rickeylev marked this pull request as ready for review August 1, 2026 09:01
@rickeylev
rickeylev enabled auto-merge August 1, 2026 09:13
@rickeylev

Copy link
Copy Markdown
Collaborator Author

OK! Finally ready.

# - https://github.com/python/cpython/issues/67169
# - https://github.com/python/cpython/commit/03a144bb6ac3d7631a3bdb895e2a1f2d021fb08b
version_parts = ctx.attr.python_version.split(".")
prefix = "cp" if ctx.attr.sys_platform == "win32" else "cpython-"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have the implementation as well? cp and cpython seem to be well known names.


libc = ctx.attr.libc or LibcFlag.get_value(ctx)

platform_tag = _get_platform_tag(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a reminder that we have the EnvMarkerInfo provider, that might be useful.

@@ -0,0 +1,140 @@
# Copyright 2025 The Bazel Authors. All rights reserved.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would vote to remove these headers everywhere, because they are unmaintainable.

@rickeylev
rickeylev added this pull request to the merge queue Aug 1, 2026
Merged via the queue into main with commit 0d6016f Aug 1, 2026
14 checks passed
@rickeylev
rickeylev deleted the py-extension branch August 1, 2026 15:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement py_extension rule

3 participants