Skip to content
Merged
15 changes: 14 additions & 1 deletion crates/socket-patch-bench/src/fixtures/other.rs
Original file line number Diff line number Diff line change
Expand Up @@ -762,7 +762,20 @@ fn jvm_pom(arts: &[Pkg], p: &Pkg) -> String {
pub fn build_maven(t: &mut Tree, size: Size) -> std::io::Result<Fixture> {
let arts = jvm_universe("maven", size);
let mut pom = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\">\n <modelVersion>4.0.0</modelVersion>\n <groupId>dev.socket.bench</groupId>\n <artifactId>bench-app</artifactId>\n <version>1.0.0</version>\n <packaging>jar</packaging>\n\n <properties>\n <maven.compiler.release>17</maven.compiler.release>\n </properties>\n\n <dependencies>\n");
for p in arts.iter().filter(|p| p.direct) {
// A project-mode crawl keeps only what the project's poms reach (#265),
// so every cached artifact the direct dependencies do not reach is
// declared directly too: the fixture's whole cache is the project's.
let mut reached = vec![false; arts.len()];
let mut queue: Vec<usize> = (0..arts.len()).filter(|&i| arts[i].direct).collect();
while let Some(i) = queue.pop() {
if !std::mem::replace(&mut reached[i], true) {
queue.extend(arts[i].deps.iter().copied());
}
}
for (i, p) in arts.iter().enumerate() {
if !p.direct && reached[i] {
continue;
}
let (g, a) = ga(p);
let _ = write!(pom, " <dependency>\n <groupId>{g}</groupId>\n <artifactId>{a}</artifactId>\n <version>{}</version>\n </dependency>\n", p.version);
}
Expand Down
4 changes: 3 additions & 1 deletion crates/socket-patch-cli/CLI_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,9 @@ own coordinates spell their path); then every **Ivy** cache (`-Dsbt.ivy.home=<h>
`<h>/cache`, then `~/.ivy2/cache`, whose package directory is the module's `jars/` / `bundles/` / `orbits/`).
Every existing location is kept (an empty value counts as unset; a relative one resolves against the
project); an Ivy home whose path does not end in `<…ivy…>/cache` is skipped. The crawl is **not scoped** to
the build: as for `~/.m2`, every cached GAV is queried and patched. `--global-prefix` names exactly one
the build: every cached GAV is queried and patched (a pure Maven build's `~/.m2` crawl is
scoped to the coordinates its poms reach; see `docs/ecosystems.md`, "A Maven project's scan is
scoped to its dependency graph"). `--global-prefix` names exactly one
root, its layout read from its path. Patch keys are whole files in the package directory (`<a>-<v>.jar`),
the same parity as `~/.m2`. A GAV cached in several roots (say `~/.m2`, a Coursier cache and an Ivy cache)
is patched and restored in **every** root, one summary event per copy, since the build loads whichever its
Expand Down
9 changes: 7 additions & 2 deletions crates/socket-patch-cli/tests/e2e_maven.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,17 @@ async fn scan_discovers_maven_artifacts() {
)
.unwrap();

// Create a pom.xml in the project directory so local mode activates
// A pom.xml in the project directory activates local mode; it declares
// both artifacts, since a project-mode crawl keeps only what the
// project's poms reach (#265).
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
std::fs::write(
project_dir.join("pom.xml"),
r#"<project><modelVersion>4.0.0</modelVersion></project>"#,
r#"<project><modelVersion>4.0.0</modelVersion><dependencies>
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.12.0</version></dependency>
<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>32.1.2-jre</version></dependency>
</dependencies></project>"#,
)
.unwrap();

Expand Down
13 changes: 10 additions & 3 deletions crates/socket-patch-cli/tests/in_process_scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1431,9 +1431,16 @@ async fn scan_discovers_maven_and_nuget_in_every_mode() {

let tmp = tempfile::tempdir().unwrap();
write_root_package_json(tmp.path());
// Maven: java-project marker + a local repository the crawler reaches
// via MAVEN_REPO_LOCAL (verified by the version dir's `.pom`).
std::fs::write(tmp.path().join("pom.xml"), "<project></project>\n").unwrap();
// Maven: a pom declaring the artifact (a project-mode crawl keeps only
// what the project's poms reach, #265) + a local repository the crawler
// reaches via MAVEN_REPO_LOCAL (verified by the version dir's `.pom`).
std::fs::write(
tmp.path().join("pom.xml"),
"<project><dependencies><dependency><groupId>org.example</groupId>\
<artifactId>foo</artifactId><version>1.0.0</version></dependency>\
</dependencies></project>\n",
)
.unwrap();
let artifact_dir = tmp.path().join("m2repo/org/example/foo/1.0.0");
std::fs::create_dir_all(&artifact_dir).unwrap();
std::fs::write(artifact_dir.join("foo-1.0.0.pom"), "<project/>").unwrap();
Expand Down
144 changes: 142 additions & 2 deletions crates/socket-patch-core/src/crawlers/maven_crawler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -921,16 +921,72 @@ impl MavenCrawler {

/// Crawl all discovered Maven repository paths and return every
/// package found.
///
/// In project mode for a Maven build (a `pom.xml`, no Gradle or
/// Scala-tool build beside it), a Maven local repository is shared by
/// every project on the machine, so only the coordinates the project's
/// dependency graph reaches are kept ([`maven_scope`](super::maven_scope),
/// #265): another project's cached artifacts are not this project's
/// packages. A root pom that is not readable leaves the crawl unscoped.
pub async fn crawl_all(&self, options: &CrawlerOptions) -> Vec<CrawledPackage> {
self.crawl_all_with(options, &JvmEnv::from_process()).await
}

/// [`Self::crawl_all`] under the caches `env` names.
pub async fn crawl_all_with(
&self,
options: &CrawlerOptions,
env: &JvmEnv,
) -> Vec<CrawledPackage> {
let mut packages = Vec::new();
let mut seen = HashSet::new();
let scoped = options.global_prefix.is_none() && !options.global && {
let cwd = options.cwd.clone();
run_walk(move || {
layout::has_build(&cwd, BuildTool::Maven)
&& !layout::has_build(&cwd, BuildTool::Gradle)
&& !layout::is_scala_tool_build(&cwd)
})
.await
};

for root in self.get_jvm_cache_roots(options).await {
for root in self.get_jvm_cache_roots_with(options, env).await {
let scope_cwd =
(scoped && root.layout == JvmCacheLayout::Maven2 && !coursier_spelled(&root.path))
.then(|| options.cwd.clone());
// The walkdir walk and POM reads are blocking: run each repo
// on the walk pool so concurrently crawled ecosystems keep
// making progress (the dedup set rides along and comes back).
let (found, returned_seen) = run_walk(move || {
let found = MavenCrawler.scan_cache_root(&root, &mut seen);
// The scope reads a pom per reachable artifact while the scan
// only walks directories: run them side by side.
let shared = std::sync::Mutex::new(seen);
let mut halves = par_map(vec![false, true], |scope_half| {
if scope_half {
let scope = scope_cwd
.as_ref()
.and_then(|cwd| super::maven_scope::project_scope(cwd, &root.path));
(None, scope)
} else {
let mut seen = shared.lock().unwrap_or_else(|e| e.into_inner());
(Some(MavenCrawler.scan_cache_root(&root, &mut seen)), None)
}
});
let scope = halves.pop().and_then(|(_, scope)| scope);
let mut found = halves
.pop()
.and_then(|(found, _)| found)
.unwrap_or_default();
let seen = shared.into_inner().unwrap_or_else(|e| e.into_inner());
if let Some(scope) = scope {
found.retain(|p| {
scope.admits(
p.namespace.as_deref().unwrap_or_default(),
&p.name,
&p.version,
)
});
}
(found, seen)
})
.await;
Expand Down Expand Up @@ -2483,6 +2539,90 @@ mod tests {
assert!(purls.contains("pkg:maven/com.google.guava/guava@32.1.3-jre"));
}

/// #265: in project mode a Maven build's crawl keeps only what its
/// poms reach; the rest of the shared local repository (cached by other
/// projects) is not this project's. `--global` still lists everything.
#[tokio::test]
#[serial_test::serial]
async fn project_mode_crawl_keeps_only_the_projects_graph() {
let dir = tempfile::tempdir().unwrap();
let (cwd, repo) = (dir.path().join("app"), dir.path().join("m2"));
let pom = |g: &str, a: &str, v: &str, deps: &str| {
format!(
"<project><modelVersion>4.0.0</modelVersion><groupId>{g}</groupId>\
<artifactId>{a}</artifactId><version>{v}</version>\
<dependencies>{deps}</dependencies></project>"
)
};
let dep = |g: &str, a: &str, v: &str, scope: &str| {
format!(
"<dependency><groupId>{g}</groupId><artifactId>{a}</artifactId>\
<version>{v}</version><scope>{scope}</scope></dependency>"
)
};
std::fs::create_dir_all(&cwd).unwrap();
std::fs::write(
cwd.join("pom.xml"),
pom(
"com.example",
"unrelated-app",
"1.0.0",
&dep("junit", "junit", "4.13.2", "test"),
),
)
.unwrap();
for (g, a, v, deps) in [
(
"junit",
"junit",
"4.13.2",
dep("org.hamcrest", "hamcrest-core", "1.3", "compile"),
),
("org.hamcrest", "hamcrest-core", "1.3", String::new()),
// Cached by another project.
(
"org.apache.commons",
"commons-lang3",
"3.12.0",
String::new(),
),
] {
let d = repo.join(g.replace('.', "/")).join(a).join(v);
std::fs::create_dir_all(&d).unwrap();
std::fs::write(d.join(format!("{a}-{v}.pom")), pom(g, a, v, &deps)).unwrap();
}
let env = JvmEnv {
m2_repo: Some(repo.clone()),
gradle: None,
};
let purls = |packages: Vec<CrawledPackage>| {
let mut p: Vec<String> = packages.into_iter().map(|p| p.purl).collect();
p.sort();
p
};
let local = CrawlerOptions {
cwd: cwd.clone(),
global: false,
global_prefix: None,
};
assert_eq!(
purls(MavenCrawler::new().crawl_all_with(&local, &env).await),
[
"pkg:maven/junit/junit@4.13.2",
"pkg:maven/org.hamcrest/hamcrest-core@1.3",
]
);
let global = CrawlerOptions {
global: true,
..local
};
assert_eq!(
purls(MavenCrawler::new().crawl_all_with(&global, &env).await).len(),
3,
"a global crawl is not scoped"
);
}

#[tokio::test]
async fn test_crawl_all_deduplication() {
let dir = tempfile::tempdir().unwrap();
Expand Down
Loading
Loading