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
2 changes: 1 addition & 1 deletion crates/socket-patch-cli/CLI_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ the model is **not uniform** today:
`workspaces`). One repo-root invocation discovers every member. A member that is itself a
workspace root is recursed into (bounded depth).
- **cwd-only (single project):** gem, pypi, composer. The crawler inspects only the project
rooted at `--cwd` (pypi looks at `$VIRTUAL_ENV`, `<cwd>/.venv` / `venv`, then a Poetry project's out-of-tree virtualenv(s) under Poetry's `virtualenvs.path`; composer at the vendor tree); it does **not**
rooted at `--cwd` (pypi first takes the env the project's manager records: PDM's `.pdm-python` interpreter, meaning its venv or, for a base interpreter, PEP 582 `__pypackages__/<X.Y>/lib`, and uv's `UV_PROJECT_ENVIRONMENT`. Otherwise it looks at `$VIRTUAL_ENV`, `<cwd>/.venv` / `venv`, then a Poetry project's out-of-tree virtualenv(s) under Poetry's `virtualenvs.path`; composer at the vendor tree); it does **not**
descend into sibling subprojects. A monorepo with several independent lockfiles in subdirectories
(`backend/Gemfile.lock` + `frontend/Gemfile.lock`, multiple `.venv`, multiple `go.mod` /
`composer.json`) is handled by invoking the tool **once per subproject** (`--cwd` each), as a
Expand Down
113 changes: 113 additions & 0 deletions crates/socket-patch-cli/tests/in_process_python_envs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ fn assert_not_discovered(bodies: &[String], needle: &str) {
/// `scan_run` directly.
async fn scan_scrubbed(args: ScanArgs) -> i32 {
std::env::remove_var("VIRTUAL_ENV");
std::env::remove_var("UV_PROJECT_ENVIRONMENT");
scan_run(args).await
}

Expand Down Expand Up @@ -637,3 +638,115 @@ async fn pipenv_stray_venv_dirs_do_not_shadow_the_pipenv_venv() {
assert_not_discovered(&bodies, "pkg:pypi/stray-decoy@6.6.6");
}
}

// ---------------------------------------------------------------------------
// Package-manager-recorded envs: PDM's saved interpreter / PEP 582, and uv's
// UV_PROJECT_ENVIRONMENT, ahead of a stray `./.venv` the manager never uses
// ---------------------------------------------------------------------------

/// A project at `<tmp>/app` with `pyproject` and an in-project `.venv`
/// holding `stray_decoy 6.6.6` that the project's manager does not use.
fn project_with_stray_venv(pyproject: &str) -> (tempfile::TempDir, std::path::PathBuf) {
let tmp = tempfile::tempdir().unwrap();
let project = tmp.path().join("app");
std::fs::create_dir_all(&project).unwrap();
std::fs::write(project.join("pyproject.toml"), pyproject).unwrap();
let stray = venv_site_packages(&project.join(".venv"), "python3.12");
std::fs::create_dir_all(&stray).unwrap();
write_dist_info(&stray, "stray_decoy", "6.6.6");
(tmp, project)
}

/// A venv at `root` holding `pkg 1.0.0`; returns its interpreter path.
fn venv_with(root: &Path, pkg: &str) -> std::path::PathBuf {
let site = venv_site_packages(root, "python3.12");
std::fs::create_dir_all(&site).unwrap();
write_dist_info(&site, pkg, "1.0.0");
std::fs::write(root.join("pyvenv.cfg"), "home = /usr/bin\n").unwrap();
if cfg!(windows) {
root.join("Scripts").join("python.exe")
} else {
root.join("bin").join("python")
}
}

async fn assert_scan_finds(project: &Path, wanted: &str) {
let server = MockServer::start().await;
mock_batch_empty(&server).await;
assert_eq!(scan_scrubbed(default_args(project, server.uri())).await, 0);
let bodies = batch_bodies(&server).await;
assert_discovered(&bodies, wanted);
assert_not_discovered(&bodies, "pkg:pypi/stray-decoy@6.6.6");
}

/// #502: PDM's `.pdm-python` names an out-of-tree venv
/// (`venv.in_project = false`, or `pdm use <venv>`); that is the env scanned.
#[tokio::test]
#[serial]
async fn pdm_saved_interpreter_venv_is_scanned_not_a_stray_dot_venv() {
let (tmp, project) =
project_with_stray_venv("[project]\nname = \"app\"\n[tool.pdm]\ndistribution = false\n");
std::fs::write(project.join("pdm.lock"), "[metadata]\n").unwrap();
let python = venv_with(
&tmp.path().join("pdm").join("venvs").join("app-AbCd-3.12"),
"pdm_pkg",
);
std::fs::write(project.join(".pdm-python"), python.display().to_string()).unwrap();
assert_scan_finds(&project, "pkg:pypi/pdm-pkg@1.0.0").await;
}

/// #528: a PEP 582 PDM project installs into `__pypackages__/<X.Y>/lib`.
#[tokio::test]
#[serial]
async fn pdm_pep582_pypackages_is_scanned_not_a_stray_dot_venv() {
let (tmp, project) = project_with_stray_venv("[project]\nname = \"app\"\n");
std::fs::write(project.join("pdm.lock"), "[metadata]\n").unwrap();
let lib = project.join("__pypackages__").join("3.11").join("lib");
std::fs::create_dir_all(&lib).unwrap();
write_dist_info(&lib, "pep582_pkg", "1.0.0");
// The saved interpreter is a base Python, not a venv, and the project
// turned PDM's venvs off (`pdm config -l python.use_venv false`).
let base = tmp.path().join("usr").join("bin").join("python3.11");
std::fs::write(project.join(".pdm-python"), base.display().to_string()).unwrap();
std::fs::write(project.join("pdm.toml"), "[python]\nuse_venv = false\n").unwrap();
assert_scan_finds(&project, "pkg:pypi/pep582-pkg@1.0.0").await;
}

/// #525: uv syncs into `UV_PROJECT_ENVIRONMENT`, absolute or relative to the
/// project, and ignores an activated `VIRTUAL_ENV` for project commands.
#[tokio::test]
#[serial]
async fn uv_project_environment_is_scanned_not_a_stray_dot_venv() {
let other = tempfile::tempdir().unwrap();
let decoy = other.path().join("tool-venv");
let decoy_site = venv_site_packages(&decoy, "python3.12");
std::fs::create_dir_all(&decoy_site).unwrap();
write_dist_info(&decoy_site, "activated_decoy", "6.6.6");

for relative in [false, true] {
let (tmp, project) = project_with_stray_venv("[project]\nname = \"app\"\n");
std::fs::write(project.join("uv.lock"), "version = 1\n").unwrap();
let env = if relative {
project.join(".venv-ci")
} else {
tmp.path().join("opt").join("venv")
};
venv_with(&env, "uv_pkg");
let server = MockServer::start().await;
mock_batch_empty(&server).await;
std::env::set_var("VIRTUAL_ENV", &decoy);
if relative {
std::env::set_var("UV_PROJECT_ENVIRONMENT", ".venv-ci");
} else {
std::env::set_var("UV_PROJECT_ENVIRONMENT", &env);
}
let code = scan_run(default_args(&project, server.uri())).await;
std::env::remove_var("VIRTUAL_ENV");
std::env::remove_var("UV_PROJECT_ENVIRONMENT");
assert_eq!(code, 0);
let bodies = batch_bodies(&server).await;
assert_discovered(&bodies, "pkg:pypi/uv-pkg@1.0.0");
assert_not_discovered(&bodies, "pkg:pypi/stray-decoy@6.6.6");
assert_not_discovered(&bodies, "pkg:pypi/activated-decoy@6.6.6");
}
}
111 changes: 111 additions & 0 deletions crates/socket-patch-cli/tests/in_process_redirect_pdm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -594,3 +594,114 @@ async fn assert_relock_roundtrip(lock: &str, relocked: &str) {
"rollback restores the relocked lock byte for byte"
);
}

/// Spawn `scan --mode hosted --json` on `root` with a scrubbed environment
/// (no ambient `SOCKET_*`, `PDM_*` or `VIRTUAL_ENV`).
async fn scan_json(
root: &Path,
server: &MockServer,
extra: &[&str],
) -> (Option<i32>, serde_json::Value) {
let mut cmd = tokio::process::Command::new(binary());
for (key, _) in std::env::vars_os() {
let name = key.to_string_lossy();
if name.starts_with("SOCKET_")
|| name.starts_with("PDM_")
|| name == "VIRTUAL_ENV"
|| name == "UV_PROJECT_ENVIRONMENT"
{
cmd.env_remove(key);
}
}
cmd.env("SOCKET_TELEMETRY_DISABLED", "1")
.args(["scan", "--mode", "hosted", "--yes", "--json", "--cwd"])
.arg(root)
.args([
"--api-url",
&server.uri(),
"--org",
ORG,
"--api-token",
"fake",
])
.args(extra);
let out = cmd.output().await.unwrap();
let json = serde_json::from_slice(&out.stdout).unwrap_or_else(|error| {
panic!(
"{error}: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
)
});
(out.status.code(), json)
}

fn stale_warning(json: &serde_json::Value) -> bool {
json["redirect"]["warnings"]
.as_array()
.is_some_and(|warnings| {
warnings
.iter()
.any(|warning| warning["code"] == "redirect_pypi_stale_install")
})
}

/// Lay `urllib3 1.26.18` with `bytes` as its `response.py` into `site`.
fn install_urllib3(site: &Path, bytes: &[u8]) {
std::fs::create_dir_all(site.join("urllib3-1.26.18.dist-info")).unwrap();
std::fs::create_dir_all(site.join("urllib3")).unwrap();
std::fs::write(site.join("urllib3").join("response.py"), bytes).unwrap();
}

/// #502 / #528: the env PDM installs into is the one its `.pdm-python`
/// records: an out-of-tree venv, or `__pypackages__/<X.Y>/lib` when the
/// interpreter is a base Python (PEP 582). A warm, still-upstream copy there
/// must raise the stale-install warning and block the VEX attestation, as an
/// in-project `.venv` does; the empty stray `.venv` from `write_project`
/// must not stand in for it. Once PDM's env holds the patched bytes, the
/// warning is gone and the redirect attests.
#[tokio::test]
async fn pdm_recorded_env_is_probed_for_stale_hosted_installs() {
for pep582 in [false, true] {
let server = MockServer::start().await;
mock_api(&server).await;
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("app");
std::fs::create_dir_all(&root).unwrap();
write_project(&root, LOCK);
let site = if pep582 {
let base = tmp.path().join("usr").join("bin").join("python3.11");
std::fs::write(root.join(".pdm-python"), base.display().to_string()).unwrap();
std::fs::write(root.join("pdm.toml"), "[python]\nuse_venv = false\n").unwrap();
root.join("__pypackages__").join("3.11").join("lib")
} else {
let venv = tmp.path().join("pdm-venvs").join("app-AbCd-3.12");
std::fs::create_dir_all(&venv).unwrap();
std::fs::write(venv.join("pyvenv.cfg"), "home = /usr/bin\n").unwrap();
let python = if cfg!(windows) {
venv.join("Scripts").join("python.exe")
} else {
venv.join("bin").join("python")
};
std::fs::write(root.join(".pdm-python"), python.display().to_string()).unwrap();
if cfg!(windows) {
venv.join("Lib").join("site-packages")
} else {
venv.join("lib").join("python3.12").join("site-packages")
}
};
install_urllib3(&site, UPSTREAM);

let vex = tmp.path().join("out.vex.json");
let (code, json) = scan_json(&root, &server, &["--vex", vex.to_str().unwrap()]).await;
assert_eq!(code, Some(1), "pep582={pep582}: {json}");
assert!(stale_warning(&json), "pep582={pep582}: {json}");
assert!(!vex.exists(), "stale bytes cannot produce a VEX file");

install_urllib3(&site, PATCHED);
let (code, json) = scan_json(&root, &server, &["--vex", vex.to_str().unwrap()]).await;
assert_eq!(code, Some(0), "pep582={pep582}: {json}");
assert!(!stale_warning(&json), "pep582={pep582}: {json}");
assert_eq!(json["vex"]["statements"], 1, "pep582={pep582}: {json}");
}
}
Loading
Loading