Skip to content

perf(pypi): extract downloaded wheels once and reuse them - #3856

Draft
aignas wants to merge 22 commits into
bazel-contrib:mainfrom
aignas:aignas.refactor.single_dep_whl_library
Draft

aignas wants to merge 22 commits into
bazel-contrib:mainfrom
aignas:aignas.refactor.single_dep_whl_library

Conversation

@aignas

@aignas aignas commented Jun 27, 2026 •

Copy link
Copy Markdown
Collaborator

Extract each wheel that is downloaded by the Bazel downloader once and reuse
the extracted repository across Python versions and hub repositories.

Before this change each whl_library repository downloaded and extracted the
wheel, so a pure Python wheel used by 5 Python versions was extracted 5 times.
Now each unique (URL, filename, integrity) combination is extracted once into a
whl_<name>_<version>_<tags> repository, and the original whl_library
repository names become whl_deps_repo repositories that read the METADATA
from the extracted repository and wire the dependencies for the hub. The hub
repositories still point to the same repository names.

Details:

  • The download arguments (index_url, envsubst, netrc, auth_patterns)
    are taken from the first registration of the wheel. The URLs are compared
    using the unresolved index_url, so the credentials that envsubst may
    inject never end up in the repository arguments.
  • If the same wheel is extracted with different arguments (e.g.
    pip_data_exclude), then the later registrations fall back to a regular
    whl_library.
  • Wheels with whl_patches, annotations (whl_modifications) or
    extra_hub_aliases are not shared, because the extra targets would not be
    available in the whl_deps_repo.
  • whl_deps_repo now also aliases the srcs and whl_file targets that the
    hub references.
  • The resolved experimental_index_url is no longer passed to
    simpleapi_download and whl_library, only the unresolved template is.

Fixes #2948
Work towards #2530

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request implements an optimized mode for whl_library (gated by the RULES_PYTHON_WHL_LIBRARY_OPTIMIZED environment variable) to allow wheel reuse across different Python versions by omitting the Python version from spoke repository names, generating per-extra targets, and creating explicit aliases in the hub repository. The code review identified several critical issues with this implementation: static aliases in render_pkg_aliases.bzl break multi-version/multi-platform setups, and iterating over extras_info causes aliases to overwrite each other. Additionally, a naming mismatch in extension.bzl between the unified hub alias and the hub repository package name will result in broken aliases, and skipping pkg__extra target generation when there are no extra dependencies leads to build failures.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +90 to +106
def _render_extra_alias(*, name, repo, target):
return """\
package(default_visibility = ["//visibility:public"])

alias(
name = "pkg",
actual = "@{repo}//:{target}",
)

alias(
name = "whl",
actual = "@{repo}//:{target}",
)
""".format(
repo = repo,
target = target,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

The _render_extra_alias function generates a static alias pointing to a single spoke repository. However, in a multi-version or multi-platform setup, there are multiple spoke repositories. We should replace this with a multiplatform alias generator that uses select to choose the correct spoke repository based on the active configuration.

def _render_extra_alias_multiplatform(*, name, repo_mapping, target_suffix):
    if type(repo_mapping) == type(""):
        actual_expr = repr("@{}//:{}".format(repo_mapping, target_suffix))
        load_statement = ""
    else:
        actual_dict = {}
        for config_setting, repo_name in repo_mapping.items():
            key = _repr_config_setting(config_setting)
            actual_dict[key] = repr("@{}//:{}".format(repo_name, target_suffix))

        if len(actual_dict) == 1 and list(actual_dict.keys())[0] == repr("//conditions:default"):
            actual_expr = list(actual_dict.values())[0]
        else:
            sorted_pairs = sorted(actual_dict.items())
            actual_expr = "select({\n"
            for k, v in sorted_pairs:
                actual_expr += "        {}: {},\n".format(k, v)
            actual_expr += "    })"

        needs_load = any(["whl_config_setting" in k for k in actual_dict.keys()])
        load_statement = ""
        if needs_load:
            load_statement = 'load("@rules_python//python/private/pypi:whl_config_setting.bzl", "whl_config_setting")\n'

    return """\
{load_statement}package(default_visibility = ["//visibility:public"])

alias(
    name = "pkg",
    actual = {actual_expr},
)

alias(
    name = "whl",
    actual = {actual_expr},
)
""".format(
        load_statement = load_statement,
        actual_expr = actual_expr,
    )

Comment on lines +158 to +176
# Generate extra alias directories for optimized mode extras.
for name, extras_info in whl_extras.items():
normalized = normalize_name(name)
for repo_name, extra_names in extras_info.items():
for extra in extra_names:
extra_pkg = "{}_{}".format(normalized, extra)
files["{}/BUILD.bazel".format(extra_pkg)] = _render_extra_alias(
name = extra_pkg,
repo = repo_name,
target = "{}__{}".format(normalized, extra),
)

# Also create pkg__ alias (no extras)
no_extras_pkg = "{}__".format(normalized)
files["{}/BUILD.bazel".format(no_extras_pkg)] = _render_extra_alias(
name = no_extras_pkg,
repo = repo_name,
target = normalized,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

In a multi-version or multi-platform setup, whl_extras contains multiple spoke repositories for the same wheel. Iterating over extras_info.items() and writing directly to files causes the alias for one spoke to overwrite the others, breaking multi-version/multi-platform support for extras. We should use the multiplatform alias generator to create a single select-based alias that correctly routes to the appropriate spoke repository.

    # Generate extra alias directories for optimized mode extras.
    for name, extras_info in whl_extras.items():
        normalized = normalize_name(name)
        pkg_aliases = aliases.get(normalized)
        if not pkg_aliases:
            continue

        # Find all unique extras across all repos for this wheel
        unique_extras = {}
        for extra_names in extras_info.values():
            for extra in extra_names:
                unique_extras[extra] = True

        for extra in unique_extras:
            extra_pkg = "{}_{}".format(normalized, extra)
            files["{}/BUILD.bazel".format(extra_pkg)] = _render_extra_alias_multiplatform(
                name = extra_pkg,
                repo_mapping = pkg_aliases,
                target_suffix = "{}__{}".format(normalized, extra),
            )

        # Also create pkg__ alias (no extras)
        no_extras_pkg = "{}__".format(normalized)
        files["{}/BUILD.bazel".format(no_extras_pkg)] = _render_extra_alias_multiplatform(
            name = no_extras_pkg,
            repo_mapping = pkg_aliases,
            target_suffix = normalized,
        )

Comment thread python/private/pypi/extension.bzl Outdated
norm_pkg = normalize_name(whl_name)
for extra_names in extras_info.values():
for extra in extra_names:
alias_name = "%s__%s" % (norm_pkg, extra)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

There is a naming mismatch between the unified hub alias name and the hub repository package name. The unified hub alias is generated with double underscores __ (e.g., requests__security), whereas render_pkg_aliases.bzl generates the hub repository package with a single underscore _ (e.g., requests_security). This mismatch will cause the unified hub aliases to point to non-existent packages. Update this to use a single underscore to match the hub repository package naming.

Suggested change
alias_name = "%s__%s" % (norm_pkg, extra)
alias_name = "%s_%s" % (norm_pkg, extra)

Comment on lines +182 to +183
if not extra_only_deps and not extra_only_deps_select:
continue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Skipping the generation of the pkg__extra target when there are no extra dependencies (or when all extra dependencies are already present in the base dependencies) will cause the hub repository's alias to point to a non-existent target, leading to build failures. We should always generate the pkg__extra target, even if it only depends on ":pkg", to ensure that the hub aliases remain valid.

@rickeylev rickeylev left a comment

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.

saw this was draft, so didn't look too thoroughly, just for things that looked like the bot was obviously wrong

Comment thread .agents/plans/single_dep_whl_library.md Resolved
Comment thread .agents/plans/single_dep_whl_library.md Resolved
Comment thread .agents/plans/single_dep_whl_library.md Resolved
@aignas

aignas commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator Author

Again, ran out of free tokens, so will postpone for another day.

@aignas
aignas force-pushed the aignas.refactor.single_dep_whl_library branch from 453b0b3 to 23c95ab Compare June 28, 2026 14:06
pull Bot pushed a commit to garymm/rules_python that referenced this pull request Jun 29, 2026
This is no longer used starting when we enabled pipstar by default and
did a code cleanup where Python is no longer used to extract the wheels.

Split from bazel-contrib#3856
@aignas

aignas commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator Author
  • 2 types of whl_library repos - one with extracted files, one wiring the deps.

@aignas aignas closed this Jul 2, 2026
@aignas
aignas force-pushed the aignas.refactor.single_dep_whl_library branch from 23c95ab to e433644 Compare July 2, 2026 13:22
@aignas

aignas commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

So it seems that it is much better to just separate the repositories - one with extracted whl sources, one with the parsed METADATA. This way the change is surgical and very easy to reason about. This was spiked by hand and then vibed and debugged. The build should work but there are many things missing.

TODO:

  • split the new repo into a separate file.
  • Create a separate function for the BUILD.bazel file generation and add unit tests.
  • Update existing hub_builder.bzl unit tests.
  • Remove the confabulated "fixes" to make the code easier to read.

@aignas aignas reopened this Jul 2, 2026
Comment thread python/private/pypi/extension.bzl Resolved
Comment thread python/private/pypi/hub_builder.bzl Resolved
Comment thread python/private/pypi/hub_builder.bzl Resolved
Comment thread python/private/pypi/hub_builder.bzl Resolved
@aignas aignas changed the title refactor(pypi): reuse the same whl_library instances perf(pypi): extract wheels once and reuse Jul 2, 2026
@aignas
aignas force-pushed the aignas.refactor.single_dep_whl_library branch from db1494b to c3a21b3 Compare July 4, 2026 05:20
@rickeylev

Copy link
Copy Markdown
Collaborator

/review

Comment thread python/private/pypi/extension.bzl Resolved
@aignas

aignas commented Jul 9, 2026 •

Copy link
Copy Markdown
Collaborator Author

This should now work.

EDIT: just realized that merging main into this branch should not have affected it.

@aignas

aignas commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator Author

/review

rickeylev added a commit to rickeylev/rules_python that referenced this pull request Jul 24, 2026
…ive and pip_archive (bazel-contrib#3948)

Before this PR the `whl_library` would be a do-all repository rule.
Whilst it
is convenient to reuse the code, it is actually really difficult to
maintain
and make it more performant. Side effect here is that the python
dependencies
(like `setuptools`, etc) will no longer be downloaded for whl-only
extracts,
it makes it a tiny bit faster.

With this split we can drop certain dependencies from the whl extraction
and
optimize the common path - whl extraction where the URL for downloading
the
wheel is known. This also allows us to start handling the sdists in an
entirely
different way.

In a followup PR I plan to split the part which just extracts the wheel
to
lay a more surgical foundation to bazel-contrib#3856.

Foundation work for bazel-contrib#2410.
Split out of bazel-contrib#3856.
Work towards bazel-contrib#2948.

---------

Co-authored-by: Richard Levasseur <richardlev@gmail.com>
aignas added a commit to aignas/rules_python that referenced this pull request Jul 25, 2026
With this we are starting to separate some of the targets.

Split out of bazel-contrib#3856
Work towards bazel-contrib#2948
pull Bot pushed a commit to garymm/rules_python that referenced this pull request Jul 26, 2026
)

With this we are starting to separate whl_library_targets
into 2 parts - one for sources only (without deps) and
another one is just the deps parts.

Next PR I'll create a way to create 2 separate instances.

Split out of bazel-contrib#3856
Work towards bazel-contrib#2948
rickeylev added a commit to rickeylev/rules_python that referenced this pull request Aug 1, 2026
Summary:
- Add a new repo rule to just read metadata.json
- Add integration tests for the repository rules in `whl_library.bzl`
  file.
- Make some of the arguments optional in the BUILD.bazel code
  generation.

No changelog, because the rule is not yet exposed to the user in any
way.

Split out of bazel-contrib#3856
Work towards bazel-contrib#2948
Fixes bazel-contrib#3071

---------

Co-authored-by: Richard Levasseur <richardlev@gmail.com>
@aignas
aignas force-pushed the aignas.refactor.single_dep_whl_library branch 2 times, most recently from 80a0a90 to ffbe7c1 Compare August 1, 2026 12:37
This is the common denominator and for now no warnings are printed, but
there are opportunities to do this.

This approach is way more surgical than the previous one.

Fixes bazel-contrib#2948
@aignas
aignas force-pushed the aignas.refactor.single_dep_whl_library branch from 1441ff7 to 8a10382 Compare August 23, 2026 13:59
@aignas

aignas commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator Author

Given the recent refactors, I've did it again in a more surgical way.

rickeylev pushed a commit to rickeylev/rules_python that referenced this pull request Oct 26, 2026
URL-resolved wheels currently retain both the downloaded archive and
extracted files even when a build only consumes the Python library.
Download the archive temporarily for extraction and remove it afterward,
while preserving the existing `whl` and `whl_file` targets through a
lazy sibling `<repo>_whl` repository. Ordinary Python builds do not
fetch that sibling repository.

Apply matching patches in the separate wheel-file repository and extract
from its patched archive. This keeps both original and patched archives
out of the extracted repository. Preserve caller-owned wheel inputs,
using a distinct `_patched_whl` repository for their patched
replacements to avoid collisions with existing source repositories.
Source-built wheels and direct `whl_archive` URL calls retain their
current archive behavior. Both download paths share checksum validation,
authentication, and URL/environment substitution.

Extend the existing wheel-library integration test with a generated
wheel served over HTTP. Separate Bazel invocations check archive
cleanup, lazy repository creation, both wheel targets, matching and
nonmatching patches, preservation of caller-owned inputs, and the full
`pip.parse(uv_lock = ...)` path. Retain the existing pip-backend,
source-distribution, direct `whl_archive`, and dependency-wiring
assertions in the same integration target. Add analysis coverage for
archive ownership and exclusion from runtime files.

Gazelle's module mapping still consumes wheel archives and therefore
materializes their archive repositories. Changing that consumer to use
extracted libraries is separate follow-up work.

Validation:
- All 31 bzlmod example tests pass, including an assertion that the
public wheel archive contains the same patched metadata as the extracted
library. Both `bazel vendor //...` and building all targets with the
resulting vendor directory pass locally.
- All 24 affected Bazel test targets pass with `--config=fast-tests`,
including the consolidated
`//tests/integration:whl_library_test_bazel_self` target and its five
test methods. The nested integration target is run with tag filters
overridden and local test concurrency limited to one.
- The storage analysis suite passes under bzlmod. Under Bazel 7.4.1
WORKSPACE mode, the bzlmod-specific ownership assertion is skipped and
runtime archive exclusion passes.
- All applicable pre-commit hooks pass, including buildifier, Ruff,
deleted-package generation, and Gazelle.
- The NumPy 2.3.3 PoC imports successfully without materializing an
archive repository; explicitly building its `whl` target materializes
the archive. Its extracted repository uses 57.74 MiB instead of 73.61
MiB allocated on the test filesystem. Shared download caches are outside
that measurement.

Implements the delete-by-default / optional-archive-access suggestion in
bazel-contrib#3091 (comment).
Preserves the wheel-consumer functionality introduced in bazel-contrib#364 and fixed
in bazel-contrib#458. Related to bazel-contrib#3968 and complements the extraction-reuse work in
bazel-contrib#3856.
The hub repository references the srcs and whl_file targets of each
spoke repository, so whl_deps_repo needs to alias them to the repository
holding the extracted wheel.
The experimental_index_url is resolved using envsubst to decide if the
bazel downloader should be used. Pass the unresolved template to the
simpleapi_download and whl_library instead of the resolved value, so that
credentials that may be stored in the environment variables do not end up
in the whl_library arguments or the lock file.
Extract each wheel downloaded by the bazel downloader once for every
unique URL, filename and integrity combination and replace the
whl_library repositories with whl_deps_repo repositories that wire the
dependencies by reading the METADATA from the extracted repository.

The download arguments, such as the index_url and envsubst, are taken
from the first registration of the wheel. The URLs are compared without
resolving envsubst, so that credentials do not end up in the arguments.

Wheels with patches, annotations or extra hub aliases are still extracted
in the whl_library repository.
@aignas aignas changed the title perf(pypi): extract wheels once and reuse perf(pypi): extract downloaded wheels once and reuse them Nov 8, 2026
The wheels downloaded by the bazel downloader are now extracted in a
repository named after the wheel filename, so check for the python tag
of the wheel in addition to the python version in the spoke repo name.
Labels are rendered with a single @ in WORKSPACE mode.
Move the lookup of the repos that have extra hub aliases into
shared_whl_srcs so that it is covered by the unit tests.
Wheels that are extracted differently, e.g. with a different
pip_data_exclude, now get their own extracted repo instead of falling back
to a regular whl_library. Also name the envsubst news entry after the PR.
The downloaded wheels are now extracted in a repository named after the
wheel filename, which also owns the lazily fetched archive repository.
aignas added a commit to aignas/rules_python that referenced this pull request Nov 8, 2026
The previous approach on this branch is dropped: parts of it landed
separately (bazel-contrib#4087, bazel-contrib#4101, bazel-contrib#4105, bazel-contrib#4106) and the rest is obsolete. The
tree is reset to the state of bazel-contrib#3856 (shared extracted wheel repos) so
that the uv.lock dependency graph plumbing can be reimplemented on top
of it.

This branch has not been deployed

No deployments
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.

Setup fewer wheel repos in pip.parse

2 participants