дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
feat(tomcat): skip the before-launch build when the WAR is up to date (#2276)
* feat(tomcat): skip the before-launch build when the WAR is up to date * fix(i18n): remove obsolete Tomcat WAR build messages from catalogs
Цей коміт міститься в:
9 files changed
+164
-29
No files matched your search
@@ -218,12 +218,13 @@ 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 mut plan = ide_build::web::build(
|
||||
&project.root,
|
||||
&model,
|
||||
id,
|
||||
&ide_run::tomcat::effective_base_dir(&project.root, &project.cache_dir, &config)?,
|
||||
)?;
|
||||
let base = ide_run::tomcat::effective_base_dir(&project.root, &project.cache_dir, &config)?;
|
||||
// "Build before launch" is skipped when the built WAR is already newer than the
|
||||
// module's sources, so a repeated run does not rebuild (IDEA's up-to-date check).
|
||||
if ide_run::tomcat::artifact_is_current(&project.root, &base, &config) {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut plan = ide_build::web::build(&project.root, &model, id, &base)?;
|
||||
plan.env.extend(config.env.clone());
|
||||
let mut build = RunConfig {
|
||||
id: format!("tomcat-build:{}", config.id),
|
||||
|
||||
@@ -849,21 +849,115 @@ fn sync_tree(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A built `*.war` under a module's usual output directory (`target/`, `build/libs/`).
|
||||
fn is_war(path: &Path) -> bool {
|
||||
path.extension().is_some_and(|e| e.eq_ignore_ascii_case("war"))
|
||||
}
|
||||
|
||||
/// The module directory a config's `cwd` names (absolute, or relative to the project root).
|
||||
fn module_dir(root: &Path, module: Option<&Path>) -> PathBuf {
|
||||
match module {
|
||||
Some(m) if m.is_absolute() => m.to_path_buf(),
|
||||
Some(m) => root.join(m),
|
||||
None => root.to_path_buf(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The newest modification time of `path` or, for a directory, anything below it.
|
||||
fn newest_mtime_under(path: &Path) -> Option<std::time::SystemTime> {
|
||||
let meta = fs::metadata(path).ok()?;
|
||||
let mut newest = meta.modified().ok();
|
||||
if meta.is_dir() {
|
||||
for entry in fs::read_dir(path).ok()?.flatten() {
|
||||
if let Some(time) = newest_mtime_under(&entry.path())
|
||||
&& newest.is_none_or(|cur| time > cur)
|
||||
{
|
||||
newest = Some(time);
|
||||
}
|
||||
}
|
||||
}
|
||||
newest
|
||||
}
|
||||
|
||||
/// The newest `*.war` a build tool left in the module's output directories. Used when no
|
||||
/// build ran this launch and nothing was recorded, so a project that was already built can
|
||||
/// start directly (IDEA deploys the existing artifact instead of rebuilding it).
|
||||
fn find_built_war(root: &Path, module: Option<&Path>) -> Option<PathBuf> {
|
||||
let module = module_dir(root, module);
|
||||
let mut newest: Option<(std::time::SystemTime, PathBuf)> = None;
|
||||
for dir in [module.join("target"), module.join("build/libs")] {
|
||||
let Ok(entries) = fs::read_dir(dir) else { continue };
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_file() && is_war(&path) {
|
||||
let modified = entry.metadata().and_then(|m| m.modified()).unwrap_or(std::time::UNIX_EPOCH);
|
||||
if newest.as_ref().is_none_or(|(at, _)| modified > *at) {
|
||||
newest = Some((modified, path));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
newest.map(|(_, path)| path)
|
||||
}
|
||||
|
||||
/// Whether the WAR a Tomcat config deploys is newer than the module's sources, so its
|
||||
/// "Before launch › Build" task can be skipped (IDEA's up-to-date check). Conservative:
|
||||
/// a missing WAR or a newer source/build file means it is not current. Only the module's
|
||||
/// own sources and build files are inspected, not dependency modules.
|
||||
pub fn artifact_is_current(root: &Path, base: &Path, config: &RunConfig) -> bool {
|
||||
let p = &config.extras.params;
|
||||
if p.get("deployment").map(String::as_str).unwrap_or("artifact") != "artifact" {
|
||||
return true;
|
||||
}
|
||||
let module = module_dir(root, config.cwd.as_ref().map(|c| c.0.as_path()));
|
||||
let selected = p.get("artifact").filter(|s| !s.is_empty());
|
||||
let recorded_id = fs::read_to_string(base.join("artifact-id.txt")).ok();
|
||||
let id_matches = selected.is_none_or(|id| recorded_id.as_deref().map(str::trim) == Some(id.as_str()));
|
||||
let recorded = fs::read_to_string(base.join("artifact-path.txt")).ok().map(|s| PathBuf::from(s.trim()));
|
||||
let war = match recorded.filter(|_| id_matches) {
|
||||
Some(war) if war.is_file() && is_war(&war) => war,
|
||||
_ => match find_built_war(root, Some(&module)) {
|
||||
Some(war) => war,
|
||||
None => return false,
|
||||
},
|
||||
};
|
||||
let Ok(war_time) = fs::metadata(&war).and_then(|m| m.modified()) else { return false };
|
||||
let mut newest = newest_mtime_under(&module.join("src"));
|
||||
for dir in [module.as_path(), root] {
|
||||
for name in ["pom.xml", "build.gradle", "build.gradle.kts"] {
|
||||
if let Ok(time) = fs::metadata(dir.join(name)).and_then(|m| m.modified())
|
||||
&& newest.is_none_or(|cur| time > cur)
|
||||
{
|
||||
newest = Some(time);
|
||||
}
|
||||
}
|
||||
}
|
||||
newest.is_none_or(|time| time <= war_time)
|
||||
}
|
||||
|
||||
pub fn deployment(root: &Path, base: &Path, config: &RunConfig, resources_only: bool) -> IdeResult<PathBuf> {
|
||||
let p = &config.extras.params;
|
||||
match p.get("deployment").map(String::as_str).unwrap_or("artifact") {
|
||||
"artifact" => {
|
||||
if let Some(id) = p.get("artifact").filter(|s| !s.is_empty())
|
||||
&& fs::read_to_string(base.join("artifact-id.txt")).ok().as_ref() != Some(id)
|
||||
{
|
||||
return Err(invalid("Build the selected WAR artifact before launching Tomcat"));
|
||||
}
|
||||
let war = fs::read_to_string(base.join("artifact-path.txt"))
|
||||
.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(format!("The built WAR artifact does not exist: {}", war.display())));
|
||||
}
|
||||
// A build run this launch records its archive here. Without one (Before launch ›
|
||||
// Build removed, as IDEA allows), reuse the last build's archive, or an archive a
|
||||
// build tool already left in the module output, instead of rebuilding.
|
||||
let selected = p.get("artifact").filter(|s| !s.is_empty());
|
||||
let recorded_id = fs::read_to_string(base.join("artifact-id.txt")).ok();
|
||||
let id_matches = selected.is_none_or(|id| recorded_id.as_deref().map(str::trim) == Some(id.as_str()));
|
||||
let recorded = fs::read_to_string(base.join("artifact-path.txt")).ok().map(|s| PathBuf::from(s.trim()));
|
||||
let war = match recorded.filter(|_| id_matches) {
|
||||
Some(war) => {
|
||||
if war.is_file() && is_war(&war) {
|
||||
war
|
||||
} else {
|
||||
return Err(invalid(format!("The built WAR artifact does not exist: {}", war.display())));
|
||||
}
|
||||
}
|
||||
None => find_built_war(root, config.cwd.as_ref().map(|c| c.0.as_path())).ok_or_else(|| {
|
||||
invalid("No built WAR artifact found - build the project first, or add 'Build' to Before launch")
|
||||
})?,
|
||||
};
|
||||
let dest = base.join("deployment");
|
||||
explode(&war, &dest, resources_only)?;
|
||||
Ok(dest)
|
||||
@@ -1402,6 +1496,57 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn reuses_a_built_war_when_no_build_ran() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let module = root.join("web");
|
||||
fs::create_dir_all(module.join("target")).unwrap();
|
||||
let war = module.join("target/app.war");
|
||||
archive(&war, &[("index.html", "built")]);
|
||||
let base = root.join("cache");
|
||||
fs::create_dir_all(&base).unwrap();
|
||||
let mut c = config();
|
||||
c.cwd = Some(ide_core::IpcPath(PathBuf::from("web")));
|
||||
c.extras.params.insert("artifact".into(), "maven:web/pom.xml".into());
|
||||
// No artifact-path.txt / artifact-id.txt: it was never built through Tomcat.
|
||||
let dest = deployment(root, &base, &c, false).unwrap();
|
||||
assert_eq!(fs::read_to_string(dest.join("index.html")).unwrap(), "built");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reports_a_missing_artifact_when_nothing_was_built() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let base = tmp.path().join("cache");
|
||||
fs::create_dir_all(&base).unwrap();
|
||||
let error = deployment(tmp.path(), &base, &config(), false).unwrap_err().to_string();
|
||||
assert!(error.contains("No built WAR artifact found"), "{error}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_the_build_when_the_built_war_is_current() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let module = root.join("web");
|
||||
fs::create_dir_all(module.join("src/main/java")).unwrap();
|
||||
fs::create_dir_all(module.join("target")).unwrap();
|
||||
fs::write(module.join("pom.xml"), "<project/>").unwrap();
|
||||
let source = module.join("src/main/java/App.java");
|
||||
fs::write(&source, "class App {}").unwrap();
|
||||
let war = module.join("target/app.war");
|
||||
archive(&war, &[("index.html", "built")]);
|
||||
let base = root.join("cache");
|
||||
fs::create_dir_all(&base).unwrap();
|
||||
let mut c = config();
|
||||
c.cwd = Some(ide_core::IpcPath(PathBuf::from("web")));
|
||||
c.extras.params.insert("artifact".into(), "maven:web/pom.xml".into());
|
||||
// The WAR was packed after the source was written.
|
||||
assert!(artifact_is_current(root, &base, &c));
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
fs::write(&source, "class App { int changed; }").unwrap();
|
||||
assert!(!artifact_is_current(root, &base, &c));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepares_isolated_base_and_jdwp_only_for_run() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -21,6 +21,7 @@ export const tomcatType: RunConfigTypeContribution = {
|
||||
deployment: "artifact",
|
||||
httpPort: "8080",
|
||||
jmxPort: "1099",
|
||||
// Build before launch (IDEA default); skipped automatically when the built WAR is current.
|
||||
buildArtifact: "true",
|
||||
openBrowser: "true",
|
||||
...updatePolicyDefaults,
|
||||
|
||||
@@ -5131,8 +5131,6 @@
|
||||
"Choose a WAR artifact": "Elija un artefacto WAR",
|
||||
"Choose a Maven WAR module": "Elija un módulo WAR de Maven",
|
||||
"Choose a Gradle WAR module": "Elija un módulo WAR de Gradle",
|
||||
"Build the selected WAR artifact before launching Tomcat": "Compilar el artefacto WAR seleccionado antes de iniciar Tomcat",
|
||||
"Build the WAR artifact before launching Tomcat": "Compilar el artefacto WAR antes de iniciar Tomcat",
|
||||
"The built WAR artifact does not exist: {path}": "El artefacto WAR compilado no existe: {path}",
|
||||
"Cannot resolve Maven WAR artifact: {details}": "No se puede resolver el artefacto WAR de Maven: {details}",
|
||||
"Choose a WAR file or exploded directory": "Elija un archivo WAR o un directorio exploded",
|
||||
|
||||
@@ -4953,8 +4953,6 @@
|
||||
"Choose a WAR artifact": "Choisissez un artefact WAR",
|
||||
"Choose a Maven WAR module": "Choisissez un module WAR Maven",
|
||||
"Choose a Gradle WAR module": "Choisissez un module WAR Gradle",
|
||||
"Build the selected WAR artifact before launching Tomcat": "Compiler l'artefact WAR sélectionné avant de lancer Tomcat",
|
||||
"Build the WAR artifact before launching Tomcat": "Compiler l'artefact WAR avant de lancer Tomcat",
|
||||
"The built WAR artifact does not exist: {path}": "L'artefact WAR compilé n'existe pas : {path}",
|
||||
"Cannot resolve Maven WAR artifact: {details}": "Impossible de résoudre l'artefact WAR Maven : {details}",
|
||||
"Choose a WAR file or exploded directory": "Choisissez un fichier WAR ou un répertoire éclaté",
|
||||
|
||||
@@ -5204,8 +5204,6 @@
|
||||
"Choose a WAR artifact": "WAR 아티팩트 선택",
|
||||
"Choose a Maven WAR module": "Maven WAR 모듈 선택",
|
||||
"Choose a Gradle WAR module": "Gradle WAR 모듈 선택",
|
||||
"Build the selected WAR artifact before launching Tomcat": "Tomcat을 실행하기 전에 선택한 WAR 아티팩트 빌드",
|
||||
"Build the WAR artifact before launching Tomcat": "Tomcat을 실행하기 전에 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 파일 또는 exploded 디렉터리를 선택하세요",
|
||||
|
||||
@@ -5082,8 +5082,6 @@
|
||||
"Choose a WAR artifact": "Wybierz artefakt WAR",
|
||||
"Choose a Maven WAR module": "Wybierz moduł WAR Maven",
|
||||
"Choose a Gradle WAR module": "Wybierz moduł WAR Gradle",
|
||||
"Build the selected WAR artifact before launching Tomcat": "Zbuduj wybrany artefakt WAR przed uruchomieniem serwera Tomcat",
|
||||
"Build the WAR artifact before launching Tomcat": "Zbuduj artefakt WAR przed uruchomieniem serwera Tomcat",
|
||||
"The built WAR artifact does not exist: {path}": "Zbudowany artefakt WAR nie istnieje: {path}",
|
||||
"Cannot resolve Maven WAR artifact: {details}": "Nie można odnaleźć artefaktu WAR Maven: {details}",
|
||||
"Choose a WAR file or exploded directory": "Wybierz plik WAR lub rozpakowany katalog (exploded)",
|
||||
|
||||
@@ -5096,8 +5096,6 @@
|
||||
"Choose a WAR artifact": "Escolha um artefato WAR",
|
||||
"Choose a Maven WAR module": "Escolha um módulo WAR do Maven",
|
||||
"Choose a Gradle WAR module": "Escolha um módulo WAR do Gradle",
|
||||
"Build the selected WAR artifact before launching Tomcat": "Compilar o artefato WAR selecionado antes de iniciar o Tomcat",
|
||||
"Build the WAR artifact before launching Tomcat": "Compilar o artefato WAR antes de iniciar o Tomcat",
|
||||
"The built WAR artifact does not exist: {path}": "O artefato WAR compilado não existe: {path}",
|
||||
"Cannot resolve Maven WAR artifact: {details}": "Não é possível resolver o artefato WAR do Maven: {details}",
|
||||
"Choose a WAR file or exploded directory": "Escolha um arquivo WAR ou diretório exploded",
|
||||
|
||||
@@ -5464,8 +5464,6 @@
|
||||
"Choose a WAR artifact": "请选择 WAR 构件",
|
||||
"Choose a Maven WAR module": "请选择 Maven WAR 模块",
|
||||
"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: {path}": "构建的 WAR 构件不存在:{path}",
|
||||
"Cannot resolve Maven WAR artifact: {details}": "无法解析 Maven WAR 构件:{details}",
|
||||
"Choose a WAR file or exploded directory": "请选择 WAR 文件或展开目录",
|
||||
|
||||
Reference in new issue
Заблокувати користувача