feat(java): Add Project JDK and Maven home / settings.xml / local repository settings

The JDK was always picked automatically (JAVA_HOME, PATH, the usual install dirs, by the
module's Java version) and Maven was the project's mvnw or mvn on PATH, with no way to
override either. languages.java.jdk (Languages & Frameworks › Java › Project JDK) now sets
the default jdtls runtime, the JDK of Run/Debug and JAVA_HOME for Maven and Gradle;
languages.java.maven (Build Tools › Maven) sets the Maven home, -s and
-Dmaven.repo.local for sync, builds, tool-window goals and detected run configurations.
JDKs and Maven installations are detected for the pickers.

Closes #13
This commit is contained in:
l0ng-ai committed 2026-10-23 23:00:48 +08:00
1 parent c59f93988b
commit 7fa1d08b94
22 files changed
+484 -48

No files matched your search

+1
View File
@@ -225,6 +225,7 @@ fn maven_steps(root: &Path, model: &BuildModel, kind: BuildKind, module: Option<
}
let step = |p: &maven::MavenProject, extra: &[String]| {
let mut s = BuildStep::new(&p.command, &["-B", "-Dstyle.color=never"], &subdir(root, &p.dir), Format::Maven);
s.args.extend(p.options.iter().cloned());
s.args.extend(extra.iter().cloned());
s.args.extend(goals.iter().map(|g| (*g).to_owned()));
s.env = p.env.clone();
+3
View File
@@ -92,6 +92,8 @@ pub struct MavenProject {
pub java_version: Option<u32>,
/// Maven launcher: the nearest `mvnw` up to the project root (absolute), else `mvn`.
pub command: String,
/// Options before the goals (`-s`, `-Dmaven.repo.local=`); filled in by the shell from settings.
pub options: Vec<String>,
/// Environment for runs (`JAVA_HOME` of the matching JDK); filled in by the shell.
pub env: BTreeMap<String, String>,
}
@@ -924,6 +926,7 @@ pub(crate) fn load_with_repo(root: &Path, local_repo: Option<PathBuf>) -> MavenM
profiles: pom.profiles.iter().map(|p| p.id.clone()).collect(),
java_version: jv,
command: wrapper(root, &dir),
options: Vec::new(),
env: BTreeMap::new(),
});
for m in modules.into_iter().rev() {
+7 -1
View File
@@ -29,6 +29,10 @@ pub struct SyncOptions {
pub profiles: Vec<String>,
/// Extra environment (`JAVA_HOME`).
pub env: BTreeMap<String, String>,
/// Maven launcher replacing the project's `mvnw` / `mvn` (Settings › Maven home).
pub maven_command: Option<String>,
/// Maven options before the goals (`-s`, `-Dmaven.repo.local=`).
pub maven_options: Vec<String>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
@@ -184,6 +188,7 @@ pub fn sync_maven(
let effective = cache_dir.join(format!("effective-pom-{n}.xml"));
let _ = std::fs::remove_file(&effective);
let mut args: Vec<String> = vec!["-B".into(), "-fae".into(), "-Dstyle.color=never".into()];
args.extend(options.maven_options.iter().cloned());
if options.offline {
args.push("-o".into());
}
@@ -194,7 +199,8 @@ pub fn sync_maven(
args.push(format!("-Doutput={}", effective.to_string_lossy()));
args.push("dependency:tree".into());
progress(SyncProgress { message: format!("Reading {}", top.display_name), fraction: None });
let out = run(&top.command, &args, &root.join(&top.dir), &options.env, cancel, |line| {
let command = options.maven_command.as_deref().unwrap_or(&top.command);
let out = run(command, &args, &root.join(&top.dir), &options.env, cancel, |line| {
if let Some((name, i, total)) = maven_building(line) {
let fraction = (i.saturating_sub(1) as f32 / total.max(1) as f32).min(1.0);
progress(SyncProgress { message: format!("Building model of {name}"), fraction: Some(fraction) });
+17
View File
@@ -401,9 +401,26 @@ pub struct GoSettings {
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Type)]
#[serde(default, rename_all = "camelCase")]
pub struct JavaSettings {
/// Project JDK home; empty = per module, the installed JDK matching the
/// Java version it targets.
pub jdk: String,
/// Extra JVM options for the Java language server (jdtls), space-separated
/// (VS Code's `java.jdt.ls.vmargs`).
pub jdtls_vm_args: String,
/// Build Tools › Maven.
pub maven: MavenSettings,
}
/// Build Tools › Maven.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Type)]
#[serde(default, rename_all = "camelCase")]
pub struct MavenSettings {
/// Maven home (or its `mvn` executable); empty = the project's `mvnw`, else `mvn` on `PATH`.
pub home: String,
/// User `settings.xml` (`-s`); empty = `~/.m2/settings.xml`.
pub user_settings: String,
/// Local repository (`-Dmaven.repo.local`); empty = from `settings.xml`.
pub local_repository: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
+54 -1
View File
@@ -56,6 +56,42 @@ fn goroot(root: &Path, s: &Settings) -> Option<PathBuf> {
setting_path(root, &s.languages.go.goroot)
}
/// The project JDK (`languages.java.jdk`): a JDK home, or its `bin/java`.
pub fn project_jdk(root: &Path, s: &Settings) -> Option<crate::java::Jdk> {
let p = setting_path(root, &s.languages.java.jdk)?;
let jdk = crate::java::jdk_at(&p).or_else(|| crate::java::jdk_at(p.parent()?.parent()?));
if jdk.is_none() {
tracing::warn!("project JDK {} is not a JDK home", p.display());
}
jdk
}
/// `mvn` of the configured Maven home (`languages.java.maven.home`): the
/// executable itself, `<home>/bin/mvn` or `<bin>/mvn`.
pub fn maven_command(root: &Path, s: &Settings) -> Option<PathBuf> {
let p = setting_path(root, &s.languages.java.maven.home)?;
let name = if cfg!(windows) { "mvn.cmd" } else { "mvn" };
let found = [p.clone(), p.join("bin").join(name), p.join(name)].into_iter().find(|c| c.is_file());
if found.is_none() {
tracing::warn!("Maven home {} has no {name}", p.display());
}
found
}
/// Maven options from settings, placed before the goals: `-s <settings.xml>`
/// and `-Dmaven.repo.local=<dir>`.
pub fn maven_options(root: &Path, s: &Settings) -> Vec<String> {
let m = &s.languages.java.maven;
let mut out = Vec::new();
if let Some(file) = setting_path(root, &m.user_settings) {
out.extend(["-s".to_owned(), file.to_string_lossy().into_owned()]);
}
if let Some(dir) = setting_path(root, &m.local_repository) {
out.push(format!("-Dmaven.repo.local={}", dir.to_string_lossy()));
}
out
}
/// TypeScript `lib` directory for the selected version; `None` = the server's bundled one.
pub fn tsdk(root: &Path, s: &Settings) -> Option<PathBuf> {
match s.languages.typescript.version.trim() {
@@ -206,7 +242,8 @@ const PY_RUNNERS: [&str; 3] = ["uv", "poetry", "pdm"];
/// Points run configurations at the selected toolchains: Python configs use the
/// project interpreter, npm scripts the chosen package manager and Node.js,
/// cargo / go configs the selected toolchain.
/// cargo / go configs the selected toolchain, Maven / Gradle the project JDK
/// (and Maven the configured home and options).
pub fn apply_to_run_configs(configs: &mut [RunConfig], root: &Path, s: &Settings) {
let explicit_python = setting_path(root, &s.languages.python.interpreter);
let python = python_interpreter(root, s);
@@ -215,6 +252,9 @@ pub fn apply_to_run_configs(configs: &mut [RunConfig], root: &Path, s: &Settings
let rust = rust_bin(root, s);
let go = goroot(root, s);
let gopath = setting_path(root, &s.languages.go.gopath);
let jdk = project_jdk(root, s);
let mvn = maven_command(root, s);
let mvn_options = maven_options(root, s);
for c in configs.iter_mut() {
let mut path_dirs: Vec<PathBuf> = Vec::new();
@@ -281,6 +321,19 @@ pub fn apply_to_run_configs(configs: &mut [RunConfig], root: &Path, s: &Settings
c.env.entry("GOPATH".into()).or_insert_with(|| gopath.to_string_lossy().into_owned());
}
}
"maven" | "gradle" => {
if let Some(jdk) = &jdk {
c.env.entry("JAVA_HOME".into()).or_insert_with(|| jdk.home.to_string_lossy().into_owned());
}
if c.kind == "maven" && c.detected {
if let Some(mvn) = &mvn {
c.command = mvn.to_string_lossy().into_owned();
}
if !c.args.starts_with(&mvn_options) {
c.args.splice(0..0, mvn_options.iter().cloned());
}
}
}
_ => {}
}
if !path_dirs.is_empty() && !c.env.contains_key("PATH") {
+99
View File
@@ -18,6 +18,8 @@ pub fn detect(kind: SdkKind, root: Option<&Path>) -> Vec<Sdk> {
SdkKind::Rust => rust(),
SdkKind::Go => go(),
SdkKind::TypeScript => typescript(root),
SdkKind::Jdk => jdk(),
SdkKind::Maven => maven(),
}
}
@@ -382,3 +384,100 @@ fn typescript(root: Option<&Path>) -> Vec<Sdk> {
}
out
}
// ------------------------------------------------------------------ jdk
/// `KEY="value"` of a JDK `release` file.
fn release_value(release: &str, key: &str) -> Option<String> {
let line = release.lines().find_map(|l| l.trim().strip_prefix(key)?.strip_prefix('='))?;
Some(line.trim().trim_matches('"').to_owned()).filter(|v| !v.is_empty())
}
fn jdk() -> Vec<Sdk> {
let java_home = std::env::var_os("JAVA_HOME").and_then(|h| dunce::canonicalize(h).ok());
crate::java::rescan_jdks()
.iter()
.map(|j| {
let release = std::fs::read_to_string(j.home.join("release")).unwrap_or_default();
let version = release_value(&release, "JAVA_VERSION").unwrap_or_else(|| j.major.to_string());
let name = match release_value(&release, "IMPLEMENTOR") {
Some(vendor) => format!("{vendor} {version}"),
None => format!("JDK {version}"),
};
let source = if java_home.as_ref() == Some(&j.home) { "JAVA_HOME" } else { "system" };
Sdk {
path: j.home.clone().into(),
name,
version: Some(version),
source: source.to_owned(),
home: Some(j.home.clone().into()),
extra: None,
}
})
.collect()
}
// ------------------------------------------------------------------ maven
/// Maven version from `lib/maven-core-<version>.jar`.
fn maven_version(home: &Path) -> Option<String> {
std::fs::read_dir(home.join("lib")).ok()?.flatten().find_map(|e| {
let name = e.file_name().to_string_lossy().into_owned();
Some(name.strip_prefix("maven-core-")?.strip_suffix(".jar")?.to_owned())
})
}
/// A Maven home (`bin/mvn` and `lib/`); Homebrew's formula keeps it in `libexec`.
fn maven_home(dir: &Path) -> Option<PathBuf> {
let mvn = if cfg!(windows) { "mvn.cmd" } else { "mvn" };
[dir.join("libexec"), dir.to_path_buf()]
.into_iter()
.find(|h| h.join("bin").join(mvn).is_file() && h.join("lib").is_dir())
}
fn maven() -> Vec<Sdk> {
let mut found: Vec<(PathBuf, &'static str)> = Vec::new();
if let Some(mvn) = which("mvn").and_then(|p| dunce::canonicalize(p).ok())
&& let Some(home) = mvn.parent().and_then(Path::parent)
{
found.push((home.to_path_buf(), "path"));
}
for var in ["MAVEN_HOME", "M2_HOME"] {
if let Some(h) = std::env::var_os(var) {
found.push((PathBuf::from(h), "env"));
}
}
for p in ["/opt/homebrew/opt/maven", "/usr/local/opt/maven", "/usr/share/maven", "/opt/maven"] {
found.push((PathBuf::from(p), "system"));
}
if let Some(home) = home_dir() {
for d in subdirs(&home.join(".sdkman/candidates/maven")) {
found.push((d, "sdkman"));
}
// Distributions the Maven wrapper downloaded: `dists/apache-maven-3.9.6-bin/<hash>/apache-maven-3.9.6`.
for dist in subdirs(&home.join(".m2/wrapper/dists")) {
for hash in subdirs(&dist) {
found.extend(subdirs(&hash).into_iter().map(|d| (d, "wrapper")));
}
}
}
let mut seen = HashSet::new();
let mut out = Vec::new();
for (dir, source) in found {
let Some(home) = maven_home(&dir) else { continue };
let home = dunce::canonicalize(&home).unwrap_or(home);
if !seen.insert(home.clone()) {
continue;
}
let version = maven_version(&home);
out.push(Sdk {
path: home.clone().into(),
name: format!("Maven {}", version.clone().unwrap_or_default()).trim().to_owned(),
version,
source: source.to_owned(),
home: Some(home.into()),
extra: None,
});
}
out
}
+71 -15
View File
@@ -25,7 +25,8 @@ pub fn parse_release(release: &str) -> Option<u32> {
if first == 1 { parts.next()?.parse().ok() } else { Some(first) }
}
fn jdk_at(home: &Path) -> Option<Jdk> {
/// The JDK at `home`, if it is one (a `release` file and `bin/java`).
pub fn jdk_at(home: &Path) -> Option<Jdk> {
let release = std::fs::read_to_string(home.join("release")).ok()?;
if !home.join("bin").join(crate::exe("java")).is_file() {
return None;
@@ -117,6 +118,11 @@ fn rescan(slot: &mut Option<Scan>) -> Arc<[Jdk]> {
slot.insert(Scan { at: Instant::now(), jdks: scan().into() }).jdks.clone()
}
/// Scans for installed JDKs again (Settings › Detect again).
pub fn rescan_jdks() -> Arc<[Jdk]> {
rescan(&mut JDKS.lock().unwrap_or_else(|e| e.into_inner()))
}
/// Installed JDKs (scanned on first use; [`jdtls_jdks`] may refresh them).
pub fn installed_jdks() -> Arc<[Jdk]> {
let mut slot = JDKS.lock().unwrap_or_else(|e| e.into_inner());
@@ -258,12 +264,15 @@ const METADATA_PROPERTY: &str = "java.import.generatesMetadataFilesAtProjectRoot
/// directory, one per server root, and no Eclipse metadata is written into
/// the project; it runs on a JDK `min_java`+ (via `JAVA_HOME`, which its
/// launcher reads), installed JDKs are registered as runtimes and `bundles`
/// (e.g. the java-debug plugin jar) are loaded into the server.
/// (e.g. the java-debug plugin jar) are loaded into the server. The project
/// JDK, when set, is the default runtime and preferred for running the server.
#[allow(clippy::too_many_arguments)]
pub fn configure_jdtls(
cfg: &mut LanguageServerConfig,
server_root: &Path,
data_dir: &Path,
jdks: &[Jdk],
project_jdk: Option<&Jdk>,
min_java: u32,
bundles: &[PathBuf],
lombok: Option<&Path>,
@@ -294,6 +303,9 @@ pub fn configure_jdtls(
{
cfg.args.push(format!("--jvm-arg=-javaagent:{}", jar.to_string_lossy()));
}
let jdks: Vec<Jdk> =
project_jdk.into_iter().chain(jdks.iter().filter(|j| Some(*j) != project_jdk)).cloned().collect();
let jdks = jdks.as_slice();
if !cfg.env.contains_key("JAVA_HOME") {
match jdtls_runtime(jdks, min_java) {
Some(jdk) => {
@@ -302,7 +314,15 @@ pub fn configure_jdtls(
None => cfg.launch_error = Some(missing_runtime_message(jdks, min_java)),
}
}
let runtimes = runtimes(jdks);
let mut runtimes = runtimes(jdks);
if let Some(project) = project_jdk {
let home = project.home.to_string_lossy();
for r in &mut runtimes {
if r["path"] == *home {
r["default"] = json!(true);
}
}
}
if !runtimes.is_empty() {
merge_init_options(cfg, json!({ "settings": { "java": { "configuration": { "runtimes": runtimes } } } }));
}
@@ -424,7 +444,16 @@ mod tests {
Jdk { major: 8, home: PathBuf::from("/jdk8") },
];
let bundles = [PathBuf::from("/tools/java-debug/bin/com.microsoft.java.debug.plugin.jar")];
configure_jdtls(&mut cfg, Path::new("/p/demo"), Path::new("/data"), &jdks, JDTLS_MIN_JAVA, &bundles, None);
configure_jdtls(
&mut cfg,
Path::new("/p/demo"),
Path::new("/data"),
&jdks,
None,
JDTLS_MIN_JAVA,
&bundles,
None,
);
assert_eq!(cfg.args[0], "-data");
assert!(cfg.args[1].starts_with("/data/lsp/jdtls/"), "{}", cfg.args[1]);
assert_eq!(cfg.args[2], "--jvm-arg=-Djava.import.generatesMetadataFilesAtProjectRoot=false");
@@ -439,11 +468,38 @@ mod tests {
// Deterministic (reconfigure compares configs) and idempotent.
let before = cfg.clone();
configure_jdtls(&mut cfg, Path::new("/p/demo"), Path::new("/data"), &jdks, JDTLS_MIN_JAVA, &bundles, None);
configure_jdtls(
&mut cfg,
Path::new("/p/demo"),
Path::new("/data"),
&jdks,
None,
JDTLS_MIN_JAVA,
&bundles,
None,
);
assert_eq!(cfg, before);
// The project JDK is the default runtime, and runs the server when new enough.
let project = Jdk { major: 21, home: PathBuf::from("/jdk21b") };
let mut cfg: LanguageServerConfig = toml::from_str("name = \"jdtls\"\ncommand = \"jdtls\"").unwrap();
configure_jdtls(
&mut cfg,
Path::new("/p"),
Path::new("/data"),
&jdks,
Some(&project),
JDTLS_MIN_JAVA,
&[],
None,
);
let opts = serde_json::to_value(cfg.initialization_options.clone().unwrap()).unwrap();
let rt = &opts["settings"]["java"]["configuration"]["runtimes"];
assert_eq!(rt[1], json!({ "name": "JavaSE-21", "path": "/jdk21b", "default": true }));
assert_eq!(cfg.env["JAVA_HOME"], "/jdk21b");
let mut other: LanguageServerConfig = toml::from_str("name = \"pyright\"\ncommand = \"x\"").unwrap();
configure_jdtls(&mut other, Path::new("/p"), Path::new("/data"), &jdks, JDTLS_MIN_JAVA, &bundles, None);
configure_jdtls(&mut other, Path::new("/p"), Path::new("/data"), &jdks, None, JDTLS_MIN_JAVA, &bundles, None);
assert!(other.args.is_empty() && other.initialization_options.is_none());
}
@@ -452,23 +508,23 @@ mod tests {
let jdtls = || -> LanguageServerConfig { toml::from_str("name = \"jdtls\"\ncommand = \"jdtls\"").unwrap() };
let jar = Path::new("/m2/lombok-1.18.48.jar");
let mut cfg = jdtls();
configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &[], JDTLS_MIN_JAVA, &[], Some(jar));
configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &[], None, JDTLS_MIN_JAVA, &[], Some(jar));
let agent = format!("--jvm-arg=-javaagent:{}", jar.to_string_lossy());
assert!(cfg.args.contains(&agent), "{:?}", cfg.args);
let mut plain = jdtls();
configure_jdtls(&mut plain, Path::new("/p"), Path::new("/data"), &[], JDTLS_MIN_JAVA, &[], None);
configure_jdtls(&mut plain, Path::new("/p"), Path::new("/data"), &[], None, JDTLS_MIN_JAVA, &[], None);
assert_ne!(cfg.args[1], plain.args[1], "a separate workspace, so everything is rebuilt with the agent");
let before = cfg.clone();
configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &[], JDTLS_MIN_JAVA, &[], Some(jar));
configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &[], None, JDTLS_MIN_JAVA, &[], Some(jar));
assert_eq!(cfg, before, "idempotent");
let mut cfg = jdtls();
cfg.args.push("--jvm-arg=-javaagent:/own/lombok.jar".into());
configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &[], JDTLS_MIN_JAVA, &[], Some(jar));
configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &[], None, JDTLS_MIN_JAVA, &[], Some(jar));
assert!(!cfg.args.contains(&agent), "the user's agent wins");
let mut own = jdtls();
own.args.push("--jvm-arg=-javaagent:/own/lombok.jar".into());
configure_jdtls(&mut own, Path::new("/p"), Path::new("/data"), &[], JDTLS_MIN_JAVA, &[], None);
configure_jdtls(&mut own, Path::new("/p"), Path::new("/data"), &[], None, JDTLS_MIN_JAVA, &[], None);
assert_eq!(cfg.args, own.args, "its workspace follows the agent in use");
}
@@ -477,19 +533,19 @@ mod tests {
let jdtls = || -> LanguageServerConfig { toml::from_str("name = \"jdtls\"\ncommand = \"jdtls\"").unwrap() };
let old = [Jdk { major: 17, home: PathBuf::from("/jdk17") }, Jdk { major: 8, home: PathBuf::from("/jdk8") }];
let mut cfg = jdtls();
configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &old, JDTLS_MIN_JAVA, &[], None);
configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &old, None, JDTLS_MIN_JAVA, &[], None);
assert!(!cfg.env.contains_key("JAVA_HOME"));
let err = cfg.launch_error.unwrap();
assert!(err.contains("JDK 21 or newer") && err.contains("found JDK 8, 17"), "{err}");
let mut cfg = jdtls();
configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &[], JDTLS_MIN_JAVA, &[], None);
configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &[], None, JDTLS_MIN_JAVA, &[], None);
assert!(cfg.launch_error.unwrap().contains("no JDK was found"));
// An explicit JAVA_HOME is the user's call.
let mut cfg = jdtls();
cfg.env.insert("JAVA_HOME".into(), "/custom".into());
configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &old, JDTLS_MIN_JAVA, &[], None);
configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &old, None, JDTLS_MIN_JAVA, &[], None);
assert_eq!((cfg.env["JAVA_HOME"].as_str(), cfg.launch_error), ("/custom", None));
let mixed = [old[0].clone(), Jdk { major: 21, home: PathBuf::from("/jdk21") }];
@@ -497,7 +553,7 @@ mod tests {
// A jdtls whose bundles need 25 doesn't start on 21, and says what it needs.
let mut cfg = jdtls();
configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &mixed, 25, &[], None);
configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &mixed, None, 25, &[], None);
let err = cfg.launch_error.unwrap();
assert!(err.contains("JDK 25 or newer") && err.contains("found JDK 17, 21"), "{err}");
}
+4
View File
@@ -31,6 +31,10 @@ pub enum SdkKind {
Rust,
Go,
TypeScript,
/// JDK homes.
Jdk,
/// Maven homes.
Maven,
}
/// One installed interpreter / toolchain.
+50 -1
View File
@@ -1,4 +1,7 @@
use crate::apply::{apply_to_run_configs, configure_server, python_interpreter, terminal_activation, tsdk};
use crate::apply::{
apply_to_run_configs, configure_server, maven_command, maven_options, project_jdk, python_interpreter,
terminal_activation, tsdk,
};
use crate::python::parse_pip_list;
use crate::{bin_dir, exe, venv_of};
use ide_core::settings::Settings;
@@ -153,6 +156,52 @@ fn run_configs_use_selected_interpreter_and_package_manager() {
assert_eq!(configs[4].command, "python3", "shell configs are left alone");
}
fn fake_jdk(dir: &Path) {
std::fs::create_dir_all(dir.join("bin")).unwrap();
std::fs::write(dir.join("bin").join(exe("java")), "").unwrap();
std::fs::write(dir.join("release"), "JAVA_VERSION=\"17.0.9\"\n").unwrap();
}
#[test]
fn project_jdk_and_maven_settings() {
if cfg!(windows) {
return; // Unix paths and shells
}
let tmp = tempfile::tempdir().unwrap();
let jdk = tmp.path().join("jdk17");
fake_jdk(&jdk);
let maven = tmp.path().join("maven");
std::fs::create_dir_all(maven.join("bin")).unwrap();
std::fs::write(maven.join("bin/mvn"), "").unwrap();
let mut s = Settings::default();
assert_eq!(project_jdk(tmp.path(), &s), None);
assert_eq!(maven_command(tmp.path(), &s), None);
assert!(maven_options(tmp.path(), &s).is_empty());
// Project-relative paths, a JDK's `bin/java`, a Maven home.
s.languages.java.jdk = "jdk17/bin/java".into();
s.languages.java.maven.home = maven.to_string_lossy().into_owned();
s.languages.java.maven.user_settings = "/m2/settings.xml".into();
s.languages.java.maven.local_repository = "/m2/repo".into();
assert_eq!(project_jdk(tmp.path(), &s).map(|j| j.major), Some(17));
assert_eq!(maven_command(tmp.path(), &s), Some(maven.join("bin/mvn")));
assert_eq!(maven_options(tmp.path(), &s), ["-s", "/m2/settings.xml", "-Dmaven.repo.local=/m2/repo"]);
let mut configs = vec![run("maven", "./mvnw", &["package"]), run("gradle", "./gradlew", &["build"])];
apply_to_run_configs(&mut configs, tmp.path(), &s);
let home = project_jdk(tmp.path(), &s).unwrap().home.to_string_lossy().into_owned();
assert_eq!(configs[0].command, maven.join("bin/mvn").to_string_lossy());
assert_eq!(configs[0].args, ["-s", "/m2/settings.xml", "-Dmaven.repo.local=/m2/repo", "package"]);
assert_eq!(configs[0].env["JAVA_HOME"], home);
assert_eq!(configs[1].env["JAVA_HOME"], home);
apply_to_run_configs(&mut configs, tmp.path(), &s);
assert_eq!(configs[0].args.len(), 4, "applied once");
s.languages.java.jdk = "missing".into();
assert_eq!(project_jdk(tmp.path(), &s), None);
}
#[test]
fn terminal_activation_for_project_venv() {
if cfg!(windows) {