Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 102 additions & 9 deletions crates/socket-patch-cli/src/commands/vendored_backend/repair.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ use socket_patch_core::manifest::schema::{PatchManifest, PatchRecord};
use socket_patch_core::utils::fs::read_regular_to_string;
use socket_patch_core::utils::purl::normalize_purl;
use socket_patch_core::vendor::{
self, artifact_is_file_shaped, check_vendored_artifact, parse_vendor_path, ArtifactHealth,
VendorEntry, VendorState, VendorWarning,
self, artifact_is_file_shaped, check_vendored_artifact, parse_vendor_path,
path::parse_vendor_reference, ArtifactHealth, VendorEntry, VendorState, VendorWarning,
};

use super::VendoredBackend;
Expand All @@ -33,7 +33,8 @@ struct Candidate {
}

/// Scan the wiring-bearing files for vendored-artifact references,
/// returning deduped `(ecosystem, uuid, artifact relpath)` triples. Pure
/// returning deduped `(ecosystem, uuid, artifact relpath)` triples (the
/// relpath is the uuid dir itself for a directory-wired unit). Pure
/// text scan plus native binary Bun resolution records and the canonical
/// path parser. Used by repair (references the ledger does not cover), by
/// the orphan sweeps (`vendor --revert`, `scan --prune`: a dir a lockfile
Expand Down Expand Up @@ -69,19 +70,24 @@ pub(crate) async fn scan_vendor_references(project_root: &Path) -> Vec<(String,
let slice = &rest[idx..];
// `:` ends a reference too: pnpm snapshot keys are
// `name@file:<path>:` and yaml mappings suffix the path with a
// colon — npm names/versions never contain one.
// colon — npm names/versions never contain one. `<` ends an XML
// element's text (Maven's `<url>…/<uuid></url>`).
let end = slice
.find([
'"', '\'', '`', ' ', '\t', '\n', '\r', ',', ')', ']', '}', ';', ':',
'"', '\'', '`', ' ', '\t', '\n', '\r', ',', ')', ']', '}', ';', ':', '<',
])
.unwrap_or(slice.len());
let candidate = slice[..end].replace('\\', "/");
if let Some(parts) = parse_vendor_path(&candidate) {
// NuGet's feed and Maven's repository name the uuid dir itself.
if let Some(parts) = parse_vendor_reference(&candidate) {
if seen.insert((parts.eco.to_string(), parts.uuid.clone())) {
out.push((
parts.eco.to_string(),
parts.uuid.clone(),
candidate.trim_start_matches("./").to_string(),
candidate
.trim_start_matches("./")
.trim_end_matches('/')
.to_string(),
));
}
}
Expand All @@ -93,8 +99,9 @@ pub(crate) async fn scan_vendor_references(project_root: &Path) -> Vec<(String,
}

/// Every wiring-bearing file name the vendor backends may rewrite, relative
/// to `project_root`: the registry's vendored wiring files
/// ([`registry::VENDORED`]), vlt importer manifests, the Python
/// to `project_root`: every file the registry says a vendored run writes
/// ([`registry::VENDORED`]: `nuget.config`, `pom.xml` and `hatch.toml`
/// included), vlt importer manifests, the Python
/// locks the root lists (and their scripts) and the requirements `-r`
/// include tree. Sorted and deduplicated; entries need not exist.
async fn wiring_files(project_root: &Path) -> Vec<String> {
Expand Down Expand Up @@ -1015,6 +1022,92 @@ mod tests {
);
}

/// #832, #958: every file a vendored run writes is scanned, and a
/// reference to the uuid dir itself counts. NuGet's feed and Maven's
/// repository name the dir (Windows backslashes and a trailing slash
/// included); a Hatch environment in `hatch.toml` names a wheel. Every
/// caller (repair, the orphan sweeps, the `vendor` stranded-reference
/// gate, rollback's ledger-less gate) reads this one scan.
#[tokio::test]
async fn scan_recovers_unit_dir_and_hatch_toml_references() {
let tmp = tempfile::tempdir().unwrap();
let nuget = "22222222-2222-4222-8222-222222222222";
let nuget_win = "55555555-5555-4555-8555-555555555555";
let maven = "33333333-3333-4333-8333-333333333333";
let pypi = "44444444-4444-4444-8444-444444444444";
let wheel = "six-1.16.0-py2.py3-none-any.whl";
for (file, text) in [
(
"nuget.config",
format!("<add key=\"socket-patch-vendor\" value=\".socket/vendor/nuget/{nuget}/\" />"),
),
(
"pom.xml",
format!("<url>file://${{project.basedir}}/.socket/vendor/maven/{maven}</url>"),
),
(
"hatch.toml",
format!("[envs.default]\ndependencies = [\"six @ {{root:uri}}/.socket/vendor/pypi/{pypi}/{wheel}\"]\n"),
),
] {
tokio::fs::write(tmp.path().join(file), text).await.unwrap();
}
Comment thread
mikolalysenko marked this conversation as resolved.
let refs = scan_vendor_references(tmp.path()).await;
assert_eq!(
refs,
vec![
(
"maven".to_string(),
maven.to_string(),
format!(".socket/vendor/maven/{maven}")
),
(
"nuget".to_string(),
nuget.to_string(),
format!(".socket/vendor/nuget/{nuget}")
),
(
"pypi".to_string(),
pypi.to_string(),
format!(".socket/vendor/pypi/{pypi}/{wheel}")
),
]
);

// The backslashed Windows spelling, under another config spelling.
// Its own project: on a case-insensitive file system `NuGet.Config`
// and `nuget.config` are one file.
let win = tempfile::tempdir().unwrap();
tokio::fs::write(
win.path().join("NuGet.Config"),
format!(
"<add key=\"socket-patch-vendor\" value=\".socket\\vendor\\nuget\\{nuget_win}\" />"
),
)
.await
.unwrap();
assert_eq!(
scan_vendor_references(win.path()).await,
vec![(
"nuget".to_string(),
nuget_win.to_string(),
format!(".socket/vendor/nuget/{nuget_win}")
)]
);

// A bare eco dir or a non-uuid dir is still no reference.
tokio::fs::write(
tmp.path().join("pom.xml"),
"<url>file://${project.basedir}/.socket/vendor/maven</url>\n\
<url>file://${maven.multiModuleProjectDirectory}/.socket/vendor/maven2</url>\n\
<url>file://${project.basedir}/.socket/vendor/maven/not-a-uuid</url>",
)
.await
.unwrap();
let refs = scan_vendor_references(tmp.path()).await;
assert!(refs.iter().all(|(eco, _, _)| eco != "maven"), "{refs:?}");
}

/// pnpm writes vendored paths in THREE spellings — override values,
/// `tarball:` fields, and snapshot KEYS with a trailing colon. The
/// scanner must yield the clean relpath whichever form it meets first.
Expand Down
100 changes: 100 additions & 0 deletions crates/socket-patch-cli/tests/repair/repair_vendor_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,106 @@ async fn repair_rebuilds_detached_entry_without_manifest() {
assert_socket_dir_lean(tmp.path());
}

/// 7b. #832, #958: NuGet's vendored feed (`nuget.config`), Maven's vendored
/// repository (`pom.xml`) and a Hatch environment (`hatch.toml`) wire a
/// unit too. With the ledger gone, repair reports each one as
/// `vendor_ledger_missing` instead of seeing no vendored traces at all.
/// NuGet and Maven name the uuid dir itself, not a file inside it.
#[tokio::test]
async fn repair_reports_missing_ledger_for_nuget_maven_and_hatch_wiring() {
let mock = MockServer::start().await;
mount_patch_api(&mock).await;
let tmp = tempfile::tempdir().unwrap();
let nuget = "22222222-2222-4222-8222-222222222222";
let maven = "33333333-3333-4333-8333-333333333333";
let pypi = "44444444-4444-4444-8444-444444444444";
let wheel = "six-1.16.0-py2.py3-none-any.whl";
let files = [
(
"nuget.config".to_string(),
format!(
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<configuration>\n <packageSources>\n \
<add key=\"socket-patch-vendor\" value=\".socket/vendor/nuget/{nuget}\" />\n \
</packageSources>\n</configuration>\n"
),
),
(
"pom.xml".to_string(),
format!(
"<project>\n <repositories>\n <repository>\n \
<id>socket-patch-vendor-{maven}</id>\n \
<url>file://${{project.basedir}}/.socket/vendor/maven/{maven}</url>\n \
</repository>\n </repositories>\n</project>\n"
),
),
(
"hatch.toml".to_string(),
format!(
"[envs.default]\ndependencies = [\n \"six @ {{root:uri}}/.socket/vendor/pypi/{pypi}/{wheel}#sha256={}\",\n]\n",
"0".repeat(64)
),
),
];
for (name, text) in &files {
std::fs::write(tmp.path().join(name), text).unwrap();
}
for (eco, uuid, leaf) in [
("nuget", nuget, "x.nupkg"),
("maven", maven, "x.pom"),
("pypi", pypi, wheel),
] {
let dir = tmp.path().join(format!(".socket/vendor/{eco}/{uuid}"));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join(leaf), b"artifact").unwrap();
}

let (code, stdout, stderr) = run_cli(tmp.path(), &mock.uri(), &["repair"]);
assert_eq!(code, 1, "stdout={stdout} stderr={stderr}");
let v = parse_env(&stdout);
let mut missing: Vec<(String, String, String)> = events_of(&v)
.into_iter()
.filter(|e| e["errorCode"] == "vendor_ledger_missing")
.map(|e| {
(
e["details"]["ecosystem"].as_str().unwrap_or("").to_string(),
e["uuid"].as_str().unwrap_or("").to_string(),
e["details"]["path"].as_str().unwrap_or("").to_string(),
)
})
.collect();
missing.sort();
assert_eq!(
missing,
vec![
(
"maven".to_string(),
maven.to_string(),
format!(".socket/vendor/maven/{maven}")
),
(
"nuget".to_string(),
nuget.to_string(),
format!(".socket/vendor/nuget/{nuget}")
),
(
"pypi".to_string(),
pypi.to_string(),
format!(
".socket/vendor/pypi/{pypi}/{wheel}#sha256={}",
"0".repeat(64)
)
),
],
"envelope={v}"
);
for (name, text) in &files {
assert_eq!(
&std::fs::read_to_string(tmp.path().join(name)).unwrap(),
text
);
}
}

/// G6 for a manifest-free vendored project: after the run, `.socket/` holds
/// exactly `vendor/` — no `apply.lock` outlives it, no blobs/diffs/packages
/// are conjured by a repair that rebuilds from the ledger's embedded record.
Expand Down
Loading
Loading