Skip to content

py_wheel: support PEP 639 license metadata (License-Expression / License-File)Β #4042

Description

@justincui-adv

πŸš€ feature request

Relevant Rules

py_wheel (python/private/py_wheel.bzl) β€” extending an existing rule.

Description

py_wheel cannot produce a wheel whose METADATA conforms to PEP 639, which is now Final.

On main today:

  • Metadata-Version is hardcoded to 2.1 (python/private/py_wheel.bzl:422); the PEP 639 fields require 2.4.
  • The only license-related attribute is the legacy free-text license (:250), emitted as the License: field (:432). PEP 639 deprecates that field.
  • There is no way to emit License-Expression, no way to emit License-File, and no way to place license texts under <name>-<version>.dist-info/licenses/.
  • There is no escape hatch for appending arbitrary METADATA lines either, so this cannot be worked around from a BUILD file.

The practical impact is that a py_wheel cannot declare its license as an SPDX expression, and cannot ship the license texts of its bundled third-party dependencies in the location PEP 639 defines. For anyone who has to produce auditable license metadata for a redistributed wheel, that is a hard blocker.

Other backends have already moved: setuptools 77.0.0 emits License-Expression, stores License-Files in .dist-info/licenses/, and bumps core metadata to 2.4.

Describe the solution you'd like

Two new attributes on py_wheel:

  • license_expression (string) β€” an SPDX license expression, emitted as License-Expression.
  • license_files (label-keyed string dict) β€” license file targets mapped to their destination path relative to .dist-info/licenses/. Each entry emits one License-File line and packages the file at that path.

Metadata-Version would be raised to 2.4 only when one of the two is set, so existing users are unaffected and the change stays backwards compatible. license and license_expression would be mutually exclusive, as PEP 639 intends.

py_wheel(
    name = "my_wheel",
    distribution = "my_package",
    version = "1.0.0",
    license_expression = "Apache-2.0 AND MIT",
    license_files = {
        "//:LICENSE": "LICENSE",
        "//third_party:some_dep_license.txt": "third_party/some_dep.txt",
    },
)

produces:

Metadata-Version: 2.4
Name: my_package
Version: 1.0.0
License-Expression: Apache-2.0 AND MIT
License-File: LICENSE
License-File: third_party/some_dep.txt

with both files packaged under my_package-1.0.0.dist-info/licenses/.

Describe alternatives you've considered

  • The legacy license attribute β€” deprecated by PEP 639, carries no SPDX semantics, and does not package license files at all.
  • extra_distinfo_files β€” can place files inside .dist-info/, but Metadata-Version stays 2.1 and no License-File lines are emitted. The files end up present but undeclared, which is not PEP 639 conformant.
  • Post-processing the built wheel in a separate rule β€” means unzipping and rezipping the artifact just to rewrite METADATA; awkward, and easy to get wrong for reproducibility.
  • Carrying a local patch to python/private/py_wheel.bzl β€” what we do today. It works, but it has to be rebased on every rules_python bump, which is exactly what we would rather not carry.

I have this implemented and working locally and would be glad to send a PR. I need to clear the CLA on my employer's side first, so I am opening the issue now to check whether the approach and the attribute shape are acceptable before going down that route.

Activity

  1. aignas commented on Aug 14, 2026

    @aignas
    Collaborator

    The proposed solution sounds reasonable. PRs are welcome.

  2. added theissue type on Aug 14, 2026
  3. justincui-adv commented on Aug 14, 2026

    @justincui-adv
    Author

    Thanks for the quick response.

    I can't send the PR immediately - the CLA needs clearing on my employer's side first, and I'd rather not hold the feature hostage to that. If anyone wants to pick this up in the meantime, please go ahead; the shape described above is what I have working locally and it has been running in production for our wheel.

  4. rickeylev commented on Aug 17, 2026

    @rickeylev
    Collaborator

    I threw together some proto type code. Part of the thinking is to give more flexibility so that we don't have to add an attribute for everything, so that users don't have to wait for us to add something as simple as a license_expression field.

    So in that vein, the gist:

    1. license_expression: String attribute for SPDX license expressions
      (mutually exclusive with license). Automatically elevates Metadata-Version
      to 2.4.
    2. metadata_fields: Configurable dict[str, list[str]] to add/override
      arbitrary METADATA lines (e.g. License-File, Dynamic, Keywords).
      Repeated list items produce repeated header lines.
    3. metadata_file: Label of an RFC 822 metadata file to merge into the
      generated metadata (takes precedence for single-use headers).
    4. extra_distinfo_files: Enhance with strip_prefix|prefix path syntax
      (e.g. "//pkg:licenses": "pkg/|licenses") and multi-file directory placement
      for arbitrary non-code files (licenses, SBOMs, signatures).

    Example:

    py_wheel(
        name = "my_wheel",
        distribution = "my_package",
        version = "1.0.0",
        license_expression = "Apache-2.0 AND MIT",
        extra_distinfo_files = {
            "//:LICENSE": "licenses/LICENSE",
            "//:sbom.spdx.json": "sboms/sbom.spdx.json",
            ":third_party_licenses": "third_party/|licenses/third_party",
        },
        metadata_fields = {
            "License-File": ["LICENSE", "licenses/third_party/dep.txt"],
            "Keywords": ["bazel", "python"],
            "Dynamic": ["classifiers"],
        },
    )
  5. added a commit that references this issue on Aug 17, 2026
    b05b6ed
  6. sanchiagarwal0 commented on Aug 17, 2026

    @sanchiagarwal0

    Hi! I would like to work on this issue as my first contribution. I plan to implement the PEP 639 license metadata support while keeping the changes focused on py_wheel. Please let me know if this is okay.

  7. rickeylev commented on Aug 17, 2026

    @rickeylev
    Collaborator

    Oh, okay! Thanks for contributing!

  8. rickeylev commented on Sep 2, 2026

    @rickeylev
    Collaborator

    Hi @sanchiagarwal0 if you don't plan on coming back to this soon, a maintainer will either take over the PR or close it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions