Skip to content

Commit 07a6b88

Browse files
Support hosted Pipenv patches and safe vendoring (#242)
* Support hosted Pipenv patches safely Preserve Pipenv categories and source identity when applying patches. Handle legacy hosted references and vendored extras, reject unsupported installers, and restore lock entries safely during rollback. Assisted-by: Codex:gpt-6-astra * Clarify Pipenv installer requirements Explain the version requirement when Pipenv is unavailable, and point local-wheel verification guidance at the existing VEX command. Assisted-by: Codex:gpt-6-astra
1 parent 649d457 commit 07a6b88

26 files changed

Lines changed: 77315 additions & 158 deletions

File tree

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
name: Pipenv compatibility
2+
3+
# Drives the real CLI and real Pipenv releases through hosted, vendored,
4+
# agent and out-of-tree agent mode on Linux and macOS (the `test` job already
5+
# runs the in-process hosted Pipenv CLI tests on all three platforms,
6+
# Windows included). The pre-2018 releases need Docker and stay in the local
7+
# matrix (`docs/testing/pipenv-compatibility.md`). Needs network (PyPI + the
8+
# public patch service); no Socket token.
9+
10+
on:
11+
pull_request:
12+
paths:
13+
- 'crates/socket-patch-core/src/patch/redirect/pipenv.rs'
14+
- 'crates/socket-patch-core/src/vendor/pypi_pipenv.rs'
15+
- 'crates/socket-patch-core/src/vendor/pypi.rs'
16+
- 'crates/socket-patch-core/src/vendor/lock_inventory.rs'
17+
- 'crates/socket-patch-core/src/crawlers/python_crawler.rs'
18+
- 'crates/socket-patch-core/src/utils/pipenv.rs'
19+
- 'crates/socket-patch-cli/src/commands/scan/hosted.rs'
20+
- 'crates/socket-patch-cli/src/commands/vendor.rs'
21+
- 'crates/socket-patch-cli/src/commands/rollback.rs'
22+
- 'crates/socket-patch-cli/src/commands/vex.rs'
23+
- 'crates/socket-patch-core/src/patch/redirect/replay.rs'
24+
- 'scripts/backtest-pipenv.py'
25+
- '.github/workflows/pipenv-compatibility.yml'
26+
push:
27+
branches: [main]
28+
workflow_dispatch:
29+
30+
permissions:
31+
contents: read
32+
33+
jobs:
34+
matrix:
35+
strategy:
36+
fail-fast: false
37+
matrix:
38+
include:
39+
# Every 2018+ major, plus the shapes that exercise the relock and
40+
# line-ending paths on the newest and the last Python-3.8 release.
41+
- os: ubuntu-latest
42+
versions: 2018.11.26 2020.11.15 2021.11.23 2022.12.19 2023.12.1 2024.4.1 2025.1.3 2026.8.0
43+
shapes: direct
44+
- os: ubuntu-latest
45+
versions: 2022.12.19 2026.8.0
46+
shapes: crlf marker-excluded extras category
47+
- os: macos-latest
48+
versions: 2018.11.26 2022.12.19 2023.12.1 2026.8.0
49+
shapes: direct
50+
runs-on: ${{ matrix.os }}
51+
timeout-minutes: 60
52+
steps:
53+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
54+
with:
55+
persist-credentials: false
56+
- name: Install Rust
57+
run: rustup show
58+
- name: Cache cargo
59+
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
60+
with:
61+
key: pipenv-compat
62+
save-if: ${{ github.ref == 'refs/heads/main' }}
63+
- name: Install uv
64+
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
65+
- name: Install the Pythons the Pipenv releases run on
66+
run: uv python install 3.8 3.12
67+
- name: Build the CLI
68+
run: |
69+
cargo build --locked -p socket-patch-cli
70+
mkdir -p "$RUNNER_TEMP/bin"
71+
cp target/debug/socket-patch "$RUNNER_TEMP/bin/socket-patch"
72+
- name: Run the Pipenv matrix
73+
env:
74+
SOCKET_NO_CONFIG: '1'
75+
SOCKET_NO_UPDATE_CHECK: '1'
76+
# The runners carry whatever patch release uv ships; the harness
77+
# only needs one 3.8 and one 3.12.
78+
BACKTEST_PY38: '3.8'
79+
BACKTEST_PY312: '3.12'
80+
PIPENV_VERSIONS: ${{ matrix.versions }}
81+
PIPENV_SHAPES: ${{ matrix.shapes }}
82+
run: |
83+
# shellcheck disable=SC2086
84+
python3 scripts/backtest-pipenv.py \
85+
--socket-patch "$RUNNER_TEMP/bin/socket-patch" \
86+
--socket-patch-revision "$GITHUB_SHA" \
87+
--output "$RUNNER_TEMP/pipenv-compat" \
88+
--versions $PIPENV_VERSIONS \
89+
--shapes $PIPENV_SHAPES \
90+
--modes hosted vendored agent agent-oot \
91+
--jobs 4
92+
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
93+
if: always()
94+
with:
95+
name: pipenv-compat-${{ matrix.os }}-${{ strategy.job-index }}
96+
path: |
97+
${{ runner.temp }}/pipenv-compat/summary.json
98+
${{ runner.temp }}/pipenv-compat/summary.md
99+
if-no-files-found: warn
100+
retention-days: 7

‎CHANGELOG.md‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,43 @@ into the new version's section — see docs/releasing.md.
5151

5252
### Added
5353

54+
- **Pipenv projects can use hosted patches, and vendored patches keep every
55+
category.** `scan --mode hosted` rewrites every `Pipfile.lock` category
56+
(`default`, `develop`, Pipenv 2022+ named categories) that pins the patched
57+
release to the hosted wheel — `file` references for Pipenv 2018 and later,
58+
`path` for 7–11 (probed once with `pipenv --version`; `SOCKET_PIPENV_MAJOR`
59+
pins it), pipfile-spec < 6 refused — preserving markers, extras, unrelated
60+
entries, the Pipfile and its content hash, with per-entry rollback
61+
(`redirect_pipenv_entry`). Vendored mode keeps custom categories and extras,
62+
uses `path` for wheels with extras (Pipenv 2022's file-URL bug) and refuses
63+
installers older than 2018 (`pypi_pipenv_installer_unsupported`). A stale
64+
Pipfile.lock only vetoes the sibling Python rewriters on a real pin/source
65+
conflict (`redirect_pipenv_refused`); anything else is
66+
`redirect_pipenv_skipped`. Measured across the last stable release of all
67+
18 published Pipenv majors — see `docs/testing/pipenv-compatibility.md` and
68+
`scripts/backtest-pipenv.py`.
69+
- **`Pipfile.lock` is inventoried.** Lock-only Pipenv checkouts (a fresh
70+
clone with nothing installed) now discover their pins in every mode —
71+
hosted redirects them, vendored fetches the pristine wheel by one of the
72+
lock's recorded digests (`LockIntegrity::Sha256AnyOf`, resolved through
73+
PyPI's JSON API and verified against the same digest) and agent/scan list
74+
them as lockfile-only packages. Previously they discovered nothing and
75+
exited 0. Socket's own references stay discoverable, so a re-scan of an
76+
already-redirected or already-vendored lock-only checkout re-confirms it;
77+
a lock that resolves only from private indexes is never looked up on
78+
pypi.org.
79+
- **Pipenv's out-of-tree virtualenv is discovered.** Agent mode (bare `scan`,
80+
`rollback`, `vex`) now finds `$WORKON_HOME/<dir>-<hash>[-<python>]` (the
81+
`.venv` file pointer, `PIPENV_CUSTOM_VENV_NAME` and `PIPENV_PIPFILE`
82+
included) exactly as Pipenv 7 through 2026 place it, instead of falling
83+
through to the global interpreter's site-packages.
84+
- **Pipenv stale-install guard.** Pipenv never reinstalls a release that is
85+
already present, so a hosted or vendored rewrite over a warm venv leaves the
86+
upstream bytes installed; `redirect_pypi_stale_install` /
87+
`pypi_pipenv_stale_install` now say so, naming the site-packages dir and
88+
the verified remedy (`pipenv run pip uninstall -y <pkg> && pipenv sync`, or
89+
a clean `pipenv --rm && pipenv sync`), and the stale purl is excluded from
90+
the same-run `--vex`.
5491
- **Poetry projects take hosted patches, and vendored patches now cover every
5592
`poetry.lock` generation.** `scan --mode hosted` rewrites `poetry.lock` to a
5693
`[package.source] type = "url"` pointing at the Socket-hosted, SHA-256-pinned
@@ -200,6 +237,23 @@ into the new version's section — see docs/releasing.md.
200237

201238
### Fixed
202239

240+
- **Rollback after a Pipenv relock no longer refuses forever.** `pipenv lock`
241+
(and `update`, and `install <other>` before 2024) regenerates a redirected
242+
or vendored entry to registry shape on every Pipenv major; that is now the
243+
desired end state — the hosted edit retires and the vendored record is
244+
dropped (`vendor_lock_entry_relocked`) — instead of a permanent drift
245+
refusal that held every pypi revert and kept the orphaned wheel dir. A
246+
foreign `file`/`path` reference is still drift.
247+
- **Same-run `--vex` attests lock-only pypi redirects.** The confirmed purl is
248+
unqualified while the ledger records the API's artifact-qualified purl;
249+
both sides now match on the qualifier-stripped purl, so a lock-only Pipenv
250+
(or uv) checkout no longer exits 1 `no_applicable_patches` after
251+
redirecting its lock.
252+
- **The Pipenv installer probe runs only when a patch targets the lock**, warns
253+
only when the lock was actually rewritten, resolves `pipenv` on absolute
254+
`PATH` entries only (a relative entry would have executed a `pipenv` planted
255+
in the scanned repository), finds `.bat`/`.cmd` shims on Windows, and takes
256+
only the token after `version` (never a stray `Python 3.12` banner).
203257
- Hosted Python redirects now warn when installed files still contain upstream
204258
or modified bytes and omit those packages from same-run VEX. The read-only
205259
probe covers Poetry virtualenvs, repeats on re-scans, and uses persisted patch

‎README.md‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,51 @@ Mode support varies by ecosystem — e.g. Go can't do hosted, Rush monorepos can
254254
vendored. See the full **[mode × ecosystem matrix](docs/ecosystems.md#mode--ecosystem-matrix)**
255255
for details and per-ecosystem caveats.
256256
257+
### Pipenv compatibility
258+
259+
Hosted mode rewrites every `Pipfile.lock` category that pins the patched
260+
release (`default`, `develop`, and Pipenv 2022+ named categories) and
261+
preserves the Pipfile, its content hash, markers, extras, and unrelated lock
262+
entries, so `pipenv install --deploy`, `pipenv sync` and `pipenv verify` keep
263+
passing. The reference shape follows the installing Pipenv: releases 7–11
264+
need `path` references, 2018 and later use `file` references, and lock
265+
formats before `pipfile-spec: 6` (Pipenv 0–6) are refused without changing
266+
the lock. Socket Patch probes `pipenv --version` once per run (only when a
267+
patch targets the lock); `SOCKET_PIPENV_MAJOR=<major>` pins the answer for
268+
machines without pipenv on PATH. Hosted references carry both the `#sha256=`
269+
URL fragment (verified by Pipenv 2023+) and a `hashes` entry (verified by
270+
2018–2022; Pipenv 11 verifies either), so a tampered lock fails to install
271+
on every supported release.
272+
273+
Vendored mode requires Pipenv 2018 or later. Wheels with extras use `path`
274+
references to avoid Pipenv 2022's local-file URL parsing bug. Pipenv 2023+
275+
does not enforce hashes on local wheels; commit the wheel and run
276+
`socket-patch vex --product <purl>` (a Pipfile names no project, so pass the
277+
product purl explicitly).
278+
279+
Fresh checkouts work in every mode: a clone with only `Pipfile` +
280+
`Pipfile.lock` is discovered from the lock (hosted redirects it, vendored
281+
fetches the pristine wheel by one of the lock's recorded digests), and agent
282+
mode finds Pipenv's default out-of-tree virtualenv under `WORKON_HOME`
283+
without `pipenv run`.
284+
285+
Pipenv never reinstalls a release that is already present: `pipenv install`,
286+
`pipenv install --deploy` and `pipenv sync` all exit 0 and keep the installed
287+
bytes, on every Pipenv major. A hosted or vendored rewrite therefore
288+
protects fresh installs, and Socket Patch warns
289+
(`redirect_pypi_stale_install` / `pypi_pipenv_stale_install`) when a venv
290+
still holds the upstream release, with the verified remedy:
291+
`pipenv run pip uninstall -y <pkg> && pipenv sync` (or `pipenv --rm &&
292+
pipenv sync`). Do not use `pipenv uninstall <pkg>` for this — it rewrites the
293+
Pipfile and re-locks the patch away. `pipenv lock` / `pipenv update`
294+
regenerate the entry to its registry reference (a silent unpatch): re-run
295+
Socket Patch afterwards; `rollback` retires the stale record cleanly.
296+
297+
`scripts/backtest-pipenv.py` drives the real CLI and the last stable release
298+
of every published Pipenv major through hosted, vendored, agent and
299+
out-of-tree agent mode, and `docs/testing/pipenv-compatibility.md` holds the
300+
measured boundaries and results.
301+
257302
## Common tasks
258303
259304
### Patch everything that can be patched

0 commit comments

Comments
 (0)