Skip to content

Commit c99ddb6

Browse files
committed
Merge main into arch-refactor/792-retire-diff-download-mode
Resolve docs/migrating-to-v5.md: keep this PR's --download-mode removal row and main's new SOCKET_FORCE row (#1021) in the removed-spellings table. Co-Authored-By: Claude <noreply@anthropic.com>
2 parents 9fc3cd3 + cf8b164 commit c99ddb6

192 files changed

Lines changed: 6511 additions & 3303 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/composer-compatibility.yml‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,10 @@ jobs:
181181
include:
182182
- {os: macos-latest, composer: '1.10.28', php: '8.1', sha256: 0915af36eb01e3f0e16cd309adff7051832b9ef014e38371756804b20425cd5a}
183183
- {os: macos-latest, composer: '2.2.30', php: '8.3', sha256: 8c2b4478b64f8f7cdf1574838fdb0033b29049ca821dad452db7a3dcfcdbffc2}
184-
- {os: macos-latest, composer: '2.10.3', php: '8.5', sha256: 7a2d379d5b8ffdaa028580ef26494c36d2feef4b178d3dd1473a4dbc5e17c8d6}
184+
# PHP 8.4 on macOS: setup-php 2.37.2 cannot install 8.5 from Homebrew
185+
# since PHP 8.6 shipped (every run from 2026-10-07 23:51Z failed in
186+
# "Setup PHP"). Ubuntu and Windows keep the 2.10.3 / 8.5 cell.
187+
- {os: macos-latest, composer: '2.10.3', php: '8.4', sha256: 7a2d379d5b8ffdaa028580ef26494c36d2feef4b178d3dd1473a4dbc5e17c8d6}
185188
runs-on: ${{ matrix.os }}
186189
timeout-minutes: 60
187190
steps: *native-steps

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

Lines changed: 27 additions & 19 deletions
Large diffs are not rendered by default.

‎crates/socket-patch-cli/src/args.rs‎

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -676,7 +676,6 @@ pub const GLOBAL_ARG_ENV_VARS: &[&str] = &[
676676
/// missing here escapes the empty-var scrub — the invariant tests below
677677
/// parse every entry against its owning subcommand to keep this honest.
678678
pub const LOCAL_ARG_ENV_VARS: &[&str] = &[
679-
"SOCKET_FORCE",
680679
"SOCKET_PATCH_VERSION",
681680
"SOCKET_SAVE_ONLY",
682681
"SOCKET_ALL_RELEASES",
@@ -1619,9 +1618,6 @@ mod tests {
16191618
// (env var, argv of a subcommand that binds it) — every bool entry
16201619
// of `LOCAL_ARG_ENV_VARS`, on each subcommand that binds it.
16211620
const BOOL_BINDINGS: &[(&str, &[&str])] = &[
1622-
("SOCKET_FORCE", &["socket-patch", "apply"]),
1623-
("SOCKET_FORCE", &["socket-patch", "vendor"]),
1624-
("SOCKET_FORCE", &["socket-patch", "self-update"]),
16251621
("SOCKET_SAVE_ONLY", &["socket-patch", "get", "x"]),
16261622
("SOCKET_ALL_RELEASES", &["socket-patch", "get", "x"]),
16271623
("SOCKET_ALL_RELEASES", &["socket-patch", "scan"]),
@@ -1659,6 +1655,48 @@ mod tests {
16591655
});
16601656
}
16611657

1658+
/// v5 retired `SOCKET_FORCE` (#615): `--force` is flag-only on `apply`,
1659+
/// `vendor` and `--update`. A stale export must be ignored, so it can
1660+
/// never quietly skip the beforeHash check or the managed-install
1661+
/// refusal; the flag itself still works.
1662+
#[test]
1663+
#[serial_test::serial]
1664+
fn socket_force_env_is_ignored_and_force_flag_still_works() {
1665+
fn force_of(argv: &[&str]) -> bool {
1666+
let argv = argv.iter().map(|s| s.to_string()).collect();
1667+
match crate::parse_argv_with_shortcuts(argv)
1668+
.unwrap_or_else(|e| panic!("parse failed: {e}"))
1669+
.command
1670+
{
1671+
crate::Commands::Apply(a) => a.force,
1672+
crate::Commands::Vendor(a) => a.force,
1673+
crate::Commands::SelfUpdate(a) => a.force,
1674+
_ => panic!("unexpected subcommand"),
1675+
}
1676+
}
1677+
1678+
const ARGVS: &[&[&str]] = &[
1679+
&["socket-patch", "apply"],
1680+
&["socket-patch", "vendor"],
1681+
&["socket-patch", "self-update"],
1682+
&["socket-patch", "--update"],
1683+
];
1684+
1685+
with_clean_socket_env(|| {
1686+
with_env_cleared(&["SOCKET_FORCE"], || {
1687+
for val in ["1", "true", "yes"] {
1688+
std::env::set_var("SOCKET_FORCE", val);
1689+
for &argv in ARGVS {
1690+
assert!(!force_of(argv), "SOCKET_FORCE={val} set force on {argv:?}");
1691+
let mut with_flag = argv.to_vec();
1692+
with_flag.push("--force");
1693+
assert!(force_of(&with_flag), "--force ignored on {argv:?}");
1694+
}
1695+
}
1696+
});
1697+
});
1698+
}
1699+
16621700
/// Companion invariant for the **value-typed** local env vars: an
16631701
/// exported-but-empty value (`VAR=`) must not crash its subcommand —
16641702
/// [`scrub_empty_env_vars`] (run by `main` before clap) removes it, and

‎crates/socket-patch-cli/src/commands/apply.rs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ pub struct ApplyArgs {
106106
#[arg(
107107
short = 'f',
108108
long,
109-
env = "SOCKET_FORCE",
110109
default_value_t = false,
111110
value_parser = crate::args::parse_bool_flag,
112111
)]

‎crates/socket-patch-cli/src/commands/get.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1496,7 +1496,7 @@ async fn filter_to_installed_purls(
14961496
if mode != super::scan::ScanMode::Agent {
14971497
present.extend(supplement.entries.iter().map(|e| PurlKey::new(&e.purl)));
14981498
let vendored =
1499-
super::scan::project_vendored_supplement(common, &[], &ctx.loaded().await.vendor)
1499+
super::scan::project_vendored_supplement(&ctx, &[], &ctx.loaded().await.vendor)
15001500
.await;
15011501
present.extend(vendored.packages.iter().map(|p| PurlKey::new(&p.purl)));
15021502
}

‎crates/socket-patch-cli/src/commands/scan/discovery.rs‎

Lines changed: 106 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -173,16 +173,20 @@ pub(crate) struct LedgerSupplement {
173173
/// the ledger `run` already loaded (`vendor::load_state`).
174174
///
175175
/// That holds only while the lock still wires the artifact. An entry the
176-
/// lockfile in-use probe (the one the prune GC reverts by) answers
177-
/// `Some(false)` for is the dependency having left the lock — bumped or
176+
/// project no longer consumes ([`Discovery::vendor_entry_in_use`] is
177+
/// `Some(false)` — the verdict the prune GC reverts by, read from `ctx`'s
178+
/// discovery) is the dependency having left the lock — bumped or
178179
/// uninstalled — and is reported in [`LedgerSupplement::unwired`] instead:
179180
/// re-vendoring it would fail against a lock that no longer has it. `None`
180-
/// (no probe for the ecosystem, or no readable lock) keeps the entry.
181+
/// (no readable lock for the ecosystem) keeps the entry.
182+
///
183+
/// [`Discovery::vendor_entry_in_use`]: socket_patch_core::vex::discover::Discovery::vendor_entry_in_use
181184
pub(crate) async fn vendored_ledger_supplement(
182-
common: &GlobalArgs,
185+
ctx: &crate::commands::context::ProjectContext<'_>,
183186
crawled: &[socket_patch_core::crawlers::types::CrawledPackage],
184187
state: &std::io::Result<VendorState>,
185188
) -> LedgerSupplement {
189+
let common = ctx.common;
186190
let mut out = LedgerSupplement::default();
187191
if common.is_global() {
188192
return out;
@@ -226,7 +230,12 @@ pub(crate) async fn vendored_ledger_supplement(
226230
continue;
227231
}
228232
if let Some(entry) = entry {
229-
if crate::commands::vendor::dispatch_in_use_one(entry, &common.cwd).await == Some(false)
233+
if ctx
234+
.discovery()
235+
.await
236+
.vendor_entry_in_use(&common.cwd, entry)
237+
.await
238+
== Some(false)
230239
{
231240
out.unwired.push(ledger_key.clone());
232241
continue;
@@ -1120,9 +1129,13 @@ mod tests {
11201129
..GlobalArgs::default()
11211130
};
11221131
let state = socket_patch_core::vendor::load_state(root).await;
1123-
vendored_ledger_supplement(&args, crawled, &state)
1124-
.await
1125-
.packages
1132+
vendored_ledger_supplement(
1133+
&crate::commands::context::ProjectContext::new(&args),
1134+
crawled,
1135+
&state,
1136+
)
1137+
.await
1138+
.packages
11261139
}
11271140

11281141
/// A ledger entry vendored as `@3.0.2.0` is the crawled composer
@@ -1148,18 +1161,26 @@ mod tests {
11481161
cwd: tmp.path().to_path_buf(),
11491162
..GlobalArgs::default()
11501163
};
1151-
let out = vendored_ledger_supplement(&args, &[crawled], &Ok(state.clone()))
1152-
.await
1153-
.packages;
1164+
let out = vendored_ledger_supplement(
1165+
&crate::commands::context::ProjectContext::new(&args),
1166+
&[crawled],
1167+
&Ok(state.clone()),
1168+
)
1169+
.await
1170+
.packages;
11541171
assert!(
11551172
out.is_empty(),
11561173
"{:?}",
11571174
out.iter().map(|p| &p.purl).collect::<Vec<_>>()
11581175
);
11591176

1160-
let out = vendored_ledger_supplement(&args, &[], &Ok(state))
1161-
.await
1162-
.packages;
1177+
let out = vendored_ledger_supplement(
1178+
&crate::commands::context::ProjectContext::new(&args),
1179+
&[],
1180+
&Ok(state),
1181+
)
1182+
.await
1183+
.packages;
11631184
assert_eq!(
11641185
out.iter().map(|p| p.purl.as_str()).collect::<Vec<_>>(),
11651186
vec!["pkg:composer/psr/log@3.0.2.0"]
@@ -1229,19 +1250,82 @@ mod tests {
12291250
})
12301251
.to_string();
12311252
let state = npm_ledger_with_lock(tmp.path(), Some(&bumped)).await;
1232-
let out = vendored_ledger_supplement(&args(tmp.path()), &[], &state).await;
1253+
let out = vendored_ledger_supplement(
1254+
&crate::commands::context::ProjectContext::new(&args(tmp.path())),
1255+
&[],
1256+
&state,
1257+
)
1258+
.await;
12331259
assert!(out.packages.is_empty(), "{:?}", out.packages);
12341260
assert_eq!(out.unwired, vec!["pkg:npm/left-pad@1.3.0".to_string()]);
12351261

12361262
// Uninstalled: the lock has no left-pad at all.
12371263
let tmp = tempfile::tempdir().unwrap();
12381264
let removed = r#"{"name":"app","lockfileVersion":3,"packages":{"":{"name":"app"}}}"#;
12391265
let state = npm_ledger_with_lock(tmp.path(), Some(removed)).await;
1240-
let out = vendored_ledger_supplement(&args(tmp.path()), &[], &state).await;
1266+
let out = vendored_ledger_supplement(
1267+
&crate::commands::context::ProjectContext::new(&args(tmp.path())),
1268+
&[],
1269+
&state,
1270+
)
1271+
.await;
12411272
assert!(out.packages.is_empty(), "{:?}", out.packages);
12421273
assert_eq!(out.unwired, vec!["pkg:npm/left-pad@1.3.0".to_string()]);
12431274
}
12441275

1276+
/// B19: the supplement and the prune GC share one in-use verdict for
1277+
/// every ecosystem, not only npm/cargo/pypi-requirements. A COMPOSER
1278+
/// entry whose dependency composer.lock bumped to a registry release is
1279+
/// unwired — before, it was resurrected as a discovered package forever.
1280+
#[tokio::test]
1281+
async fn ledger_supplement_reports_a_bumped_composer_entry_unwired() {
1282+
const COMPOSER_PURL: &str = "pkg:composer/monolog/monolog@3.0.0";
1283+
let tmp = tempfile::tempdir().unwrap();
1284+
let root = tmp.path();
1285+
let leaf = format!(".socket/vendor/composer/{VENDORED_UUID}/monolog/monolog@3.0.0");
1286+
let entry: socket_patch_core::vendor::VendorEntry =
1287+
serde_json::from_value(serde_json::json!({
1288+
"ecosystem": "composer",
1289+
"basePurl": COMPOSER_PURL,
1290+
"uuid": VENDORED_UUID,
1291+
"artifact": {"path": leaf, "sha256": ""},
1292+
"wiring": [],
1293+
"detached": true,
1294+
}))
1295+
.unwrap();
1296+
let mut state = VendorState::default();
1297+
state.entries.insert(COMPOSER_PURL.to_string(), entry);
1298+
std::fs::write(
1299+
root.join("composer.lock"),
1300+
serde_json::json!({
1301+
"packages": [{
1302+
"name": "monolog/monolog",
1303+
"version": "3.1.0",
1304+
"dist": {
1305+
"type": "zip",
1306+
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/abc",
1307+
"reference": "abc",
1308+
},
1309+
}],
1310+
"packages-dev": [],
1311+
})
1312+
.to_string(),
1313+
)
1314+
.unwrap();
1315+
let args = GlobalArgs {
1316+
cwd: root.to_path_buf(),
1317+
..GlobalArgs::default()
1318+
};
1319+
let out = vendored_ledger_supplement(
1320+
&crate::commands::context::ProjectContext::new(&args),
1321+
&[],
1322+
&Ok(state),
1323+
)
1324+
.await;
1325+
assert!(out.packages.is_empty(), "{:?}", out.packages);
1326+
assert_eq!(out.unwired, vec![COMPOSER_PURL.to_string()]);
1327+
}
1328+
12451329
/// The fresh-clone case the supplement exists for: the lock still
12461330
/// resolves through the committed artifact, so the entry stays
12471331
/// discoverable. With no lock at all, nothing proves the entry unused,
@@ -1260,7 +1344,12 @@ mod tests {
12601344
..GlobalArgs::default()
12611345
};
12621346
let state = npm_ledger_with_lock(tmp.path(), lock.as_deref()).await;
1263-
let out = vendored_ledger_supplement(&args, &[], &state).await;
1347+
let out = vendored_ledger_supplement(
1348+
&crate::commands::context::ProjectContext::new(&args),
1349+
&[],
1350+
&state,
1351+
)
1352+
.await;
12641353
assert_eq!(
12651354
out.packages
12661355
.iter()

0 commit comments

Comments
 (0)