fix(maven): resolve current toolchain settings for each run

Цей коміт міститься в:
l0ng-ai committed 2026-11-04 11:41:59 +08:00
1 джерело 30c464f024
коміт 821ff73e1f
6 files changed
+294 -24

No files matched your search

+5 -3
Переглянути файл
@@ -41,7 +41,7 @@ pub fn configs(host: &Host, window: &str) -> IdeResult<Vec<RunConfig>> {
}
/// 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
@@ -111,7 +111,9 @@ pub async fn start(host: &Host, window: &str, config: RunConfig, on_output: Emit
};
// 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);
+101 -15
Переглянути файл
@@ -192,6 +192,100 @@ pub fn maven_options(root: &Path, s: &Settings) -> Vec<String> {
out
}
fn maven_settings_prefixes(args: &[String]) -> Vec<usize> {
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()
}
/// 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((_, goals)) = config.id.split_once(".xml:")
&& let Some(prefix) = maven_settings_prefixes(&config.args)
.into_iter()
.find(|&start| (start + 1..=config.args.len()).any(|end| config.args[start..end].join(" ") == goals))
{
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 source = ide_run::detect(root).into_iter().find(|base| {
base.kind == "maven"
&& (config.id == base.id
|| config.id.starts_with(&format!("context:{}:", base.id))
|| config.id.starts_with(&format!("context:target:context:{}:", base.id)))
&& !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;
}
fn maven_launch_options(defaults: &[String], args: &[String]) -> Vec<String> {
let own_settings = args.iter().any(|a| matches!(a.as_str(), "-s" | "--settings") || a.starts_with("--settings="));
let own_repository = args.iter().any(|a| a.starts_with("-Dmaven.repo.local="))
|| args.windows(2).any(|a| a[0] == "-D" && a[1].starts_with("maven.repo.local="));
let mut out = Vec::new();
let mut defaults = defaults.iter();
while let Some(arg) = defaults.next() {
if arg == "-s" {
if let Some(value) = defaults.next()
&& !own_settings
{
out.extend([arg.clone(), value.clone()]);
}
} else if !own_repository {
out.push(arg.clone());
}
}
out
}
/// The Maven local repository from settings: the configured one, else
/// `<localRepository>` of the configured user `settings.xml`; `None` = Maven's default.
pub fn maven_local_repository(root: &Path, s: &Settings) -> Option<PathBuf> {
@@ -783,24 +877,16 @@ pub fn apply_to_run_configs(configs: &mut [RunConfig], root: &Path, s: &Settings
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 c.kind == "maven"
&& (c.detected
|| generated_maven_goal(c)
|| matches!(c.command.as_str(), "mvn" | "mvn.cmd" | "mvn.bat"))
{
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());
}
} else if c.kind == "maven"
&& matches!(c.command.as_str(), "mvn" | "mvn.cmd" | "mvn.bat")
&& let Some(mvn) = &mvn
{
// A goal configuration saved from the tool window keeps the
// literal `mvn` / `mvn.cmd` the launcher resolved to; without a
// wrapper it cannot start when Maven lives on a version
// manager's shims or a configured home (#328). Detected configs
// take the Maven options too, but the saved ones already baked
// them into `args`.
c.command = mvn.to_string_lossy().into_owned();
let options = maven_launch_options(&mvn_options, &c.args);
c.args.splice(0..0, options);
}
}
_ => {}
+76
Переглянути файл
@@ -645,3 +645,79 @@ fn maven() -> Vec<Sdk> {
}
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"));
}
#[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());
}
}
+92
Переглянути файл
@@ -284,6 +284,98 @@ 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"), "<project/>").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",
] {
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", "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", "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", "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 language_server_settings_disable_add_and_override() {
use crate::apply::{language_servers, server_launch_overrides, server_protocol_overrides, with_custom_servers};
+18 -4
Переглянути файл
@@ -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", () => {
+2 -2
Переглянути файл
@@ -426,12 +426,12 @@ export function mavenGoalConfig(
model: Pick<MavenModel, "profiles">,
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 },