Skip to content

Commit 1a043ec

Browse files
mikolalysenkoclaude
andcommitted
Read Cargo workspace members in VEX discovery
Hosted mode pins a patched crate in the root Cargo.toml AND every workspace-member manifest that declares it, and vendored mode's live-redirect refusal reads member manifests too. Discovery read only the root manifest, so a crate the root does not declare was "the lock is the only word": a member whose declaration was reverted to crates.io (no `registry = "socket-patch-<uuid>"`) left a ref attested although cargo re-resolves that dependency from crates.io. Discovery now enumerates the members the way the hosted rewriter does (`cargo_workspace::member_manifests_in`, filtered by the rewriter's own `is_cargo_member_manifest_key`, so nothing under `.socket/` or `target/`) and applies its manifest-declaration check across root + members. Each declaration carries its file: diagnostics, the conflict-pair Cargo.toml ref and lockless `UnlockedPin`s name the member manifest that holds the pin. `[patch]` stays root-only (cargo ignores a member's). The member walk over a `DiskSnapshot` now notes every manifest it reads and directory it probes or lists with the snapshot instead of asking for its raw root, so a discovery run under a read-set recording (the re-scan cache) keeps the recording usable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 43fd5aa commit 1a043ec

4 files changed

Lines changed: 374 additions & 56 deletions

File tree

‎crates/socket-patch-core/src/patch/redirect/mod.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2415,7 +2415,7 @@ fn crlf_to_lf(text: &str) -> Option<String> {
24152415
/// A workspace-member manifest key the caller supplied: `<dir>/Cargo.toml`,
24162416
/// a plain repo-relative path (never absolute, never `..`, never under the
24172417
/// ledger's `.socket/` or a build `target/`).
2418-
fn is_cargo_member_manifest_key(key: &str) -> bool {
2418+
pub(crate) fn is_cargo_member_manifest_key(key: &str) -> bool {
24192419
let Some(dir) = key.strip_suffix("/Cargo.toml") else {
24202420
return false;
24212421
};

‎crates/socket-patch-core/src/utils/cargo_workspace.rs‎

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use std::sync::Arc;
1818
use toml_edit::{DocumentMut, Item, Table};
1919

2020
use crate::utils::workspace_globs::segment_glob_matches;
21-
use crate::vendor::lock_inventory::view::{MemoryProject, ProjectView};
21+
use crate::vendor::lock_inventory::view::{DiskSnapshot, MemoryProject, ProjectView};
2222
use crate::vendor::parse_memo::ParseMemo;
2323

2424
/// Upper bound on discovered manifests — a runaway glob (or a hostile tree)
@@ -36,16 +36,47 @@ pub fn member_manifests(root: &Path) -> Vec<String> {
3636
/// reads the manifests the host supplied and expands member globs against
3737
/// the supplied paths (a directory exists when some supplied path lives
3838
/// under it; symbolic links are never directories).
39+
///
40+
/// Over a [`DiskSnapshot`] the walk reads the disk, but notes every
41+
/// manifest it reads and every directory it probes or lists with the
42+
/// snapshot, so a recording read set covers the walk (asking for the
43+
/// snapshot's raw root would end the recording).
3944
pub fn member_manifests_in(view: &ProjectView<'_>) -> Vec<String> {
4045
match view {
41-
ProjectView::Disk(_) | ProjectView::Snapshot(_) => {
42-
let root = view.disk_root().expect("a disk view has a root");
43-
member_manifests(root)
44-
}
46+
ProjectView::Disk(root) => member_manifests(root),
47+
ProjectView::Snapshot(snap) => member_manifests_with(&SnapshotTree(snap)),
4548
ProjectView::Memory(project) => member_manifests_with(&MemoryTree(project)),
4649
}
4750
}
4851

52+
/// The disk under a [`DiskSnapshot`], each access noted with it first.
53+
struct SnapshotTree<'a, 'b>(&'a DiskSnapshot<'b>);
54+
55+
impl SnapshotTree<'_, '_> {
56+
fn disk(&self) -> DiskTree<'_> {
57+
DiskTree(self.0.root_reading(std::iter::empty::<&str>()))
58+
}
59+
}
60+
61+
impl Tree for SnapshotTree<'_, '_> {
62+
fn read_manifest(&self, rel: &str) -> Option<Arc<ManifestFacts>> {
63+
self.0.touch(rel);
64+
self.disk().read_manifest(rel)
65+
}
66+
}
67+
68+
impl DirTree for SnapshotTree<'_, '_> {
69+
fn is_real_dir(&self, rel: &str) -> bool {
70+
self.0.touch(rel);
71+
self.disk().is_real_dir(rel)
72+
}
73+
74+
fn child_dirs(&self, rel: &str) -> Option<Vec<String>> {
75+
self.0.touch_listing(rel);
76+
self.disk().child_dirs(rel)
77+
}
78+
}
79+
4980
/// The two directory questions a glob walk asks, keyed by `/`-separated
5081
/// root-relative paths (`""` is the root). Shared with the pnpm workspace
5182
/// member walk ([`crate::utils::pnpm_workspace`]).
@@ -478,6 +509,42 @@ mod tests {
478509
format!("[package]\nname = \"{name}\"\nversion = \"0.1.0\"\n")
479510
}
480511

512+
/// Over a tracked snapshot the walk answers as on disk and keeps the
513+
/// recording usable: it notes what it reads and lists, so a member
514+
/// added under a glob afterwards changes the read set.
515+
#[test]
516+
fn snapshot_walk_records_what_it_reads() {
517+
let tmp = tempfile::tempdir().unwrap();
518+
let root = tmp.path();
519+
write(
520+
root,
521+
"Cargo.toml",
522+
"[workspace]\nmembers = [\"crates/*\"]\n",
523+
);
524+
write(root, "crates/a/Cargo.toml", &pkg("a"));
525+
// Files stamped just now are racy: the walk reads around the
526+
// snapshot's content cache, so such a read set never holds.
527+
let old = std::time::SystemTime::now() - std::time::Duration::from_secs(3600);
528+
for rel in ["Cargo.toml", "crates/a/Cargo.toml"] {
529+
std::fs::File::options()
530+
.write(true)
531+
.open(root.join(rel))
532+
.unwrap()
533+
.set_modified(old)
534+
.unwrap();
535+
}
536+
let snap = DiskSnapshot::tracked(root);
537+
snap.begin_recording();
538+
assert_eq!(
539+
member_manifests_in(&ProjectView::Snapshot(&snap)),
540+
member_manifests(root)
541+
);
542+
let reads = snap.end_recording().expect("the walk keeps the recording");
543+
assert!(reads.unchanged());
544+
write(root, "crates/b/Cargo.toml", &pkg("b"));
545+
assert!(!reads.unchanged(), "a new member dir changes the listing");
546+
}
547+
481548
#[test]
482549
fn members_globs_excludes_and_path_dependencies() {
483550
let tmp = tempfile::tempdir().unwrap();

‎crates/socket-patch-core/src/vendor/lock_inventory/view.rs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -501,12 +501,14 @@ impl<'a> DiskSnapshot<'a> {
501501
}
502502

503503
/// Note that `rel` is about to be read or probed (see [`Self::tracked`]).
504-
fn touch(&self, rel: &str) {
504+
/// Crate-visible for a walk the view does not mediate that reads only
505+
/// what it touches (the cargo workspace member walk).
506+
pub(crate) fn touch(&self, rel: &str) {
505507
self.touch_as(rel, Access::Probe);
506508
}
507509

508510
/// Note that directory `rel` is about to be listed.
509-
fn touch_listing(&self, rel: &str) {
511+
pub(crate) fn touch_listing(&self, rel: &str) {
510512
self.touch_as(rel, Access::List);
511513
}
512514

0 commit comments

Comments
 (0)