Skip to content
Merged
8 changes: 6 additions & 2 deletions crates/socket-patch-cli/CLI_CONTRACT.md

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions crates/socket-patch-cli/src/commands/scan/hosted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use std::time::Duration;
use futures_util::StreamExt;
use socket_patch_core::api::client::hold_back_debug;
use socket_patch_core::api::types::BatchPackagePatches;
use socket_patch_core::crawlers::CargoCrawler;
use socket_patch_core::patch::apply_lock::LockGuard;
use socket_patch_core::patch::redirect::yarnrc::resolve_outer_yarn_mirror_for_process;
use socket_patch_core::patch::redirect::DepOverride;
Expand Down Expand Up @@ -1292,6 +1293,10 @@ async fn run_redirect_selected_untracked(
// ancestor rc files), located the way yarn 1 does: a mirror set in any
// of them refuses the classic rewrite like a project one.
let yarn_classic_outer = || resolve_outer_yarn_mirror_for_process(&common.cwd);
// The cargo configs cargo reads for a build here (the project's, every
// parent directory's and `$CARGO_HOME`'s): a directory source or local
// registry in any of them refuses the cargo rewrite like a project one.
let cargo_outer = || CargoCrawler::crates_io_source_for(&common.cwd);
let rewrite_options = || {
RewriteOptions {
dry_run: common.dry_run,
Expand All @@ -1305,6 +1310,7 @@ async fn run_redirect_selected_untracked(
npm_allow_remote_config: !common.no_npm_allow_remote_config,
npm_outer: &npm_outer,
yarn_classic_outer: &yarn_classic_outer,
cargo_outer: &cargo_outer,
blocking: true,
takeover_uuids: takeover_uuids.clone(),
patch_server_origins: patch_server_origins.clone(),
Expand Down
12 changes: 10 additions & 2 deletions crates/socket-patch-cli/src/ecosystem_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ pub async fn cargo_copies_still_patched<'a>(
/// The copies of the Cargo `purls` [`find_all_packages_for_rollback`]
/// locates (the roots rollback restores), plus, with `shadowed_registry`,
/// the shared `$CARGO_HOME/registry/src` copies of a local Cargo project
/// with a `cargo vendor` dir, whose crawl searches only that dir — where
/// whose cargo config replaces crates.io with a `cargo vendor` directory,
/// whose crawl searches only that dir — where
/// an apply from before `cargo vendor` may have left the patch. The prune
/// only reads them (its keep decision); rollback restores them only for a
/// vendored crate whose record it is about to drop (#336).
Expand All @@ -103,7 +104,14 @@ pub async fn find_cargo_copies(
let local = !options.global && options.global_prefix.is_none();
let cargo_project =
options.cwd.join("Cargo.toml").is_file() || options.cwd.join("Cargo.lock").is_file();
if shadowed_registry && local && cargo_project && options.cwd.join("vendor").is_dir() {
if shadowed_registry
&& local
&& cargo_project
&& CargoCrawler::new()
.directory_source(&options.cwd)
.await
.is_some()
{
let registry = CrawlerOptions {
cwd: options.cwd.clone(),
global: true,
Expand Down
14 changes: 14 additions & 0 deletions crates/socket-patch-cli/tests/e2e_cargo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,14 @@ async fn scan_discovers_vendor_crates() {
// a crate source once the root is identified as a Cargo project. A vendored
// project always carries a lockfile, so stage one as the project marker.
std::fs::write(dir.path().join("Cargo.lock"), "version = 3\n").unwrap();
// `cargo vendor`'s source replacement: only a wired tree is where
// cargo builds from (#338).
std::fs::create_dir_all(dir.path().join(".cargo")).unwrap();
std::fs::write(
dir.path().join(".cargo/config.toml"),
"[source.crates-io]\nreplace-with = \"v\"\n[source.v]\ndirectory = \"vendor\"\n",
)
.unwrap();

// Set up vendor directory
let vendor_dir = dir.path().join("vendor");
Expand Down Expand Up @@ -494,6 +502,12 @@ async fn sync_keeps_shared_cache_entry_behind_a_cargo_vendor_dir() {
.unwrap();
let registry = root.join(".cargo/registry/src/index.crates.io-test");
let itoa_old = fake_registry_crate(&registry, "itoa", "1.0.11", patched);
// The source replacement that makes `vendor/` cargo's source (#338).
std::fs::write(
root.join(".cargo/config.toml"),
"[source.crates-io]\nreplace-with = \"v\"\n[source.v]\ndirectory = \"vendor\"\n",
)
.unwrap();

let socket = root.join(".socket");
let itoa = "pkg:cargo/itoa@1.0.11";
Expand Down
96 changes: 96 additions & 0 deletions crates/socket-patch-cli/tests/e2e_safety_cargo_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,102 @@ fn apply_then_cargo_check_succeeds() {
manifestless_agent_patch_is_not_attested(&consumer, &cargo_home);
}

/// Move the consumer's directory source to `dir` (relative to `root`),
/// wired by `root/.cargo/config.toml`, as `cargo vendor <dir>` lays it
/// out when run there.
fn relocate_vendor(consumer: &Path, root: &Path, dir: &str) {
std::fs::create_dir_all(root.join(dir).parent().unwrap()).unwrap();
std::fs::rename(consumer.join("vendor"), root.join(dir)).unwrap();
std::fs::remove_dir_all(consumer.join(".cargo")).unwrap();
std::fs::create_dir_all(root.join(".cargo")).unwrap();
std::fs::write(
root.join(".cargo/config.toml"),
format!(
"[source.crates-io]\nreplace-with = \"vendored-test\"\n\n\
[source.vendored-test]\ndirectory = \"{dir}\"\n"
),
)
.unwrap();
}

/// Apply in `cwd`, then prove the copy at `vendored` (the directory
/// source cargo builds from) is patched and `cargo check --frozen
/// --offline` accepts it.
fn apply_patches_directory_source(cwd: &Path, vendored: &Path, cargo_home: &Path) {
let (stdout, _stderr) = assert_run_ok(
cwd,
&[
"apply",
"--json",
"--offline",
"--cwd",
cwd.to_str().unwrap(),
],
"socket-patch apply",
);
let env = parse_json_envelope(&stdout);
assert_eq!(env["status"], "success", "{stdout}");
assert_eq!(
std::fs::read_to_string(vendored.join("src/lib.rs")).unwrap(),
PATCHED_LIB_RS,
"the directory source cargo builds from must be patched: {stdout}"
);
let out = cargo_check(cwd, cargo_home);
assert!(
out.status.success(),
"cargo check after apply:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
}

/// #338 shape 1: `cargo vendor third_party/rust` — a directory source
/// with a custom name. Apply must patch that copy, not look for
/// `<cwd>/vendor` and fall back to the registry cache.
#[test]
#[ignore]
fn apply_patches_a_custom_named_vendor_dir() {
if !cargo_e2e_matrix::cargo_available("e2e_safety_cargo_build") {
return;
}
let root = tempfile::tempdir().unwrap();
let consumer = stage_consumer(root.path());
relocate_vendor(&consumer, &consumer, "third_party/rust");
let cargo_home = root.path().join(".cargo-home");
generate_lockfile(&consumer, &cargo_home);
assert!(cargo_check(&consumer, &cargo_home).status.success());
stage_socket_manifest(&consumer);

let vendored = consumer.join("third_party/rust/safety-fixture");
apply_patches_directory_source(&consumer, &vendored, &cargo_home);
}

/// #338 shape 2: apply run from a workspace member, with the vendor
/// directory and its config at the workspace root.
#[test]
#[ignore]
fn apply_from_a_member_patches_the_workspace_vendor_dir() {
if !cargo_e2e_matrix::cargo_available("e2e_safety_cargo_build") {
return;
}
let root = tempfile::tempdir().unwrap();
let ws = root.path().join("ws");
std::fs::create_dir_all(&ws).unwrap();
std::fs::write(
ws.join("Cargo.toml"),
"[workspace]\nmembers = [\"consumer\"]\nresolver = \"2\"\n",
)
.unwrap();
let consumer = stage_consumer(&ws);
relocate_vendor(&consumer, &ws, "vendor");
let cargo_home = root.path().join(".cargo-home");
generate_lockfile(&ws, &cargo_home);
assert!(cargo_check(&consumer, &cargo_home).status.success());
stage_socket_manifest(&consumer);

apply_patches_directory_source(&consumer, &ws.join("vendor/safety-fixture"), &cargo_home);
}

/// Step 5 of the module doc: the agent-mode patch attests only through the
/// manifest; with it deleted nothing in the lockfile / config wires a
/// Socket patch (the `[source.crates-io] replace-with` directory source is
Expand Down
10 changes: 10 additions & 0 deletions crates/socket-patch-cli/tests/e2e_vendor_cargo_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1005,7 +1005,17 @@ fn agent_to_vendored_rollback_restores_shared_cache(cargo_vendor_dir: bool) {
"vendor failed.\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
if cargo_vendor_dir {
// A wired (but empty) directory source: the project crawl then
// searches only `vendor/` (#338).
std::fs::create_dir_all(proj.join("vendor")).unwrap();
std::fs::create_dir_all(proj.join(".cargo")).unwrap();
let config = proj.join(".cargo/config.toml");
let mut text = std::fs::read_to_string(&config).unwrap_or_default();
text.push_str(
"\n[source.crates-io]\nreplace-with = \"vendored-sources\"\n\n\
[source.vendored-sources]\ndirectory = \"vendor\"\n",
);
std::fs::write(&config, text).unwrap();
}

let (code, stdout, stderr) = run_socket(
Expand Down
9 changes: 8 additions & 1 deletion crates/socket-patch-cli/tests/ecosystem_dispatch_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -686,13 +686,20 @@ fn rollback_dispatch_branch_cargo() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
write_root_package_json(root);
// Cargo crawler uses the vendor layout when `vendor/` exists.
// Cargo crawler uses the vendor layout when a config wires `vendor/`.
std::fs::write(
root.join("Cargo.toml"),
"[package]\nname = \"t\"\nversion = \"0.0.0\"\n",
)
.unwrap();
let purl = "pkg:cargo/__rollback_dispatch__@1.0.0";
// `cargo vendor`'s source replacement makes `vendor/` cargo's source (#338).
std::fs::create_dir_all(root.join(".cargo")).unwrap();
std::fs::write(
root.join(".cargo/config.toml"),
"[source.crates-io]\nreplace-with = \"v\"\n[source.v]\ndirectory = \"vendor\"\n",
)
.unwrap();
let crate_dir = root.join("vendor").join("__rollback_dispatch__");
std::fs::create_dir_all(crate_dir.join("src")).unwrap();
std::fs::write(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,13 @@ version = "1.0.0"

// Cargo crawler needs a Cargo.toml in cwd to engage.
std::fs::write(tmp.path().join("Cargo.toml"), "[workspace]\n").unwrap();
// `cargo vendor`'s source replacement makes `vendor/` cargo's source (#338).
std::fs::create_dir_all(tmp.path().join(".cargo")).unwrap();
std::fs::write(
tmp.path().join(".cargo/config.toml"),
"[source.crates-io]\nreplace-with = \"v\"\n[source.v]\ndirectory = \"vendor\"\n",
)
.unwrap();

assert_ne!(original.to_vec(), patched.to_vec());
assert_eq!(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,13 @@ async fn gc_scan_crawls_the_unselected_ecosystems() {
let tmp = tempfile::tempdir().unwrap();
stage_project(tmp.path());
let krate = tmp.path().join("vendor").join(INSTALLED_CRATE);
// `cargo vendor`'s source replacement makes `vendor/` cargo's source (#338).
std::fs::create_dir_all(tmp.path().join(".cargo")).unwrap();
std::fs::write(
tmp.path().join(".cargo/config.toml"),
"[source.crates-io]\nreplace-with = \"v\"\n[source.v]\ndirectory = \"vendor\"\n",
)
.unwrap();
std::fs::create_dir_all(&krate).unwrap();
std::fs::write(
krate.join("Cargo.toml"),
Expand Down
Loading
Loading