Skip to content

Commit 6ea6ac4

Browse files
waltossclaude
andcommitted
Add Java debugging support via DAP (java-debug.core)
Wraps Microsoft's java-debug.core library with lightweight providers for source lookup and JDI-based evaluation. Supports launch, attach, breakpoints, stepping, variable inspection, and exception pausing. Architecture: - src/dap/adapters/ — AdapterInstaller registry (lldb, java) - src/dap/adapters/java/ — Java adapter sources (tarball bundled as Bun asset) - DapSession runtime configs refactored from if/else chain to strategy pattern - build.ts generates adapter tarball at build time (never committed) Includes integration tests (launch, attach, limitations) gated on adapter availability, and detects mvn/gradle as Java runtimes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent bb9cd66 commit 6ea6ac4

29 files changed

Lines changed: 1405 additions & 198 deletions

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,10 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
3636
# C test fixture build artifacts
3737
tests/fixtures/c/hello
3838
tests/fixtures/c/*.dSYM
39+
40+
# Java build artifacts
41+
*.class
42+
tests/fixtures/java/multi-package/out/
43+
44+
# Generated at build time
45+
src/dap/adapters/java/adapter-sources.tar.gz

build.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { $ } from "bun";
2+
3+
// Generate Java adapter sources tarball
4+
await $`tar czf src/dap/adapters/java/adapter-sources.tar.gz -C src/dap/adapters/java com/debugthat/adapter`;
5+
6+
// Bundle
7+
await $`bun build src/main.ts --outdir dist --target=bun`;
8+
9+
// Clean up generated tarball
10+
await $`rm src/dap/adapters/java/adapter-sources.tar.gz`;

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
],
3333
"scripts": {
3434
"dev": "bun run src/main.ts",
35-
"build": "bun build src/main.ts --outdir dist --target=bun",
35+
"build": "bun run build.ts",
3636
"test": "bun test tests/unit/ && bun test --concurrent tests/integration/",
3737
"lint": "biome check .",
3838
"format": "biome check --write .",
@@ -54,4 +54,4 @@
5454
"zod": "^4.0.0"
5555
},
5656
"module": "index.ts"
57-
}
57+
}

src/commands/install.ts

Lines changed: 17 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,9 @@
1-
import { existsSync, mkdirSync } from "node:fs";
2-
import { join } from "node:path";
1+
import { existsSync } from "node:fs";
32
import { z } from "zod";
43
import { defineCommand } from "../cli/command.ts";
4+
import { getAdapterInstaller, listAdapterNames } from "../dap/adapters/index.ts";
55
import { getManagedAdaptersDir } from "../dap/session.ts";
66

7-
const LLVM_VERSION = "19.1.7";
8-
9-
function getPlatformArch(): { os: string; arch: string } {
10-
const os = process.platform; // "darwin", "linux", "win32"
11-
const arch = process.arch; // "arm64", "x64"
12-
return { os, arch };
13-
}
14-
15-
function getLlvmDownloadUrl(version: string, os: string, arch: string): string | null {
16-
if (os === "darwin") {
17-
if (arch === "arm64") {
18-
return `https://github.com/llvm/llvm-project/releases/download/llvmorg-${version}/LLVM-${version}-macOS-ARM64.tar.xz`;
19-
}
20-
if (arch === "x64") {
21-
return `https://github.com/llvm/llvm-project/releases/download/llvmorg-${version}/LLVM-${version}-macOS-X64.tar.xz`;
22-
}
23-
}
24-
if (os === "linux") {
25-
if (arch === "x64") {
26-
return `https://github.com/llvm/llvm-project/releases/download/llvmorg-${version}/LLVM-${version}-Linux-X64.tar.xz`;
27-
}
28-
if (arch === "arm64") {
29-
return `https://github.com/llvm/llvm-project/releases/download/llvmorg-${version}/LLVM-${version}-Linux-AArch64.tar.xz`;
30-
}
31-
}
32-
return null;
33-
}
34-
357
defineCommand({
368
name: "install",
379
description: "Download managed adapter binary",
@@ -44,6 +16,7 @@ defineCommand({
4416
}),
4517
handler: async (ctx) => {
4618
const adapter = ctx.positional || undefined;
19+
const supported = listAdapterNames();
4720

4821
if (ctx.flags.list) {
4922
const dir = getManagedAdaptersDir();
@@ -65,124 +38,32 @@ defineCommand({
6538

6639
if (!adapter) {
6740
console.error("Usage: dbg install <adapter>");
68-
console.error(" Supported adapters: lldb");
41+
console.error(` Supported adapters: ${supported.join(", ")}`);
6942
console.error(" Options: --list (show installed adapters)");
7043
return 1;
7144
}
7245

73-
if (adapter !== "lldb") {
46+
const installer = getAdapterInstaller(adapter);
47+
if (!installer) {
7448
console.error(`Unknown adapter: ${adapter}`);
75-
console.error(" Supported adapters: lldb");
49+
console.error(` Supported adapters: ${supported.join(", ")}`);
7650
return 1;
7751
}
7852

79-
const { os, arch } = getPlatformArch();
80-
const url = getLlvmDownloadUrl(LLVM_VERSION, os, arch);
81-
82-
if (!url) {
83-
console.error(`Unsupported platform: ${os}-${arch}`);
84-
console.error(" Supported: darwin-arm64, darwin-x64, linux-x64, linux-arm64");
85-
return 1;
86-
}
87-
88-
const adaptersDir = getManagedAdaptersDir();
89-
const targetPath = join(adaptersDir, "lldb-dap");
90-
91-
if (existsSync(targetPath)) {
92-
console.log(`lldb-dap already installed at ${targetPath}`);
93-
console.log(` To reinstall, remove it first: rm ${targetPath}`);
53+
if (installer.isInstalled()) {
54+
console.log(`${installer.name} is already installed.`);
55+
console.log(" To reinstall, remove ~/.debug-that/adapters/ entry first.");
9456
return 0;
9557
}
9658

97-
console.log(`Downloading LLVM ${LLVM_VERSION} for ${os}-${arch}...`);
98-
console.log(` From: ${url}`);
99-
100-
// Download the tarball
101-
const response = await fetch(url, { redirect: "follow" });
102-
if (!response.ok) {
103-
console.error(`Download failed: HTTP ${response.status}`);
104-
console.error(" -> Check your internet connection or try again later");
105-
return 1;
106-
}
107-
108-
const tarball = await response.arrayBuffer();
109-
console.log(`Downloaded ${(tarball.byteLength / 1024 / 1024).toFixed(1)} MB`);
110-
111-
// Extract lldb-dap from the tarball using tar
112-
mkdirSync(adaptersDir, { recursive: true });
113-
114-
const tmpTar = join(adaptersDir, "llvm-download.tar.xz");
115-
await Bun.write(tmpTar, tarball);
116-
117-
// Find lldb-dap inside the archive and extract just that binary
118-
console.log("Extracting lldb-dap...");
119-
const listResult = Bun.spawnSync(["tar", "-tf", tmpTar], {
120-
stdout: "pipe",
121-
});
122-
const files = listResult.stdout.toString().split("\n");
123-
const lldbDapEntry = files.find((f) => f.endsWith("/bin/lldb-dap") || f === "bin/lldb-dap");
124-
125-
if (!lldbDapEntry) {
126-
Bun.spawnSync(["rm", tmpTar]);
127-
console.error("Could not find lldb-dap in the LLVM archive");
128-
console.error(` Archive entries searched: ${files.length}`);
129-
console.error(" -> Try installing manually: brew install llvm");
130-
return 1;
131-
}
132-
133-
// Extract just the lldb-dap binary
134-
const extractResult = Bun.spawnSync(
135-
[
136-
"tar",
137-
"-xf",
138-
tmpTar,
139-
"-C",
140-
adaptersDir,
141-
"--strip-components",
142-
String(lldbDapEntry.split("/").length - 1),
143-
lldbDapEntry,
144-
],
145-
{ stdout: "pipe", stderr: "pipe" },
146-
);
147-
148-
// Clean up tarball
149-
Bun.spawnSync(["rm", tmpTar]);
150-
151-
if (extractResult.exitCode !== 0) {
152-
console.error(`Extraction failed: ${extractResult.stderr.toString()}`);
153-
return 1;
154-
}
155-
156-
// Also extract liblldb if present (needed on some platforms)
157-
const liblldbEntries = files.filter(
158-
(f) => f.includes("liblldb") && (f.endsWith(".so") || f.endsWith(".dylib")),
159-
);
160-
for (const libEntry of liblldbEntries) {
161-
Bun.spawnSync(
162-
[
163-
"tar",
164-
"-xf",
165-
tmpTar,
166-
"-C",
167-
adaptersDir,
168-
"--strip-components",
169-
String(libEntry.split("/").length - 1),
170-
libEntry,
171-
],
172-
{ stdout: "pipe", stderr: "pipe" },
173-
);
174-
}
175-
176-
// Make executable
177-
Bun.spawnSync(["chmod", "+x", targetPath]);
178-
179-
if (existsSync(targetPath)) {
180-
console.log(`Installed lldb-dap to ${targetPath}`);
59+
try {
60+
console.log(`Installing ${installer.name}...`);
61+
await installer.install((msg) => console.log(msg));
62+
console.log(`${installer.name} installed successfully.`);
18163
return 0;
64+
} catch (e) {
65+
console.error(`Installation failed: ${(e as Error).message}`);
66+
return 1;
18267
}
183-
184-
console.error("Installation failed — lldb-dap not found after extraction");
185-
console.error(" -> Try installing manually: brew install llvm");
186-
return 1;
18768
},
18869
});

src/dap/adapters/assets.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
declare module "*.tar.gz" {
2+
const path: string;
3+
export default path;
4+
}

src/dap/adapters/index.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { javaInstaller } from "./java.ts";
2+
import { lldbInstaller } from "./lldb.ts";
3+
import type { AdapterInstaller } from "./types.ts";
4+
5+
const ADAPTERS: Record<string, AdapterInstaller> = {
6+
lldb: lldbInstaller,
7+
java: javaInstaller,
8+
};
9+
10+
export function getAdapterInstaller(name: string): AdapterInstaller | undefined {
11+
return ADAPTERS[name];
12+
}
13+
14+
export function listAdapterNames(): string[] {
15+
return Object.keys(ADAPTERS);
16+
}
17+
18+
export { getJavaAdapterClasspath, isJavaAdapterInstalled } from "./java.ts";

src/dap/adapters/java.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { existsSync, mkdirSync } from "node:fs";
2+
import { delimiter, join } from "node:path";
3+
import { $ } from "bun";
4+
import { getManagedAdaptersDir } from "../session.ts";
5+
import type { AdapterInstaller } from "./types.ts";
6+
7+
const MAVEN_CENTRAL = "https://repo1.maven.org/maven2";
8+
9+
const JAVA_DEPS: Record<string, string> = {
10+
"com.microsoft.java.debug.core-0.53.0.jar":
11+
"com/microsoft/java/com.microsoft.java.debug.core/0.53.0/com.microsoft.java.debug.core-0.53.0.jar",
12+
"commons-lang3-3.14.0.jar": "org/apache/commons/commons-lang3/3.14.0/commons-lang3-3.14.0.jar",
13+
"gson-2.10.1.jar": "com/google/code/gson/gson/2.10.1/gson-2.10.1.jar",
14+
"rxjava-2.2.21.jar": "io/reactivex/rxjava2/rxjava/2.2.21/rxjava-2.2.21.jar",
15+
"reactive-streams-1.0.4.jar":
16+
"org/reactivestreams/reactive-streams/1.0.4/reactive-streams-1.0.4.jar",
17+
"commons-io-2.15.1.jar": "commons-io/commons-io/2.15.1/commons-io-2.15.1.jar",
18+
};
19+
20+
const JAVA_DEP_NAMES = Object.keys(JAVA_DEPS);
21+
22+
function getJavaAdapterDir(): string {
23+
return join(getManagedAdaptersDir(), "java");
24+
}
25+
26+
/** Check if the Java adapter is fully installed (all JARs + compiled classes). */
27+
export function isJavaAdapterInstalled(): boolean {
28+
const dir = getJavaAdapterDir();
29+
const depsDir = join(dir, "deps");
30+
if (!existsSync(join(dir, "classes", "com", "debugthat", "adapter", "Main.class"))) {
31+
return false;
32+
}
33+
return JAVA_DEP_NAMES.every((jar) => existsSync(join(depsDir, jar)));
34+
}
35+
36+
/** Build the classpath string for running the Java adapter. */
37+
export function getJavaAdapterClasspath(): string {
38+
const dir = getJavaAdapterDir();
39+
const depsDir = join(dir, "deps");
40+
const classesDir = join(dir, "classes");
41+
const jars = JAVA_DEP_NAMES.map((jar) => join(depsDir, jar));
42+
return [classesDir, ...jars].join(delimiter);
43+
}
44+
45+
/**
46+
* Extract Java adapter sources for compilation.
47+
* In dev: uses source tree directly. In bundle: extracts from bundled tarball asset.
48+
*/
49+
async function extractAdapterSources(installDir: string): Promise<string[]> {
50+
// Dev mode: source tree exists, use directly
51+
const devPackageDir = join(import.meta.dir, "java", "com", "debugthat", "adapter");
52+
if (existsSync(devPackageDir)) {
53+
return Array.from(new Bun.Glob("*.java").scanSync(devPackageDir)).map((f) =>
54+
join(devPackageDir, f),
55+
);
56+
}
57+
58+
// Bundle mode: lazy-import the tarball asset and extract
59+
const { default: tarballPath } = await import("./java/adapter-sources.tar.gz" as string);
60+
const resolved = tarballPath.startsWith("/") ? tarballPath : join(import.meta.dir, tarballPath);
61+
const srcDir = join(installDir, "src");
62+
mkdirSync(srcDir, { recursive: true });
63+
await $`tar xzf ${resolved} -C ${srcDir}`;
64+
const packageDir = join(srcDir, "com", "debugthat", "adapter");
65+
return Array.from(new Bun.Glob("*.java").scanSync(packageDir)).map((f) => join(packageDir, f));
66+
}
67+
68+
export const javaInstaller: AdapterInstaller = {
69+
name: "java (java-debug.core)",
70+
71+
isInstalled: isJavaAdapterInstalled,
72+
73+
async install(log) {
74+
const javaVersionResult = await $`java -version`.quiet().nothrow();
75+
const stderr = javaVersionResult.stderr.toString().trim();
76+
const versionMatch = stderr.match(/version "(\d+)/);
77+
const version = versionMatch?.[1] ? parseInt(versionMatch[1], 10) : 0;
78+
if (version < 11) {
79+
throw new Error(`Java 11+ required (found: ${stderr.split("\n")[0]?.trim() ?? "none"})`);
80+
}
81+
82+
const dir = getJavaAdapterDir();
83+
const depsDir = join(dir, "deps");
84+
mkdirSync(depsDir, { recursive: true });
85+
86+
for (const [jarName, mavenPath] of Object.entries(JAVA_DEPS)) {
87+
const jarPath = join(depsDir, jarName);
88+
if (!existsSync(jarPath)) {
89+
log(` Downloading ${jarName}...`);
90+
const response = await fetch(`${MAVEN_CENTRAL}/${mavenPath}`, {
91+
redirect: "follow",
92+
});
93+
if (!response.ok) {
94+
throw new Error(`Failed to download ${jarName}: HTTP ${response.status}`);
95+
}
96+
await Bun.write(jarPath, response);
97+
}
98+
}
99+
100+
log(" Compiling adapter...");
101+
const cp = JAVA_DEP_NAMES.map((jar) => join(depsDir, jar)).join(delimiter);
102+
const classesDir = join(dir, "classes");
103+
mkdirSync(classesDir, { recursive: true });
104+
105+
const sourceFiles = await extractAdapterSources(dir);
106+
await $`javac -d ${classesDir} -cp ${cp} -source 11 -target 11 ${sourceFiles}`;
107+
108+
log(" Adapter compiled.");
109+
},
110+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.debugthat.adapter;
2+
3+
import com.microsoft.java.debug.core.adapter.IVirtualMachineManagerProvider;
4+
import com.sun.jdi.Bootstrap;
5+
import com.sun.jdi.VirtualMachineManager;
6+
7+
/**
8+
* Default VMM provider using JDI Bootstrap.
9+
*/
10+
public class DefaultVirtualMachineManagerProvider implements IVirtualMachineManagerProvider {
11+
12+
@Override
13+
public VirtualMachineManager getVirtualMachineManager() {
14+
return Bootstrap.virtualMachineManager();
15+
}
16+
}

0 commit comments

Comments
 (0)