fix(java): use the project host classpath separator (#492)

This commit is contained in:
l0ng-ai
2026-10-31 10:00:01 +08:00
committed by GitHub
parent f9a7c68897
commit 4a620122e8
9 changed files with 119 additions and 12 deletions
+3
View File
@@ -71,6 +71,8 @@ pub struct JavaLaunchInfo {
pub project_root: Option<ide_core::IpcPath>,
pub class_paths: Vec<String>,
pub module_paths: Vec<String>,
/// Path list separator of the host that runs the JVM.
pub path_list_separator: String,
/// JDK home jdtls compiles the project against.
pub java_home: Option<ide_core::IpcPath>,
/// The `java` launcher to use (filled in by the caller from the installed JDKs).
@@ -351,6 +353,7 @@ impl LspManager {
project_root: cp.project_root,
class_paths: cp.class_paths,
module_paths: cp.module_paths,
path_list_separator: if cfg!(windows) { ";" } else { ":" }.into(),
java_home: java_home.map(Into::into),
java_exec: None,
shorten_command_line: None,
+2
View File
@@ -3679,6 +3679,8 @@ export type JavaLaunchInfo = {
projectRoot: IpcPath | null,
classPaths: string[],
modulePaths: string[],
/** Path list separator of the host that runs the JVM. */
pathListSeparator: string,
/** JDK home jdtls compiles the project against. */
javaHome: IpcPath | null,
/** The `java` launcher to use (filled in by the caller from the installed JDKs). */
@@ -32,6 +32,7 @@ vi.mock("@/platform/ipc", async (orig) => {
projectRoot: "/p",
classPaths: ["/p/target/classes", "/m2/a.jar"],
modulePaths: [],
pathListSeparator: ":",
javaHome: "/jdk21",
javaExec: "/jdk21/bin/java",
shortenCommandLine,
@@ -0,0 +1,73 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import type { JavaLaunchInfo, RunConfig } from "@/platform/ipc";
import { resolveJavaCommand } from "@/platform/javaLaunch";
const ipc = vi.hoisted(() => ({
javaResolveLaunch: vi.fn(),
javaDebugServer: vi.fn(async () => ({ status: "ok", data: 5005 })),
fsReadDir: vi.fn(async () => ({ status: "ok", data: [] })),
}));
vi.mock("@/platform/ipc", async (importOriginal) => ({
...(await importOriginal<typeof import("@/platform/ipc")>()),
commands: ipc,
}));
import { javaLaunchConfig } from "../java";
const config: RunConfig = {
id: "main",
name: "Main",
kind: "application",
command: "java",
args: ["-cp", "${classpath}", "Main"],
cwd: null,
env: {},
detected: false,
isTest: false,
debugAdapter: null,
};
afterEach(() => vi.restoreAllMocks());
const hosts = [
{ client: "Win32", host: "Linux", separator: ":", paths: ["/p/classes", "/lib/a;b.jar"] },
{ client: "MacIntel", host: "Windows", separator: ";", paths: ["C:\\p\\classes", "D:\\lib\\a.jar"] },
{ client: "MacIntel", host: "Mac", separator: ":", paths: ["/p/classes", "/lib/a.jar"] },
{ client: "Win32", host: "Windows", separator: ";", paths: ["C:\\p\\classes", "D:\\lib\\a.jar"] },
{ client: "Linux x86_64", host: "Linux", separator: ":", paths: ["/p/classes", "/lib/a.jar"] },
];
describe.each(hosts)("Java launch on $host from $client", ({ client, separator, paths }) => {
const info: JavaLaunchInfo = {
build: "succeeded",
projectRoot: null,
classPaths: paths,
modulePaths: paths,
pathListSeparator: separator,
javaHome: null,
javaExec: "java",
shortenCommandLine: null,
errors: [],
errorCount: 0,
hints: [],
};
it.each(["${classpath}", "${testClasspath}"])("joins %s and module paths using the host separator", (placeholder) => {
vi.spyOn(navigator, "platform", "get").mockReturnValue(client);
const result = resolveJavaCommand({ ...config, args: ["-cp", placeholder, "Main"] }, info);
expect(result.args).toEqual(["--module-path", paths.join(separator), "-cp", paths.join(separator), "Main"]);
});
it.each(["java-debug", "kotlin-debug-adapter"])(
"splits explicit classpaths for %s without damaging paths",
async (adapter) => {
vi.spyOn(navigator, "platform", "get").mockReturnValue(client);
ipc.javaResolveLaunch.mockResolvedValue({ status: "ok", data: info });
const result = await javaLaunchConfig(
{ ...config, debugAdapter: adapter, args: ["-cp", paths.join(separator), "Main"] },
"/p",
);
expect(JSON.parse(result.extraJson!).classPaths).toEqual(paths);
},
);
});
+3 -1
View File
@@ -107,7 +107,9 @@ export async function javaLaunchConfig(
const resolved = resolveJavaCommand(rc, info);
const kotlin = rc.debugAdapter === KOTLIN_ADAPTER || (await moduleHasKotlinSources(join(root, module)));
preparation?.signal.throwIfAborted();
const classPaths = parsed.explicitClasspath ? parsed.explicitClasspath.split(/[:;]/) : info.classPaths;
const classPaths = parsed.explicitClasspath
? parsed.explicitClasspath.split(info.pathListSeparator)
: info.classPaths;
if (kotlin) {
return {
name: rc.name,
+5 -4
View File
@@ -194,6 +194,7 @@ describe("java compile errors", () => {
projectRoot: "/p",
classPaths: [],
modulePaths: [],
pathListSeparator: ":",
javaHome: null,
javaExec: null,
shortenCommandLine: null,
@@ -220,6 +221,7 @@ describe("java command lines", () => {
projectRoot: "/p",
classPaths: ["/p/target/classes", "/m2/a.jar"],
modulePaths: [],
pathListSeparator: ":",
javaHome: "/jdk21",
javaExec: "/jdk21/bin/java",
shortenCommandLine: "none",
@@ -245,10 +247,9 @@ describe("java command lines", () => {
const r = resolveJavaCommand({ ...c, args: ["-Dx=1", ...c.args, "arg"] }, info);
expect(r.command).toBe("/jdk21/bin/java");
expect(r.args).toEqual(["-Dx=1", "-cp", "/p/target/classes:/m2/a.jar", "a.Main", "arg"]);
expect(resolveJavaCommand(c, { ...info, modulePaths: ["/m/x.jar"] }, ";").args.slice(0, 2)).toEqual([
"--module-path",
"/m/x.jar",
]);
expect(
resolveJavaCommand(c, { ...info, modulePaths: ["/m/x.jar"], pathListSeparator: ";" }).args.slice(0, 2),
).toEqual(["--module-path", "/m/x.jar"]);
});
it("joins arguments for java-debug and reads JDWP's port", () => {
+28 -1
View File
@@ -38,6 +38,7 @@ const info: JavaLaunchInfo = {
projectRoot: "/p",
classPaths: ["/p/target/classes"],
modulePaths: [],
pathListSeparator: ":",
javaHome: null,
javaExec: "java",
shortenCommandLine: null,
@@ -50,7 +51,10 @@ beforeEach(() => {
vi.clearAllMocks();
provideNotificationService({ notify });
});
afterEach(() => resetRuns());
afterEach(() => {
resetRuns();
vi.restoreAllMocks();
});
describe("Stop during Java launch resolution", () => {
it.each(["cancelled", "success"])("cancels the request and ignores a late %s response", async (response) => {
@@ -81,3 +85,26 @@ describe("Stop during Java launch resolution", () => {
expect(runtime(session.id)!.resolveRequest).toBeUndefined();
});
});
describe("Java run host separator", () => {
it.each([
{ client: "Win32", separator: ":", paths: ["/p/classes", "/lib/a.jar"] },
{ client: "MacIntel", separator: ";", paths: ["C:\\p\\classes", "D:\\lib\\a.jar"] },
{ client: "MacIntel", separator: ":", paths: ["/p/classes", "/lib/a.jar"] },
])("uses $separator from the host on $client", async ({ client, separator, paths }) => {
vi.spyOn(navigator, "platform", "get").mockReturnValue(client);
ipc.javaResolveLaunch.mockResolvedValue({
status: "ok",
data: { ...info, classPaths: paths, modulePaths: paths, pathListSeparator: separator },
});
ipc.runStart.mockResolvedValue({ status: "ok", data: 42 });
await startRun(config);
expect(ipc.runStart.mock.calls[0]![0].args).toEqual([
"--module-path",
paths.join(separator),
"-cp",
paths.join(separator),
"Main",
]);
});
});
+1 -3
View File
@@ -691,8 +691,6 @@ function revealSession(sessionId: string) {
}
}
const pathSeparator = () => (typeof navigator !== "undefined" && /win/i.test(navigator.platform) ? ";" : ":");
/**
* Builds the module (jdtls workspace build) and resolves `${classpath}` and the
* project JDK of a Java application configuration. Null (and a console message) on failure.
@@ -737,7 +735,7 @@ async function resolveJvmApplication(sessionId: string, config: RunConfig): Prom
say("The classpath is empty: is the module imported by the Java language server?\n", "stderr");
return null;
}
return resolveJavaCommand(config, info, pathSeparator());
return resolveJavaCommand(config, info);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
if (request.cancelled || isCancelled(e)) {
+3 -3
View File
@@ -72,11 +72,11 @@ export function parseJavaArgs(args: readonly string[]): JavaCommandLine {
}
/** The configuration with its placeholders and launcher resolved. */
export function resolveJavaCommand(config: RunConfig, info: JavaLaunchInfo, pathSeparator = ":"): RunConfig {
const cp = info.classPaths.join(pathSeparator);
export function resolveJavaCommand(config: RunConfig, info: JavaLaunchInfo): RunConfig {
const cp = info.classPaths.join(info.pathListSeparator);
const args: string[] = [];
if (info.modulePaths.length && !config.args.some((a) => a === "-p" || a === "--module-path"))
args.push("--module-path", info.modulePaths.join(pathSeparator));
args.push("--module-path", info.modulePaths.join(info.pathListSeparator));
args.push(...config.args.map((a) => (a === CLASSPATH || a === TEST_CLASSPATH ? cp : a)));
const command = config.command === "java" && info.javaExec ? info.javaExec : config.command;
return { ...config, command, args };