Skip to content

Commit 94e91a1

Browse files
committed
Merge remote-tracking branch 'origin/main' into agent/fix-ci-hermetic-npm-bun
# Conflicts: # crates/socket-patch-cli/tests/spawn_env_hygiene.rs
2 parents 2bf2c2e + 887a5f7 commit 94e91a1

19 files changed

Lines changed: 912 additions & 197 deletions

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1540,10 +1540,10 @@ command's integration tests check its output.
15401540
| `vendor_lock_descriptor_unlocked` | revert `warnings[]` | vendor `--revert` / rollback / remove (yarn berry, #759): a descriptor of the vendored package was added while it was vendored, and the restored registry version does not satisfy it (or its range is one socket-patch cannot read, such as a dist-tag), so it has no `yarn.lock` entry after the revert. The revert still succeeds and the artifact is removed. Run `yarn install` and commit `yarn.lock`. |
15411541
| `pypi_{poetry,pdm,pipenv}_no_lockfile` | `failed` | vendor (pypi): a lock-less tool marker with no `requirements.txt` fallback — run `<tool> lock`. |
15421542
| `pypi_poetry_integrity_unverified` | `skipped` (warning) | vendor (pypi / poetry): the lock was written by Poetry < 1.4 (0.12 `[metadata.hashes]`, lock 1.0/1.1, or a 2.0 lock without a `@generated by Poetry X.Y.Z` header — 1.3 wrote those). That installer does not verify local wheel hashes (the committed wheel bytes are the protection) and does not replace an already-installed package at the same version; recreate the virtualenv or `pip uninstall` the package before `poetry install`, or upgrade Poetry. |
1543-
| `pypi_poetry_source_constraint` | `failed` / `skipped` (warning) | vendor (pypi / poetry): the pyproject pins the package to a named source (`source = "pypi"`), which Poetry records nothing for in the lock. Over a lock written by Poetry 1.3 – 2.2 (whose installer refuses that constraint on a file-sourced package, so every `poetry install` would fail) the package is refused before any write, `failed`; an already-wired lock is refused the same way instead of reading as in sync. Over any other lock the wiring stands and this is a warning naming the 1.3 – 2.2 range. The detail names the remedy: drop the `source` key or upgrade to Poetry >= 2.3, re-lock and re-run; when the lock is already wired it first says to run `socket-patch vendor --revert` (which reverts EVERY vendored package), since re-locking discards the wiring. |
1543+
| `pypi_poetry_source_constraint` | `failed` / `skipped` (warning) | vendor (pypi / poetry): the pyproject pins the package to a named source (`source = "pypi"`), which Poetry records nothing for in the lock. Over a lock written by Poetry 1.3 – 2.2 (whose installer refuses that constraint on a file-sourced package, so every `poetry install` would fail), or one with `lock-version` 1.0 / 1.1 (which every Poetry 1.3+ re-solves on install and then refuses the same way, #1503), the package is refused before any write, `failed`, and the detail says which; an already-wired lock is refused the same way instead of reading as in sync. Over a Poetry 2.3+ (or 0.x) lock the wiring stands and this is a warning naming the 1.3 – 2.2 range. The detail names the remedy: drop the `source` key or upgrade to Poetry >= 2.3, re-lock and re-run; when the lock is already wired it first says to run `socket-patch vendor --revert` (which reverts EVERY vendored package), since re-locking discards the wiring. |
15441544
| `redirect_poetry_stale_install_risk` | `warnings[]` (warning) | scan `--mode hosted` (poetry): same writer test as above — a warm virtualenv keeps the upstream package after the redirect on Poetry < 1.4 (1.4+ re-installs from the new source); fresh installs pick up the patched wheel. Emitted once per rewritten lock, only on the run that rewrites it. |
1545-
| `redirect_poetry_source_constraint` | `warnings[]` (warning) | scan `--mode hosted` (poetry): the pyproject pins the redirected package to a named source (`source = "pypi"`) and the lock was not written by Poetry 1.3 – 2.2 (those writers are refused with `redirect_poetry_lock_unsupported`, also when the lock is already redirected and before a vendored → hosted takeover reverts anything). The rewrite stands, and an already-redirected lock is still confirmed; the detail says a Poetry 1.3 – 2.2 installer would refuse the lock and to drop the `source` key if the project is installed with one. Exit code and `status` unchanged. |
1546-
| `redirect_poetry_entry_not_found` / `redirect_poetry_missing_sha256` / `redirect_poetry_lock_unsupported` | `warnings[]` (warning) | scan `--mode hosted` (poetry): the lock has no `[[package]]` at the granted version (uv-parity twin of `redirect_uv_entry_not_found`); the grant carries no SHA-256 (gated once per dep, not per lock); the lock is refused — Poetry 0.12 layout (URL sources ignored), an unsupported `lock-version`, a forked package listed at several versions, a user-authored `[package.source]` on another origin (an earlier Socket URL for the same wheel is superseded in place), a package the pyproject pins to a named source (`source = "pypi"`) over a lock written by Poetry 1.3 – 2.2, whose installer refuses that constraint on a URL-sourced package, a malformed `[metadata.files]`/`[metadata.hashes]`, or a wheel whose filename does not match the locked package. Exit code and `status` unchanged (hosted-refusal posture). A vendored → hosted takeover over a Poetry 0.x lock is retracted with this code (wet and `--dry-run`, see **Staged takeover**): the purl stays vendored and patched and is skipped with this code as `skipped` (hosted event). The detail names the remedy and its reach: run `socket-patch vendor --revert` (it reverts EVERY vendored package in the project, not just this one), upgrade to Poetry >= 1.0 and re-lock, then re-run `scan --mode hosted`. |
1545+
| `redirect_poetry_source_constraint` | `warnings[]` (warning) | scan `--mode hosted` (poetry): the pyproject pins the redirected package to a named source (`source = "pypi"`) and the lock is in format 2.1+ and was not written by Poetry 1.3 – 2.2 (those writers, and any `lock-version` 1.0 / 1.1 lock, which every Poetry 1.3+ re-solves and refuses (#1503), are refused with `redirect_poetry_lock_unsupported`, also when the lock is already redirected and before a vendored → hosted takeover reverts anything). The rewrite stands, and an already-redirected lock is still confirmed; the detail says a Poetry 1.3 – 2.2 installer would refuse the lock and to drop the `source` key if the project is installed with one. Exit code and `status` unchanged. |
1546+
| `redirect_poetry_entry_not_found` / `redirect_poetry_missing_sha256` / `redirect_poetry_lock_unsupported` | `warnings[]` (warning) | scan `--mode hosted` (poetry): the lock has no `[[package]]` at the granted version (uv-parity twin of `redirect_uv_entry_not_found`); the grant carries no SHA-256 (gated once per dep, not per lock); the lock is refused — Poetry 0.12 layout (URL sources ignored), an unsupported `lock-version`, a forked package listed at several versions, a user-authored `[package.source]` on another origin (an earlier Socket URL for the same wheel is superseded in place), a package the pyproject pins to a named source (`source = "pypi"`) over a lock written by Poetry 1.3 – 2.2, whose installer refuses that constraint on a URL-sourced package, or over a `lock-version` 1.0 / 1.1 lock, which every Poetry 1.3+ re-solves and refuses the same way (#1503), a malformed `[metadata.files]`/`[metadata.hashes]`, or a wheel whose filename does not match the locked package. Exit code and `status` unchanged (hosted-refusal posture). A vendored → hosted takeover over a Poetry 0.x lock is retracted with this code (wet and `--dry-run`, see **Staged takeover**): the purl stays vendored and patched and is skipped with this code as `skipped` (hosted event). The detail names the remedy and its reach: run `socket-patch vendor --revert` (it reverts EVERY vendored package in the project, not just this one), upgrade to Poetry >= 1.0 and re-lock, then re-run `scan --mode hosted`. |
15471547
| `redirect_pdm_refused` / `redirect_pdm_legacy_sync_required` | `warnings[]` (warning) | scan `--mode hosted` (pdm): the `pdm.lock` rewrite was refused — an unsupported `[metadata] lock_version` (the identity-losing `3.1` / `4.0`–`4.2` formats or an untested future format), an unsupported `strategy`, a package listed at several versions (fork) or absent, a user-authored `url`/`path`/VCS/`editable` source, hash-less or malformed `files`, or a wheel whose filename does not match the locked package (`redirect_pdm_refused`); or the lock was written in format `2` (PDM 0.12–1.4), whose upstream freshness bug lets `pdm install` regenerate the lock — use `pdm sync` (`redirect_pdm_legacy_sync_required`). A refused uuid is withheld from every other PyPI rewriter when `pdm.lock` is the install driver, and its patch is not confirmed. Exit code and `status` unchanged (hosted-refusal posture). |
15481548
| `redirect_bun_lock_unsupported` | `warnings[]` (warning) | scan/get `--mode hosted` (bun): the text lock's `lockfileVersion` is not 0, 1 or 2 (a newer version: update socket-patch, re-locking would reproduce it; no integer: re-lock with Bun ≥ 1.2 — the shared gate's text, identical to vendored's `vendor_lockfile_version_unsupported`), or its `packages` section is not bun's single-line grammar. Nothing rewritten; exit 0 (hosted-refusal posture). |
15491549
| `redirect_bun_workspace_unsupported` | `warnings[]` (warning) | scan/get `--mode hosted` (bun): a lockfileVersion-0 lock (Bun 1.1.39–1.1.45 `--save-text-lockfile`) holds `workspace:` packages; frozen installs of that grammar cannot keep the hosted tuple. Detail: "Bun version-0 workspace locks cannot preserve hosted tarballs on frozen installs; delete bun.lock and re-run `bun install` with Bun >= 1.2 (which writes lockfileVersion 1, accepted by hosted mode) — a plain in-place `bun install` bumps the version only when a workspace depends on another workspace (e.g. root -> member); otherwise it keeps version 0 or fails to resolve" (measured: Bun 1.2.0 keeps 0, 1.2.23–1.4.2 exit 1 "failed to resolve" on a root that does not depend on its members). Version-1/2 workspace locks are rewritten. Exit 0. |

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

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
2121
#[path = "common/mod.rs"]
2222
mod common;
23-
use common::{binary, git_sha256};
23+
use common::{binary, git_sha256, hermetic};
2424

2525
use std::path::{Path, PathBuf};
2626
use std::process::{Command, Output};
@@ -60,20 +60,20 @@ fn git_sha256_file(path: &Path) -> String {
6060
git_sha256(&content)
6161
}
6262

63+
/// Run the binary with the hermetic `SOCKET_*` environment (no token, so the
64+
/// scans take the public-proxy path `--proxy-url` pins). The gem crawler
65+
/// reads `BUNDLE_PATH`, `BUNDLE_APP_CONFIG`, the user's `~/.bundle/config`
66+
/// and the `gem env` homes (`GEM_HOME`, `GEM_PATH`, `~/.gem`), and the scans
67+
/// are unscoped, so every `BUNDLE_*` var, both gem-home vars and an
68+
/// activated venv are scrubbed and `HOME` points at a stand-in
69+
/// (`jvm_env::isolate_cli`): any of them would add the developer's gems or
70+
/// packages to the exact purl sets these tests assert.
6371
fn run(cwd: &Path, args: &[&str]) -> (i32, String, String) {
64-
let mut cmd = Command::new(binary());
72+
let mut cmd = hermetic::command(&binary());
73+
hermetic::scrub_extra(&mut cmd, &[hermetic::Extra::Bundle, hermetic::Extra::Venv]);
74+
cmd.env_remove("GEM_HOME").env_remove("GEM_PATH");
75+
common::jvm_env::isolate_cli(&mut cmd);
6576
cmd.args(args).current_dir(cwd);
66-
// Hermeticity, mirroring the production e2e suites: scrub every ambient
67-
// `SOCKET_*` (token, api/proxy URL, org, legacy aliases — not just the
68-
// two token vars) and block the socket-cli config.json, so a developer's
69-
// login or configured apiBaseUrl cannot silently repoint or re-tier
70-
// these runs.
71-
for (k, _) in std::env::vars_os() {
72-
if k.to_string_lossy().starts_with("SOCKET_") {
73-
cmd.env_remove(&k);
74-
}
75-
}
76-
cmd.env("SOCKET_NO_CONFIG", "true");
7777
let out: Output = cmd.output().expect("failed to execute socket-patch binary");
7878

7979
let code = out.status.code().unwrap_or(-1);
@@ -94,6 +94,11 @@ fn assert_run_ok(cwd: &Path, args: &[&str], context: &str) -> (String, String) {
9494
fn bundle_run(cwd: &Path, args: &[&str]) {
9595
let mut cmd = Command::new("bundle");
9696
cmd.args(args).current_dir(cwd);
97+
// Scrub first: the sweeps would otherwise remove the BUNDLE_PATH below,
98+
// and an ambient `BUNDLE_*` (frozen, deployment, without, ...) would
99+
// change what this install does.
100+
hermetic::scrub_socket_vars(&mut cmd);
101+
hermetic::scrub_extra(&mut cmd, &[hermetic::Extra::Bundle]);
97102
// Bundler 4 removed `bundle install --path`; BUNDLE_PATH is honored by
98103
// bundler 2 through 4 and keeps the vendor/bundle/ruby/*/gems layout
99104
// `find_gem_dir` expects. It also upholds cache_env's hermeticity

‎crates/socket-patch-cli/tests/e2e_vex_build/poetry.rs‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,9 +1179,10 @@ fn poetry_vendored_revendors_to_a_superseding_patch() {
11791179
/// `six = {version = "1.16.0", source = "pypi"}` leaves no trace in the
11801180
/// lock, and Poetry 1.3 – 2.2 refuse to install a lock that then sources six
11811181
/// from a URL or file ("Repository \"pypi\" does not exist."; 1.3 and 1.4
1182-
/// report a solver failure). Over a lock one of them wrote, both modes
1183-
/// refuse and leave the lock installable; other releases install the
1184-
/// rewired lock, with a warning (#1282, #1447).
1182+
/// report a solver failure), and every Poetry 1.3+ re-solves, then refuses,
1183+
/// a lock-version 1.0 / 1.1 lock (Poetry 1.0 – 1.2 write those). Over such a lock
1184+
/// both modes refuse and leave the lock installable; 2.3+ install the
1185+
/// rewired lock, with a warning (#1282, #1447, #1503).
11851186
#[test]
11861187
#[ignore = "real Poetry + PyPI; run by the CI e2e matrix per Poetry release"]
11871188
fn poetry_source_pypi_constraint_never_breaks_install() {
@@ -1206,7 +1207,10 @@ fn poetry_source_pypi_constraint_never_breaks_install() {
12061207
return;
12071208
};
12081209
// The lock was written by this Poetry, so it is also the installer.
1209-
let refused = ((1, 3)..(2, 3)).contains(&poetry.major_minor());
1210+
// 1.1 and 1.2 install their own lock, but they write lock format 1.1,
1211+
// which every Poetry 1.3+ re-solves and then refuses (#1503): refused
1212+
// too, since the installer usually outlives the lock.
1213+
let refused = poetry.major_minor() < (2, 3);
12101214
let mut patched = pristine.clone();
12111215
patched.extend_from_slice(MARKER);
12121216
let wheel = build_wheel(&patched);

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

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,105 @@ async fn uv_no_sources_is_refused_in_both_modes() {
864864
}
865865
}
866866

867+
/// #1501: a PEP 723 script takes uv's settings from the nearest directory
868+
/// at or above it that holds a `uv.toml` or a `pyproject.toml` with
869+
/// `[tool.uv]`. A parent project's `no-sources = true` therefore drops the
870+
/// script's source too, so both modes must refuse a script scanned from a
871+
/// subdirectory just as they do beside the setting. A parent pyproject
872+
/// without `[tool.uv]` sets nothing, and the script is still wired.
873+
#[tokio::test]
874+
async fn uv_script_no_sources_in_a_parent_is_refused_in_both_modes() {
875+
let parent_pyproject = "[project]\nname = \"root\"\nversion = \"0.1.0\"\n";
876+
for (file, setting, refused) in [
877+
("uv.toml", "no-sources = true\n".to_string(), true),
878+
(
879+
"pyproject.toml",
880+
format!("{parent_pyproject}\n[tool.uv]\nno-sources = true\n"),
881+
true,
882+
),
883+
(
884+
"pyproject.toml",
885+
format!("{parent_pyproject}\n[tool.uv]\nno-sources-package = [\"Six\"]\n"),
886+
true,
887+
),
888+
("pyproject.toml", parent_pyproject.to_string(), false),
889+
] {
890+
let (_tmp, root) = project();
891+
stage_script_lock(&root);
892+
std::fs::write(root.parent().unwrap().join(file), &setting).unwrap();
893+
let before = tree(&root);
894+
895+
let server = MockServer::start().await;
896+
mount_hosted_api(&server, true).await;
897+
let (code, env) = hosted_scan(&root, &server);
898+
assert_eq!(code, 0, "hosted {file}: {env:#}");
899+
if refused {
900+
assert_eq!(hosted_pinned(&env), 0, "hosted {file}: {env:#}");
901+
assert!(
902+
env.to_string().contains("redirect_uv_script_unsupported")
903+
&& env.to_string().contains(&format!("../{file}")),
904+
"hosted {file}: {env:#}"
905+
);
906+
assert_eq!(tree(&root), before, "hosted {file} wrote nothing");
907+
} else {
908+
assert_eq!(hosted_pinned(&env), 1, "hosted {file}: {env:#}");
909+
continue;
910+
}
911+
912+
stage_manifest(&root);
913+
let before = tree(&root);
914+
let (code, env) = run_cli(&root, &["vendor"], &[]);
915+
assert_ne!(code, 0, "vendored {file}: {env:#}");
916+
assert!(
917+
env.to_string().contains("pypi_uv_no_sources")
918+
&& env.to_string().contains(&format!("../{file}")),
919+
"vendored {file}: {env:#}"
920+
);
921+
assert_eq!(tree(&root), before, "vendored {file} wrote nothing");
922+
}
923+
}
924+
925+
/// In a uv workspace, uv takes a member's script settings from the
926+
/// workspace root, so a member's own `[tool.uv]` does not shield the script
927+
/// from the root's `no-sources`. The #1138 workspace-member guard refuses
928+
/// the run before the script lane's own lookup, in both modes.
929+
#[tokio::test]
930+
async fn uv_script_in_a_workspace_member_is_not_wired() {
931+
let (_tmp, root) = project();
932+
stage_script_lock(&root);
933+
std::fs::write(
934+
root.join("pyproject.toml"),
935+
"[project]\nname = \"s\"\nversion = \"0.1.0\"\n\n[tool.uv]\npackage = false\n",
936+
)
937+
.unwrap();
938+
std::fs::write(
939+
root.parent().unwrap().join("pyproject.toml"),
940+
"[project]\nname = \"b\"\nversion = \"0.1.0\"\n\n[tool.uv.workspace]\nmembers = [\"proj\"]\n\n[tool.uv]\nno-sources = true\n",
941+
)
942+
.unwrap();
943+
let before = tree(&root);
944+
945+
let server = MockServer::start().await;
946+
mount_hosted_api(&server, true).await;
947+
let (code, env) = hosted_scan(&root, &server);
948+
assert_eq!(code, 1, "hosted: {env:#}");
949+
assert_eq!(
950+
env["error"]["code"], "redirect_workspace_lockfile_elsewhere",
951+
"hosted: {env:#}"
952+
);
953+
assert_eq!(tree(&root), before, "hosted wrote nothing");
954+
955+
stage_manifest(&root);
956+
let before = tree(&root);
957+
let (code, env) = run_cli(&root, &["vendor"], &[]);
958+
assert_ne!(code, 0, "vendored: {env:#}");
959+
assert!(
960+
env.to_string().contains("pypi_uv_workspace_unsupported"),
961+
"vendored: {env:#}"
962+
);
963+
assert_eq!(tree(&root), before, "vendored wrote nothing");
964+
}
965+
867966
/// A Hatch project; returns its wiring files.
868967
fn stage_hatch(root: &Path) -> &'static [&'static str] {
869968
std::fs::write(

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ use serial_test::serial;
3030
/// scrubs by hand.
3131
const PENDING_RAW_SPAWNS: &[&str] = &[
3232
"apply/apply_network.rs",
33-
"e2e_gem.rs",
3433
"e2e_hosted_production.rs",
3534
"e2e_maven.rs",
3635
"e2e_redirect_cargo_shapes.rs",
@@ -39,8 +38,6 @@ const PENDING_RAW_SPAWNS: &[&str] = &[
3938
"e2e_vendored_production.rs",
4039
"e2e_vex_vendor.rs",
4140
"rollback/rollback_multicopy_blob_gate.rs",
42-
"vendor/vendor_gem_lockfile_only_e2e.rs",
43-
"vendor/vendor_rerun_no_network_e2e.rs",
4441
];
4542

4643
/// The environment `cmd`'s child would see: the parent environment with

0 commit comments

Comments
 (0)