From 53bebe4836dd25b3fe2ca39f86d884cc7c9b5fa3 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sat, 31 Oct 2026 12:08:27 +0800 Subject: [PATCH] fix(tomcat): resolve the WAR artifact of the selected Maven module (#529) A Tomcat run with a WAR from a nested Maven module failed with "The built WAR artifact does not exist": the artifact path query was evaluated for the reactor parent, not the selected module. The build now resolves the selected POM's effective identity first, using the same options and environment as the build, and asks for that project's artifact. Custom outputs, .mvn settings and root modules are covered, Gradle WAR manifests are written as UTF-8, and Tomcat deployment errors now name the checked path. Fixes #515 --- Cargo.lock | 1 + crates/ide-build/src/process.rs | 17 ++- crates/ide-build/src/web.rs | 112 ++++++++++++++++-- crates/ide-host/src/ops/run/commands.rs | 24 ++-- crates/ide-run/Cargo.toml | 1 + crates/ide-run/src/lib.rs | 5 + crates/ide-run/src/process.rs | 12 ++ crates/ide-run/src/tomcat.rs | 31 ++++- crates/ide-run/tests/process.rs | 5 + crates/ide-run/tests/tomcat_artifact.rs | 150 ++++++++++++++++++++++++ src/platform/locales/text/zh.json | 5 +- 11 files changed, 340 insertions(+), 23 deletions(-) create mode 100644 crates/ide-run/tests/tomcat_artifact.rs diff --git a/Cargo.lock b/Cargo.lock index 9b16ceaa..94b70892 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4461,6 +4461,7 @@ name = "ide-run" version = "0.1.29" dependencies = [ "encoding_rs", + "ide-build", "ide-core", "libc", "parking_lot", diff --git a/crates/ide-build/src/process.rs b/crates/ide-build/src/process.rs index d0335def..b598eb79 100644 --- a/crates/ide-build/src/process.rs +++ b/crates/ide-build/src/process.rs @@ -58,6 +58,18 @@ pub(crate) fn run( cwd: &Path, env: &BTreeMap, cancel: &CancellationToken, + on_line: impl FnMut(Stream, &str), +) -> IdeResult { + run_with_env(program, args, cwd, env, false, cancel, on_line) +} + +pub(crate) fn run_with_env( + program: &str, + args: &[String], + cwd: &Path, + env: &BTreeMap, + clear_env: bool, + cancel: &CancellationToken, mut on_line: impl FnMut(Stream, &str), ) -> IdeResult { tracing::info!("build: {program} {} (in {})", args.join(" "), cwd.display()); @@ -66,7 +78,7 @@ pub(crate) fn run( ide_core::output_encoding::fix(program, args, env, |k| std::env::var(k).ok(), "UTF-8", cfg!(windows)); let program = program_path(program, env); let mut command = Command::new(&program); - command.args(args).current_dir(cwd).envs(env).envs(encoding.env).stdin(Stdio::null()); + command.args(args).current_dir(cwd).envs(env).envs(encoding.env.iter().map(|(k, v)| (k, v))).stdin(Stdio::null()); command.stdout(Stdio::piped()).stderr(Stdio::piped()); #[cfg(unix)] { @@ -74,6 +86,9 @@ pub(crate) fn run( command.process_group(0); } ide_core::process::prepare(&mut command); + if clear_env { + command.env_clear().envs(env).envs(encoding.env); + } let mut child = command.spawn().map_err(|e| IdeError::unavailable(format!("cannot start {}: {e}", program.display())))?; let (tx, rx) = mpsc::channel::<(Stream, String)>(); diff --git a/crates/ide-build/src/web.rs b/crates/ide-build/src/web.rs index c918bb8e..9f25039a 100644 --- a/crates/ide-build/src/web.rs +++ b/crates/ide-build/src/web.rs @@ -1,6 +1,6 @@ //! WAR modules and build-tool-owned archive resolution (including custom output names). use crate::BuildModel; -use ide_core::{IdeError, IdeResult}; +use ide_core::{CancellationToken, IdeError, IdeResult}; use serde::{Deserialize, Serialize}; use specta::Type; use std::{collections::BTreeMap, fs, path::Path}; @@ -47,6 +47,50 @@ pub struct WebBuild { pub args: Vec, pub cwd: String, pub env: BTreeMap, + maven_project: Option<(Vec, std::path::PathBuf)>, +} + +impl WebBuild { + /// Resolve the selected module's effective identity using the build's options and environment. + /// Runs off the UI thread, before launching the reactor build. + pub fn resolve_project( + &mut self, + root: &Path, + env: &BTreeMap, + cancel: &CancellationToken, + ) -> IdeResult<()> { + let Some((args, output)) = &self.maven_project else { + return Ok(()); + }; + if output.exists() { + fs::remove_file(output)?; + } + let cwd = root.join(&self.cwd).canonicalize()?; + let mut env = env.clone(); + if !env.iter().any(|(k, v)| k.eq_ignore_ascii_case("MAVEN_BASEDIR") && !v.is_empty()) { + // -f changes Maven's .mvn search origin; keep the reactor's launcher settings. + let base = cwd.ancestors().find(|p| p.join(".mvn").is_dir()).unwrap_or(&cwd); + env.retain(|k, _| !k.eq_ignore_ascii_case("MAVEN_BASEDIR")); + env.insert("MAVEN_BASEDIR".into(), base.to_string_lossy().into_owned()); + } + let result = crate::process::run_with_env(&self.command, args, &cwd, &env, true, cancel, |_, _| {})?; + if !result.success { + return Err(IdeError::invalid(format!("Cannot resolve Maven WAR artifact: {}", result.text.trim()))); + } + let id = fs::read_to_string(output)?; + let parts: Vec<_> = id.trim().split(':').collect(); + if parts.len() != 4 || parts[2] != "war" || parts.iter().any(|s| s.is_empty()) { + return Err(IdeError::invalid(format!("Cannot resolve Maven WAR artifact: {}", id.trim()))); + } + let project = format!("{}:{}:{}", parts[0], parts[1], parts[3]); + self.args.push(format!("-Dexpression=session.projectMap({project}).artifact.file")); + self.maven_project = None; + Ok(()) + } +} + +fn groovy_literal(s: &str) -> String { + format!("'{}'", s.replace('\\', "\\\\").replace('\'', "\\'")) } pub fn build(root: &Path, model: &BuildModel, id: &str, cache: &Path) -> IdeResult { @@ -81,21 +125,36 @@ pub fn build(root: &Path, model: &BuildModel, id: &str, cache: &Path) -> IdeResu .replace('\\', "/"); args.extend(["-pl".into(), module, "-am".into()]); } + // help:evaluate runs once, in the reactor's first project. Query the selected + // POM's effective identity first; profiles and -D options can change its GAV. + let output = cache.join("artifact-project.txt"); + let mut probe = top.options.clone(); + probe.extend([ + "-f".into(), + root.join(&p.pom).to_string_lossy().into_owned(), + "help:evaluate".into(), + "-Dexpression=project.id".into(), + format!("-Doutput={}", output.display()), + ]); args.extend([ "-DskipTests".into(), "package".into(), "help:evaluate".into(), - "-Dexpression=project.artifact.file".into(), format!("-Doutput={}", manifest.display()), ]); - return Ok(WebBuild { command: top.command.clone(), args, cwd: top.dir.clone(), env: p.env.clone() }); + return Ok(WebBuild { + command: top.command.clone(), + args, + cwd: top.dir.clone(), + env: p.env.clone(), + maven_project: Some((probe, output)), + }); } if let Some(path) = id.strip_prefix("gradle:") { let g = model.gradle.as_ref().ok_or_else(|| IdeError::invalid("Choose a Gradle WAR module"))?; if !artifacts(root, model).iter().any(|a| a.id == id) { return Err(IdeError::invalid("Choose a Gradle WAR module")); } - let literal = |s: &str| format!("'{}'", s.replace('\\', "\\\\").replace('\'', "\\'")); let script = cache.join("war.init.gradle"); fs::write( &script, @@ -105,12 +164,12 @@ pub fn build(root: &Path, model: &BuildModel, id: &str, cache: &Path) -> IdeResu def warTask = p.tasks.named('war') p.tasks.register('openideaTomcatArtifact') {{ dependsOn warTask - doLast {{ new File({}).text = warTask.get().archiveFile.get().asFile.absolutePath }} + doLast {{ new File({}).setText(warTask.get().archiveFile.get().asFile.absolutePath, 'UTF-8') }} }} }} "#, - literal(path), - literal(&manifest.to_string_lossy()) + groovy_literal(path), + groovy_literal(&manifest.to_string_lossy()) ), )?; let task = format!("{}:openideaTomcatArtifact", path.trim_end_matches(':')); @@ -119,6 +178,7 @@ pub fn build(root: &Path, model: &BuildModel, id: &str, cache: &Path) -> IdeResu args: vec!["--init-script".into(), script.to_string_lossy().into_owned(), task], cwd: g.dir.clone(), env: g.env.clone(), + maven_project: None, }); } Err(IdeError::invalid("Choose a WAR artifact")) @@ -136,7 +196,9 @@ mod tests { assert_eq!(a.len(), 1); assert_eq!(a[0].name, "web"); let plan = build(tmp.path(), &model, &a[0].id, &tmp.path().join("base with spaces")).unwrap(); - assert!(plan.args.contains(&"-Dexpression=project.artifact.file".into())); + let (probe, _) = plan.maven_project.as_ref().unwrap(); + assert!(probe.contains(&"-Dexpression=project.id".into())); + assert!(probe.contains(&tmp.path().join("pom.xml").to_string_lossy().into_owned())); assert!( plan.args.iter().any(|a| a.ends_with("base with spaces/artifact-path.txt") || a.ends_with("base with spaces\\artifact-path.txt")) @@ -152,6 +214,13 @@ mod tests { let plan = build(tmp.path(), &model, "maven:web/pom.xml", &tmp.path().join("base")).unwrap(); assert_eq!(plan.cwd, ""); assert_eq!(&plan.args[..3], &["-pl", "web", "-am"]); + assert!( + plan.maven_project + .as_ref() + .unwrap() + .0 + .contains(&tmp.path().join("web/pom.xml").to_string_lossy().into_owned()) + ); } #[test] fn gradle_war_uses_archive_provider_and_ignores_comments() { @@ -169,4 +238,31 @@ mod tests { assert!(fs::read_to_string(tmp.path().join("base/war.init.gradle")).unwrap().contains("archiveFile.get()")); assert!(!crate::gradle::has_war_plugin("// plugins { war }")); } + + #[test] + fn gradle_manifest_paths_preserve_windows_characters() { + for path in [r"C:\工作 区\Tomcat's cache\artifact-path.txt", r"\\server\share\artifact-path.txt"] { + let literal = groovy_literal(path); + assert_eq!(literal[1..literal.len() - 1].replace("\\'", "'").replace("\\\\", "\\"), path); + } + } + + #[test] + fn gradle_root_and_nested_modules_write_utf8_archive_paths() { + for module in [":", ":apps:timesheet"] { + let tmp = tempfile::tempdir().unwrap(); + let dir = if module == ":" { "" } else { "apps/timesheet" }; + fs::create_dir_all(tmp.path().join(dir)).unwrap(); + fs::write(tmp.path().join("settings.gradle"), "rootProject.name = 'timesheet'\ninclude 'apps:timesheet'") + .unwrap(); + fs::write(tmp.path().join(dir).join("build.gradle"), "plugins { id 'war' }").unwrap(); + let model = crate::load(tmp.path(), &tmp.path().join("cache"), None); + let cache = tmp.path().join("工作 cache"); + let plan = build(tmp.path(), &model, &format!("gradle:{module}"), &cache).unwrap(); + assert!(plan.args.contains(&format!("{}:openideaTomcatArtifact", module.trim_end_matches(':')))); + let script = fs::read_to_string(cache.join("war.init.gradle")).unwrap(); + assert!(script.contains(&format!("rootProject.project('{}')", module))); + assert!(script.contains("archiveFile.get().asFile.absolutePath, 'UTF-8'")); + } + } } diff --git a/crates/ide-host/src/ops/run/commands.rs b/crates/ide-host/src/ops/run/commands.rs index fdfc21af..86754386 100644 --- a/crates/ide-host/src/ops/run/commands.rs +++ b/crates/ide-host/src/ops/run/commands.rs @@ -81,22 +81,21 @@ pub async fn run_tomcat_build(host: &Host, window: &str, config: RunConfig) -> I .filter(|s| !s.is_empty()) .or_else(|| artifacts.first().map(|a| &a.id)) .ok_or_else(|| IdeError::invalid("Choose a WAR artifact"))?; - let plan = ide_build::web::build( + let mut plan = ide_build::web::build( &project.root, &model, id, &ide_run::tomcat::base_dir(&project.cache_dir, &config.id), )?; - let mut env = plan.env; - env.extend(config.env.clone()); - Ok(Some(RunConfig { + plan.env.extend(config.env.clone()); + let mut build = RunConfig { id: format!("tomcat-build:{}", config.id), name: format!("Build {}", config.name), kind: if id.starts_with("maven:") { "maven" } else { "gradle" }.into(), - command: plan.command, - args: plan.args, - cwd: Some(std::path::PathBuf::from(plan.cwd).into()), - env, + command: plan.command.clone(), + args: vec![], + cwd: Some(std::path::PathBuf::from(&plan.cwd).into()), + env: plan.env.clone(), detected: false, is_test: false, debug_adapter: None, @@ -106,7 +105,14 @@ pub async fn run_tomcat_build(host: &Host, window: &str, config: RunConfig) -> I env_files: config.extras.env_files, ..Default::default() }, - })) + }; + plan.resolve_project( + &project.root, + &ide_run::resolved_env(&project.root, &build)?, + &ide_core::CancellationToken::new(), + )?; + build.args = plan.args; + Ok(Some(build)) }) .await .map_err(|e| IdeError::internal(e.to_string()))? diff --git a/crates/ide-run/Cargo.toml b/crates/ide-run/Cargo.toml index 23df6603..076ad24e 100644 --- a/crates/ide-run/Cargo.toml +++ b/crates/ide-run/Cargo.toml @@ -24,4 +24,5 @@ quick-xml = "0.42" libc = "0.2" [dev-dependencies] +ide-build = { path = "../ide-build" } tempfile.workspace = true diff --git a/crates/ide-run/src/lib.rs b/crates/ide-run/src/lib.rs index b32ec8b5..0c6e746a 100644 --- a/crates/ide-run/src/lib.rs +++ b/crates/ide-run/src/lib.rs @@ -240,6 +240,11 @@ pub fn system_env() -> BTreeMap { ide_core::process::host_vars().filter_map(|(k, v)| Some((k.into_string().ok()?, v.into_string().ok()?))).collect() } +/// Complete environment for a preparatory tool belonging to this run configuration. +pub fn resolved_env(root: &Path, config: &RunConfig) -> IdeResult> { + process::resolved_env(root, config) +} + /// Path of the user run configuration file for `root`. pub fn user_configs_path(root: &Path) -> PathBuf { config::config_path(root) diff --git a/crates/ide-run/src/process.rs b/crates/ide-run/src/process.rs index 6fbf8b58..7a1f6a86 100644 --- a/crates/ide-run/src/process.rs +++ b/crates/ide-run/src/process.rs @@ -661,6 +661,18 @@ fn launch_env(root: &Path, config: &RunConfig) -> IdeResult { Ok(LaunchEnv { clear: !extras.include_system_env, vars }) } +pub(crate) fn resolved_env(root: &Path, config: &RunConfig) -> IdeResult> { + let env = launch_env(root, config)?; + let mut vars = if env.clear { BTreeMap::new() } else { crate::system_env() }; + if !env.clear { + vars.extend( + ide_core::process::shell_env().filter_map(|(k, v)| Some((k.into_string().ok()?, v.into_string().ok()?))), + ); + } + vars.extend(env.vars); + Ok(vars) +} + fn apply_env(cmd: &mut Command, env: &LaunchEnv) { if env.clear { cmd.env_clear(); diff --git a/crates/ide-run/src/tomcat.rs b/crates/ide-run/src/tomcat.rs index 0f452e0b..dffb377a 100644 --- a/crates/ide-run/src/tomcat.rs +++ b/crates/ide-run/src/tomcat.rs @@ -216,7 +216,7 @@ pub fn deployment(root: &Path, base: &Path, config: &RunConfig, resources_only: .map_err(|_| invalid("Build the WAR artifact before launching Tomcat"))?; let war = PathBuf::from(war.trim()); if !war.is_file() || !war.extension().is_some_and(|e| e.eq_ignore_ascii_case("war")) { - return Err(invalid("The built WAR artifact does not exist")); + return Err(invalid(format!("The built WAR artifact does not exist: {}", war.display()))); } let dest = base.join("deployment"); explode(&war, &dest, resources_only)?; @@ -227,11 +227,14 @@ pub fn deployment(root: &Path, base: &Path, config: &RunConfig, resources_only: .get("deploymentPath") .filter(|v| !v.trim().is_empty()) .ok_or_else(|| invalid("Choose a WAR file or exploded directory"))?; - let path = root.join(value).canonicalize()?; + let path = root.join(value); + let path = path + .canonicalize() + .map_err(|e| invalid(format!("Invalid Tomcat deployment: {} ({e})", path.display())))?; if (kind == "war" && (!path.is_file() || !path.extension().is_some_and(|e| e.eq_ignore_ascii_case("war")))) || (kind == "directory" && !path.is_dir()) { - return Err(invalid("Invalid Tomcat deployment")); + return Err(invalid(format!("Invalid Tomcat deployment: {}", path.display()))); } Ok(path) } @@ -436,6 +439,28 @@ mod tests { assert!(deployment(tmp.path(), &base, &c, false).unwrap().ends_with("WebContent")); } #[test] + fn deployment_errors_identify_the_checked_path() { + let tmp = tempfile::tempdir().unwrap(); + let base = tmp.path().join("cache"); + fs::create_dir_all(&base).unwrap(); + for path in ["missing.war", r"C:\工作\timesheet\target\timesheet.war", r"\\server\share\timesheet.war"] { + fs::write(base.join("artifact-path.txt"), format!("{path}\r\n")).unwrap(); + let error = deployment(tmp.path(), &base, &config(), false).unwrap_err().to_string(); + assert!(error.contains(path), "{error}"); + } + for kind in ["war", "directory"] { + let mut c = config(); + c.extras.params.insert("deployment".into(), kind.into()); + c.extras.params.insert("deploymentPath".into(), "missing".into()); + let error = deployment(tmp.path(), &base, &c, false).unwrap_err().to_string(); + assert!(error.contains(&tmp.path().join("missing").display().to_string()), "{error}"); + c.extras.params.insert("deploymentPath".into(), "cache".into()); + if kind == "war" { + assert!(deployment(tmp.path(), &base, &c, false).unwrap_err().to_string().contains("cache")); + } + } + } + #[test] fn prepares_isolated_base_and_jdwp_only_for_run() { let tmp = tempfile::tempdir().unwrap(); let home = tmp.path().join("Tomcat Home"); diff --git a/crates/ide-run/tests/process.rs b/crates/ide-run/tests/process.rs index de49fcbd..316a411f 100644 --- a/crates/ide-run/tests/process.rs +++ b/crates/ide-run/tests/process.rs @@ -291,6 +291,10 @@ fn env_files_system_env_and_redirects() { config.extras.include_system_env = false; config.extras.stdin_file = Some("input.txt".into()); config.extras.output_file = Some("logs/out.log".into()); + let prepared = ide_run::resolved_env(root, &config).unwrap(); + assert_eq!(prepared.get("FROM_FILE").map(String::as_str), Some("file")); + assert_eq!(prepared.get("OVERRIDDEN").map(String::as_str), Some("config")); + assert!(!prepared.contains_key("HOME")); let (id, chunks) = start(&h, &config); // stdin comes from the file: writing to it is refused. assert!(h.pm.write_stdin(id, "x").is_err()); @@ -302,6 +306,7 @@ fn env_files_system_env_and_redirects() { // With the system environment, HOME is inherited. config.extras.include_system_env = true; + assert!(ide_run::resolved_env(root, &config).unwrap().contains_key("HOME")); config.extras.stdin_file = None; config.extras.output_file = None; config.args = vec!["-c".into(), "printf '%s' \"${HOME:+set}\"".into()]; diff --git a/crates/ide-run/tests/tomcat_artifact.rs b/crates/ide-run/tests/tomcat_artifact.rs new file mode 100644 index 00000000..52c5045d --- /dev/null +++ b/crates/ide-run/tests/tomcat_artifact.rs @@ -0,0 +1,150 @@ +//! Opt-in build-tool integration checks: cargo test -p ide-run --test tomcat_artifact -- --ignored +use std::{fs, path::Path, process::Command}; + +fn write(root: &Path, path: &str, text: &str) { + let path = root.join(path); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(path, text).unwrap(); +} + +fn build_and_deploy(root: &Path, id: &str, expected: &Path) { + let cache = root.join("工作 cache"); + let model = ide_build::load(root, &cache, None); + let mut plan = ide_build::web::build(root, &model, id, &cache).unwrap(); + plan.resolve_project(root, &std::env::vars().collect(), &ide_core::CancellationToken::new()).unwrap(); + let output = Command::new(&plan.command) + .args(&plan.args) + .envs(&plan.env) + .current_dir(root.join(&plan.cwd)) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let manifest = fs::read_to_string(cache.join("artifact-path.txt")).unwrap(); + assert_eq!(Path::new(manifest.trim()).canonicalize().unwrap(), expected.canonicalize().unwrap()); + let mut config: ide_run::RunConfig = serde_json::from_value(serde_json::json!({ + "id": "tomcat:test", "name": "Tomcat", "kind": "tomcat", "command": "catalina", + "args": [], "env": {}, "detected": false, "isTest": false + })) + .unwrap(); + config.cwd = Some("timesheet".into()); + config.extras.params.insert("artifact".into(), id.into()); + let deployed = ide_run::tomcat::deployment(root, &cache, &config, false).unwrap(); + assert_eq!(fs::read_to_string(deployed.join("index.html")).unwrap(), "issue 515 timesheet"); + let siblings: Vec<_> = fs::read_dir(expected.parent().unwrap()) + .unwrap() + .map(|e| e.unwrap().file_name().to_string_lossy().into_owned()) + .filter(|name| name.contains(".war.")) + .collect(); + assert!(siblings.is_empty(), "Unexpected WAR siblings: {siblings:?}"); +} + +#[test] +#[ignore = "requires Maven and downloads Maven plugins"] +fn maven_packaged_artifacts_deploy_from_root_and_nested_reactors() { + for (reactor, module, custom) in + [("", "", false), ("", "timesheet", false), ("parent", "apps/timesheet", true), ("timesheet", "", true)] + { + let tmp = tempfile::Builder::new().prefix("tomcat maven ").tempdir().unwrap(); + let root = tmp.path(); + let reactor_dir = root.join(reactor); + let module_dir = reactor_dir.join(module); + let plugins = "maven-war-plugin3.4.0false"; + let parent = if module.is_empty() && custom { + write(&reactor_dir, ".mvn/maven.config", "-Drevision=2\n"); + "test${revision}1" + .to_owned() + } else if module.is_empty() { + "test1".to_owned() + } else { + write( + &reactor_dir, + "pom.xml", + &format!( + "4.0.0testparent1pomcore{module}{plugins}" + ), + ); + write( + &reactor_dir, + "core/pom.xml", + "4.0.0testparent1core", + ); + let relative = "../".repeat(module.split('/').count()); + format!( + "testparent1{relative}pom.xml" + ) + }; + let dependency = if module.is_empty() { + "" + } else { + "testcore1" + }; + let output = if custom { + "${project.basedir}/custom outputcustom-timesheet" + } else { + "timesheet" + }; + write( + &module_dir, + "pom.xml", + &format!( + "4.0.0{parent}timesheetwar{dependency}{plugins}{output}" + ), + ); + write(&module_dir, "src/main/webapp/index.html", "issue 515 timesheet"); + if custom && !module.is_empty() { + for dir in [&reactor_dir, &reactor_dir.join("core"), &module_dir] { + let pom = dir.join("pom.xml"); + let text = + fs::read_to_string(&pom).unwrap().replace("1", "${revision}"); + fs::write(pom, text).unwrap(); + } + let pom = reactor_dir.join("pom.xml"); + fs::write( + &pom, + fs::read_to_string(&pom).unwrap().replace( + "pom", + "pom1", + ), + ) + .unwrap(); + write(&reactor_dir, ".mvn/maven.config", "-Drevision=2\n"); + write(&module_dir, ".mvn/maven.config", "-Drevision=3\n"); + } + let pom = module_dir.join("pom.xml").strip_prefix(root).unwrap().to_string_lossy().replace('\\', "/"); + let expected = + module_dir.join(if custom { "custom output/custom-timesheet.war" } else { "target/timesheet.war" }); + build_and_deploy(root, &format!("maven:{pom}"), &expected); + } +} + +#[test] +#[ignore = "requires Gradle"] +fn gradle_packaged_artifacts_deploy_with_custom_archive_names() { + for module in ["", "apps/timesheet"] { + let tmp = tempfile::Builder::new().prefix("tomcat gradle ").tempdir().unwrap(); + let root = tmp.path(); + let settings = if module.is_empty() { + "rootProject.name = 'timesheet'\n" + } else { + "rootProject.name = 'timesheet'\ninclude 'apps:timesheet'\n" + }; + write(root, "settings.gradle", settings); + write( + &root.join(module), + "build.gradle", + "plugins { id 'war' }\ntasks.named('war') { archiveFileName = 'custom-timesheet.war'; destinationDirectory = layout.projectDirectory.dir('custom output') }\n", + ); + write(&root.join(module), "src/main/webapp/index.html", "issue 515 timesheet"); + let project = if module.is_empty() { ":" } else { ":apps:timesheet" }; + build_and_deploy( + root, + &format!("gradle:{project}"), + &root.join(module).join("custom output/custom-timesheet.war"), + ); + } +} diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json index 927270a0..e34a7191 100644 --- a/src/platform/locales/text/zh.json +++ b/src/platform/locales/text/zh.json @@ -4936,9 +4936,10 @@ "Choose a Gradle WAR module": "请选择 Gradle WAR 模块", "Build the selected WAR artifact before launching Tomcat": "请先构建所选 WAR 构件,再启动 Tomcat", "Build the WAR artifact before launching Tomcat": "请先构建 WAR 构件,再启动 Tomcat", - "The built WAR artifact does not exist": "构建的 WAR 构件不存在", + "The built WAR artifact does not exist: {path}": "构建的 WAR 构件不存在:{path}", + "Cannot resolve Maven WAR artifact: {details}": "无法解析 Maven WAR 构件:{details}", "Choose a WAR file or exploded directory": "请选择 WAR 文件或展开目录", - "Invalid Tomcat deployment": "无效的 Tomcat 部署", + "Invalid Tomcat deployment: {path}": "无效的 Tomcat 部署:{path}", "Unknown Tomcat deployment type": "未知的 Tomcat 部署类型", "Choose a Tomcat installation directory": "请选择 Tomcat 安装目录", "HTTP port must be between 1 and 65535": "HTTP 端口必须介于 1 和 65535 之间",