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
65 changes: 64 additions & 1 deletion crates/socket-patch-cli/src/commands/repair_vendor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,24 @@ const WIRING_FILES: &[&str] = &[
pub(crate) async fn scan_vendor_references(project_root: &Path) -> Vec<(String, String, String)> {
let mut seen: HashSet<(String, String)> = HashSet::new();
let mut out = Vec::new();
for file in WIRING_FILES {
let mut files: Vec<String> = WIRING_FILES
.iter()
.map(|file| (*file).to_string())
.collect();
if let Ok(paths) = socket_patch_core::utils::python_lock::python_lock_paths(project_root) {
for path in paths {
if let Some(script) = path
.strip_suffix(".py.lock")
.map(|prefix| format!("{prefix}.py"))
{
files.push(script);
}
files.push(path);
}
}
files.sort();
files.dedup();
for file in files {
let Ok(text) = tokio::fs::read_to_string(project_root.join(file)).await else {
continue;
};
Expand Down Expand Up @@ -202,6 +219,26 @@ fn synth_entry(eco: &str, uuid: &str, artifact_path: &str, base_purl: &str) -> V
/// routes to the package-lock backend, whose guard also fails closed on
/// unwired entries.
async fn detect_reference_flavor(project_root: &Path, eco: &str, uuid: &str) -> Option<String> {
if eco == "pypi" {
let needle = format!(".socket/vendor/pypi/{uuid}/");
for file in socket_patch_core::utils::python_lock::python_lock_paths(project_root).ok()? {
if tokio::fs::read_to_string(project_root.join(&file))
.await
.ok()
.is_some_and(|text| text.contains(&needle))
{
return Some(
if file == "uv.lock" {
"uv"
} else {
"python-lock"
}
.to_string(),
);
}
}
return None;
}
if eco != "npm" {
return None;
}
Expand Down Expand Up @@ -1409,6 +1446,32 @@ fn npm_coords(base_purl: &str) -> Option<(String, String)> {
mod tests {
use super::*;

#[tokio::test]
async fn scan_recovers_script_and_pep751_vendor_references() {
let tmp = tempfile::tempdir().unwrap();
let uuid = "11111111-1111-4111-8111-111111111111";
let path = format!(".socket/vendor/pypi/{uuid}/requests-2.28.1-py3-none-any.whl");
for file in ["example.py.lock", "pylock.dev.toml"] {
tokio::fs::write(
tmp.path().join(file),
format!("archive = {{ path = '{path}' }}"),
)
.await
.unwrap();
}
let references = scan_vendor_references(tmp.path()).await;
assert_eq!(
references,
vec![("pypi".to_string(), uuid.to_string(), path)]
);
assert_eq!(
detect_reference_flavor(tmp.path(), "pypi", uuid)
.await
.as_deref(),
Some("python-lock")
);
}

/// 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
77 changes: 75 additions & 2 deletions crates/socket-patch-cli/src/commands/scan/hosted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const REDIRECT_CANDIDATE_FILES: &[&str] = &[
"bun.lock",
"requirements.txt",
"uv.lock",
"pyproject.toml",
"Cargo.toml",
"Cargo.lock",
".cargo/config.toml",
Expand Down Expand Up @@ -785,7 +786,7 @@ pub(crate) async fn run_redirect_selected(
) -> i32 {
use socket_patch_core::manifest::schema::PatchRecord;
use socket_patch_core::patch::redirect::{
rewrite_registry_redirect, DepOverride, RedirectState,
rewrite_registry_redirect_with_python_metadata, DepOverride, RedirectState,
};

let mut skipped: Vec<serde_json::Value> = Vec::new();
Expand Down Expand Up @@ -1260,6 +1261,22 @@ pub(crate) async fn run_redirect_selected(
}
}

if let Ok(paths) = socket_patch_core::utils::python_lock::python_lock_paths(&common.cwd) {
for path in paths {
if let Some(script_path) = path
.strip_suffix(".py.lock")
.map(|prefix| format!("{prefix}.py"))
{
if let Ok(content) = std::fs::read_to_string(common.cwd.join(&script_path)) {
files.insert(script_path, content);
}
}
if let Ok(content) = std::fs::read_to_string(common.cwd.join(&path)) {
files.insert(path, content);
}
}
}

// Rush monorepos have no root package.json/lock pair: the single pnpm
// source-of-truth lock lives at common/config/rush/pnpm-lock.yaml, and
// (when subspaces are enabled) one lock per subspace under
Expand Down Expand Up @@ -1299,7 +1316,63 @@ pub(crate) async fn run_redirect_selected(
// `mut`: the pnpm trustLockfile auto-config below may fold a
// pnpm-workspace.yaml write (plus its ledger edit) into the rewrite set so
// it rides the same atomic-write / ledger-first machinery as the locks.
let mut rewrite = rewrite_registry_redirect(&files, &overrides);
let mut python_metadata = std::collections::BTreeMap::new();
let mut unavailable_python_artifacts = std::collections::BTreeSet::new();
for dep in overrides.iter().filter(|dep| dep.ecosystem == "pypi") {
let Some(sha256) = dep.integrity.sha256.as_deref() else {
continue;
};
if !dep
.artifact_url
.split(['?', '#'])
.next()
.is_some_and(|path| path.ends_with(".whl"))
{
continue;
}
let native_target = files
.iter()
.filter(|(path, _)| *path == "uv.lock" || path.ends_with(".py.lock"))
.any(|(_, text)| {
socket_patch_core::utils::python_lock::rewrite_python_lock(
text,
&dep.name,
&dep.version,
socket_patch_core::utils::python_lock::ArtifactSource::Url(&dep.artifact_url),
sha256,
)
.ok()
.flatten()
.is_some()
});
if !native_target {
continue;
}
match socket_patch_core::vendor::pypi::fetch_hosted_wheel_metadata(
api_client,
&dep.artifact_url,
sha256,
)
.await
{
Ok(Some(metadata)) => {
python_metadata.insert(dep.artifact_url.clone(), metadata);
}
Ok(None) => {}
Err(detail) => {
unavailable_python_artifacts.insert(dep.artifact_url.clone());
skipped.push(serde_json::json!({
"purl": format!("pkg:pypi/{}@{}", dep.name, dep.version),
"uuid": dep.patch_uuid,
"reason": "python_metadata_unavailable",
"detail": detail.replace(&dep.artifact_url, "<hosted artifact>"),
}));
}
}
}
overrides.retain(|dep| !unavailable_python_artifacts.contains(&dep.artifact_url));
let mut rewrite =
rewrite_registry_redirect_with_python_metadata(&files, &overrides, &python_metadata);

// The lockb→text migration is only KEPT when the rewrite actually landed
// in the migrated bun.lock. Otherwise nothing was redirected there and the
Expand Down
2 changes: 1 addition & 1 deletion crates/socket-patch-core/src/crawlers/python_crawler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ pub async fn is_python_project(cwd: &Path) -> bool {
return true;
}
}
false
crate::utils::python_lock::python_lock_paths(cwd).is_ok_and(|paths| !paths.is_empty())
}

// ---------------------------------------------------------------------------
Expand Down
Loading
Loading