Skip to content
Draft
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
195 changes: 154 additions & 41 deletions crates/socket-patch-core/src/hosted/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -487,64 +487,63 @@ async fn read_advisory(
}
}

/// An in-memory project's Go files as the hosted engine reads them
/// ([`read_advisory`]: a link or an entry without content is unreadable,
/// but still exists).
struct AdvisoryGoFiles<'v, 'p> {
view: &'v ProjectView<'p>,
unreadable: &'v BTreeSet<String>,
}

impl crate::vendor::go_workspace::MemoryFiles for AdvisoryGoFiles<'_, '_> {
async fn exists(&mut self, rel: &str) -> bool {
self.unreadable.contains(rel) || self.view.exists_no_follow(rel).await
}

async fn read(&mut self, rel: &str) -> Option<String> {
read_advisory(self.view, self.unreadable, rel).await
}

async fn read_sum(&mut self, rel: &str) -> Option<String> {
read_advisory(self.view, self.unreadable, rel).await
}

fn unparseable(&mut self, _rel: &str, _problem: &str) {}
}

/// The Go workspace in effect for the project, handed to the golang
/// rewriter under [`crate::patch::redirect::GO_WORKSPACE_KEY`]. On disk it is
/// the one go resolves (`GOWORK`, else a go.work at or above the root, with
/// members anywhere, plus a `-modfile` that GOFLAGS sets); in memory a root
/// `go.work` and its in-root members. A root go.work and go.work.sum are candidate files the rewriter writes.
/// `go.work` and its in-root members, read as discovery reads them
/// ([`crate::vendor::go_workspace::read_memory`]), whose paths every check
/// compares lexically, never probing the disk or the process's working
/// directory. A root go.work and go.work.sum are candidate files the
/// rewriter writes.
async fn read_go_workspace(
view: &ProjectView<'_>,
unreadable: &BTreeSet<String>,
out: &mut CandidateFiles,
) {
use crate::vendor::go_workspace::{self, GoWorkspace, Loaded, Member};
use crate::vendor::go_workspace::{self, Loaded};
let (root, loaded, modfile, module_graph) = match view {
ProjectView::Memory(_) => 'memory: {
ProjectView::Memory(_) => {
let Some(work) = read_advisory(view, unreadable, "go.work").await else {
return;
};
let work = crate::vendor::go_mod_edit::normalize_for_read(&work);
// As on disk (`go_workspace::load`): a go.work go cannot parse
// is a workspace go refuses to build, never module mode.
if let Some(problem) = crate::vendor::go_mod_edit::block_structure_error(&work) {
break 'memory (
std::path::PathBuf::new(),
Loaded::Invalid(format!("go.work: {problem}, so go refuses to build")),
None,
None,
);
}
let mut members = Vec::new();
for raw in crate::vendor::go_mod_edit::parse_use_dirs(&work) {
let Some(dir) = go_workspace::in_root_dir(&raw) else {
members.push(Member {
dir: raw.into(),
go_mod: None,
go_sum: None,
});
continue;
};
let at = |file: &str| match dir.as_str() {
"" => file.to_string(),
dir => format!("{dir}/{file}"),
};
members.push(Member {
go_mod: read_advisory(view, unreadable, &at("go.mod"))
.await
.map(|t| crate::vendor::go_mod_edit::normalize_for_read(&t)),
go_sum: read_advisory(view, unreadable, &at("go.sum")).await,
dir: dir.into(),
});
}
let work_sum = read_advisory(view, unreadable, "go.work.sum").await;
let ws = GoWorkspace {
work_file: "go.work".into(),
work,
work_sum,
members,
graph: None,
let loaded = match crate::vendor::go_mod_edit::block_structure_error(&work) {
Some(problem) => {
Loaded::Invalid(format!("go.work: {problem}, so go refuses to build"))
}
None => {
let mut files = AdvisoryGoFiles { view, unreadable };
Loaded::Workspace(go_workspace::read_memory(&work, &mut files).await.0)
}
};
(std::path::PathBuf::new(), Loaded::Workspace(ws), None, None)
(std::path::PathBuf::new(), loaded, None, None)
}
ProjectView::Disk(_) | ProjectView::Snapshot(_) => {
let Some(root) = view.disk_root_reading(std::iter::empty::<&std::path::Path>()) else {
Expand Down Expand Up @@ -3691,6 +3690,120 @@ mod tests {
);
}

/// The in-memory engine's workspace is judged lexically, from the
/// project's own files: a root module go.work doesn't `use` is "not a
/// member", whatever the process's working directory holds. The disk
/// checks probed `<cwd>/go.mod` instead, so with this crate's directory
/// as the cwd (no go.mod) the rewriter called the root module-less
/// ("nowhere to go"), and with a go.mod there it gave the member answer.
/// A root go.mod go cannot parse, or one that cannot be read (a link),
/// still exists: the same files, the same verdict, as discovery's
/// `in_memory_workspace_files_are_the_files_that_exist` finds.
#[tokio::test]
async fn memory_go_workspace_never_probes_the_cwd() {
use crate::patch::redirect::{
rewrite_registry_redirect, HostedGoWorkspace, Integrity, RegistryOverride,
RegistryOverrideIdentifiers, GO_WORKSPACE_KEY,
};
use crate::vendor::go_workspace::{Loaded, WorkspaceConflict};
const MODULE: &str = "github.com/foo/bar";
let socket = format!("patch.socket.dev/gopatch/{PATCH_UUID}");
let hash = format!("h1:{}=", "A".repeat(43));
let ovr = DepOverride {
ecosystem: "golang".into(),
name: MODULE.into(),
version: "v1.4.2".into(),
registry_override: Some(RegistryOverride {
kind: "goproxy".into(),
index_url: "https://patch.socket.dev/patch-registry/golang".into(),
identifiers: RegistryOverrideIdentifiers {
name: MODULE.into(),
version: "v1.4.2".into(),
go_module_path: Some(socket.clone()),
go_module_version: Some("v1.4.2-socketpatch.1".into()),
..Default::default()
},
}),
integrity: Integrity {
dirhash_h1: Some(hash.clone()),
go_mod_h1: Some(hash),
..Default::default()
},
..grant_dep("golang", String::new())
};
let go_mod = format!("module example.com/app\n\ngo 1.21\n\nrequire {MODULE} v1.4.2\n");
let b = format!("module example.com/b\n\ngo 1.21\n\nrequire {MODULE} v1.4.2\n");
let work = "go 1.21\n\nuse ./b\n";
let ours = crate::vendor::go_mod_edit::ReplaceEntry {
module: MODULE.into(),
version: Some("v1.4.2".into()),
path: None,
rhs_module: Some(socket),
rhs_version: Some("v1.4.2-socketpatch.1".into()),
owner: Some(crate::vendor::go_mod_edit::ReplaceOwner::Hosted),
};
let unparseable = MemoryEntry::Text("module example.com/app\n\nrequire (\n".into());
for root in [
MemoryEntry::Text(go_mod.as_str().into()),
unparseable,
MemoryEntry::Symlink,
] {
let mut p = MemoryProject::new();
p.insert("go.mod", root.clone());
p.insert_text("go.work", work);
p.insert_text("b/go.mod", b.as_str());
let mut out = CandidateFiles::default();
read_go_workspace(&ProjectView::Memory(&p), &BTreeSet::new(), &mut out).await;
let hosted: HostedGoWorkspace =
serde_json::from_str(&out.files[GO_WORKSPACE_KEY]).unwrap();
let Loaded::Workspace(ws) = &hosted.loaded else {
panic!("{root:?}: {:?}", hosted.loaded);
};
assert_eq!(
ws.files,
Some(
["go.mod", "go.work", "b/go.mod"]
.into_iter()
.map(std::path::PathBuf::from)
.collect()
),
"{root:?}"
);
assert!(
matches!(
hosted
.loaded
.conflict(&hosted.root, MODULE, "v1.4.2", &ours),
Some(WorkspaceConflict::NotAMember { .. })
),
"{root:?}"
);
}
// End to end through the rewriter.
let mut p = MemoryProject::new();
p.insert_text("go.mod", go_mod.as_str());
p.insert_text("go.work", work);
p.insert_text("b/go.mod", b.as_str());
let mut read = CandidateFiles::default();
read_go_workspace(&ProjectView::Memory(&p), &BTreeSet::new(), &mut read).await;
let mut files = read.files;
for (path, text) in [("go.mod", go_mod.as_str()), ("go.work", work)] {
files.insert(path.into(), text.into());
}
let out = rewrite_registry_redirect(&files, std::slice::from_ref(&ovr));
assert!(out.files.is_empty(), "{:?}", out.files);
assert!(out.confirmed_golang_uuids.is_empty());
assert_eq!(out.warnings.len(), 1, "{:?}", out.warnings);
assert_eq!(out.warnings[0].code, "redirect_golang_workspace_conflict");
assert!(
out.warnings[0]
.detail
.contains("go.work does not `use` this module"),
"{:?}",
out.warnings
);
}

fn no_cargo_outer() -> CratesIoSource {
CratesIoSource::Registry
}
Expand Down
51 changes: 44 additions & 7 deletions crates/socket-patch-core/src/patch/redirect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24245,8 +24245,13 @@ packages:
}

/// The engine's [`GO_WORKSPACE_KEY`] for an in-memory go.work whose
/// members are `(dir, go.mod)` pairs (`.` for the root module).
fn go_workspace_key(work_extra: &str, members: &[(&str, &str)]) -> (String, String) {
/// members are `(dir, go.mod)` pairs (`.` for the root module), in a
/// project with a root go.mod when `root_go_mod`.
fn go_workspace_key(
work_extra: &str,
members: &[(&str, &str)],
root_go_mod: bool,
) -> (String, String) {
use crate::vendor::go_workspace::{GoWorkspace, Loaded, Member};
let uses: String = members.iter().map(|(d, _)| format!("\t{d}\n")).collect();
let hosted = HostedGoWorkspace {
Expand Down Expand Up @@ -24278,6 +24283,10 @@ packages:
})
.collect(),
),
files: Some(memory_go_files(
members.iter().map(|(dir, _)| *dir),
root_go_mod,
)),
}),
modfile: None,
module_graph: None,
Expand All @@ -24288,14 +24297,37 @@ packages:
)
}

/// The files an in-memory workspace of the `use` members `dirs` holds,
/// as [`crate::vendor::go_workspace::read_memory`] lists them: go.work,
/// the root go.mod when the project has one (`root_go_mod`, whether or
/// not `.` is a member) and each in-root member's go.mod.
fn memory_go_files<'a>(
dirs: impl IntoIterator<Item = &'a str>,
root_go_mod: bool,
) -> std::collections::BTreeSet<std::path::PathBuf> {
let mut files = std::collections::BTreeSet::from(["go.work".into()]);
if root_go_mod {
files.insert("go.mod".into());
}
for dir in dirs {
files.extend(crate::vendor::go_workspace::in_root_dir(dir).map(
|dir| match dir.as_str() {
"" => "go.mod".into(),
dir => format!("{dir}/go.mod").into(),
},
));
}
files
}

/// [`go_workspace_key`] plus the root go.work itself as a candidate
/// file, as the engine reads it when go.work sits at the root.
fn with_root_go_work(
files: &mut BTreeMap<String, String>,
work_extra: &str,
members: &[(&str, &str)],
) {
let (k, v) = go_workspace_key(work_extra, members);
let (k, v) = go_workspace_key(work_extra, members, files.contains_key("go.mod"));
let uses: String = members.iter().map(|(d, _)| format!("\t{d}\n")).collect();
files.insert(k, v);
files.insert(
Expand Down Expand Up @@ -24324,8 +24356,11 @@ packages:
("\nreplace github.com/foo/bar => ./fork\n", ""),
] {
let mut files = files.clone();
let (k, v) =
go_workspace_key(work, &[(".", &root), ("b", &go_member("v1.4.2", other))]);
let (k, v) = go_workspace_key(
work,
&[(".", &root), ("b", &go_member("v1.4.2", other))],
true,
);
files.insert(k, v);
let out = rewrite_registry_redirect(&files, std::slice::from_ref(&ovr));
assert!(out.files.is_empty(), "{work}{other}: {:?}", out.files);
Expand All @@ -24343,6 +24378,7 @@ packages:
let (k, v) = go_workspace_key(
"\nreplace github.com/foo/bar v1.0.0 => ./fork\n",
&[(".", &root), ("b", &go_member("v1.4.2", ""))],
true,
);
files.insert(k, v);
let out = rewrite_registry_redirect(&files, std::slice::from_ref(&ovr));
Expand All @@ -24358,7 +24394,7 @@ packages:
let ovr = golang_override();
let mut files = golang_files();
let root = files["go.mod"].clone();
let (k, v) = go_workspace_key("", &[(".", &root), ("b", &go_member("v1.5.0", ""))]);
let (k, v) = go_workspace_key("", &[(".", &root), ("b", &go_member("v1.5.0", ""))], true);
files.insert(k, v);
let out = rewrite_registry_redirect(&files, std::slice::from_ref(&ovr));
assert!(out.files.is_empty());
Expand All @@ -24368,7 +24404,7 @@ packages:
let bare = "module example.com/app\n\ngo 1.21\n";
let mut files = BTreeMap::new();
files.insert("go.mod".to_string(), bare.to_string());
let (k, v) = go_workspace_key("", &[(".", bare), ("b", &go_member("v1.4.2", ""))]);
let (k, v) = go_workspace_key("", &[(".", bare), ("b", &go_member("v1.4.2", ""))], true);
files.insert(k, v);
let out = rewrite_registry_redirect(&files, std::slice::from_ref(&ovr));
assert!(out.warnings.is_empty(), "{:?}", out.warnings);
Expand Down Expand Up @@ -24501,6 +24537,7 @@ packages:
member("../out", None),
],
graph: None,
files: Some(memory_go_files([".", "../out"], true)),
}),
modfile: None,
module_graph: None,
Expand Down
Loading
Loading