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 之间",