From 43c85ce8f39aadda822ae8d7421f351785826252 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Thu, 5 Nov 2026 15:04:11 +0800 Subject: [PATCH] fix(gradle): unlink a build by the path the UI sends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Linked builds are saved normalised (canonical, forward slashes), but unlinking compared the paths it was given verbatim, so a build outside the project could not be unlinked on Windows (C:\… vs C:/…), nor through a symlinked path. Unlink now normalises each path the way linking saves it. The Gradle linkage tests also create the platform's wrapper (gradlew.bat on Windows) and compare canonical paths without the \\?\ prefix. --- crates/ide-build/src/compile/mod.rs | 12 ++++++----- crates/ide-build/src/gradle.rs | 33 +++++++++++++++++++++++++++++ crates/ide-build/src/lib.rs | 2 +- crates/ide-host/src/ops/build.rs | 3 ++- 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/crates/ide-build/src/compile/mod.rs b/crates/ide-build/src/compile/mod.rs index fcfa45f07..91bbdbf35 100644 --- a/crates/ide-build/src/compile/mod.rs +++ b/crates/ide-build/src/compile/mod.rs @@ -821,13 +821,14 @@ fn launch_class_dirs(system: BuildSystem, module: &Path, test: bool) -> Vec ide_core::IdeR Ok(out) } +/// The form `normalize_linked_projects` saves `dir` in, also for a folder that no longer +/// exists, so a path the UI sends (`C:\…` on Windows) matches the saved entry. +pub fn linked_project_key(root: &Path, dir: &str) -> String { + match (dunce::canonicalize(root), dunce::canonicalize(root.join(dir))) { + (Ok(root), Ok(path)) => rel(&root, &path), + _ => dir.replace('\\', "/"), + } +} + /// Linked build identities without reading their scripts (also used by index hot paths). pub(crate) fn linked_dirs(root: &Path, settings: &ide_core::settings::GradleSettings) -> Vec { match &settings.linked_projects { @@ -675,6 +684,30 @@ pub(crate) fn has_war_plugin(script: &str) -> bool { #[cfg(test)] mod tests { + #[test] + fn a_path_from_the_ui_finds_the_saved_link() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("workspace"); + let external = temp.path().join("external"); + for dir in [root.join("a"), external.clone()] { + std::fs::create_dir_all(&dir).unwrap(); + std::fs::write(dir.join("settings.gradle"), "").unwrap(); + } + let native = |p: &std::path::Path| p.to_string_lossy().into_owned(); + // Saved by linking; unlinking may send a native, absolute or non-canonical spelling. + for (dir, sent) in [ + ("a", native(&root.join("a"))), + ("a", "a".to_owned()), + ("a", native(&root.join("a").join("..").join("a"))), + ("../external", native(&external)), + ] { + let saved = super::normalize_linked_projects(&root, &[dir.into()]).unwrap(); + assert_eq!(super::linked_project_key(&root, &sent), saved[0], "{sent}"); + } + // A folder deleted since it was linked still matches its saved entry. + assert_eq!(super::linked_project_key(&root, r"gone\build"), "gone/build"); + } + #[test] fn explicit_linkage_preserves_default_discovery_and_can_unlink_every_build() { let temp = tempfile::tempdir().unwrap(); diff --git a/crates/ide-build/src/lib.rs b/crates/ide-build/src/lib.rs index e18682766..eb613c5cf 100644 --- a/crates/ide-build/src/lib.rs +++ b/crates/ide-build/src/lib.rs @@ -216,7 +216,7 @@ mod tests { }; assert_eq!( gradle_external_roots(&root, &temp.path().join("cache"), &settings), - [external.canonicalize().unwrap()] + [dunce::canonicalize(&external).unwrap()] ); settings.linked_projects = Some(vec![]); assert!(gradle_external_roots(&root, &temp.path().join("cache"), &settings).is_empty()); diff --git a/crates/ide-host/src/ops/build.rs b/crates/ide-host/src/ops/build.rs index 5d0fb052f..074cbcad5 100644 --- a/crates/ide-host/src/ops/build.rs +++ b/crates/ide-host/src/ops/build.rs @@ -137,7 +137,8 @@ fn linked_gradle_paths( } } } else { - linked.retain(|path| !paths.contains(path)); + let unlinked: Vec = paths.iter().map(|p| ide_build::gradle::linked_project_key(root, p)).collect(); + linked.retain(|path| !paths.contains(path) && !unlinked.contains(path)); } Ok(linked) }