mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-14 16:07:52 +00:00
fix(java): rebuild when a launch's output folders were deleted outside the IDE (#2608)
Deleting a module's output folder outside the IDE (mvn clean in a terminal or by an agent) left JDT's incremental build state current, so the build before a launch compiled nothing and Run failed with NoClassDefFoundError. After the incremental build, the launch classpath is checked for folders inside the project that no longer exist; if any, the workspace is built fully once and the classpath resolved again. Folders still missing afterwards are remembered so later launches don't rebuild for them. Refs #2476
This commit is contained in:
@@ -478,6 +478,17 @@ pub fn parse_classpaths(v: &Value) -> JavaClasspath {
|
||||
}
|
||||
}
|
||||
|
||||
/// Folders of `cp` inside the project that don't exist: build outputs deleted outside
|
||||
/// the IDE. Archives and folders outside the project (dependencies) are left out.
|
||||
fn missing_outputs(cp: &JavaClasspath, project_root: &Path) -> Vec<PathBuf> {
|
||||
cp.class_paths
|
||||
.iter()
|
||||
.chain(&cp.module_paths)
|
||||
.map(PathBuf::from)
|
||||
.filter(|p| p.starts_with(project_root) && p.extension().is_none() && !p.exists())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `vscode.java.startDebugSession` answers the port as a number (or numeric string).
|
||||
pub fn parse_port(v: &Value) -> Option<u16> {
|
||||
v.as_u64().or_else(|| v.as_str().and_then(|s| s.trim().parse().ok())).and_then(|p| u16::try_from(p).ok())
|
||||
@@ -905,6 +916,43 @@ impl LspManager {
|
||||
};
|
||||
(cp, java_home)
|
||||
};
|
||||
// An output folder deleted outside the IDE (`mvn clean` in a terminal) leaves JDT's
|
||||
// incremental state current: the build compiles nothing and the launch fails with
|
||||
// NoClassDefFoundError. Rebuild fully, once per folder that stays missing.
|
||||
let missing = match (build, &cp) {
|
||||
(Some(_), Some(found)) if !full_build && !build_lock_held => {
|
||||
let (server, _) = self.jdtls(module, cancel).await?;
|
||||
let absent = server.java_absent_outputs.lock();
|
||||
missing_outputs(found, &self.project_root).into_iter().filter(|p| !absent.contains(p)).collect()
|
||||
}
|
||||
_ => Vec::new(),
|
||||
};
|
||||
let (build, cp) = if missing.is_empty() {
|
||||
(build, cp)
|
||||
} else {
|
||||
tracing::info!(?missing, "Java output folders are missing; rebuilding the workspace");
|
||||
let rebuilt = self.java_build(module, true, cancel).await?;
|
||||
if rebuilt == JavaBuildStatus::Cancelled {
|
||||
return Err(IdeError::cancelled());
|
||||
}
|
||||
let (server, _) = self.jdtls(module, cancel).await?;
|
||||
let fresh = {
|
||||
let _lock = cancellable_wait(cancel, server.java_build_lock.lock()).await?;
|
||||
self.java_classpath(module, test, cancel).await
|
||||
};
|
||||
let cp = match fresh {
|
||||
Ok(fresh) => Some(fresh),
|
||||
Err(e) if e.kind == ErrorKind::Cancelled => return Err(e),
|
||||
Err(e) => {
|
||||
tracing::debug!("java.project.getClasspaths after rebuild: {e}");
|
||||
cp
|
||||
}
|
||||
};
|
||||
if let Some(cp) = &cp {
|
||||
server.java_absent_outputs.lock().extend(missing_outputs(cp, &self.project_root));
|
||||
}
|
||||
(Some(rebuilt), cp)
|
||||
};
|
||||
let (errors, error_count, scoped) = match (build, build_lock_held) {
|
||||
(Some(JavaBuildStatus::WithErrors | JavaBuildStatus::Failed), _) | (None, true) => {
|
||||
let scope_module = module.to_path_buf();
|
||||
@@ -1329,6 +1377,22 @@ mod tests {
|
||||
assert_eq!(java_style.project_root, Some("/p/my demo".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_outputs_are_project_folders_that_no_longer_exist() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let root = dir.path().join("demo");
|
||||
let kept = root.join("app/target/classes");
|
||||
std::fs::create_dir_all(&kept).unwrap();
|
||||
let deleted = root.join("core/target/classes");
|
||||
let s = |p: &Path| p.to_string_lossy().into_owned();
|
||||
let cp = JavaClasspath {
|
||||
project_root: None,
|
||||
class_paths: vec![s(&kept), s(&deleted), s(&root.join("lib/absent.jar")), s(&dir.path().join("elsewhere"))],
|
||||
module_paths: vec![s(&root.join("api/target/classes"))],
|
||||
};
|
||||
assert_eq!(missing_outputs(&cp, &root), [deleted, root.join("api/target/classes")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ports_settings_and_status() {
|
||||
assert_eq!(parse_port(&json!(53122)), Some(53122));
|
||||
|
||||
@@ -120,6 +120,9 @@ pub struct Server {
|
||||
phase_notify: Notify,
|
||||
transient: Mutex<HashMap<PathBuf, Transient>>,
|
||||
pub java_build_lock: Arc<tokio::sync::Mutex<()>>,
|
||||
/// Classpath folders still missing after a full build (a module without output), so a
|
||||
/// launch rebuilds for a missing output folder only once.
|
||||
pub java_absent_outputs: Mutex<std::collections::HashSet<PathBuf>>,
|
||||
/// JDT retirement waits for launch and initialization cleanup, even before `Ready`.
|
||||
pub startup_lock: Arc<tokio::sync::Mutex<()>>,
|
||||
pub java_autobuild_suspended: std::sync::atomic::AtomicBool,
|
||||
@@ -167,6 +170,7 @@ impl Server {
|
||||
phase_notify: Notify::new(),
|
||||
transient: Mutex::new(HashMap::new()),
|
||||
java_build_lock: Arc::new(tokio::sync::Mutex::new(())),
|
||||
java_absent_outputs: Mutex::new(Default::default()),
|
||||
startup_lock: Arc::new(tokio::sync::Mutex::new(())),
|
||||
java_autobuild_suspended: std::sync::atomic::AtomicBool::new(false),
|
||||
external_project: tokio::sync::Mutex::new(Default::default()),
|
||||
|
||||
Reference in New Issue
Block a user