mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
fix(gradle): discover nested builds and select compatible JVMs
This commit is contained in:
@@ -218,21 +218,41 @@ fn normalize(p: &Path) -> PathBuf {
|
||||
out
|
||||
}
|
||||
|
||||
/// The directory of the Gradle build under `root`: the root itself, or a direct
|
||||
/// subfolder, holding a settings or build script.
|
||||
/// The first automatically discovered build, for callers operating on one build.
|
||||
pub fn build_dir(root: &Path) -> Option<PathBuf> {
|
||||
build_dirs(root).into_iter().next()
|
||||
}
|
||||
|
||||
fn build_dirs(root: &Path) -> Vec<PathBuf> {
|
||||
let has = |d: &Path| SETTINGS_FILES.iter().chain(BUILD_FILES).any(|f| d.join(f).is_file());
|
||||
if has(root) {
|
||||
return Some(root.to_path_buf());
|
||||
return vec![root.to_path_buf()];
|
||||
}
|
||||
let mut dirs: Vec<PathBuf> = std::fs::read_dir(root)
|
||||
.ok()?
|
||||
.flatten()
|
||||
.filter(|e| e.file_type().is_ok_and(|t| t.is_dir()) && !e.file_name().to_string_lossy().starts_with('.'))
|
||||
.map(|e| e.path())
|
||||
.collect();
|
||||
dirs.sort();
|
||||
dirs.into_iter().find(|d| SETTINGS_FILES.iter().any(|f| d.join(f).is_file()))
|
||||
let mut builds = Vec::new();
|
||||
let mut pending = vec![(root.to_path_buf(), 0)];
|
||||
while let Some((dir, depth)) = pending.pop() {
|
||||
let Ok(entries) = std::fs::read_dir(&dir) else { continue };
|
||||
for entry in entries.flatten() {
|
||||
if !entry.file_type().is_ok_and(|t| t.is_dir()) {
|
||||
continue;
|
||||
}
|
||||
let name = entry.file_name();
|
||||
let name = name.to_string_lossy();
|
||||
if name.starts_with('.')
|
||||
|| matches!(name.as_ref(), "node_modules" | "target" | "build" | "dist" | "vendor" | "src" | "out")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let path = entry.path();
|
||||
if has(&path) {
|
||||
builds.push(path);
|
||||
} else if depth + 1 < 3 {
|
||||
pending.push((path, depth + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
builds.sort();
|
||||
builds
|
||||
}
|
||||
|
||||
fn wrapper(dir: &Path) -> String {
|
||||
@@ -495,7 +515,7 @@ pub fn linked_project_key(root: &Path, dir: &str) -> String {
|
||||
/// Linked build identities without reading their scripts (also used by index hot paths).
|
||||
pub(crate) fn linked_dirs(root: &Path, settings: &ide_core::settings::GradleSettings) -> Vec<String> {
|
||||
match &settings.linked_projects {
|
||||
None => build_dir(root).map(|dir| rel(root, &dir)).into_iter().collect(),
|
||||
None => build_dirs(root).iter().map(|dir| rel(root, dir)).collect(),
|
||||
Some(dirs) => {
|
||||
let mut out = Vec::new();
|
||||
for dir in dirs {
|
||||
@@ -686,6 +706,55 @@ pub(crate) fn has_war_plugin(script: &str) -> bool {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn default_linkage_discovers_all_shallow_builds() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let root = temp.path();
|
||||
for file in [
|
||||
"Cargo.toml",
|
||||
"package.json",
|
||||
"fixtures/kotlin-gradle/settings.gradle.kts",
|
||||
"fixtures/kotlin-gradle/build.gradle.kts",
|
||||
"fixtures/kotlin-gradle/tools/settings.gradle",
|
||||
"services/backend/api/settings.gradle",
|
||||
"standalone/build.gradle",
|
||||
"ignored/src/settings.gradle",
|
||||
"ignored/out/build.gradle",
|
||||
"node_modules/example/settings.gradle",
|
||||
"target/example/build.gradle",
|
||||
"build/example/settings.gradle",
|
||||
"dist/example/build.gradle",
|
||||
"vendor/example/settings.gradle",
|
||||
".hidden/settings.gradle",
|
||||
"too/deep/to/find/settings.gradle",
|
||||
] {
|
||||
let path = root.join(file);
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
std::fs::write(path, "").unwrap();
|
||||
}
|
||||
let models = super::load_linked(root, &Default::default());
|
||||
assert_eq!(
|
||||
models.iter().map(|m| m.dir.as_str()).collect::<Vec<_>>(),
|
||||
["fixtures/kotlin-gradle", "services/backend/api", "standalone"]
|
||||
);
|
||||
std::fs::write(root.join("build.gradle.kts"), "").unwrap();
|
||||
let models = super::load_linked(root, &Default::default());
|
||||
assert_eq!(models.len(), 1);
|
||||
assert!(models[0].dir.is_empty());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn default_linkage_does_not_follow_symlinks() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let root = temp.path();
|
||||
std::fs::create_dir(root.join("real")).unwrap();
|
||||
std::fs::write(root.join("real/build.gradle"), "").unwrap();
|
||||
std::os::unix::fs::symlink(root.join("real"), root.join("alias")).unwrap();
|
||||
let models = super::load_linked(root, &Default::default());
|
||||
assert_eq!(models.iter().map(|m| m.dir.as_str()).collect::<Vec<_>>(), ["real"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_path_from_the_ui_finds_the_saved_link() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
@@ -718,7 +787,10 @@ mod tests {
|
||||
std::fs::create_dir(root.join(dir)).unwrap();
|
||||
std::fs::write(root.join(dir).join("settings.gradle"), format!("rootProject.name = '{dir}'")).unwrap();
|
||||
}
|
||||
assert_eq!(super::load_linked(root, &Default::default())[0].dir, "a");
|
||||
assert_eq!(
|
||||
super::load_linked(root, &Default::default()).iter().map(|m| m.dir.as_str()).collect::<Vec<_>>(),
|
||||
["a", "b"]
|
||||
);
|
||||
let mut settings = ide_core::settings::GradleSettings {
|
||||
linked_projects: Some(vec!["b".into(), "./b".into(), "a".into()]),
|
||||
..Default::default()
|
||||
|
||||
@@ -47,8 +47,8 @@ pub fn load(root: &Path, cache: &Path, settings: &Settings) -> BuildModel {
|
||||
}
|
||||
}
|
||||
for g in &mut model.gradle {
|
||||
g.env = ide_sdk::apply::gradle_env(root, settings, java_env(jdk.as_ref(), g.java_version));
|
||||
g.command = ide_sdk::apply::gradle_command(root, &root.join(&g.dir), settings).to_string_lossy().into_owned();
|
||||
g.env = ide_sdk::apply::gradle_build_env(root, &root.join(&g.dir), &g.command, settings, g.java_version);
|
||||
}
|
||||
// A failed sync of a project no installed JDK can build says so first (with a Download JDK action).
|
||||
let missing = |release: Option<u32>| match jdk {
|
||||
|
||||
@@ -301,6 +301,21 @@ fn nested_builds_of_every_kind_and_depth() {
|
||||
let builds: Vec<&str> =
|
||||
ids(&c).into_iter().filter(|id| id.starts_with("maven:package") || id.starts_with("gradle:build")).collect();
|
||||
assert_eq!(builds, ["gradle:build:services/api", "maven:package:billing"]);
|
||||
let model = ide_build::load(dir.path(), &dir.path().join("cache"), None);
|
||||
for config in c.iter().filter(|c| c.kind == "gradle") {
|
||||
let cwd = config.cwd.as_deref().unwrap();
|
||||
assert!(model.gradle.iter().any(|g| Path::new(&g.dir) == cwd), "{} has no build model", config.id);
|
||||
}
|
||||
let plan = ide_build::compile::plan(
|
||||
dir.path(),
|
||||
&model,
|
||||
ide_build::compile::BuildKind::Build,
|
||||
Some(&dir.path().join("services/api/app/src/main/kotlin/com/acme/Api.kt")),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(plan.steps[0].cwd, dir.path().join("services/api"));
|
||||
assert_eq!(plan.steps[0].args, ["--console=plain", ":app:classes", ":app:testClasses"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+158
-7
@@ -245,6 +245,102 @@ fn resolve_gradle_command(root: &Path, build_dir: &Path, s: &Settings, use_wrapp
|
||||
.unwrap_or_else(|| name.into())
|
||||
}
|
||||
|
||||
fn gradle_run_jdk<'a>(
|
||||
release: Option<u32>,
|
||||
configured: Option<&'a crate::java::Jdk>,
|
||||
installed: &'a [crate::java::Jdk],
|
||||
gradle: Option<(u32, u32)>,
|
||||
) -> Option<&'a crate::java::Jdk> {
|
||||
configured.or_else(|| {
|
||||
let release = release?;
|
||||
let gradle = gradle?;
|
||||
let compatible: Vec<_> = installed.iter().filter(|jdk| gradle_supports_java(gradle, jdk.major)).collect();
|
||||
compatible
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|jdk| jdk.major == release)
|
||||
.or_else(|| compatible.iter().copied().filter(|jdk| jdk.major > release).min_by_key(|jdk| jdk.major))
|
||||
.or_else(|| compatible.iter().copied().max_by_key(|jdk| jdk.major))
|
||||
})
|
||||
}
|
||||
|
||||
/// Default environment shared by Gradle model builds and Run configurations.
|
||||
pub fn gradle_build_env(
|
||||
root: &Path,
|
||||
build_dir: &Path,
|
||||
command: &str,
|
||||
settings: &Settings,
|
||||
release: Option<u32>,
|
||||
) -> std::collections::BTreeMap<String, String> {
|
||||
let configured = project_jdk(root, settings);
|
||||
let installed = crate::java::installed_jdks();
|
||||
let jdk = gradle_run_jdk(
|
||||
release.or_else(|| crate::java::project_java_version(build_dir)),
|
||||
configured.as_ref(),
|
||||
&installed,
|
||||
gradle_distribution_version(build_dir, command),
|
||||
);
|
||||
let env = jdk
|
||||
.map(|jdk| std::collections::BTreeMap::from([("JAVA_HOME".into(), jdk.home.to_string_lossy().into_owned())]))
|
||||
.unwrap_or_default();
|
||||
gradle_env(root, settings, env)
|
||||
}
|
||||
|
||||
fn gradle_supports_java(gradle: (u32, u32), java: u32) -> bool {
|
||||
// https://docs.gradle.org/current/userguide/compatibility.html#java_runtime
|
||||
let minimum = match java {
|
||||
8 => (2, 0),
|
||||
9 => (4, 3),
|
||||
10 => (4, 7),
|
||||
11 => (5, 0),
|
||||
12 => (5, 4),
|
||||
13 => (6, 0),
|
||||
14 => (6, 3),
|
||||
15 => (6, 7),
|
||||
16 => (7, 0),
|
||||
17 => (7, 3),
|
||||
18 => (7, 5),
|
||||
19 => (7, 6),
|
||||
20 => (8, 3),
|
||||
21 => (8, 5),
|
||||
22 => (8, 8),
|
||||
23 => (8, 10),
|
||||
24 => (8, 14),
|
||||
25 => (9, 1),
|
||||
26 => (9, 4),
|
||||
27 => (9, 8),
|
||||
_ => return false,
|
||||
};
|
||||
gradle >= minimum && (java >= 17 || gradle < (9, 0))
|
||||
}
|
||||
|
||||
fn gradle_distribution_version(build_dir: &Path, command: &str) -> Option<(u32, u32)> {
|
||||
let version = |text: &str| {
|
||||
let mut parts = text.split('.');
|
||||
Some((parts.next()?.parse().ok()?, parts.next()?.parse().ok()?))
|
||||
};
|
||||
if Path::new(command).file_stem().is_some_and(|name| name == "gradlew") {
|
||||
let properties = std::fs::read_to_string(build_dir.join("gradle/wrapper/gradle-wrapper.properties")).ok()?;
|
||||
let url = properties.lines().find_map(|line| line.trim().strip_prefix("distributionUrl="))?;
|
||||
return version(url.rsplit_once("gradle-")?.1.split('-').next()?);
|
||||
}
|
||||
let path = Path::new(command);
|
||||
let path = if path.is_absolute() { path.to_path_buf() } else { crate::exec::which(command)? };
|
||||
let path = dunce::canonicalize(path).ok()?;
|
||||
let home = path.parent()?.parent()?;
|
||||
for lib in [home.join("lib"), home.join("libexec/lib")] {
|
||||
let Ok(entries) = std::fs::read_dir(lib) else { continue };
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name();
|
||||
let name = name.to_string_lossy();
|
||||
if let Some(v) = name.strip_prefix("gradle-launcher-").and_then(|name| name.strip_suffix(".jar")) {
|
||||
return version(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Gradle JVM overrides Project JDK; model callers supply the module's fallback environment.
|
||||
pub fn gradle_env(
|
||||
root: &Path,
|
||||
@@ -1044,13 +1140,7 @@ pub fn apply_to_run_configs(configs: &mut [RunConfig], root: &Path, s: &Settings
|
||||
if matches!(c.command.as_str(), "gradle" | "gradle.bat") {
|
||||
c.command = resolve_gradle_command(root, &build_dir, s, false).to_string_lossy().into_owned();
|
||||
}
|
||||
let defaults = jdk
|
||||
.as_ref()
|
||||
.map(|j| {
|
||||
std::collections::BTreeMap::from([("JAVA_HOME".into(), j.home.to_string_lossy().into_owned())])
|
||||
})
|
||||
.unwrap_or_default();
|
||||
for (key, value) in gradle_env(root, s, defaults) {
|
||||
for (key, value) in gradle_build_env(root, &build_dir, &c.command, s, None) {
|
||||
c.env.entry(key).or_insert(value);
|
||||
}
|
||||
}
|
||||
@@ -1302,6 +1392,67 @@ mod java_compiler_tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod gradle_runtime_tests {
|
||||
use super::*;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[test]
|
||||
fn gradle_runs_use_the_builds_toolchain_without_overriding_user_choices() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let installed =
|
||||
[25, 21, 17, 11, 8].map(|major| crate::java::Jdk { major, home: temp.path().join(format!("jdk{major}")) });
|
||||
for (name, script, expected) in [
|
||||
("kotlin", "kotlin { jvmToolchain(21) }", Some(21)),
|
||||
("java", "java { toolchain { languageVersion = JavaLanguageVersion.of(17) } }", Some(17)),
|
||||
("unspecified", "plugins { id 'java' }", None),
|
||||
] {
|
||||
let dir = temp.path().join(name);
|
||||
std::fs::create_dir(&dir).unwrap();
|
||||
std::fs::write(dir.join("build.gradle.kts"), script).unwrap();
|
||||
let picked = gradle_run_jdk(crate::java::project_java_version(&dir), None, &installed, Some((9, 0)));
|
||||
assert_eq!(picked.map(|jdk| jdk.major), expected);
|
||||
assert_eq!(gradle_run_jdk(expected, Some(&installed[0]), &installed, Some((9, 0))), Some(&installed[0]));
|
||||
let mut settings = Settings::default();
|
||||
settings.languages.java.gradle.jvm = "custom-gradle-jdk".into();
|
||||
let env = gradle_env(temp.path(), &settings, BTreeMap::from([("JAVA_HOME".into(), "/fallback".into())]));
|
||||
assert_eq!(env["JAVA_HOME"], temp.path().join("custom-gradle-jdk").to_string_lossy());
|
||||
}
|
||||
let dir = temp.path().join("kotlin");
|
||||
for (gradle, target, expected) in
|
||||
[((9, 0), 8, 17), ((8, 4), 21, 17), ((8, 5), 21, 21), ((7, 2), 21, 11), ((9, 0), 25, 21), ((9, 1), 25, 25)]
|
||||
{
|
||||
std::fs::write(dir.join("build.gradle.kts"), format!("kotlin {{ jvmToolchain({target}) }}")).unwrap();
|
||||
assert_eq!(gradle_run_jdk(Some(target), None, &installed, Some(gradle)).unwrap().major, expected);
|
||||
}
|
||||
assert!(gradle_run_jdk(Some(21), None, &installed, None).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_wrapper_and_local_gradle_versions_without_running_them() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let root = temp.path();
|
||||
std::fs::create_dir_all(root.join("gradle/wrapper")).unwrap();
|
||||
std::fs::write(
|
||||
root.join("gradle/wrapper/gradle-wrapper.properties"),
|
||||
"distributionUrl=https\\://services.gradle.org/distributions/gradle-8.4.0-bin.zip\n",
|
||||
)
|
||||
.unwrap();
|
||||
for command in ["./gradlew", "gradlew.bat"] {
|
||||
assert_eq!(gradle_distribution_version(root, command), Some((8, 4)));
|
||||
}
|
||||
for layout in ["lib", "libexec/lib"] {
|
||||
let home = root.join(layout.replace('/', "-"));
|
||||
std::fs::create_dir_all(home.join("bin")).unwrap();
|
||||
std::fs::create_dir_all(home.join(layout)).unwrap();
|
||||
let command = home.join("bin/gradle");
|
||||
std::fs::write(&command, "").unwrap();
|
||||
std::fs::write(home.join(layout).join("gradle-launcher-9.0.0.jar"), "").unwrap();
|
||||
assert_eq!(gradle_distribution_version(root, &command.to_string_lossy()), Some((9, 0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod maven_linkage_tests {
|
||||
use super::*;
|
||||
|
||||
@@ -540,10 +540,12 @@ pub fn project_java_version(module_dir: &Path) -> Option<u32> {
|
||||
}
|
||||
for name in ["build.gradle.kts", "build.gradle"] {
|
||||
let Some(text) = read(name) else { continue };
|
||||
if let Some(i) = text.find("JavaLanguageVersion.of(") {
|
||||
let rest = &text[i + "JavaLanguageVersion.of(".len()..];
|
||||
if let Some(v) = parse_java_version(rest.split(')').next().unwrap_or_default()) {
|
||||
return Some(v);
|
||||
for key in ["JavaLanguageVersion.of(", "jvmToolchain("] {
|
||||
if let Some(i) = text.find(key) {
|
||||
let rest = &text[i + key.len()..];
|
||||
if let Some(v) = parse_java_version(rest.split(')').next().unwrap_or_default()) {
|
||||
return Some(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
for key in ["sourceCompatibility", "targetCompatibility"] {
|
||||
@@ -1138,6 +1140,14 @@ mod tests {
|
||||
assert_eq!(project_java_version(dir), Some(11));
|
||||
std::fs::write(dir.join("build.gradle.kts"), "sourceCompatibility = '1.8'").unwrap();
|
||||
assert_eq!(project_java_version(dir), Some(8));
|
||||
for (script, version) in [
|
||||
("kotlin { jvmToolchain(21) }", Some(21)),
|
||||
("kotlin { jvmToolchain(17) }", Some(17)),
|
||||
("kotlin { jvmToolchain(projectVersion) }", None),
|
||||
] {
|
||||
std::fs::write(dir.join("build.gradle.kts"), script).unwrap();
|
||||
assert_eq!(project_java_version(dir), version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user