Skip to content
Open
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
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 @@ -786,7 +786,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 @@ -503,7 +503,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 @@ -1429,9 +1429,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
143 changes: 141 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,89 @@ 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]
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