mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
Opening a folder of independent Gradle builds selected only the first build. Link Gradle Projects… (pick a build's settings or build file) and Unlink on a build's root let an ordinary project window manage several independent builds without changing their scripts. The build model holds a list of Gradle builds; each keeps its own wrapper, working directory, environment, task routing and sync cache, keyed by the owning build directory. The links persist in the project's Gradle settings (linkedProjects); unlinking leaves files and saved run configurations alone. Existing includeBuild support is unchanged, and an explicitly linked build takes precedence over a composite that also includes it. Workspace windows keep Link/Unlink disabled. Co-authored-by: gudcks0305 <[email protected]>
304 lines
13 KiB
Rust
304 lines
13 KiB
Rust
//! WAR modules and build-tool-owned archive resolution (including custom output names).
|
|
use crate::BuildModel;
|
|
use ide_core::{CancellationToken, IdeError, IdeResult};
|
|
use serde::{Deserialize, Serialize};
|
|
use specta::Type;
|
|
use std::{collections::BTreeMap, fs, path::Path};
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct WebArtifact {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub module: String,
|
|
pub tool: String,
|
|
}
|
|
|
|
pub fn artifacts(root: &Path, model: &BuildModel) -> Vec<WebArtifact> {
|
|
let mut out = Vec::new();
|
|
if let Some(maven) = &model.maven {
|
|
for p in maven.projects.iter().filter(|p| p.packaging == "war") {
|
|
out.push(WebArtifact {
|
|
id: format!("maven:{}", p.pom),
|
|
name: p.artifact_id.clone(),
|
|
module: p.dir.clone(),
|
|
tool: "maven".into(),
|
|
});
|
|
}
|
|
}
|
|
for gradle in &model.gradle {
|
|
for p in &gradle.projects {
|
|
let script = p.build_file.as_ref().and_then(|f| fs::read_to_string(root.join(f)).ok()).unwrap_or_default();
|
|
if p.tasks.iter().any(|t| t.name == "war") || crate::gradle::has_war_plugin(&script) {
|
|
out.push(WebArtifact {
|
|
id: gradle_artifact_id(gradle, &p.path),
|
|
name: p.name.clone(),
|
|
module: p.dir.clone(),
|
|
tool: "gradle".into(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
fn gradle_artifact_id(build: &crate::GradleModel, path: &str) -> String {
|
|
if build.dir.is_empty() {
|
|
format!("gradle:{path}")
|
|
} else {
|
|
format!("gradle:{}", serde_json::to_string(&(&build.dir, path)).unwrap())
|
|
}
|
|
}
|
|
|
|
pub struct WebBuild {
|
|
pub command: String,
|
|
pub args: Vec<String>,
|
|
pub cwd: String,
|
|
pub env: BTreeMap<String, String>,
|
|
maven_project: Option<(Vec<String>, 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<String, String>,
|
|
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<WebBuild> {
|
|
fs::create_dir_all(cache)?;
|
|
let manifest = cache.join("artifact-path.txt");
|
|
// A failed build must never silently reuse an older archive manifest.
|
|
if manifest.exists() {
|
|
fs::remove_file(&manifest)?;
|
|
}
|
|
fs::write(cache.join("artifact-id.txt"), id)?;
|
|
if let Some(pom) = id.strip_prefix("maven:") {
|
|
let p = model
|
|
.maven
|
|
.as_ref()
|
|
.and_then(|m| m.projects.iter().find(|p| p.pom == pom && p.packaging == "war"))
|
|
.ok_or_else(|| IdeError::invalid("Choose a Maven WAR module"))?;
|
|
let projects = &model.maven.as_ref().unwrap().projects;
|
|
let top = projects
|
|
.iter()
|
|
.filter(|t| {
|
|
!t.aggregated && (t.dir.is_empty() || p.dir == t.dir || p.dir.starts_with(&format!("{}/", t.dir)))
|
|
})
|
|
.max_by_key(|t| t.dir.len())
|
|
.unwrap_or(p);
|
|
let mut args = top.options.clone();
|
|
if top.pom != p.pom {
|
|
let module = root
|
|
.join(&p.dir)
|
|
.strip_prefix(root.join(&top.dir))
|
|
.map_err(|e| IdeError::invalid(e.to_string()))?
|
|
.to_string_lossy()
|
|
.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(),
|
|
format!("-Doutput={}", manifest.display()),
|
|
]);
|
|
return Ok(WebBuild {
|
|
command: top.command.clone(),
|
|
args,
|
|
cwd: top.dir.clone(),
|
|
env: p.env.clone(),
|
|
maven_project: Some((probe, output)),
|
|
});
|
|
}
|
|
if let Some(legacy_path) = id.strip_prefix("gradle:") {
|
|
let identity = serde_json::from_str::<(String, String)>(legacy_path).ok();
|
|
let (g, path) = match &identity {
|
|
Some((dir, path)) => (model.gradle.iter().find(|g| g.dir == *dir), path.as_str()),
|
|
None => (model.gradle.first(), legacy_path),
|
|
};
|
|
let g = g.ok_or_else(|| IdeError::invalid("Choose a Gradle WAR module"))?;
|
|
if !artifacts(root, model).iter().any(|a| a.id == gradle_artifact_id(g, path)) {
|
|
return Err(IdeError::invalid("Choose a Gradle WAR module"));
|
|
}
|
|
let script = cache.join("war.init.gradle");
|
|
fs::write(
|
|
&script,
|
|
format!(
|
|
r#"gradle.projectsEvaluated {{
|
|
def p = gradle.rootProject.project({})
|
|
def warTask = p.tasks.named('war')
|
|
p.tasks.register('openideaTomcatArtifact') {{
|
|
dependsOn warTask
|
|
doLast {{ new File({}).setText(warTask.get().archiveFile.get().asFile.absolutePath, 'UTF-8') }}
|
|
}}
|
|
}}
|
|
"#,
|
|
groovy_literal(path),
|
|
groovy_literal(&manifest.to_string_lossy())
|
|
),
|
|
)?;
|
|
let task = format!("{}:openideaTomcatArtifact", path.trim_end_matches(':'));
|
|
return Ok(WebBuild {
|
|
command: g.command.clone(),
|
|
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"))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
#[test]
|
|
fn independent_war_modules_have_distinct_ids_and_build_in_their_own_directory() {
|
|
let temp = tempfile::tempdir().unwrap();
|
|
let root = temp.path();
|
|
for dir in ["a", "b"] {
|
|
std::fs::create_dir(root.join(dir)).unwrap();
|
|
std::fs::write(root.join(dir).join("build.gradle"), "plugins { id 'war' }").unwrap();
|
|
}
|
|
let settings = ide_core::settings::GradleSettings {
|
|
linked_projects: Some(vec!["a".into(), "b".into()]),
|
|
..Default::default()
|
|
};
|
|
let model = crate::load_linked(root, &root.join("cache"), None, &Default::default(), &settings);
|
|
let artifacts = super::artifacts(root, &model);
|
|
assert_eq!(artifacts.len(), 2);
|
|
assert_ne!(artifacts[0].id, artifacts[1].id);
|
|
for (artifact, dir) in artifacts.iter().zip(["a", "b"]) {
|
|
let plan = super::build(root, &model, &artifact.id, &root.join(format!("cache/{dir}"))).unwrap();
|
|
assert_eq!(plan.cwd, dir);
|
|
}
|
|
assert_eq!(super::build(root, &model, "gradle::", &root.join("legacy")).unwrap().cwd, "a");
|
|
}
|
|
use super::*;
|
|
#[test]
|
|
fn maven_war_modules_use_resolved_artifact_file() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
fs::write(tmp.path().join("pom.xml"), "<project><modelVersion>4.0.0</modelVersion><groupId>test</groupId><artifactId>web</artifactId><version>1</version><packaging>war</packaging><build><directory>custom</directory><finalName>renamed</finalName></build></project>").unwrap();
|
|
let model = crate::load(tmp.path(), &tmp.path().join("cache"), None);
|
|
let a = artifacts(tmp.path(), &model);
|
|
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();
|
|
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"))
|
|
);
|
|
}
|
|
#[test]
|
|
fn maven_module_build_includes_reactor_dependencies() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
fs::create_dir_all(tmp.path().join("web")).unwrap();
|
|
fs::write(tmp.path().join("pom.xml"), "<project><groupId>test</groupId><artifactId>parent</artifactId><version>1</version><packaging>pom</packaging><modules><module>web</module></modules></project>").unwrap();
|
|
fs::write(tmp.path().join("web/pom.xml"), "<project><parent><groupId>test</groupId><artifactId>parent</artifactId><version>1</version></parent><artifactId>web</artifactId><packaging>war</packaging></project>").unwrap();
|
|
let model = crate::load(tmp.path(), &tmp.path().join("cache"), None);
|
|
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() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
fs::write(
|
|
tmp.path().join("build.gradle.kts"),
|
|
"plugins { war }\ntasks.war { archiveFileName.set(\"custom.war\") }",
|
|
)
|
|
.unwrap();
|
|
let model = crate::load(tmp.path(), &tmp.path().join("cache"), None);
|
|
let a = artifacts(tmp.path(), &model);
|
|
assert_eq!(a.len(), 1);
|
|
let plan = build(tmp.path(), &model, &a[0].id, &tmp.path().join("base")).unwrap();
|
|
assert!(plan.args.contains(&":openideaTomcatArtifact".into()));
|
|
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'"));
|
|
}
|
|
}
|
|
}
|