Skip to content

Commit 1a8608b

Browse files
Match Composer patch versions like Composer does (#270)
* Match composer patch versions by release identity A composer patch's base purl can carry the padded version Socket's SBOM ingestion stores (pkg:composer/psr/log@3.0.2.0), while the project's installed.json and composer.lock say 3.0.2 or v3.0.2. The CLI compared those as strings after stripping a leading v, so such a patch was "not found" by apply and vendor, refused by the hosted redirect as a version mismatch, rejected by VEX discovery, and deleted by scan --prune. Compare composer versions the way Composer does. The new utils::composer_version ports composer/semver 3.4.4 VersionParser::normalize (padding, v tags, stability spellings such as -rc.1 and RC1, +build, x-dev branches); a spelling Composer rejects only matches itself. purl_eq, the crawler, the vendor lock lookup, the hosted lock rewriter, the redirect ledger, VEX discovery and sources, scan discovery and the prune step now use it for composer purls. Stored spellings (manifest keys, vendored leaf dirs, ledger keys) are unchanged. The shared vector file tests/fixtures/composer-version-vectors.json is generated from real Composer 2.10.3 and is byte-identical to depscan's copy, so the CLI and the server agree on every case; the port also matches Composer on 12,129 fuzzed inputs. New tests cover the crawler, lock lookup, ledger, VEX leaf, prune, a padded-version redirect golden, and apply, vendor (with VEX) and hosted runs against a mock API that serves only the padded spelling. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Test exact Composer releases on every OS Composer patching was only proven on Ubuntu against three floating releases, so a regression on macOS, Windows, or a specific 2.x line (2.9 still falls back to the git source; 2.10 does not) went unseen. composer-compatibility.yml runs the real vendored and hosted capstones plus the composer VEX cells against checksum-pinned 1.10.28, 2.0.14, 2.1.14, 2.2.30, 2.5.8, 2.8.12, 2.9.8 and 2.10.3 phars on Ubuntu, and the 1.10/2.2/2.9/2.10 lines on Windows and macOS. A Docker job runs the vendored Docker capstone on exact 2.2.30 and 2.10.3 images. The capstones can now run a given composer.phar through php, which is also what makes them work on Windows. Dockerfile.composer installs an exact, checksum-verified Composer instead of the latest one, and the ci.yml comment on the git-source fallback is corrected. Assisted-by: Claude Code:claude-opus-5-5 * Keep Composer patches installed on every version Hosted redirects now remove a patched package's source wherever it sits in its composer.lock entry, and strip dist mirrors. Before, a failed or skipped hosted download let Composer 1 through 2.9 quietly install the unpatched upstream code from git. Locks that an older CLI or the GitHub app already redirected are healed on the next run. Vendored copies no longer lose files when installed. Composer's path mirror skipped anything matched by the copy's .gitignore (Composer 1 to 2.1), .hgignore (Composer 1) or .gitattributes export-ignore rules (every version), so a patched file could silently go missing. Those rules are now neutralized in the copy, and re-runs heal copies vendored earlier. A patch that edits one of those files is refused. vendor, scan and get now tell users to run composer install, and to remove the package directory first on Composer 1, which does not reinstall a changed package. Real-Composer tests cover the new cases on 1.10 through 2.10, and docs/testing/composer-compatibility.md records what each version does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Keep CRLF composer.lock line endings when vendoring Vendoring a Composer patch (vendor, scan --mode vendored, get --mode vendored) rewrote a CRLF composer.lock, as a Windows or core.autocrlf checkout has, with LF line endings. The commit diff covered every line instead of the patched entry, and vendor --revert also wrote LF, so it did not restore the original bytes. Both writes now keep the line endings of the lock they replace, as the hosted mode already did. A CRLF lock now round-trips byte for byte through vendor and vendor --revert. Unit tests and a binary e2e cover vendor, scan --vendor and get --mode vendored on a CRLF lock. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Match SBOM-padded Composer date versions Socket's SBOM pads every numeric Composer version to four parts, so a date release locked as 20231001 reaches the CLI as 20231001.0.0.0. Composer rejects that spelling (a 6+ digit major is only valid as a date), so the version key fell back to the raw text and never matched the lock: apply, vendor, hosted redirect and VEX reported the patch as not found for its own package. A 2-4 part numeric spelling Composer rejects is now normalized with its trailing .0 parts dropped. The shared vector file, copied from depscan, gains the padded date cases checked against Composer 2.10.3. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Strip Composer dist mirrors before redirecting A dist that lists mirrors before its url had the mirrors' url rewritten instead of the dist's own, and a dist whose only url is a mirror is now refused with redirect_composer_no_dist_url. Also drop the dead composer_source_before_dist helper. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Edit only the vendored entry in composer.lock Vendoring rewrote the whole composer.lock through serde_json, so a lock with \/ escapes, \uXXXX escapes or mixed line endings did not revert byte for byte. Splice only the patched entry's text, keeping the lock's indentation, line endings and escaping. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Match vendored Composer ownership by release Vendored ownership checks compared composer purls as exact strings, so a ledger keyed pkg:composer/psr/log@3.0.2.0 never covered the lock's @3.0.2 and apply, rollback, gc and scan treated a live vendored patch as foreign or dead. Match composer purls by release identity. The vendor hint now reads composer.lock only when this run wired composer, and never blocks on a special file. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Do not report source-installed Composer as fixed Composer installs from source first when preferred-install resolves to source for the package, or when it is auto with a dev version on a Composer 1 or 2.0 lock. A leftover source entry then means pristine bytes, so VEX no longer reports those entries as fixed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Name the vendor dir in Composer reinstall hints After a hosted redirect that removed no source entry, Composer 2 keeps an existing vendor/ copy, so the hint now names the vendor directory to remove on every Composer version. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Run Composer compatibility on every related change The composer compatibility workflow now also runs for vendor, patch, manifest, lockfile and command changes. Docker composer tests fail instead of skipping when SOCKET_PATCH_DOCKER_E2E_REQUIRED=1, and the source-fallback test disables autocrlf so Windows compares LF bytes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Satisfy clippy in the Composer lock splice Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Read composer.json through the VEX discovery context The source-install veto read composer.json directly, which the VEX discovery rules forbid: every extractor reads through DiscoverCtx so unreadable files are diagnosed and Socket identities are recognized. Also add the golden entries for the dist-mirrors-before-url fixture. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Check Composer source and mirror layouts against the redirect oracle The in-place composer.lock rewrite oracle now also generates locks with dist mirrors listed before the url or as the only url, a source member placed before name, and patched versions spelled padded or v-prefixed. Every randomized lock is also checked to revert byte for byte when each recorded fragment is undone, newest first, the way the ledger reverts it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Run Composer compatibility when the crawler oracle or group commit changes The Composer crawler is now a directory module with its equivalence oracle, and vendored Composer lock writes go through the per-run group commit and the durable writer, so changes to any of them run the exact release matrix too. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Treat a Composer date release's trailing .0 parts as padding Adopt depscan's final shared vectors (4ecd8543d3) byte for byte. A date release (6+ digit major) now drops its trailing .0 parts from its identity, since SBOM padding erases whether the lock said X, X.0 or X.0.0. Versions Composer rejects key into their own space, matching depscan's composerVersionIdentityKey, so purl identity keys cannot collide with a normalized one. Also point the drop_superseded_purl composer test at the real redirect_composer_dist edit kind: main's unknown-kind guard (#269) correctly refuses the made-up kind it used. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Match package/-prefixed patch keys and guard reads in mirror filters The conflict check compared patch file keys to `.gitignore` / `.hgignore` / `.gitattributes` verbatim, but API records key files as `package/<path>`, so a patch rewriting a filter file slipped past it and neutralization broke the patched file's afterHash. Normalize the key first. Read the copy's filter files through the FIFO-safe `read_regular_to_bytes` instead of a bare `tokio::fs::read`, so a FIFO at the path is skipped instead of wedging the vendor run. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 5e1ebac commit 1a8608b

129 files changed

Lines changed: 8537 additions & 313 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -724,9 +724,12 @@ jobs:
724724
# coverage.
725725
#
726726
# composer: 1 (packagist stopped serving composer 1 on 2025-09-01,
727-
# so the fixture resolves from an inline repository), 2.2 LTS (the
728-
# other release with the git-source download fallback the hosted
729-
# redirect must drop) and current 2.
727+
# so the fixture resolves from an inline repository), 2.2 LTS and
728+
# current 2. Every release from 1.x through 2.9 falls back to the
729+
# git `source` when the dist download fails (the fallback the
730+
# hosted redirect must drop); 2.10 does not. The exact-release
731+
# matrix, 2.9.8 and macOS/Windows included, is
732+
# composer-compatibility.yml.
730733
- {os: ubuntu-latest, suite: e2e_vendor_composer_build, composer: '2'}
731734
- {os: ubuntu-latest, suite: e2e_vendor_composer_build, composer: '2.2'}
732735
- {os: ubuntu-latest, suite: e2e_vendor_composer_build, composer: '1'}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
name: Composer patch compatibility
2+
3+
# Exact Composer releases on ubuntu, windows and macOS through the real
4+
# vendored (`e2e_vendor_composer_build`) and hosted
5+
# (`e2e_redirect_composer_build`) capstones, each ending in the
6+
# manifest-less VEX step, plus the hermetic `composer::` VEX cells.
7+
#
8+
# Release boundaries: 1.10.28 = Composer 1 (inline-repository fixture,
9+
# packagist dropped Composer 1 metadata); 2.0.14 / 2.1.14 = the 2.x lines
10+
# whose path mirror still drops `.gitignore`d files; 2.2.30 = LTS;
11+
# 2.5.8 / 2.8.12 = mid 2.x; 2.9.8 = the last release whose dist failure
12+
# falls back to `source`, so the hosted redirect's source drop is load
13+
# bearing there; 2.10.3 = current, no fallback.
14+
#
15+
# PHP: Composer 1 never on 8.5 (broken there). No 7.4 cell: the psr/log
16+
# 3.0.2 capstones need PHP >= 8.0; the depscan harness covers 7.4.
17+
#
18+
# Each leg runs one checksum-pinned composer.phar through `php` (the
19+
# capstones' SOCKET_PATCH_COMPOSER_PHAR mode), which is also what makes
20+
# Windows work: `Command::new("composer")` cannot resolve `composer.bat`.
21+
22+
on:
23+
pull_request:
24+
paths:
25+
- '.github/workflows/composer-compatibility.yml'
26+
- 'tests/docker/Dockerfile.composer'
27+
- 'Cargo.lock'
28+
- 'Cargo.toml'
29+
- 'crates/*/Cargo.toml'
30+
- 'crates/socket-patch-core/src/vendor/**'
31+
- 'crates/socket-patch-core/src/patch/**'
32+
- 'crates/socket-patch-core/src/manifest/**'
33+
- 'crates/socket-patch-core/tests/fixtures/redirect/composer/**'
34+
- 'crates/socket-patch-core/tests/fixtures/composer-version-vectors.json'
35+
- 'crates/socket-patch-core/src/crawlers/composer_crawler.rs'
36+
- 'crates/socket-patch-core/src/crawlers/composer_crawler/**'
37+
- 'crates/socket-patch-core/src/utils/composer*.rs'
38+
- 'crates/socket-patch-core/src/utils/purl.rs'
39+
- 'crates/socket-patch-core/src/utils/group_commit.rs'
40+
- 'crates/socket-patch-core/src/utils/durability.rs'
41+
- 'crates/socket-patch-core/src/vex/**'
42+
- 'crates/socket-patch-cli/src/commands/vendor*'
43+
- 'crates/socket-patch-cli/src/commands/get*.rs'
44+
- 'crates/socket-patch-cli/src/commands/apply.rs'
45+
- 'crates/socket-patch-cli/src/commands/rollback.rs'
46+
- 'crates/socket-patch-cli/src/commands/remove.rs'
47+
- 'crates/socket-patch-cli/src/commands/composer_hints.rs'
48+
- 'crates/socket-patch-cli/src/commands/scan/**'
49+
- 'crates/socket-patch-cli/src/commands/vex*.rs'
50+
- 'crates/socket-patch-cli/tests/e2e_*composer*.rs'
51+
- 'crates/socket-patch-cli/tests/docker_e2e_vendor_composer.rs'
52+
- 'crates/socket-patch-cli/tests/composer_e2e_common/**'
53+
- 'crates/socket-patch-cli/tests/docker_vendor_common/**'
54+
- 'crates/socket-patch-cli/tests/e2e_vex_lockfile/composer.rs'
55+
- 'crates/socket-patch-cli/tests/vex_e2e_common/**'
56+
push:
57+
branches: [main]
58+
workflow_dispatch:
59+
60+
permissions:
61+
contents: read
62+
63+
concurrency:
64+
group: composer-compat-${{ github.event.pull_request.number || github.ref }}
65+
cancel-in-progress: true
66+
67+
env:
68+
SOCKET_NO_CONFIG: '1'
69+
SOCKET_NO_UPDATE_CHECK: '1'
70+
71+
jobs:
72+
native:
73+
name: composer ${{ matrix.composer }} / php ${{ matrix.php }} / ${{ matrix.os }}
74+
strategy:
75+
fail-fast: false
76+
matrix:
77+
include:
78+
- {os: ubuntu-latest, composer: '1.10.28', php: '8.1', sha256: 0915af36eb01e3f0e16cd309adff7051832b9ef014e38371756804b20425cd5a}
79+
- {os: ubuntu-latest, composer: '2.0.14', php: '8.0', sha256: 29454b41558968ca634bf5e2d4d07ff2275d91b637a76d7a05e6747d36dd3473}
80+
- {os: ubuntu-latest, composer: '2.1.14', php: '8.1', sha256: d44a904520f9aaa766e8b4b05d2d9a766ad9a6f03fa1a48518224aad703061a4}
81+
- {os: ubuntu-latest, composer: '2.2.30', php: '8.1', sha256: 8c2b4478b64f8f7cdf1574838fdb0033b29049ca821dad452db7a3dcfcdbffc2}
82+
- {os: ubuntu-latest, composer: '2.2.30', php: '8.3', sha256: 8c2b4478b64f8f7cdf1574838fdb0033b29049ca821dad452db7a3dcfcdbffc2}
83+
- {os: ubuntu-latest, composer: '2.5.8', php: '8.2', sha256: f07934fad44f9048c0dc875a506cca31cc2794d6aebfc1867f3b1fbf48dce2c5}
84+
- {os: ubuntu-latest, composer: '2.8.12', php: '8.4', sha256: f446ea719708bb85fcbf4ef18def5d0515f1f9b4d703f6d820c9c1656e10a2f2}
85+
- {os: ubuntu-latest, composer: '2.9.8', php: '8.4', sha256: 59b2c50e10cafa0d8efc19ede9a326d782f096c674a26baf98cf042ce23de890}
86+
- {os: ubuntu-latest, composer: '2.10.3', php: '8.5', sha256: 7a2d379d5b8ffdaa028580ef26494c36d2feef4b178d3dd1473a4dbc5e17c8d6}
87+
- {os: windows-latest, composer: '1.10.28', php: '8.1', sha256: 0915af36eb01e3f0e16cd309adff7051832b9ef014e38371756804b20425cd5a}
88+
- {os: windows-latest, composer: '2.2.30', php: '8.3', sha256: 8c2b4478b64f8f7cdf1574838fdb0033b29049ca821dad452db7a3dcfcdbffc2}
89+
- {os: windows-latest, composer: '2.9.8', php: '8.4', sha256: 59b2c50e10cafa0d8efc19ede9a326d782f096c674a26baf98cf042ce23de890}
90+
- {os: windows-latest, composer: '2.10.3', php: '8.5', sha256: 7a2d379d5b8ffdaa028580ef26494c36d2feef4b178d3dd1473a4dbc5e17c8d6}
91+
- {os: macos-latest, composer: '1.10.28', php: '8.1', sha256: 0915af36eb01e3f0e16cd309adff7051832b9ef014e38371756804b20425cd5a}
92+
- {os: macos-latest, composer: '2.2.30', php: '8.3', sha256: 8c2b4478b64f8f7cdf1574838fdb0033b29049ca821dad452db7a3dcfcdbffc2}
93+
- {os: macos-latest, composer: '2.10.3', php: '8.5', sha256: 7a2d379d5b8ffdaa028580ef26494c36d2feef4b178d3dd1473a4dbc5e17c8d6}
94+
runs-on: ${{ matrix.os }}
95+
timeout-minutes: 60
96+
steps:
97+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
98+
with:
99+
persist-credentials: false
100+
- run: rustup show
101+
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
102+
with:
103+
key: composer-compat
104+
save-if: ${{ github.ref == 'refs/heads/main' }}
105+
- uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
106+
with:
107+
php-version: ${{ matrix.php }}
108+
tools: none
109+
extensions: zip, mbstring, curl, openssl
110+
ini-values: memory_limit=-1
111+
coverage: none
112+
- name: Download and verify composer ${{ matrix.composer }}
113+
shell: bash
114+
env:
115+
COMPOSER_RELEASE: ${{ matrix.composer }}
116+
COMPOSER_PHAR_SHA256: ${{ matrix.sha256 }}
117+
# The pinned digest is the contract; the published .sha256sum is a
118+
# second, independent check. php computes the digest so the step
119+
# needs no sha256sum/shasum on any runner image.
120+
run: |
121+
set -euo pipefail
122+
dir="$RUNNER_TEMP"
123+
if command -v cygpath >/dev/null 2>&1; then
124+
dir="$(cygpath -m "$RUNNER_TEMP")"
125+
fi
126+
phar="$dir/composer-$COMPOSER_RELEASE.phar"
127+
base="https://getcomposer.org/download/$COMPOSER_RELEASE/composer.phar"
128+
curl -fsSL --retry 3 -o "$phar" "$base"
129+
published="$(curl -fsSL --retry 3 "$base.sha256sum" | cut -d' ' -f1)"
130+
# shellcheck disable=SC2016 # $argv is PHP, not shell
131+
actual="$(php -r 'echo hash_file("sha256", $argv[1]);' "$phar")"
132+
if [ "$actual" != "$COMPOSER_PHAR_SHA256" ] || [ "$actual" != "$published" ]; then
133+
echo "::error::composer $COMPOSER_RELEASE phar sha256 $actual (pinned $COMPOSER_PHAR_SHA256, published $published)"
134+
exit 1
135+
fi
136+
reported="$(php "$phar" --version --no-ansi)"
137+
echo "$reported"
138+
case "$reported" in
139+
*"Composer version $COMPOSER_RELEASE "*) ;;
140+
*) echo "::error::expected composer $COMPOSER_RELEASE"; exit 1 ;;
141+
esac
142+
echo "SOCKET_PATCH_COMPOSER_PHAR=$phar" >> "$GITHUB_ENV"
143+
- name: Real-composer vendored + hosted flows with manifest-less VEX
144+
shell: bash
145+
env:
146+
SOCKET_PATCH_COMPOSER_E2E_REQUIRED: '1'
147+
SOCKET_PATCH_COMPOSER_E2E_VERSION: ${{ matrix.composer }}
148+
# The build capstones are `#[ignore]`-gated (the unpinned `test` job
149+
# skips them). The hermetic `composer::` VEX cells are a module of
150+
# the shared `e2e_vex_lockfile` binary; their filter goes in a second
151+
# command so it does not also filter the build suites.
152+
run: |
153+
cargo test -p socket-patch-cli --no-fail-fast \
154+
--test e2e_vendor_composer_build --test e2e_redirect_composer_build \
155+
-- --ignored --nocapture
156+
cargo test -p socket-patch-cli --test e2e_vex_lockfile -- composer:: --nocapture
157+
158+
docker:
159+
# The vendored Docker capstone against an exact composer image. Its
160+
# fixture resolves psr/log from packagist, which no longer serves
161+
# Composer 1, so 1.10.28 is covered by the native legs only.
162+
name: docker composer ${{ matrix.composer }}
163+
runs-on: ubuntu-latest
164+
timeout-minutes: 35
165+
strategy:
166+
fail-fast: false
167+
matrix:
168+
include:
169+
- {composer: '2.2.30', sha256: 8c2b4478b64f8f7cdf1574838fdb0033b29049ca821dad452db7a3dcfcdbffc2}
170+
- {composer: '2.10.3', sha256: 7a2d379d5b8ffdaa028580ef26494c36d2feef4b178d3dd1473a4dbc5e17c8d6}
171+
steps:
172+
- name: Checkout
173+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
174+
with:
175+
persist-credentials: false
176+
177+
- name: Set up Docker Buildx
178+
# `driver: docker` so the composer image's
179+
# `FROM socket-patch-test-base:latest` resolves against the host
180+
# daemon (as in ci.yml's e2e-docker job).
181+
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
182+
with:
183+
driver: docker
184+
185+
- name: Install Rust
186+
run: rustup show
187+
188+
# No `actions/cache`: this job builds Docker images (zizmor
189+
# cache-poisoning audit), as in ci.yml's e2e-docker job.
190+
191+
- name: Build base image
192+
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
193+
with:
194+
context: .
195+
file: tests/docker/Dockerfile.base
196+
tags: socket-patch-test-base:latest
197+
load: true
198+
199+
- name: Build composer ${{ matrix.composer }} image
200+
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
201+
with:
202+
context: .
203+
file: tests/docker/Dockerfile.composer
204+
build-args: |
205+
COMPOSER_VERSION=${{ matrix.composer }}
206+
COMPOSER_SHA256=${{ matrix.sha256 }}
207+
tags: socket-patch-test-composer:latest
208+
load: true
209+
210+
- name: Run the vendored composer Docker capstone
211+
env:
212+
SOCKET_PATCH_DOCKER_E2E_REQUIRED: '1'
213+
run: cargo test -p socket-patch-cli --features docker-e2e --test docker_e2e_vendor_composer

‎crates/socket-patch-cli/CLI_CONTRACT.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -718,7 +718,7 @@ to **six flavors**.
718718
| npm / vlt (`vlt-lock.json`, lockfileVersion 0 or 1 — A0 locks without a version and every other version refuse `vendor_lockfile_version_unsupported`; flavor `vlt`) | patched package **directory** `.socket/vendor/npm/<uuid>/[@scope/]<name>-<version>/node_modules/<name>/` (the extra `node_modules/<name>` level lets a package `require()` its own name), its `package.json` without `devDependencies`, plus `<uuid>/.gitignore` (re-includes the payload against the project's ignores, ignores vlt's links inside it) and `<uuid>/.gitattributes` (`-text`) | direct dependencies of the root or a workspace member only: the lock node becomes a `file` node for the directory, its importer edges and outgoing edges are re-keyed, and each importer's `package.json` spec becomes `file:<path>`; every moved entry lands where vlt's serializer puts it. A node whose only extra is one peer context (`ṗ:N`, `peer.N`, `peer.<16 hex>`: from vlt 1.0.8 a root dependency with resolved peers, from rc.15 a workspace member's) becomes a `file` node without the extra, as vlt writes `file:` dependencies, keeping its peer edges; revert restores the extra-bearing DepID. Refused before any write: transitive targets (`vendor_vlt_transitive_unsupported`), two or more instances of one `name@version` or a modifier extra, importer `peer` edges, foreign registries, a git, remote-tarball or local-directory node of the same package name (vlt records no version for it), a package `vlt build` would build in place (`vendor_vlt_build_scripts_unsupported`), a name declared in several dependency fields (`vendor_lock_entry_unsupported`), a spec that disagrees with the lock (`vendor_vlt_lock_out_of_sync`), a payload git would ignore (`vendor_artifact_gitignored`), a purl vendored under another flavor (`vendor_flavor_changed`); era-A locks warn `vendor_vlt_legacy_lockfile`; an optional dependency (or any dependency node_modules still links to its installed upstream copy) gets `vendor_vlt_reinstall_required` | fresh checkout, `vlt ci` with cold caches: the patched bytes load and `vlt-lock.json` stays byte-identical, also through a warm and a cold `vlt install --frozen-lockfile` (checked on 1.2.0, 1.0.10, 1.0.4, 1.0.0-rc.32 and 1.0.0-rc.14, and on every release by `docs/testing/vlt-compatibility.md`); no-op installs, `vlt install <new>`, `uninstall` and `vlt update` keep the direct dependency vendored. `vendor --revert` restores the registry node, edges and specs, keeping what vlt re-laid since, and refuses on drift |
719719
| cargo | crate dir `<name>-<version>/` (no `.cargo-checksum.json`) | (v5.0) `[patch.crates-io]` path entry in the **workspace-root `Cargo.toml`** (the manifest beside the `Cargo.lock` it detaches — never `.cargo/config*`) **+** Cargo.lock surgery (the `[[package]]` entry's `source`/`checksum` removed and its `version` set to the copy's TAGGED version `<version>+socket.<uuid>` — `<core>+<meta>.socket.<uuid>` when the version already has build metadata — with every lock reference that spells the old version rewritten, formats v1–v4; the copy's own `Cargo.toml` version carries the same tag, so the patched crate sees it in `CARGO_PKG_VERSION`; revert restores the lock byte for byte). Key: always the Socket-owned `<name>-socket-<first 8 hex of the uuid>` with `package = "<name>"` (the full uuid hex when that key is taken), never the bare crate name — cargo lets a config-file `[patch]` item (project, ancestor directory or `$CARGO_HOME`) replace the manifest item with the same key whatever its version, so keys any of those configs use are avoided and a re-run moves an entry off a now-shadowed key; two versions of one crate are wired side by side. Pre-v5 wiring in `.cargo/config.toml` / `.cargo/config` is moved into `Cargo.toml` by a re-run (`vendor`, `scan`/`get --mode vendored`) or `repair` (`cargo_wiring_migrated` note; the ledger's `cargo_patch_entry` record then names `Cargo.toml`); a detached lock entry left unwired by the pre-v5 multi-version overwrite is re-wired the same way (`cargo_wiring_restored`); every revert removes both spellings | `cargo build --locked --offline` on a fresh checkout — single-version manifest `[patch]` also builds with no network on cargo older than 1.56 (the old config-file wiring's floor); two vendored versions of ONE crate need `--offline` on cargo 1.56 and a populated registry index (or network access) on older cargo such as 1.41, which loads the index to tell them apart. Note: path deps build **without** `--cap-lints allow` |
720720
| golang | module dir `<module>@<version>/` | `go.mod` `replace <module> <ver> => ./.socket/vendor/golang/<uuid>/<module>@<ver>` | `go build` with `GOPROXY=off` + empty `GOMODCACHE` (directory replaces bypass go.sum entirely; survives `go mod tidy`) |
721-
| composer | package dir `<vendor>/<name>@<version>/` | `composer.lock` only: entry's `dist` → `{type: "path", url, reference: null}`, `source` removed, `transport-options: {symlink: false}` added. `content-hash` unaffected; `composer.json` untouched | `composer install` (from the lock alone, real copy not symlink, works under `--network none`). `composer update <pkg>` reverts it |
721+
| composer | package dir `<vendor>/<name>@<version>/`; the copy's `.gitignore` / `.hgignore` are emptied and its `.gitattributes` `export-ignore` rules dropped, because Composer's path mirror skips the files they match (`vendor_composer_mirror_filters_neutralized`; a patch that rewrites one of them is refused `vendor_composer_mirror_filter_conflict`). Re-runs heal copies vendored before this | `composer.lock` only: entry's `dist` → `{type: "path", url, reference: "<patch-uuid>"}`, `source` removed, `transport-options: {symlink: false}` added. `content-hash` unaffected; `composer.json` untouched | `composer install` (from the lock alone, real copy not symlink, works under `--network none`). Composer 1 does not reinstall an already-installed package whose dist changed: remove `vendor/<vendor>/<name>` first. `composer update <pkg>` reverts it. See `docs/testing/composer-compatibility.md` |
722722
| gem | gem dir `<name>-<version>/` + gemspec materialized from `specifications/` | **Gemfile + Gemfile.lock pair**: the `gem` line gains `path:` (or a managed block for transitive deps); the lock's spec block moves GEM→PATH and the DEPENDENCIES entry becomes `<name> (= <ver>)!`, in bundler's exact canonical form | `bundle install` (normal **and** `BUNDLE_FROZEN=true`), byte-stable lock. Lock-only edits are a silent unpatch — hence the mandatory pair |
723723
| pypi / uv (uv.lock) | rebuilt wheel (canonical PEP 427 filename; RECORD regenerated) | `[tool.uv.sources] <name> = {path}` in pyproject + surgical uv.lock rewrite; transitive deps via `[tool.uv] override-dependencies` | `uv sync --locked` / `--frozen --offline` (hash-verified, byte-stable lock) |
724724
| 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` |

0 commit comments

Comments
 (0)