дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
fix(maven): resolve current toolchain settings for each run
Цей коміт міститься в:
1 джерело
30c464f024
коміт
821ff73e1f
6 files changed
+294
-24
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in new issue
Заблокувати користувача