diff --git a/crates/ide-host/src/ops/run.rs b/crates/ide-host/src/ops/run.rs index 511846bf..ae24f9f8 100644 --- a/crates/ide-host/src/ops/run.rs +++ b/crates/ide-host/src/ops/run.rs @@ -41,7 +41,7 @@ pub fn configs(host: &Host, window: &str) -> IdeResult> { } /// A project's detected configurations (minus removed ones and unlinked Maven projects), then -/// its saved ones; toolchains from Languages & Frameworks applied. Gradle choices stay as saved: +/// its saved ones; toolchains from Languages & Frameworks applied. Build-tool choices stay as saved: /// derived defaults must not be written back as explicit environment. fn detected_and_saved( root: &std::path::Path, @@ -56,7 +56,7 @@ fn detected_and_saved( .filter(|d| !suppressed.contains(&d.id) && !saved.iter().any(|u| u.id == d.id)) .collect(); all.extend(saved.iter().cloned()); - for config in all.iter_mut().filter(|c| c.kind != "gradle") { + for config in all.iter_mut().filter(|c| !matches!(c.kind.as_str(), "gradle" | "maven")) { ide_sdk::apply::apply_to_run_configs(std::slice::from_mut(config), root, settings); } all @@ -75,7 +75,7 @@ pub fn save_configs( let configs: Vec = configs .iter() .map(|c| match c.kind.as_str() { - "gradle" => c.clone(), + "gradle" | "maven" => c.clone(), _ => ide_sdk::apply::without_derived(c, &project.root, &settings), }) .collect(); @@ -156,7 +156,9 @@ pub async fn start(host: &Host, window: &str, config: RunConfig, on_output: Emit let root = &root; // Configurations made in the UI name bare tools (`godot`) that may live outside PATH. let mut config = config; - if matches!(config.kind.as_str(), "gradle" | "tomcat") { + if config.kind == "maven" { + ide_sdk::apply::prepare_maven_run(&mut config, root, &settings); + } else if matches!(config.kind.as_str(), "gradle" | "tomcat") { ide_sdk::apply::apply_to_run_configs(std::slice::from_mut(&mut config), root, &settings); } else { ide_sdk::apply::resolve_run_command(&mut config, root, &settings); diff --git a/crates/ide-host/tests/run_configs.rs b/crates/ide-host/tests/run_configs.rs index 9313c756..c45bccda 100644 --- a/crates/ide-host/tests/run_configs.rs +++ b/crates/ide-host/tests/run_configs.rs @@ -81,6 +81,46 @@ async fn gradle_defaults_are_not_saved_as_explicit_run_choices() { assert_eq!(saved.command, gradle.command); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn maven_defaults_are_resolved_at_launch_and_explicit_choices_survive_reload() { + let tmp = tempfile::tempdir().unwrap(); + let root = dunce::canonicalize(tmp.path()).unwrap(); + std::fs::write(root.join("pom.xml"), "").unwrap(); + let home = root.join("maven"); + std::fs::create_dir_all(home.join("bin")).unwrap(); + let launcher = home.join("bin").join(if cfg!(windows) { "mvn.cmd" } else { "mvn" }); + std::fs::write(&launcher, "").unwrap(); + let host = host(&root.join("app")); + host.trust.decide(&root, true, false).unwrap(); + host.open_project("main", &root).unwrap(); + for (key, value) in [ + ("languages.java.maven.home", home.to_string_lossy().into_owned()), + ("languages.java.maven.userSettings", "settings.xml".into()), + ] { + ops::project::set_settings( + &host, + "main", + ide_workspace::settings::SettingsScope::Project, + key, + &serde_json::to_string(&value).unwrap(), + ) + .unwrap(); + } + let mut config = ops::run::configs(&host, "main").unwrap().into_iter().find(|c| c.id == "maven:test").unwrap(); + assert_eq!(config.command, "mvn"); + assert_eq!(config.args, ["test"]); + assert!(config.env.is_empty()); + config.command = launcher.to_string_lossy().into_owned(); + config.args = vec!["-s".into(), "own-settings.xml".into(), "test".into()]; + config.env.insert("JAVA_HOME".into(), "own-jdk".into()); + config.detected = false; + ops::run::save_configs(&host, "main", &[config.clone()], &[]).unwrap(); + let saved = ops::run::configs(&host, "main").unwrap().into_iter().find(|c| c.id == config.id).unwrap(); + assert_eq!(saved.command, config.command); + assert_eq!(saved.args, config.args); + assert_eq!(saved.env, config.env); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn workspace_cmake_before_launch_tasks_resolve_in_the_member() { let tmp = tempfile::tempdir().unwrap(); diff --git a/crates/ide-sdk/src/apply.rs b/crates/ide-sdk/src/apply.rs index 7ff5e54c..a496a433 100644 --- a/crates/ide-sdk/src/apply.rs +++ b/crates/ide-sdk/src/apply.rs @@ -192,6 +192,88 @@ pub fn maven_options(root: &Path, s: &Settings) -> Vec { out } +fn maven_settings_prefixes(args: &[String]) -> Vec { + let mut prefixes = vec![0]; + let mut i = 0; + while let Some(arg) = args.get(i) { + if arg == "-s" && args.get(i + 1).is_some() { + i += 2; + } else if arg.starts_with("-Dmaven.repo.local=") { + i += 1; + } else { + break; + } + prefixes.push(i); + } + prefixes +} + +fn generated_maven_goal(config: &RunConfig) -> bool { + config.id.starts_with("maven:goal:") && config.extras.params.is_empty() +} + +fn maven_goal_prefix(config: &RunConfig) -> Option { + let (_, goals) = config.id.split_once(".xml:")?; + maven_settings_prefixes(&config.args) + .into_iter() + .find(|&start| (start + 1..=config.args.len()).any(|end| config.args[start..end].join(" ") == goals)) +} + +/// Applies the current Maven settings at launch, including temporary configurations +/// made before build-tool defaults stopped being stored in their command and arguments. +pub fn prepare_maven_run(config: &mut RunConfig, root: &Path, s: &Settings) { + let mut detected = config.detected; + if generated_maven_goal(config) { + // The id records the user's original goals; only the settings prefix before them is derived. + if let Some(prefix) = maven_goal_prefix(config) { + config.args.drain(..prefix); + } + let name = config.command.rsplit(['/', '\\']).next().unwrap_or_default().to_ascii_lowercase(); + if !matches!(name.as_str(), "mvnw" | "mvnw.cmd" | "mvnw.bat") { + config.command = "mvn".into(); + if maven_command(root, s).is_none() { + let cwd = config.cwd.as_ref().map_or_else(|| root.to_path_buf(), |dir| root.join(dir.as_ref())); + for dir in cwd.ancestors().take_while(|dir| dir.starts_with(root)) { + let wrapper = dir.join(if cfg!(windows) { "mvnw.cmd" } else { "mvnw" }); + if wrapper.is_file() { + config.command = wrapper.to_string_lossy().into_owned(); + break; + } + } + } + } + } else if config.detected || config.id.starts_with("context:") { + let saved = ide_run::load_user_configs(root).unwrap_or_default(); + let id = config + .id + .strip_prefix("context:target:context:") + .or_else(|| config.id.strip_prefix("context:")) + .unwrap_or(&config.id); + let id = id.strip_prefix("workspace:").and_then(|id| id.split_once(':')).map_or(id, |(_, id)| id); + let source = ide_run::detect(root) + .into_iter() + .chain(saved.iter().cloned()) + .filter(|base| base.kind == "maven" && (id == base.id || id.starts_with(&format!("{}:", base.id)))) + .max_by_key(|base| base.id.len()) + .filter(|base| !saved.iter().any(|user| user.id == base.id)); + if let Some(base) = source.filter(|_| config.extras.params.is_empty()) { + if let Some(prefix) = maven_settings_prefixes(&config.args) + .into_iter() + .find(|&start| config.args[start..].starts_with(&base.args)) + { + config.args.drain(..prefix); + } + config.command = base.command; + } else { + detected = false; + } + } + let original = config.detected; + config.detected = detected; + apply_to_run_configs(std::slice::from_mut(config), root, s); + config.detected = original; +} + /// The Maven local repository from settings: the configured one, else /// `` of the configured user `settings.xml`; `None` = Maven's default. pub fn maven_local_repository(root: &Path, s: &Settings) -> Option { @@ -666,6 +748,9 @@ fn sets_maven_option(args: &[String], group: &[String]) -> bool { Some(flag) => { let key = flag.split_once('=').map_or(flag, |(k, _)| k); args.iter().any(|a| a.split_once('=').map_or(a.as_str(), |(k, _)| k) == key) + || args + .windows(2) + .any(|a| a[0] == "-D" && format!("-D{}", a[1].split('=').next().unwrap_or_default()) == key) } None => true, } @@ -815,11 +900,10 @@ pub fn apply_to_run_configs(configs: &mut [RunConfig], root: &Path, s: &Settings c.env.entry("JAVA_HOME".into()).or_insert_with(|| jdk.home.to_string_lossy().into_owned()); } if c.kind == "maven" { - // A configuration made or saved in the UI names the literal `mvn` / - // `mvn.cmd`; without a wrapper it cannot start when Maven lives on a - // version manager's shims or a configured home (#328). if let Some(mvn) = &mvn - && (c.detected || matches!(c.command.as_str(), "mvn" | "mvn.cmd" | "mvn.bat")) + && (c.detected + || generated_maven_goal(c) + || matches!(c.command.as_str(), "mvn" | "mvn.cmd" | "mvn.bat")) { c.command = mvn.to_string_lossy().into_owned(); } @@ -870,9 +954,15 @@ pub fn without_derived(config: &RunConfig, root: &Path, s: &Settings) -> RunConf if out.kind == "maven" { let options = maven_options(root, s); let mut probe = out.clone(); + let mut prefix = generated_maven_goal(&out).then(|| maven_goal_prefix(&out)).flatten(); for group in maven_option_groups(&options) { - if let Some(at) = probe.args.windows(group.len()).position(|w| w == group) { + if let Some(at) = probe.args.windows(group.len()).position(|w| w == group) + && prefix.is_none_or(|limit| at + group.len() <= limit) + { probe.args.drain(at..at + group.len()); + if let Some(limit) = &mut prefix { + *limit -= group.len(); + } } } if applied(&probe).args == out.args { diff --git a/crates/ide-sdk/src/discover.rs b/crates/ide-sdk/src/discover.rs index 92b58a23..c0de0540 100644 --- a/crates/ide-sdk/src/discover.rs +++ b/crates/ide-sdk/src/discover.rs @@ -545,7 +545,7 @@ fn jdk() -> Vec { /// A JDK as the SDK pickers list it (`Eclipse Adoptium 25.0.1`). pub fn jdk_sdk(j: &crate::java::Jdk) -> Sdk { - let java_home = std::env::var_os("JAVA_HOME").and_then(|h| dunce::canonicalize(h).ok()); + let java_home = std::env::var_os("JAVA_HOME").and_then(|h| crate::launch_path(Path::new(&h)).ok()); 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") { @@ -595,7 +595,7 @@ fn maven() -> Vec { } else { which("mvn") }; - if let Some(mvn) = on_path.and_then(|p| dunce::canonicalize(p).ok()) + if let Some(mvn) = on_path.and_then(|p| crate::launch_path(&p).ok()) && let Some(home) = mvn.parent().and_then(Path::parent) { found.push((home.to_path_buf(), "path")); @@ -629,7 +629,7 @@ fn maven() -> Vec { 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); + let home = crate::launch_path(&home).unwrap_or(home); if !seen.insert(home.clone()) { continue; } @@ -645,3 +645,84 @@ fn maven() -> Vec { } out } + +#[cfg(all(test, windows))] +mod maven_path_tests { + use super::*; + use crate::launch_path; + use std::process::Command; + + struct Subst { + name: String, + root: PathBuf, + } + + impl Subst { + fn new(target: &Path) -> Self { + for letter in (b'D'..=b'Z').rev() { + let name = format!("{}:", char::from(letter)); + let root = PathBuf::from(format!("{name}\\")); + if root.exists() { + continue; + } + let output = Command::new("subst").arg(&name).arg(target).output().unwrap(); + if output.status.success() { + return Self { name, root }; + } + } + panic!("no free drive for Maven path verification"); + } + } + + impl Drop for Subst { + fn drop(&mut self) { + let _ = Command::new("subst").args([&self.name, "/D"]).output(); + } + } + + fn install(home: &Path) { + std::fs::create_dir_all(home.join("bin")).unwrap(); + std::fs::create_dir_all(home.join("lib")).unwrap(); + std::fs::write(home.join("bin/mvn.cmd"), "@echo off\n").unwrap(); + std::fs::write(home.join("lib/maven-core-3.9.16.jar"), "").unwrap(); + } + + #[test] + fn mapped_maven_home_and_launcher_keep_their_drive() { + let mapped = tempfile::tempdir().unwrap(); + install(&mapped.path().join("apache-maven")); + let drive = Subst::new(mapped.path()); + let home = drive.root.join("apache-maven"); + let launcher = home.join("bin/mvn.cmd"); + assert_ne!(dunce::canonicalize(&home).unwrap(), home); + assert_eq!(launch_path(&home).unwrap(), home); + assert_eq!(launch_path(&launcher).unwrap(), launcher); + assert_eq!(maven_home(&home), Some(home.clone())); + assert_eq!(maven_version(&home).as_deref(), Some("3.9.16")); + let jdk = drive.root.join("jdk"); + std::fs::create_dir_all(jdk.join("bin")).unwrap(); + std::fs::write(jdk.join("bin/java.exe"), "").unwrap(); + std::fs::write(jdk.join("release"), "JAVA_VERSION=\"17.0.9\"\n").unwrap(); + assert_eq!(crate::java::jdk_at(&jdk).unwrap().home, jdk); + } + + #[test] + fn a_launcher_link_outside_the_mapped_root_still_finds_its_install() { + let mapped = tempfile::tempdir().unwrap(); + let real = tempfile::tempdir().unwrap(); + install(real.path()); + let drive = Subst::new(mapped.path()); + let link = mapped.path().join("shims"); + let output = Command::new(std::env::var_os("COMSPEC").unwrap_or_else(|| "cmd.exe".into())) + .args(["/D", "/C", "mklink", "/J"]) + .arg(&link) + .arg(real.path().join("bin")) + .output() + .unwrap(); + assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + let launcher = launch_path(&drive.root.join("shims/mvn.cmd")).unwrap(); + assert_eq!(launcher, dunce::canonicalize(real.path().join("bin/mvn.cmd")).unwrap()); + let home = launcher.parent().and_then(Path::parent).and_then(maven_home).unwrap(); + assert_eq!(home, dunce::canonicalize(real.path()).unwrap()); + } +} diff --git a/crates/ide-sdk/src/java.rs b/crates/ide-sdk/src/java.rs index a2f60063..05f89fc9 100644 --- a/crates/ide-sdk/src/java.rs +++ b/crates/ide-sdk/src/java.rs @@ -38,10 +38,7 @@ pub fn jdk_at(home: &Path) -> Option { if !home.join("bin").join(crate::exe("java")).is_file() { return None; } - Some(Jdk { - major: parse_release(&release)?, - home: dunce::canonicalize(home).unwrap_or_else(|_| home.to_path_buf()), - }) + Some(Jdk { major: parse_release(&release)?, home: crate::launch_path(home).unwrap_or_else(|_| home.to_path_buf()) }) } fn children(dir: &Path) -> Vec { @@ -90,7 +87,7 @@ fn scan() -> Vec { let path = std::env::var_os("PATH").unwrap_or_default(); if let Some(java) = std::env::split_paths(&path).map(|d| d.join(crate::exe("java"))).find(|j| j.is_file()) { // Through symlinks (`/usr/bin/java`, Homebrew, SDKMAN `current`) to `/bin/java`. - if let Some(home) = dunce::canonicalize(java).ok().and_then(|j| j.parent()?.parent().map(Path::to_path_buf)) { + if let Some(home) = crate::launch_path(&java).ok().and_then(|j| j.parent()?.parent().map(Path::to_path_buf)) { homes.push(home); } } diff --git a/crates/ide-sdk/src/lib.rs b/crates/ide-sdk/src/lib.rs index ef4efaf6..ac8c34eb 100644 --- a/crates/ide-sdk/src/lib.rs +++ b/crates/ide-sdk/src/lib.rs @@ -82,6 +82,36 @@ pub(crate) fn exe(name: &str) -> String { if cfg!(windows) { format!("{name}.exe") } else { name.to_owned() } } +/// `path` with symlinks resolved (Homebrew's `bin/mvn` → its Cellar), except that a Windows +/// mapped drive letter is kept when the target stays inside that drive: canonicalizing gives +/// `\\?\UNC\server\share\…` or another drive, and cmd.exe can't start a script from a +/// `\\?\` path (`mvn.cmd`, or `mvn.cmd` with such a `JAVA_HOME`), nor is it the path the user knows. +pub(crate) fn launch_path(path: &Path) -> std::io::Result { + let real = dunce::canonicalize(path)?; + #[cfg(windows)] + { + use std::path::{Component, Prefix}; + let drive = |p: &Path| match p.components().next() { + Some(Component::Prefix(x)) => match x.kind() { + Prefix::Disk(d) | Prefix::VerbatimDisk(d) => Some(d.to_ascii_uppercase()), + _ => None, + }, + _ => None, + }; + if let Some(d) = drive(path) + && drive(&real) != Some(d) + { + let root = PathBuf::from(format!("{}:\\", char::from(d))); + if let Ok(mapped) = dunce::canonicalize(&root) + && let Ok(relative) = real.strip_prefix(mapped) + { + return Ok(root.join(relative)); + } + } + } + Ok(real) +} + pub(crate) fn home_dir() -> Option { std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")).map(PathBuf::from) } diff --git a/crates/ide-sdk/src/tests.rs b/crates/ide-sdk/src/tests.rs index af8bae61..4a61ad1d 100644 --- a/crates/ide-sdk/src/tests.rs +++ b/crates/ide-sdk/src/tests.rs @@ -284,6 +284,129 @@ fn saved_maven_goal_configs_take_the_configured_launcher() { assert_eq!(configs[2].command, "./mvnw"); } +#[test] +fn maven_launch_refreshes_legacy_defaults_but_keeps_user_overrides() { + use crate::apply::prepare_maven_run; + + let tmp = tempfile::tempdir().unwrap(); + std::fs::write(tmp.path().join("pom.xml"), "").unwrap(); + let home = tmp.path().join("new-maven"); + std::fs::create_dir_all(home.join("bin")).unwrap(); + let launcher = home.join("bin").join(if cfg!(windows) { "mvn.cmd" } else { "mvn" }); + std::fs::write(&launcher, "").unwrap(); + let mut s = Settings::default(); + s.languages.java.maven.home = home.to_string_lossy().into_owned(); + s.languages.java.maven.user_settings = "new-settings.xml".into(); + s.languages.java.maven.local_repository = "new-repo".into(); + let options = maven_options(tmp.path(), &s); + + for id in [ + "maven:goal:pom.xml:test", + "maven:test", + "context:maven:test:src/test/java/ExampleTest.java", + "context:target:context:maven:test:src/test/java/ExampleTest.java:ExampleTest#example", + "workspace:abc:maven:test", + "context:target:context:workspace:abc:maven:test:src/test/java/ExampleTest.java:ExampleTest#example", + ] { + let mut config = run( + "maven", + "//?/UNC/server/old-maven/bin/mvn.cmd", + &["-s", "old-settings.xml", "-Dmaven.repo.local=old-repo", "test", "-Dtest=ExampleTest#example"], + ); + config.id = id.into(); + config.detected = !id.starts_with("maven:goal:"); + let original = config.clone(); + prepare_maven_run(&mut config, tmp.path(), &s); + assert_eq!(config.command, launcher.to_string_lossy(), "{id}"); + assert_eq!( + config.args, + [options.clone(), vec!["test".into(), "-Dtest=ExampleTest#example".into()]].concat(), + "{id}" + ); + // Rerunning the stored configuration reads settings again, without changing the stored input. + s.languages.java.maven.local_repository = "other-repo".into(); + let mut rerun = original; + prepare_maven_run(&mut rerun, tmp.path(), &s); + assert!(rerun.args.contains(&format!("-Dmaven.repo.local={}", tmp.path().join("other-repo").display()))); + s.languages.java.maven.local_repository = "new-repo".into(); + } + + for args in [ + vec!["-s", "own-settings.xml", "-Dmaven.repo.local=own-repo", "test"], + vec!["--settings=own-settings.xml", "-D", "maven.repo.local=own-repo", "test"], + ] { + let mut config = run("maven", "mvn", &args); + config.detected = false; + prepare_maven_run(&mut config, tmp.path(), &s); + assert_eq!(config.command, launcher.to_string_lossy()); + assert_eq!(config.args, args); + } + + let mut explicit = run("maven", "/custom/mvn", &["-s", "own-settings.xml", "-Dmaven.repo.local=own-repo", "test"]); + explicit.detected = false; + prepare_maven_run(&mut explicit, tmp.path(), &s); + assert_eq!(explicit.command, "/custom/mvn"); + assert_eq!(explicit.args, ["-s", "own-settings.xml", "-Dmaven.repo.local=own-repo", "test"]); + + // A gutter configuration derived from a user-edited default must keep that user's choices. + let mut saved = explicit.clone(); + saved.id = "maven:test".into(); + ide_run::save_user_configs(tmp.path(), &[saved]).unwrap(); + explicit.id = "context:maven:test:src/test/java/ExampleTest.java".into(); + explicit.detected = true; + prepare_maven_run(&mut explicit, tmp.path(), &s); + assert_eq!(explicit.command, "/custom/mvn"); + assert_eq!(explicit.args, ["-s", "own-settings.xml", "-Dmaven.repo.local=own-repo", "test"]); + + std::fs::create_dir(tmp.path().join("child")).unwrap(); + std::fs::write(tmp.path().join("child/pom.xml"), "").unwrap(); + let mut nested = explicit.clone(); + nested.id = "maven:test:child".into(); + nested.detected = false; + ide_run::save_user_configs(tmp.path(), &[nested]).unwrap(); + explicit.id = "context:maven:test:child:src/test/java/ExampleTest.java".into(); + prepare_maven_run(&mut explicit, tmp.path(), &s); + assert_eq!(explicit.command, "/custom/mvn", "an edited child must not fall back to the parent's defaults"); + assert_eq!(explicit.args, ["-s", "own-settings.xml", "-Dmaven.repo.local=own-repo", "test"]); +} + +#[test] +fn maven_goal_keeps_explicit_settings_and_nested_wrapper() { + use crate::apply::prepare_maven_run; + + let tmp = tempfile::tempdir().unwrap(); + std::fs::create_dir(tmp.path().join("child")).unwrap(); + let wrapper = tmp.path().join("child").join(if cfg!(windows) { "mvnw.cmd" } else { "mvnw" }); + std::fs::write(&wrapper, "").unwrap(); + let mut config = run("maven", "old-maven", &["-s", "old-settings.xml", "-s", "own-settings.xml", "test"]); + config.id = "maven:goal:child/pom.xml:-s own-settings.xml test".into(); + config.cwd = Some("child".into()); + config.detected = false; + let mut s = Settings::default(); + s.languages.java.maven.user_settings = "new-settings.xml".into(); + prepare_maven_run(&mut config, tmp.path(), &s); + assert_eq!(config.command, wrapper.to_string_lossy()); + assert_eq!(config.args, ["-s", "own-settings.xml", "test"]); +} + +#[test] +fn persisting_a_maven_goal_keeps_explicit_options_equal_to_the_defaults() { + let tmp = tempfile::tempdir().unwrap(); + let mut s = Settings::default(); + s.languages.java.maven.user_settings = tmp.path().join("settings.xml").to_string_lossy().into_owned(); + s.languages.java.maven.local_repository = tmp.path().join("repo").to_string_lossy().into_owned(); + let options = maven_options(tmp.path(), &s); + for goals in [vec![options[2].clone(), "test".into()], [options.clone(), vec!["test".into()]].concat()] { + let mut config = run("maven", "mvn", &[]); + config.id = format!("maven:goal:pom.xml:{}", goals.join(" ")); + config.detected = false; + config.args = goals.clone(); + apply_to_run_configs(std::slice::from_mut(&mut config), tmp.path(), &s); + let kept = without_derived(&config, tmp.path(), &s); + assert_eq!(kept.args, goals); + } +} + #[test] fn language_server_settings_disable_add_and_override() { use crate::apply::{language_servers, server_launch_overrides, server_protocol_overrides, with_custom_servers}; diff --git a/src/features/build-tools/model.test.ts b/src/features/build-tools/model.test.ts index 07fedfb8..40754abc 100644 --- a/src/features/build-tools/model.test.ts +++ b/src/features/build-tools/model.test.ts @@ -210,11 +210,25 @@ describe("Maven runs", () => { }); expect(root.cwd).toBeNull(); expect(root.isTest).toBe(false); - // Settings › Maven options go before the goals. - const configured = { ...core, options: ["-s", "/m2/settings.xml"] }; + const configured = { + ...core, + command: "//?/UNC/server/share/maven/bin/mvn.cmd", + options: ["-s", "/old/settings.xml", "-Dmaven.repo.local=/old/repository"], + }; + const goal = mavenGoalConfig(configured, ["test"], maven, { + offline: false, + skipTests: true, + profiles: {}, + }); + expect(goal.command).toBe("mvn"); + expect(goal.args).toEqual(["test", "-DskipTests"]); expect( - mavenGoalConfig(configured, ["test"], maven, { offline: false, skipTests: true, profiles: {} }).args, - ).toEqual(["-s", "/m2/settings.xml", "test", "-DskipTests"]); + mavenGoalConfig(configured, ["-s", "/own/settings.xml", "test"], maven, { + offline: false, + skipTests: false, + profiles: {}, + }).args, + ).toEqual(["-s", "/own/settings.xml", "test"]); }); it("parses typed command lines", () => { diff --git a/src/features/build-tools/model.ts b/src/features/build-tools/model.ts index 51957d28..c448ee94 100644 --- a/src/features/build-tools/model.ts +++ b/src/features/build-tools/model.ts @@ -426,12 +426,12 @@ export function mavenGoalConfig( model: Pick, options: MavenRunOptions, ): RunConfig { - const args = [...project.options, ...mavenArgs(goals, model, options)]; + const args = mavenArgs(goals, model, options); return { id: `maven:goal:${project.pom}:${goals.join(" ")}`, name: mavenConfigName(project, goals), kind: "maven", - command: project.command, + command: /(?:^|[/\\])mvnw(?:\.cmd|\.bat)?$/i.test(project.command) ? project.command : "mvn", args, cwd: project.dir || null, env: { ...project.env },