Skip to content

Commit a18ffd1

Browse files
mikolalysenkoclaude
andcommitted
fix(vendor): state the real cargo floor for two versions
Cargo before 1.56 resolves every source-less Cargo.lock entry for a crate through ONE [patch.crates-io] path — the entry whose key sorts last — so with two vendored versions one lock entry is pinned to the other version's copy and cargo build --locked fails closed with "patch for <crate> did not resolve to any crates". The docs claimed older cargo needed only a populated crates.io index; the index does not help. The old-toolchain e2e passed because its fixture uuids happened to sort so that the lower version's key came last, the one order 1.41 can take. Verified on the rust:1.41-slim and rust:1.56-slim images with two crates (cfg-if 1.0.5/0.1.10, lazy_static 1.5.0/0.2.11) and with three versions (bitflags 1.2.1/0.9.1/0.8.2): only the key sort order decides, the lock entry order does not, and 1.56 builds either way. The e2e fixture now uses uuids whose keys sort adversarially, asserts that order so a future uuid cannot quietly make the test toothless, and pins the refusal below 1.56 while requiring 1.56 to build both copies. Vendoring a second version of a crate warns with cargo_multi_version_old_cargo unless the project's rust-version or rust-toolchain[.toml] promises cargo 1.56 or newer; socket-patch never runs cargo, so those files are the only signal it has. A single vendored version still builds on 1.41. A key-naming scheme that puts the lowest version's key last does make 1.41 resolve every entry correctly, but it would replace the documented <name>-socket-<uuid8> key with a version-rank encoding in every project's committed manifest, and rest correctness on an undocumented cargo iteration order. Left out deliberately; the failure is loud, not silent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 4247daa commit a18ffd1

4 files changed

Lines changed: 429 additions & 63 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,13 @@ into the new version's section — see docs/releasing.md.
9797
of `.cargo/config.toml` / `.cargo/config`, so Socket scanners can recover
9898
the patch uuid from the manifest alone and single-version wiring builds
9999
on cargo older than 1.56 (the floor of config-file `[patch]`; proven on
100-
cargo 1.41 with no network); two vendored versions of one crate make older cargo
101-
load the crates.io index to tell the two entries apart: `--offline` from
102-
an empty `$CARGO_HOME` is enough on cargo 1.56, while older cargo such as
103-
1.41 needs the index itself (the crates.io index in `$CARGO_HOME`, or
104-
network access), and without `--offline` either one first tries to update
105-
the index and fails when it is unreachable. The edit is
100+
cargo 1.41 with no network). TWO vendored versions of one crate need
101+
cargo 1.45 or newer: from 1.45 `--offline` from an empty `$CARGO_HOME` is
102+
enough (without `--offline` it first tries to update the crates.io index
103+
and fails when that is unreachable), while cargo before 1.45 resolves
104+
every source-less lock entry for a crate through one `[patch]` path and
105+
fails closed on the other — see the `cargo_multi_version_old_cargo`
106+
entry under Fixed. The edit is
106107
format-preserving (comments, ordering, CRLF / mixed line endings, a
107108
UTF-8 BOM and the trailing-newline state survive; a revert restores the
108109
manifest byte for byte and keeps a user's own `[patch]` /
@@ -726,6 +727,24 @@ into the new version's section — see docs/releasing.md.
726727
`setup --remove` could not land byte-identical on the pre-setup file.
727728
`package.json` is now written in its own layout (BOM, indent, line ending,
728729
trailing-newline shape), the same helper the vendored backends use.
730+
- **Two vendored versions of one cargo crate are documented — and now
731+
warned about — as needing cargo 1.45.** The docs said older cargo (1.41)
732+
only needed a populated crates.io index. It needs more than that: cargo
733+
before 1.45 resolves every source-less `Cargo.lock` entry for a crate
734+
through ONE `[patch.crates-io]` path — the entry whose KEY sorts last —
735+
so one of the two versions is pinned to the other's copy and `cargo build
736+
--locked` fails closed with ``patch for `<crate>` … did not resolve to
737+
any crates``, index or no index. The old-toolchain e2e passed only
738+
because its fixture uuids happened to sort the other way; it now uses the
739+
adversarial order, and the floor was measured rather than assumed — on
740+
one two-version fixture in both key orders, 1.41.1, 1.42, 1.43 and 1.44
741+
refuse the adversarial order while 1.45, 1.49, 1.53, 1.56 and current
742+
stable resolve either order, each lock entry to its own copy. Vendoring a
743+
second version of a crate warns with `cargo_multi_version_old_cargo`
744+
unless the project's `rust-version` or `rust-toolchain[.toml]` promises
745+
cargo 1.45 or newer (socket-patch never runs `cargo`, so those files are
746+
the only signal it has). A SINGLE vendored version still builds on cargo
747+
1.41, as before.
729748
- **A CRLF `Cargo.lock` stays CRLF, and reverts byte-for-byte.** Vendoring
730749
rewrote every line of a lock committed with Windows line endings as LF
731750
(`toml_edit` renders LF only), and `vendor --revert` then "restored" the

‎crates/socket-patch-cli/tests/e2e_vendor_cargo_build.rs‎

Lines changed: 106 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@
3636
//! tagged lock build the patched copy with no network on cargo 1.41 / 1.56
3737
//! — the local `rust:1.41-slim` / `rust:1.56-slim` docker images, preferred,
3838
//! else installed rustup toolchains 1.36..=1.56 (type-check only) —
39-
//! config-file `[patch]` needs 1.56+; two versions of one crate there need
40-
//! `--offline`; skipped when neither is available, required by the
39+
//! config-file `[patch]` needs 1.56+; two versions of one crate need cargo
40+
//! 1.45+ and `--offline`; skipped when neither is available, required by the
4141
//! `cargo-old-toolchains` CI leg). Also: a
4242
//! URL-spelled crates.io `[patch]` table is refused, an ancestor-directory
4343
//! config entry cannot silently shadow the vendored copy, and the pre-v5
@@ -1190,7 +1190,14 @@ async fn cargo_get_uuid_vendored_fresh_checkout_locked_build() {
11901190

11911191
// ── v5 manifest wiring: multi-version, legacy migration, old toolchains ─
11921192

1193-
const UUID_OLD: &str = "3c4d5e6f-7081-4b2c-9d3e-123456789abc";
1193+
/// The second patch uuid of the multi-version fixtures. Deliberately
1194+
/// ADVERSARIAL: its `cfg-if-socket-1a2b3c4d` key sorts BEFORE `UUID`'s, so
1195+
/// the HIGHER version's `[patch.crates-io]` key sorts LAST — the order
1196+
/// cargo before 1.45 cannot resolve (see
1197+
/// `cargo_vendored_two_versions_are_refused_by_cargo_below_1_45`). With the
1198+
/// opposite order 1.41 happens to build, so a favorable pair would let a
1199+
/// regression in the modern-cargo path go unnoticed here.
1200+
const UUID_OLD: &str = "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d";
11941201
const PATCH_SUFFIX_OLD: &str =
11951202
"\n/// Socket-patch marker for the OLD major (added by the vendored patch).\npub fn socket_patched_old() -> u32 { 2 }\n";
11961203

@@ -2070,6 +2077,12 @@ const OLD_TOOLCHAINS_REQUIRED_ENV: &str = "SOCKET_PATCH_CARGO_OLD_TOOLCHAINS_REQ
20702077
/// locally (the test never pulls; the CI leg does).
20712078
const OLD_CARGO_IMAGES: [(&str, u32); 2] = [("rust:1.41-slim", 41), ("rust:1.56-slim", 56)];
20722079

2080+
/// The cargo minor from which two vendored versions of one crate resolve in
2081+
/// EITHER `[patch.crates-io]` key order — the floor
2082+
/// `socket_patch_core`'s `MULTI_VERSION_CARGO_MINOR` warns below. Measured
2083+
/// on 1.41.1/1.42/1.43/1.44 (refuse) and 1.45/1.49/1.53/1.56 (build).
2084+
const MULTI_VERSION_FLOOR: u32 = 45;
2085+
20732086
/// One old cargo under test.
20742087
#[derive(Clone, Debug)]
20752088
enum OldCargo {
@@ -2312,26 +2325,33 @@ fn cargo_vendored_manifest_patch_builds_on_old_toolchains() {
23122325
}
23132326
}
23142327

2315-
/// OLD TOOLCHAIN, TWO VERSIONS of one crate: older cargo loads the
2316-
/// crates.io index to tell two same-named `[patch]` entries apart. Every
2317-
/// clause of the documented constraint is ASSERTED here, in both
2318-
/// directions, because both arms run with no network (docker `--network
2319-
/// none`, rustup pointed at a dead proxy):
2328+
/// OLD TOOLCHAIN, TWO VERSIONS of one crate: cargo 1.45 is the FLOOR.
2329+
///
2330+
/// Cargo before 1.45 resolves every source-less `Cargo.lock` entry for a
2331+
/// crate through ONE `[patch.crates-io]` path — the entry whose KEY sorts
2332+
/// last — so with two vendored versions one lock entry is pinned to the
2333+
/// other version's copy. The fixture uuids are chosen so the HIGHER
2334+
/// version's key sorts last (see `UUID_OLD`), the order 1.41 cannot take;
2335+
/// with the opposite order 1.41 happens to build, which is how the
2336+
/// limitation went unnoticed. Asserted in every direction, with no network
2337+
/// (docker `--network none`, rustup pointed at a dead proxy):
23202338
///
2321-
/// * without `--offline` the build MUST fail, and fail on the unreachable
2322-
/// index — the negative control for "pass `--offline`";
2323-
/// * cargo 1.56 then builds under `--offline` from an EMPTY CARGO_HOME;
2324-
/// * older cargo (1.41) MUST first fail even under `--offline` (no registry
2325-
/// cache to read), and the documented remedy — a populated crates.io
2326-
/// index in `$CARGO_HOME` — must then build (and run) both patched
2327-
/// copies with no network. That step is unconditional below 1.56, so a
2328-
/// cargo that stops needing it fails this test instead of silently
2329-
/// skipping the remedy.
2339+
/// * without `--offline` EVERY old cargo fails on the unreachable
2340+
/// crates.io index (it loads the index to tell two same-named `[patch]`
2341+
/// entries apart) — the negative control for "pass `--offline`";
2342+
/// * cargo 1.45 and later then build BOTH copies under `--offline` from an
2343+
/// EMPTY CARGO_HOME (measured on 1.45, 1.49, 1.53 and 1.56);
2344+
/// * below 1.45 `--offline` fails on the patch resolution itself —
2345+
/// ``patch for `cfg-if` … did not resolve to any crates`` — from an empty
2346+
/// CARGO_HOME AND with the index remedy that makes a single vendored
2347+
/// version work there (measured on 1.41.1, 1.42, 1.43 and 1.44). A cargo
2348+
/// below 1.45 that starts building this fails the test instead of
2349+
/// silently widening the supported range.
23302350
///
2331-
/// Current stable needs neither — see
2351+
/// Current stable needs none of it — see
23322352
/// `cargo_vendor_two_versions_of_one_crate_locked_build`.
23332353
#[test]
2334-
fn cargo_vendored_two_versions_on_old_toolchains_need_offline() {
2354+
fn cargo_vendored_two_versions_are_refused_by_cargo_below_1_45() {
23352355
const SUITE: &str = "e2e_vendor_cargo_build (old-toolchain multi-version)";
23362356
let Some(cargos) = old_cargos_or_skip(SUITE) else {
23372357
return;
@@ -2345,6 +2365,22 @@ fn cargo_vendored_two_versions_on_old_toolchains_need_offline() {
23452365
};
23462366
let env = vendor_ok(&fx.proj, &fx.cargo_home, "old-toolchain multi-version");
23472367
assert_eq!(env["summary"]["failed"], 0, "{env}");
2368+
// The vendor says the project now needs cargo 1.45 (the fixture
2369+
// declares no `rust-version` and pins no toolchain).
2370+
assert!(
2371+
env["events"]
2372+
.as_array()
2373+
.unwrap()
2374+
.iter()
2375+
.any(|e| e["errorCode"] == "cargo_multi_version_old_cargo"),
2376+
"the second version must warn about the cargo floor: {env}"
2377+
);
2378+
// The adversarial key order is what this test pins.
2379+
let manifest = std::fs::read_to_string(fx.proj.join("Cargo.toml")).unwrap();
2380+
assert!(
2381+
socket_key(UUID_OLD) < socket_key(UUID),
2382+
"the HIGHER version's key must sort last: {manifest}"
2383+
);
23482384
std::fs::write(fx.proj.join("src/main.rs"), TWO_VERSION_MAIN).unwrap();
23492385
let lock = std::fs::read_to_string(fx.proj.join("Cargo.lock")).unwrap();
23502386
if !lock.contains("version = 4\n") {
@@ -2373,43 +2409,66 @@ fn cargo_vendored_two_versions_on_old_toolchains_need_offline() {
23732409
wiring:\n{online_stderr}"
23742410
);
23752411
println!("old-toolchain multi-version {name} without --offline: needs the index");
2376-
let mut run = old_cargo_run(&old, &fresh, &["--offline"]);
2377-
if old.minor() < 56 {
2378-
let stderr = String::from_utf8_lossy(&run.out.stderr).into_owned();
2379-
assert!(
2380-
!run.out.status.success(),
2381-
"{name}: below 1.56 an EMPTY CARGO_HOME cannot tell the two [patch] \
2382-
entries apart offline — the reason the index remedy is documented:\n\
2383-
{stderr}"
2384-
);
2412+
2413+
let run = old_cargo_run(&old, &fresh, &["--offline"]);
2414+
let stderr = String::from_utf8_lossy(&run.out.stderr).into_owned();
2415+
if old.minor() >= MULTI_VERSION_FLOOR {
23852416
assert!(
2386-
stderr.contains("unable to fetch registry") && stderr.contains("in offline mode"),
2387-
"{name}: the only accepted failure below 1.56 is the missing registry \
2388-
index:\n{stderr}"
2417+
run.out.status.success(),
2418+
"{name}: 1.{MULTI_VERSION_FLOOR}+ must build two vendored versions under \
2419+
--offline from an empty CARGO_HOME:\n{stderr}"
23892420
);
2390-
println!("old-toolchain multi-version {name}: needs a registry index (documented)");
2391-
seed_old_crates_io_index(&fresh.join(".old-cargo-home"), &[&fx.new_v, &fx.old_v]);
2392-
run = old_cargo_run(&old, &fresh, &["--offline"]);
2421+
if matches!(old, OldCargo::Docker { .. }) {
2422+
assert!(
2423+
String::from_utf8_lossy(&run.out.stdout).contains("MARKER:1:2"),
2424+
"{name}: both patched copies run: {}",
2425+
String::from_utf8_lossy(&run.out.stdout)
2426+
);
2427+
}
2428+
let _ = std::fs::remove_dir_all(&fresh);
2429+
continue;
23932430
}
2431+
2432+
// Below 1.45 the patch resolution itself fails, with an empty
2433+
// CARGO_HOME and with the documented index remedy alike.
23942434
assert!(
2395-
run.out.status.success(),
2396-
"{name}: two vendored versions must build under --offline:\n{}",
2397-
String::from_utf8_lossy(&run.out.stderr)
2435+
!run.out.status.success(),
2436+
"{name}: below 1.{MULTI_VERSION_FLOOR} two vendored versions must NOT build \
2437+
offline:\n{stderr}"
2438+
);
2439+
assert!(
2440+
stderr.contains("did not resolve to any crates"),
2441+
"{name}: below 1.{MULTI_VERSION_FLOOR} the failure is the unresolvable second \
2442+
`[patch]` entry:\n{stderr}"
2443+
);
2444+
seed_old_crates_io_index(&fresh.join(".old-cargo-home"), &[&fx.new_v, &fx.old_v]);
2445+
let seeded = old_cargo_run(&old, &fresh, &["--offline"]);
2446+
let seeded_stderr = String::from_utf8_lossy(&seeded.out.stderr).into_owned();
2447+
assert!(
2448+
!seeded.out.status.success(),
2449+
"{name}: below 1.{MULTI_VERSION_FLOOR} two vendored versions must NOT build \
2450+
even with a populated crates.io index — if this cargo now resolves both \
2451+
`[patch]` entries, the documented floor is wrong:\nstdout:\n{}\nstderr:\n\
2452+
{seeded_stderr}",
2453+
String::from_utf8_lossy(&seeded.out.stdout)
2454+
);
2455+
assert!(
2456+
seeded_stderr.contains("did not resolve to any crates"),
2457+
"{name}: the pinned failure is the unresolvable second `[patch]` entry, not \
2458+
something else:\n{seeded_stderr}"
2459+
);
2460+
println!(
2461+
"old-toolchain multi-version {name}: refused below 1.{MULTI_VERSION_FLOOR} \
2462+
(documented)"
23982463
);
2399-
if matches!(old, OldCargo::Docker { .. }) {
2400-
assert!(
2401-
String::from_utf8_lossy(&run.out.stdout).contains("MARKER:1:2"),
2402-
"{name}: both patched copies run: {}",
2403-
String::from_utf8_lossy(&run.out.stdout)
2404-
);
2405-
}
24062464
let _ = std::fs::remove_dir_all(&fresh);
24072465
}
24082466
}
24092467

2410-
/// The documented remedy for two vendored versions on cargo older than
2411-
/// 1.56: a populated crates.io index in `$CARGO_HOME`. Writes the minimal
2412-
/// one an old cargo reads offline — the git index at the pre-1.85
2468+
/// A populated crates.io index in `$CARGO_HOME` — what an old cargo needs
2469+
/// before it can even look at two same-named `[patch]` entries offline, and
2470+
/// the state in which the sub-1.45 refusal is pinned. Writes the minimal
2471+
/// index an old cargo reads offline — the git index at the pre-1.85
24132472
/// `registry/index/github.com-1ecc6299db9ec823` path (`origin/HEAD` names
24142473
/// the tree cargo loads), listing `versions` of the patched crate. No
24152474
/// `.crate` is cached: every listed version is patched to a path copy, so

0 commit comments

Comments
 (0)