дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
fix(java): replace module outputs with launch snapshots on Windows (#1038)
Maven launches put a snapshot of each module's class directory on the classpath in place of the original. The originals were built as module.join("target/classes") (`...\app\target/classes` on Windows) while JDT LS reports `...\app\target\classes`; compared as text, the original stayed next to its snapshot, so every resource was on the classpath twice and MyBatis failed with "XML fragments parsed from previous mappers already contains value". Launch class directories are now joined per component, and classpath entries are compared by a key that ignores the verbatim prefix, separators, trailing separators and, on Windows, case.
Fixes #704
Цей коміт міститься в:
1 джерело
bd2df0bd3a
коміт
a3b6780823
2 files changed
+96
-7
No files matched your search
@@ -158,8 +158,11 @@ fn detect_dir(dir: &Path, model: &BuildModel, root: &Path) -> Option<BuildSystem
|
||||
}
|
||||
|
||||
/// `root/rel`, without a trailing separator for the root itself.
|
||||
/// `root` / `rel`, joined per `/`-separated component: on Windows a single join of
|
||||
/// `a/b` keeps the `/`, and such a path does not compare equal to the same directory
|
||||
/// as other tools (JDT LS) spell it.
|
||||
fn subdir(root: &Path, rel: &str) -> PathBuf {
|
||||
if rel.is_empty() { root.to_path_buf() } else { root.join(rel) }
|
||||
rel.split('/').filter(|part| !part.is_empty()).fold(root.to_path_buf(), |path, part| path.join(part))
|
||||
}
|
||||
|
||||
fn rel_str(root: &Path, dir: &Path) -> String {
|
||||
@@ -737,11 +740,11 @@ fn launch_class_dirs(system: BuildSystem, module: &Path, test: bool) -> Vec<Path
|
||||
.iter()
|
||||
.flat_map(|s| {
|
||||
["classes/kotlin", "classes/groovy", "classes/scala", "classes/java", "resources"]
|
||||
.map(|d| module.join("build").join(d).join(s))
|
||||
.map(|d| subdir(module, &format!("build/{d}/{s}")))
|
||||
})
|
||||
.collect(),
|
||||
BuildSystem::Maven if test => vec![module.join("target/test-classes"), module.join("target/classes")],
|
||||
BuildSystem::Maven => vec![module.join("target/classes")],
|
||||
BuildSystem::Maven if test => vec![subdir(module, "target/test-classes"), subdir(module, "target/classes")],
|
||||
BuildSystem::Maven => vec![subdir(module, "target/classes")],
|
||||
_ => Vec::new(),
|
||||
};
|
||||
dirs.into_iter().filter(|d| d.is_dir()).collect()
|
||||
@@ -751,6 +754,30 @@ fn launch_class_dirs(system: BuildSystem, module: &Path, test: bool) -> Vec<Path
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Launch outputs use the platform separator throughout, so they compare equal to
|
||||
/// the same directories as JDT LS reports them.
|
||||
#[test]
|
||||
fn launch_dirs_use_one_separator() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let module = subdir(root.path(), "parent/app");
|
||||
for dir in ["target/classes", "target/test-classes", "build/classes/java/main", "build/resources/test"] {
|
||||
std::fs::create_dir_all(subdir(&module, dir)).unwrap();
|
||||
}
|
||||
let foreign = if std::path::MAIN_SEPARATOR == '\\' { '/' } else { '\\' };
|
||||
let dirs = [
|
||||
launch_class_dirs(BuildSystem::Maven, &module, true),
|
||||
launch_class_dirs(BuildSystem::Gradle, &module, true),
|
||||
]
|
||||
.concat();
|
||||
assert_eq!(dirs.len(), 4, "{dirs:?}");
|
||||
for dir in &dirs {
|
||||
let suffix = dir.strip_prefix(root.path()).unwrap().to_string_lossy().into_owned();
|
||||
assert!(!suffix.contains(foreign), "{suffix}");
|
||||
}
|
||||
assert_eq!(subdir(root.path(), ""), root.path());
|
||||
assert_eq!(subdir(root.path(), "a//b/"), root.path().join("a").join("b"));
|
||||
}
|
||||
|
||||
fn write(root: &Path, rel: &str, text: &str) {
|
||||
let p = root.join(rel);
|
||||
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
|
||||
|
||||
@@ -86,7 +86,7 @@ async fn resolve_launch(
|
||||
};
|
||||
let mut class_paths: Vec<String> = built.iter().map(|p| p.to_string_lossy().into_owned()).collect();
|
||||
for p in std::mem::take(&mut info.class_paths) {
|
||||
if !class_paths.contains(&p) {
|
||||
if !class_paths.iter().any(|c| same_entry(c, &p, cfg!(windows))) {
|
||||
class_paths.push(p);
|
||||
}
|
||||
}
|
||||
@@ -131,15 +131,44 @@ async fn resolve_launch(
|
||||
.map_err(|e| IdeError::internal(e.to_string()))?
|
||||
}
|
||||
|
||||
/// A classpath entry as a comparable key. Entries come from JDT LS and from our own
|
||||
/// build model, which spell one directory differently on Windows (`\\?\` prefix,
|
||||
/// `/` or `\\`, drive-letter case); compared as text, a module output stayed on the
|
||||
/// classpath next to its launch snapshot and every resource was found twice.
|
||||
fn entry_key(path: &str, windows: bool) -> String {
|
||||
let path = match path.strip_prefix(r"\\?\UNC\") {
|
||||
Some(share) => format!(r"\\{share}"),
|
||||
None => path.strip_prefix(r"\\?\").unwrap_or(path).to_owned(),
|
||||
};
|
||||
let mut key = if windows { path.replace('\\', "/") } else { path };
|
||||
while key.len() > 1 && key.ends_with('/') {
|
||||
key.pop();
|
||||
}
|
||||
if windows { key.to_lowercase() } else { key }
|
||||
}
|
||||
|
||||
fn same_entry(a: &str, b: &str, windows: bool) -> bool {
|
||||
a == b || entry_key(a, windows) == entry_key(b, windows)
|
||||
}
|
||||
|
||||
fn use_snapshot(info: &mut JavaLaunchInfo, built: &[PathBuf], snapshot: &std::path::Path) -> Vec<PathBuf> {
|
||||
use_snapshot_on(info, built, snapshot, cfg!(windows))
|
||||
}
|
||||
|
||||
fn use_snapshot_on(
|
||||
info: &mut JavaLaunchInfo,
|
||||
built: &[PathBuf],
|
||||
snapshot: &std::path::Path,
|
||||
windows: bool,
|
||||
) -> Vec<PathBuf> {
|
||||
let mut copies = Vec::new();
|
||||
for (index, source) in built.iter().enumerate() {
|
||||
let dest = snapshot.join(index.to_string());
|
||||
let original = source.to_string_lossy();
|
||||
info.class_paths.retain(|p| p != original.as_ref());
|
||||
info.class_paths.retain(|p| !same_entry(p, &original, windows));
|
||||
let mut modular = false;
|
||||
for path in &mut info.module_paths {
|
||||
if path == original.as_ref() {
|
||||
if same_entry(path, &original, windows) {
|
||||
*path = dest.to_string_lossy().into_owned();
|
||||
modular = true;
|
||||
}
|
||||
@@ -185,6 +214,39 @@ pub(crate) fn register(d: &mut Dispatcher) {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// JDT LS and our build model spell a module output differently on Windows; the
|
||||
/// snapshot must still replace it, or resources (MyBatis mapper XML) load twice.
|
||||
#[test]
|
||||
fn snapshots_replace_outputs_however_windows_spells_them() {
|
||||
let built = PathBuf::from(r"D:\workspace\arm\arm-web\target/classes");
|
||||
let jdtls = [
|
||||
r"D:\workspace\arm\arm-web\target\classes",
|
||||
r"d:\workspace\arm\arm-web\target\classes",
|
||||
r"D:/workspace/arm/arm-web/target/classes/",
|
||||
r"\\?\D:\workspace\arm\arm-web\target\classes",
|
||||
];
|
||||
for spelled in jdtls {
|
||||
let mut info = JavaLaunchInfo {
|
||||
class_paths: vec![spelled.into(), r"D:\m2\mybatis.jar".into()],
|
||||
module_paths: vec![spelled.into()],
|
||||
..Default::default()
|
||||
};
|
||||
use_snapshot_on(&mut info, std::slice::from_ref(&built), std::path::Path::new(r"C:\cache\launch-x"), true);
|
||||
assert_eq!(info.class_paths, [r"D:\m2\mybatis.jar"], "{spelled}");
|
||||
assert_eq!(
|
||||
info.module_paths,
|
||||
[std::path::Path::new(r"C:\cache\launch-x").join("0").to_string_lossy()],
|
||||
"{spelled}"
|
||||
);
|
||||
}
|
||||
assert!(same_entry(r"\\?\UNC\server\share\app\target\classes", r"\\server\share\app\target/classes", true));
|
||||
// Different directories stay different, and other platforms keep case and `\`.
|
||||
assert!(!same_entry(r"D:\a\target\classes", r"D:\a\target\test-classes", true));
|
||||
assert!(!same_entry("/work/App/target/classes", "/work/app/target/classes", false));
|
||||
assert!(!same_entry(r"/work/a\b", "/work/a/b", false));
|
||||
assert!(same_entry("/work/app/target/classes/", "/work/app/target/classes", false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launch_snapshots_survive_jdt_overwrites_and_replace_module_outputs() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
|
||||
Reference in new issue
Заблокувати користувача