Skip to content
Open
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
9 changes: 7 additions & 2 deletions crates/socket-patch-cli/src/commands/scan/hosted/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,13 @@ pub(super) async fn stale_install_warnings(
.await
.unwrap_or_default()
} else {
socket_patch_core::crawlers::python_crawler::find_local_venv_site_packages(&common.cwd)
.await
let sites =
socket_patch_core::crawlers::python_crawler::find_local_venv_site_packages(&common.cwd)
.await;
// A `--system-site-packages` venv's own copy hides its base
// interpreter's (#409).
crawler.set_search_order(&sites).await;
sites
};

#[derive(Default)]
Expand Down
11 changes: 7 additions & 4 deletions crates/socket-patch-cli/src/ecosystem_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ async fn dispatch_find(
eco = Ecosystem::Pypi,
options = options,
silent = silent,
crawler = PythonCrawler,
crawler = PythonCrawler::new(),
get_paths = get_site_packages_paths,
using_label = "",
err_label = "Python packages",
Expand Down Expand Up @@ -1151,7 +1151,7 @@ async fn crawl_every_ecosystem(
if walk_pool::fd_limit_is_tight() {
(
crawl!(Ecosystem::Npm, NpmCrawler.crawl_all_with_roots(options)).await,
crawl!(Ecosystem::Pypi, PythonCrawler.crawl_all(options)).await,
crawl!(Ecosystem::Pypi, PythonCrawler::new().crawl_all(options)).await,
crawl!(Ecosystem::Cargo, CargoCrawler.crawl_all(options)).await,
crawl!(
Ecosystem::Gem,
Expand All @@ -1167,7 +1167,7 @@ async fn crawl_every_ecosystem(
} else {
tokio::join!(
crawl!(Ecosystem::Npm, NpmCrawler.crawl_all_with_roots(options)),
crawl!(Ecosystem::Pypi, PythonCrawler.crawl_all(options)),
crawl!(Ecosystem::Pypi, PythonCrawler::new().crawl_all(options)),
crawl!(Ecosystem::Cargo, CargoCrawler.crawl_all(options)),
crawl!(
Ecosystem::Gem,
Expand Down Expand Up @@ -2400,7 +2400,10 @@ mod tests {
}};
}
serial!(Ecosystem::Npm, NpmCrawler.crawl_all(&options).await);
serial!(Ecosystem::Pypi, PythonCrawler.crawl_all(&options).await);
serial!(
Ecosystem::Pypi,
PythonCrawler::new().crawl_all(&options).await
);
serial!(Ecosystem::Cargo, CargoCrawler.crawl_all(&options).await);
serial!(Ecosystem::Gem, RubyCrawler.crawl_all(&options).await);
serial!(Ecosystem::Golang, GoCrawler.crawl_all(&options).await);
Expand Down
81 changes: 81 additions & 0 deletions crates/socket-patch-cli/tests/in_process_redirect_poetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,10 @@ async fn scan_output(root: &Path, server: &MockServer, extra: &[&str]) -> std::p
] {
cmd.env_remove(key);
}
// Hermetic against the runner's own Python user site, which a
// `--system-site-packages` venv reaches (#409).
cmd.env("SOCKET_TELEMETRY_DISABLED", "1")
.env("PYTHONNOUSERSITE", "1")
.args(["scan", "--mode", "hosted", "--yes", "--cwd"])
.arg(root)
.args([
Expand Down Expand Up @@ -490,6 +493,84 @@ async fn stale_python_install_warns_and_cannot_attest_even_on_rescan() {
}
}

/// A `.venv` created with `--system-site-packages` whose base interpreter
/// lives at `root/base`; `bytes` is installed in the BASE interpreter only,
/// which is where pip leaves a release it finds already satisfied there.
/// Returns the base copy's file.
fn install_behind_system_site_venv(root: &Path, bytes: &[u8]) -> std::path::PathBuf {
let base = root.join("base");
let (home, stdlib) = if cfg!(windows) {
(base.clone(), base.join("Lib"))
} else {
(base.join("bin"), base.join("lib").join("python3.12"))
};
std::fs::create_dir_all(&home).unwrap();
std::fs::create_dir_all(&stdlib).unwrap();
std::fs::write(stdlib.join("os.py"), "").unwrap();
let file = install_package(root, "base", bytes);
// The project venv (`write_project` made its empty site-packages)
// reaches the base interpreter.
std::fs::write(
root.join(".venv").join("pyvenv.cfg"),
format!(
"home = {}\ninclude-system-site-packages = true\nversion = 3.12.4\n",
home.display()
),
)
.unwrap();
file
}

/// #409: pip leaves a release the base interpreter already has where it
/// is, so behind a `--system-site-packages` venv the unpatched base copy is
/// the one the build imports. It must get the stale-install warning, and
/// `--vex` must not attest from the lock pin as if nothing were installed.
#[tokio::test]
async fn stale_base_copy_behind_system_site_venv_warns_and_cannot_attest() {
let server = MockServer::start().await;
mock_api(&server).await;
let tmp = tempfile::tempdir().unwrap();
write_project(tmp.path());
let installed = install_behind_system_site_venv(tmp.path(), UPSTREAM);
let vex = tmp.path().join("out.vex.json");
let out = scan_output(
tmp.path(),
&server,
&["--json", "--vex", vex.to_str().unwrap()],
)
.await;
let json = envelope(&out);
assert_eq!(out.status.code(), Some(1), "{json}");
assert!(stale_warning(&json), "{json}");
assert_eq!(json["error"]["code"], "no_applicable_patches", "{json}");
assert!(!vex.exists(), "stale base bytes cannot produce a VEX file");
assert_eq!(std::fs::read(installed).unwrap(), UPSTREAM);
}

/// #409: once the venv holds its own patched copy (`pip install
/// --force-reinstall` leaves the base copy in place), the base copy is
/// shadowed: no stale warning, and the venv's verified bytes attest.
#[tokio::test]
async fn venv_copy_shadows_stale_base_copy_behind_system_site_venv() {
let server = MockServer::start().await;
mock_api(&server).await;
let tmp = tempfile::tempdir().unwrap();
write_project(tmp.path());
install_behind_system_site_venv(tmp.path(), UPSTREAM);
install_package(tmp.path(), ".venv", PATCHED);
let vex = tmp.path().join("out.vex.json");
let out = scan_output(
tmp.path(),
&server,
&["--json", "--vex", vex.to_str().unwrap()],
)
.await;
let json = envelope(&out);
assert!(out.status.success(), "{json}");
assert!(!stale_warning(&json), "{json}");
assert_eq!(json["vex"]["statements"], 1, "{json}");
}

#[tokio::test]
async fn patched_python_install_attests_without_a_stale_warning() {
let server = MockServer::start().await;
Expand Down
Loading
Loading