diff --git a/crates/socket-patch-cli/CLI_CONTRACT.md b/crates/socket-patch-cli/CLI_CONTRACT.md index c3deb5985..32487cfdd 100644 --- a/crates/socket-patch-cli/CLI_CONTRACT.md +++ b/crates/socket-patch-cli/CLI_CONTRACT.md @@ -947,7 +947,7 @@ to **six flavors**. | pypi / uv (uv.lock) | rebuilt wheel (canonical PEP 427 filename; RECORD regenerated) | `[tool.uv.sources] = {path}` in pyproject + surgical uv.lock rewrite; transitive deps via `[tool.uv] override-dependencies` | `uv sync --locked` / `--frozen --offline` (hash-verified, byte-stable lock) | | pypi / poetry (poetry.lock: legacy `[metadata.hashes]`, lock 1.0/1.1 `[metadata.files]`, 2.x `files`) | (rebuilt wheel) | lock-only: the target `[[package]]` gets `[package.source] type="file"` (+ `reference = ""` on the 0.12/1.0 layouts, which read it unconditionally) and the single `{file, hash: sha256-of-our-wheel}` entry in whichever table the generation keeps it. pyproject + `metadata.content-hash` untouched; CRLF locks keep their line endings. A lock written by Poetry < 1.4 emits `pypi_poetry_integrity_unverified` (that installer verifies no local hashes and skips an already-installed version) | `poetry check --lock && poetry sync`, cold cache (hash fail-closed from Poetry 1.4; byte-stable lock) — see `docs/testing/poetry-compatibility.md` | | pypi / pdm (pdm.lock) | (rebuilt wheel) | lock-only: the `[[package]]` gains the local-file `path` + `files[]` hash. pyproject + `content_hash` untouched. Non-fixture `[metadata] strategy` / hash-less locks refused | `pdm sync` (+ `pdm install --check`), cold cache | -| pypi / pipenv (Pipfile.lock) | (rebuilt wheel) | lock-only: the `default`/`develop` entry → `{file, hashes:[sha256-of-our-wheel]}`. Pipfile + `_meta.hash` untouched. Emits `vendor_integrity_unverified` — pipenv does not hash-check file entries; the committed wheel bytes are the protection | `pipenv install --deploy` (+ `pipenv verify`), cold cache | +| pypi / pipenv (Pipfile.lock) | (rebuilt wheel) | lock-only: the `default`/`develop` entry → `{file, hashes:[sha256-of-our-wheel]}`. Pipfile + `_meta.hash` untouched. Emits `vendor_integrity_unverified` — pipenv does not hash-check file entries; the committed wheel bytes are the protection — and, when another entry is hashed, `pypi_pipenv_hash_export_unsupported` (`pipenv requirements --hash` drops the vendored wheel's hash) | `pipenv install --deploy` (+ `pipenv verify`), cold cache | | pypi / requirements.txt (pip / `uv pip`) | (rebuilt wheel) | pin line → `./` (markers carried over; transitive deps appended), plus `--hash=sha256:` only when the requirements tree is already in pip's hash-checking mode (any `--hash` or `--require-hashes`) | `pip install -r` / `uv pip install -r` **run from the project root** (both resolve bare paths against the CWD) | | nuget | deterministically rebuilt `.nupkg` at `..nupkg` (the uuid dir IS a NuGet folder feed; the stale embedded signature is dropped — unsigned is accepted under NuGet's default validation), plus `/.gitignore` (`!*`: re-includes the nupkg against the project's ignores, such as VisualStudio.gitignore's `*.nupkg`); refuses `vendor_artifact_gitignored` when git would still drop it | `nuget.config` source + `packageSourceMapping` for the id (creating the mapping from scratch ALSO fans a `` out to every pre-existing source, including the ones NuGet inherits from the user config and parent directories' configs (v5.0 #354), none at all when an inherited config already maps packages — mapping is exclusive, NU1100 otherwise; another source's exact pattern for the id is set aside in a comment while vendored, `vendor_nuget_mapping_set_aside`, #462) **+** `packages.lock.json` `contentHash` → `base64(sha512(nupkg))` for the entries at the patched version in every lock a project under the root restores into — the root `packages.lock.json`, member projects' locks, `packages..lock.json`, a literal `NuGetLockFilePath` (v5.0 #353/#514; an unevaluable `NuGetLockFilePath` is refused with `vendor_nuget_lock_path_unresolved`) — (`vendor_nuget_no_lockfile` warning when there is none; a lock that also resolves the id at another version is refused with `vendor_nuget_lock_other_version`, nothing written) | `dotnet restore --locked-mode`, cold cache, `--network none` (tampered nupkg fails NU1403) | | maven | the patched `.jar` + the upstream pom (only its `` suffixed; transitives survive) + `.sha1` sidecars + an ownership marker under `.socket/vendor/maven2///-socket./`; every JVM tree root (`.socket/vendor/maven2`, Gradle's `.socket/vendor/gradle`, Coursier's `.socket/vendor/coursier`) owns a `.gitignore` (`!*`) that re-includes the jars against the project's ignores, such as Java.gitignore's `*.jar`, plus a `.gitattributes` (`-text`); a tree root git ignores itself refuses `vendor_artifact_gitignored` | every pom root, single-module (a reactor of one) or multi-module: the pinned `` + `` pin, `.mvn/maven.config` (`maven.repo.local.tail`) and the `socket-patch-vendor` fallback file repository (`checksumPolicy=fail`); Gradle, sbt and scala-cli roots go to the same JVM backend (ledger ecosystem `jvm`). Pre-v5 `maven_pom_repository` entries (`` to `.socket/vendor/maven/`) are revert-only: vendoring their root is refused (`vendor_jvm_shape_unsupported`, `legacy_maven_root`) | `mvn` build on a fresh checkout with a warm local repository and behind `mirrorOf external:*` (host capstone `e2e_vendor_maven_build` across the Maven matrix) | @@ -1605,6 +1605,7 @@ command's integration tests check its output. | `vendor_yarn_berry_mixed_line_endings` | `failed` | vendor (yarn berry): `yarn.lock` or the root `package.json` mixes CRLF and LF line endings (or holds a bare CR) — no single ending can be kept, and yarn rewrites such a file wholesale on its next install (a mixed lock also fails `--immutable`, YN0028). Refused before any write; `yarn install` normalizes the files. A uniformly CRLF pair is vendored in CRLF. A hosted→vendored takeover (`vendor`, `scan`/`get --mode vendored`) raises this — and the berry `vendor_yarn_berry_cache_unsupported` gates — BEFORE restoring the hosted pin's upstream entry (dry run too), so a refused purl stays hosted. | | `vendor_override_conflict` | `failed` | vendor (pnpm/yarn-berry): a user-authored override/resolution for the package already exists. pnpm exception: a user exact pin equal to the vendored version, under the bare `` or the `@` key, in package.json `pnpm.overrides` or pnpm-workspace.yaml `overrides:` (#854; there a quoted `'1.3.0'` or a trailing `# comment` is the same pin), is taken over in place (its value becomes the vendored `file:` spec under the user's own key on every override surface) and `vendor --revert` restores it byte-for-byte. A range, another version, a `>` selector, several same-name keys, or package.json and pnpm-workspace.yaml pinning under different keys still refuse; the detail names the file that carries the override. | | `vendor_integrity_unverified` | `skipped` (warning) | vendor (pipenv): the lockfile format does not hash-check file entries; the committed wheel bytes are the protection. | +| `pypi_pipenv_hash_export_unsupported` | `skipped` (warning) | vendor / scan / get `--mode vendored` (pipenv, #981), on a run that wires the lock and on an in-sync re-run, unless the probed Pipenv is a pre-2023 release (2022 exports the wheel with its hash): another `Pipfile.lock` entry carries `hashes`, and `pipenv requirements --hash` (Pipenv 2023.7.9+) exports the vendored `file` entry with no `--hash` while those entries keep theirs, so `pip install -r` of the export refuses the whole install in hash-checking mode. No lock shape avoids it. The package is still vendored; remedy: export without `--hash`, or `--mode hosted` (its `#sha256=` URL exports a hash pip accepts). | | `vendor_content_mismatch_overwritten` | — | **No longer emitted** (service-only vendoring): `vendor` commits the patch server's verified artifact without reading the installed bytes, so it has no mismatch to report. Kept so consumers that match on it know it will not appear. | | `vendor_vlt_transitive_unsupported` | `failed` | vendor (vlt): the target has an inbound edge from another package in `vlt-lock.json` (the detail names it); vendored mode rewires only direct dependencies of the root or a workspace member, because vlt silently reverts transitive lock surgery. Remedy: `--mode hosted`. Refused before any download or write, dry runs included (`would_refuse`). | | `vendor_vlt_lock_out_of_sync` | `failed` | vendor (vlt): an importer's `package.json` is missing, unparseable, or declares a spec for the dependency that differs from the lock's importer edge. Remedy: `vlt install` first. Refused before any write. | diff --git a/crates/socket-patch-core/src/vendor/pypi.rs b/crates/socket-patch-core/src/vendor/pypi.rs index 8c4b2461d..080d08589 100644 --- a/crates/socket-patch-core/src/vendor/pypi.rs +++ b/crates/socket-patch-core/src/vendor/pypi.rs @@ -1308,6 +1308,13 @@ async fn pypi_prelude<'p>( if target == PipenvTarget::Fresh { warnings.extend(project.warnings.iter().cloned()); } + // Like the stale install below, the export gap (#981) outlives + // the run that wired the lock: an in-sync re-run warns too. + warnings.extend(super::pypi_pipenv::hash_export_warning( + &project, + &canon_name, + installer, + )); // Both a fresh vendor and a re-run over an already-wired lock // keep warning while the venv still holds the upstream release. if let Some(stale) = pipenv_stale_install_warning( @@ -7259,6 +7266,87 @@ wheels = [ ); } + /// #981: a lock with another hashed registry entry exports through + /// `pipenv requirements --hash` with hashes on every line but the + /// vendored wheel's, which pip's hash-checking mode refuses. The vendor + /// run warns, and so does an in-sync re-run; a lock that pins only the + /// target exports no hash and does not, nor does a Pipenv 2022 project, + /// whose export keeps the wheel's hash. + #[tokio::test] + async fn pipenv_vendor_warns_the_hash_export_gap() { + async fn vendor_with(fx: &E2eFixture, major: u32) -> Vec { + const PURL: &str = "pkg:pypi/six@1.16.0"; + let sources = PatchSources::blobs_only(&fx.blobs); + let service = crate::vendor::test_support::service_fixture::Fixture::new( + PURL, + fx.site_packages.as_path().into(), + &fx.record, + &sources, + ) + .await; + let outcome = vendor_pypi_with_pipenv_version( + PURL, + &fx.site_packages, + &fx.root, + &fx.record, + &sources, + "2026-06-09T00:00:00Z", + false, + false, + Some(&service.cfg), + &tokio::sync::OnceCell::new_with(Some(Some(major))), + &InstalledSiteListings::default(), + ) + .await; + let VendorOutcome::Done { + warnings, entry, .. + } = outcome + else { + panic!("vendor did not complete: {outcome:?}"); + }; + if let Some(entry) = entry { + save_ledger_entry(&fx.root, &entry).await; + } + warnings + } + let warns = |warnings: &[VendorWarning]| { + warnings + .iter() + .any(|w| w.code == "pypi_pipenv_hash_export_unsupported") + }; + + let fx = pipenv_e2e_fixture().await; + let warnings = vendor_with(&fx, 2026).await; + assert!(!warns(&warnings), "{warnings:?}"); + + let hashed_develop = PIPENV_REGISTRY_LOCK.replace( + r#""develop": {}"#, + r#""develop": { + "idna": { + "hashes": ["sha256:bbb"], + "index": "pypi", + "version": "==3.7" + } + }"#, + ); + let fx = pipenv_e2e_fixture().await; + touch(&fx.root, "Pipfile.lock", &hashed_develop).await; + let warnings = vendor_with(&fx, 2026).await; + assert!(warns(&warnings), "{warnings:?}"); + let lock = tokio::fs::read_to_string(fx.root.join("Pipfile.lock")) + .await + .unwrap(); + assert!(lock.contains(UUID), "Pipfile.lock not wired:\n{lock}"); + // The in-sync re-run keeps warning: the export gap is still there. + let warnings = vendor_with(&fx, 2026).await; + assert!(warns(&warnings), "{warnings:?}"); + + let fx = pipenv_e2e_fixture().await; + touch(&fx.root, "Pipfile.lock", &hashed_develop).await; + let warnings = vendor_with(&fx, 2022).await; + assert!(!warns(&warnings), "{warnings:?}"); + } + const SIBLING_EXPORT: &str = "-i https://pypi.org/simple\nsix==1.16.0\n"; /// Vendor with Pipfile.lock alone and persist the entry: a project diff --git a/crates/socket-patch-core/src/vendor/pypi_pipenv.rs b/crates/socket-patch-core/src/vendor/pypi_pipenv.rs index 23261cba6..872571949 100644 --- a/crates/socket-patch-core/src/vendor/pypi_pipenv.rs +++ b/crates/socket-patch-core/src/vendor/pypi_pipenv.rs @@ -150,6 +150,51 @@ pub(super) async fn load_pipenv_project( }) } +/// #981: Pipenv 2023.7.9 and later export a `file` / `path` entry without +/// `--hash` (`pipenv requirements --hash`), while every hashed registry +/// entry keeps its hashes. One hashed line turns on pip's hash-checking +/// mode, which then refuses the whole install over the vendored wheel. No +/// lock shape avoids it (a `#sha256=` fragment on the relative path breaks +/// `pipenv sync`), so warn whenever an entry other than `canon_name`'s +/// would export a hash, unless `installer` is a release before 2023 (2022 +/// exports `file:/// --hash=…`; earlier ones have no `requirements`). +/// Hosted mode exports `#sha256=`, which pip accepts as the hash. +pub(super) fn hash_export_warning( + p: &PipenvProject, + canon_name: &str, + installer: Option, +) -> Option { + if installer.is_some_and(|major| major < 2023) { + return None; + } + let hashed = category_names(&p.lock).into_iter().find_map(|section| { + let entries = p.lock[§ion].as_object()?; + entries + .iter() + .find(|(key, entry)| { + canonicalize_pypi_name(key) != canon_name + && entry + .get("hashes") + .and_then(Value::as_array) + .is_some_and(|hashes| !hashes.is_empty()) + && entry.get("file").is_none() + && NON_REGISTRY_KEYS.iter().all(|k| entry.get(*k).is_none()) + }) + .map(|(key, _)| format!("{section}.{key}")) + })?; + Some(VendorWarning::new( + "pypi_pipenv_hash_export_unsupported", + format!( + "`pipenv requirements --hash` (Pipenv 2023.7.9 and later) exports the vendored \ + {canon_name} wheel without a hash while hashed {LOCK_FILE} entries (such as \ + {hashed}) keep theirs, so `pip install -r` of an export holding both refuses \ + the whole install (\"Hashes are required in --require-hashes mode\"); export \ + without `--hash`, or use `--mode hosted`, whose reference exports with a hash \ + pip accepts" + ), + )) +} + /// Target-specific guards (also re-run by the wire step right before /// writing). Entries match by PEP 503 canonical NAME in every package /// category. Registry pins and existing vendored wheel identities @@ -1267,6 +1312,63 @@ mod tests { assert_eq!(p.warnings[0].code, "vendor_integrity_unverified"); } + /// #981: `pipenv requirements --hash` (2023.7.9+) exports the vendored + /// `file` entry without a hash while every hashed registry entry keeps + /// its `--hash`, so pip's hash-checking mode refuses the whole install. + /// A lock with another hashed registry entry warns. + #[tokio::test] + async fn hash_export_warning_when_other_entries_are_hashed() { + let tmp = write_lock(LOCK_TRANSITIVE_REGISTRY).await; + let p = load_pipenv_project(tmp.path()).await.unwrap(); + let w = hash_export_warning(&p, "six", None).expect("python-dateutil is hashed"); + assert_eq!(w.code, "pypi_pipenv_hash_export_unsupported"); + assert!( + w.detail.contains("pipenv requirements --hash"), + "{}", + w.detail + ); + assert!(w.detail.contains("--mode hosted"), "{}", w.detail); + assert!(w.detail.contains("vendored six wheel"), "{}", w.detail); + assert!(w.detail.contains("default.python-dateutil"), "{}", w.detail); + // A 2023+ installer warns; 2022 exports the wheel with its hash. + assert!(hash_export_warning(&p, "six", Some(2026)).is_some()); + assert!(hash_export_warning(&p, "six", Some(2022)).is_none()); + + // A hashed entry in another category counts too: `--dev` and + // `--categories` exports carry it. + let develop = LOCK_DIRECT_REGISTRY.replace( + r#""develop": {}"#, + r#""develop": { + "idna": { + "hashes": [ + "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc" + ], + "index": "pypi", + "version": "==3.7" + } + }"#, + ); + let tmp = write_lock(&develop).await; + let p = load_pipenv_project(tmp.path()).await.unwrap(); + assert!(hash_export_warning(&p, "six", None).is_some()); + } + + /// With no other hashed registry entry, the export has no `--hash` + /// line, pip never turns on hash-checking, and there is nothing to warn + /// about: the target alone, or beside a `file` reference. + #[tokio::test] + async fn hash_export_warning_absent_without_other_hashed_entries() { + let tmp = write_lock(LOCK_DIRECT_REGISTRY).await; + let p = load_pipenv_project(tmp.path()).await.unwrap(); + assert!(hash_export_warning(&p, "six", None).is_none()); + + // python-dateutil's only sibling is six's vendored `file` entry, + // which the export writes without a hash. + let tmp = write_lock(LOCK_TRANSITIVE_VENDORED).await; + let p = load_pipenv_project(tmp.path()).await.unwrap(); + assert!(hash_export_warning(&p, "python-dateutil", None).is_none()); + } + /// Our serializer reproduces pipenv's own /// `json.dumps(indent=4, sort_keys=True) + "\n"` byte-for-byte, so a /// parse → serialize round trip of a pipenv-written lock is the identity. diff --git a/docs/ecosystems.md b/docs/ecosystems.md index 50a78052e..772c4eb17 100644 --- a/docs/ecosystems.md +++ b/docs/ecosystems.md @@ -15,7 +15,7 @@ The backticked slug in each row is the value `-e`/`--ecosystems` accepts (e.g. | Ecosystem | agent (`--mode agent`) | vendored (`--mode vendored`) | hosted (`--mode hosted`) | |-----------|------------------------|------------------------------|--------------------------| | npm (`npm`) — pnpm / yarn / berry / bun / vlt | ✅ any install layout, vlt's `node_modules/.vlt` store included (every store copy, copy-on-write) | ✅ seven lockfile flavors: package-lock, yarn classic, yarn berry (node-modules linker; PnP refused), pnpm v9, pnpm legacy v5.4/v6.0 (`pnpm 7/8` — frozen installs are path-bound because those majors absolutize `file:` override specifiers; moved checkouts run one `pnpm install --offline --no-frozen-lockfile`, surfaced as `vendor_pnpm_legacy_absolute_specifier`), bun text `bun.lock` lockfileVersion 0/1/2 and native binary `bun.lockb` revisions 1/2/3 (binary locks stay binary; text workspace vendoring requires lockfileVersion 2 — see [Bun compatibility](testing/bun-compatibility.md)), vlt `vlt-lock.json` lockfileVersion 0/1 (patched package directories for direct dependencies of the root or a workspace member; transitive targets refused — see [vlt notes](#npm-vlt-notes)). Rush monorepos refused (`vendor_rush_unsupported`) — see [Rush notes](#npm-rush-monorepos) | ✅ package-lock / npm-shrinkwrap, pnpm-lock.yaml and legacy shrinkwrap.yaml (pnpm majors 1–12; block and flow resolutions), yarn classic, yarn berry, bun, vlt (`vlt-lock.json` without `lockfileVersion`, 0 or 1) — pnpm, berry, bun and vlt carry constraints, see [npm hosted-mode notes](#npm-hosted-mode-notes) and [vlt notes](#npm-vlt-notes) | -| PyPI (`pypi`) — uv / poetry / pdm / pipenv / pip | ✅ in place | ✅ uv project/script locks, PEP 751 `pylock.toml` / `pylock..toml`, poetry, pdm, pipenv (Pipenv 2018 or later — every `Pipfile.lock` category is rewired, lock-only checkouts included; Pipenv 2023+ does not hash-check local wheels — `vendor_integrity_unverified`; a venv still holding the upstream release is reported as `pypi_pipenv_stale_install`; see [Pipenv compatibility](testing/pipenv-compatibility.md)), and requirements.txt. Native uv vendoring requires uv ≥ 0.2.35 (the `[[package]]` lock grammar); hosted mode covers native `uv.lock` from uv 0.1.45 (the first release whose `uv lock` writes one) and requirements from uv 0.0.5; see [uv compatibility](testing/uv-compatibility.md). | ✅ requirements.txt including hash continuations, uv project/script locks, and PEP 751 locks. Version/source ambiguity is refused; see [uv compatibility](testing/uv-compatibility.md). Poetry 1.x and 2.x locks are supported; Poetry 0.x ignores URL sources and is refused. See [Poetry compatibility](testing/poetry-compatibility.md). Pipenv `Pipfile.lock` (pipfile-spec 6 — Pipenv 7 and later; `path` references for 7–11, `file` from 2018; lock-only checkouts and Pipenv's out-of-tree venv are discovered; a warm venv that Pipenv will not reinstall over warns `redirect_pypi_stale_install`; see [Pipenv compatibility](testing/pipenv-compatibility.md)). `pdm.lock` is supported for the lock formats PDM 0.12–1.4 and 2.8.1+ write (`lock_version` 2 / 4.3–4.5.1); the identity-losing 3.1 / 4.0–4.2 formats (PDM 1.8–2.7) are refused. PDM 2.8.0 writes an indistinguishable `4.3` lock but shares that identity-loss bug, so a rewritten 2.8.0 lock crashes `pdm sync` — upgrade to ≥ 2.8.1. See [PDM compatibility](testing/pdm-compatibility.md). | +| PyPI (`pypi`) — uv / poetry / pdm / pipenv / pip | ✅ in place | ✅ uv project/script locks, PEP 751 `pylock.toml` / `pylock..toml`, poetry, pdm, pipenv (Pipenv 2018 or later — every `Pipfile.lock` category is rewired, lock-only checkouts included; Pipenv 2023+ does not hash-check local wheels — `vendor_integrity_unverified`; Pipenv 2023.7.9+ exports the vendored wheel without a hash, so `pipenv requirements --hash` output fails pip's hash-checking mode — `pypi_pipenv_hash_export_unsupported`; a venv still holding the upstream release is reported as `pypi_pipenv_stale_install`; see [Pipenv compatibility](testing/pipenv-compatibility.md)), and requirements.txt. Native uv vendoring requires uv ≥ 0.2.35 (the `[[package]]` lock grammar); hosted mode covers native `uv.lock` from uv 0.1.45 (the first release whose `uv lock` writes one) and requirements from uv 0.0.5; see [uv compatibility](testing/uv-compatibility.md). | ✅ requirements.txt including hash continuations, uv project/script locks, and PEP 751 locks. Version/source ambiguity is refused; see [uv compatibility](testing/uv-compatibility.md). Poetry 1.x and 2.x locks are supported; Poetry 0.x ignores URL sources and is refused. See [Poetry compatibility](testing/poetry-compatibility.md). Pipenv `Pipfile.lock` (pipfile-spec 6 — Pipenv 7 and later; `path` references for 7–11, `file` from 2018; lock-only checkouts and Pipenv's out-of-tree venv are discovered; a warm venv that Pipenv will not reinstall over warns `redirect_pypi_stale_install`; see [Pipenv compatibility](testing/pipenv-compatibility.md)). `pdm.lock` is supported for the lock formats PDM 0.12–1.4 and 2.8.1+ write (`lock_version` 2 / 4.3–4.5.1); the identity-losing 3.1 / 4.0–4.2 formats (PDM 1.8–2.7) are refused. PDM 2.8.0 writes an indistinguishable `4.3` lock but shares that identity-loss bug, so a rewritten 2.8.0 lock crashes `pdm sync` — upgrade to ≥ 2.8.1. See [PDM compatibility](testing/pdm-compatibility.md). | | Cargo (`cargo`) | ✅ in-place + `.cargo-checksum.json` rewrite (shared registry-cache caveat — see [Cargo: shared registry cache](#cargo-shared-registry-cache)) | ✅ `[patch.crates-io]` path entry in the root `Cargo.toml` (v5; per-version Socket keys; pre-v5 `.cargo/config*` wiring migrates on re-run) | ✅ per-patch sparse registry (`[registries.socket-patch-]` + Cargo.lock source/checksum); needs cargo 1.68+ (sparse registries) — a project whose `rust-version` or `rust-toolchain[.toml]` declares an older cargo is refused (`redirect_cargo_old_cargo`), use `--mode vendored`; direct dependencies only — a crate another dependency also pulls in is refused, use `--mode vendored`; a crate the user overrides with `[patch]` (path, git or another registry) is refused too, the override is left alone; a `cargo vendor` or `cargo local-registry` project (crates.io replaced by an on-disk source in the project's, a parent directory's or `$CARGO_HOME`'s `.cargo/config*`) is refused, use agent mode; with no `Cargo.lock` the graph is unknown, so only a project whose sole dependency is the patched crate is redirected | | RubyGems (`gem`) | ✅ in place | ✅ Gemfile + Gemfile.lock path pair (`Gemfile` spelling only — a `gems.rb` twin, which bundler ≥ 2 loads instead, or a `BUNDLE_GEMFILE`-configured manifest makes vendoring refuse with `gemfile_not_loaded` before any write) | ✅ per-dep `source` block — edits `gems.rb` + `gems.locked` or `Gemfile` + `Gemfile.lock`, whichever the project holds (a `Gemfile` + `gems.rb` twin is refused with `redirect_gem_twin_manifest_ambiguous`: bundler 1.x loads the `Gemfile`, bundler ≥ 2 loads `gems.rb`, and nothing in the project says which bundler installs it; bundler 4's `BUNDLE_LOCKFILE` (environment, app config or global config) naming any other lock is refused with `redirect_gem_bundle_lockfile_unsupported`, as is vendoring with `gemfile_not_loaded`; spellings that diverge beyond Socket's own edits fail closed with `redirect_gem_gemfile_spellings_diverge`; `BUNDLE_GEMFILE` from `.bundle/config` (which outranks the environment, as in bundler), the environment, or the global `~/.bundle/config` / `$BUNDLE_USER_CONFIG` (lowest, as in bundler) is followed when it names the project's `Gemfile` / `gems.rb`, and any other configured manifest is refused with `redirect_gem_bundle_gemfile_unsupported`); a Bundler all-source, exact-source or hostname mirror in app config or the scan environment can capture the patch registry, so the redirect is refused with `redirect_gem_mirror_overrides_source` without printing mirror URLs (scope mirrors to `mirror.https://rubygems.org`; user-global config and mirrors set only in a later install environment are not inspected); the `CHECKSUMS` pin needs bundler ≥ 2.6 (older locks get a `redirect_gem_no_checksums_section` warning); a stale pre-redirect materialization that `bundle install` would reuse instead of refetching is flagged `redirect_gem_stale_install` with a prescriptive remedy (see CLI_CONTRACT.md's "Gem stale-install guard") | | Go (`golang`) | ✅ `go.mod` `replace` → `.socket/go-patches/` — see [Go: directory replaces and go.sum](#go-directory-replaces-and-gosum) | ✅ `replace` → the committed vendor tree | ✅ (free tier) fork-style `replace` → `patch.socket.dev/gopatch/` + committed `go.sum` pin; see [Go notes](#go-directory-replaces-and-gosum). Paid hosted patches are unsupported; `redirect_golang_unsupported` names the vendored remedy | diff --git a/docs/testing/pipenv-compatibility.md b/docs/testing/pipenv-compatibility.md index 7f81a3cb9..0ecadcbb4 100644 --- a/docs/testing/pipenv-compatibility.md +++ b/docs/testing/pipenv-compatibility.md @@ -98,6 +98,19 @@ import on modern Pythons); 2018–2022 on Python 3.8; 2023+ on Python 3.12. Pipenv 3, `--deploy` with 9, `pipenv sync` with 2018, `pipenv verify` with 2020, `pipenv requirements` with 2022 (it exports the hosted URL / vendored path). Pipenv 0.x has no `WORKON_HOME` placement to discover. +- **Vendored wheels export without a hash.** `pipenv requirements --hash` + on Pipenv 2023.7.9 and later writes no `--hash` for a `file` / `path` + entry, so the vendored wheel is the one unhashed line while the registry + entries keep theirs; pip then switches to `--require-hashes` and refuses + the whole `pip install -r` (#981). 2023.7.1–2023.7.4 export a `file` + entry as a bare `==` (Pipenv's own bug, any `file` reference); + 2022.12–2023.6 export `file:/// --hash=…` and install. No lock + shape avoids it (a `#sha256=` fragment on the relative path breaks + `pipenv sync`), so the vendored run warns + `pypi_pipenv_hash_export_unsupported` whenever another lock entry + carries hashes (re-runs included; not when the probed Pipenv is 2022 or + older). Export without `--hash`, or use hosted mode: its + `#sha256=` reference exports a hash pip accepts. - **Out-of-tree venv naming** is unchanged from Pipenv 7 through 2026: `sanitize()[:42]-<8 chars of urlsafe-base64(sha256())>`, plus `-` when that variable is set (the