馃寪 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

馃悰 fix(seed): avoid downgrading the bundled wheel - #3408

Merged
gaborbernat merged 2 commits into
pypa:mainfrom
gaborbernat:fix/seed-update-version-floor
Oct 17, 2026
Merged

gaborbernat merged 2 commits into
pypa:mainfrom
gaborbernat:fix/seed-update-version-floor

Conversation

@gaborbernat

@gaborbernat gaborbernat commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

virtualenv seeds pip and setuptools from the wheels bundled in its own package. The periodic update (a background child that, once every 14 days, downloads newer releases into the app-data cache and records them in an update log per distribution and Python version) lets later environments use a newer cached wheel instead. Two comparisons in that path went wrong once the bundled wheel was newer than the cache. After an upgrade of virtualenv, the selector returned the newest cached wheel although the bundled one was newer, so new environments got a pip older than the one virtualenv ships until the next update ran. The update child could also record a download older than the bundled wheel. 馃悰 Automatic selection now keeps the bundled wheel unless a cached entry is newer, and the update child stops before it records a download below the bundled version. Explicit pins such as --pip 25.2 are unchanged.

The selector recognized the bundled wheel by filename

periodic_update walks the update log newest first, one minor version at a time. For each entry it returned the bundled wheel when the entry's filename equalled the bundled wheel's name, and otherwise returned the entry once NewVersion.use accepted it: a periodic entry found more than an hour ago whose release is 28 days old, or any entry a manual upgrade recorded. The filename check dates from #1841, which introduced the periodic update, and #2273 kept it when the loop started to consider every patch release of a minor. The log holds what the child downloaded down to the bundled version at the time of that run. Upgrade virtualenv to a release that bundles a newer pip and no entry carries that filename: the loop reaches the newest cached entry, use accepts it, and the environment is seeded with the older wheel. The next child runs 14 days after the last completed one, and only its entries end the downgrade.

The loop now compares parsed versions: the first entry whose version is not above the bundled wheel's ends the search with the bundled wheel. Both sides are packaging.version.Version objects from Wheel.parsed_version, so the comparison follows PEP 440 ordering and not the spelling of the filename. An entry newer than the bundled wheel is still returned once use accepts it.

flowchart LR
    B[bundled pip 25.3, log: 25.2 periodic, 25.1.1 periodic] --> F{filename equals the bundled name?}
    F -- no, before --> U{use accepts 25.2?}
    U -- yes --> D[seed cached pip 25.2]
    B --> V{25.2 above 25.3?}
    V -- no, now --> K[seed bundled pip 25.3]
    classDef bad fill:#ffd6d6,stroke:#c1121f,color:#5c0000;
    classDef ok fill:#d8f3dc,stroke:#2d6a4f,color:#1b4332;
    class D bad;
    class K ok;
Loading

The update child recorded downloads below the bundled version

_run_do_update downloads the newest release, then the newest below it, and so on. It appended each download to the log and only then compared it with the bundled version; #1892 added that stop. The entry at or below the bundled version was therefore recorded before the loop ended. When the first download is already older than the bundled wheel, and a mirror behind PIP_INDEX_URL that lags PyPI is enough for that, the log gained an entry below the bundled version with source periodic or manual. With the filename check above, that entry was selected once use accepted it.

The loop now checks the bound before it records: a download below the bundled version ends the loop with no entry, next to the existing stops for no download and for reaching the most recent known entry. A download equal to the bundled version is still recorded and still ends the loop, as before.

sequenceDiagram
    participant U as update child
    participant I as index
    participant L as update log
    U->>I: pip download pip, bundled is 25.3
    I-->>U: pip-25.2 wheel
    rect rgb(255, 214, 214)
    U->>L: before: append 25.2, then stop at the bound
    end
    rect rgb(216, 243, 220)
    Note over U: now: 25.2 is below 25.3, stop before the append
    U->>L: log unchanged
    end
Loading

Behavior changes

Automatic bundle selection does not downgrade to an older cached update. Explicit version pins retain their existing behavior. A download equal to the bundled version still terminates the search as before; a lower version is discarded from the update results, so --upgrade-embed-wheels reports no new versions when the index has nothing as new as the bundled wheel.

Reported by Anthropic's OSS Scanner: ANT-2026-HY012DAG.

@gaborbernat
gaborbernat force-pushed the fix/seed-update-version-floor branch from f0a2e2b to c13125e Compare October 16, 2026 14:22
@gaborbernat
gaborbernat force-pushed the fix/seed-update-version-floor branch from c13125e to 5a75fd5 Compare October 16, 2026 14:25
@gaborbernat
gaborbernat merged commit 20ef3b3 into pypa:main Oct 17, 2026
141 of 144 checks passed
@gaborbernat
gaborbernat deleted the fix/seed-update-version-floor branch October 17, 2026 10:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant