avelino / avelino/jbundle

Runtime tracing for accurate module detection

Open
#16 0 comments 0 reactions 0 assignees View on GitHub
area/jlink
Dominant language
Rust
Stars
181
Forks
11
PR merge metrics
No merged PRs in 30d

Description

## Problem

Module detection via `jdeps --print-module-deps` is purely static analysis. It cannot detect modules accessed through reflection, dynamic class loading, service providers, or other runtime mechanisms. This causes two failure modes:

1. **Missing modules**: binary fails at runtime with `ClassNotFoundException` or `NoClassDefFoundError`
2. **Over-inclusion via fallback**: when jdeps fails, the fallback adds common modules that may be unnecessary, bloating the runtime

## Inspiration

GraalVM's native-image includes a [Tracing Agent](https://www.graalvm.org/latest/reference-manual/native-image/metadata/AutomaticMetadataCollection/) that solves the equivalent problem. The agent runs the application and records all dynamic accesses (reflection, resources, JNI, proxies) that static analysis cannot detect. The output is a configuration file that feeds back into the build.

Their workflow:
1. `java -agentlib:native-image-agent=config-output-dir=config -jar app.jar` (run with agent)
2. Exercise the application (manually or via tests)
3. Agent produces `reflect-config.json`, `resource-config.json`, etc.
4. `native-image --config-dir=config -jar app.jar` (build with detected metadata)

## Expected Outcome

A `jbundle trace` subcommand that runs the application and detects which Java modules are actually used at runtime:

```bash
jbundle trace --input ./my-app --exercise "run-tests"
# or
jbundle trace --input ./my-app --exercise "start-and-wait 10s"
```

### Workflow

1. Build the uberjar (if not already built)
2. Launch the application with a Java agent that hooks `ClassLoader.loadClass`, `Class.forName`, module access, etc.
3. Exercise the application (run test suite, start and wait for requests, execute specific scenarios)
4. Collect all modules accessed during the trace
5. Output a module list to `.jbundle-modules` or `.jbundle.edn`
6. Subsequent `jbundle build` reads this file for precise module inclusion

### Exercise Modes

- `--exercise "run-tests"` — runs `clojure -M:test` or `lein test` and collects during test execution
- `--exercise "start-and-wait 30s"` — starts the app, waits N seconds (for servers that load lazily), then shuts down
- `--exercise "command: curl http://localhost:8080/health"` — starts app, runs a command, collects
- `--exercise "manual"` — starts app, waits for user to Ctrl+C after exercising manually

### Output

```
Traced modules (22s, 847 classes loaded):
java.base
java.sql ← loaded via reflection (JDBC driver)
java.naming ← loaded via JNDI lookup
java.xml ← loaded by logback config parser
jdk.crypto.ec ← loaded during TLS handshake

Saved to .jbundle-modules
Use with: jbundle build (auto-detected) or --jlink-modules
```

## Why This Matters for Clojure

Clojure is inherently dynamic — much of the Java interop uses reflection:
- JDBC drivers loaded via `Class.forName`
- XML parsers via `javax.xml.parsers.SAXParserFactory.newInstance()`
- Crypto providers via `java.security.Security`
- Logging frameworks that discover appenders at runtime
- Ring/HTTP servers that load SSL contexts dynamically

Static analysis (jdeps) misses all of these. The trace approach catches them reliably.

Contributor guide

Open the contributing guide

Research direction

Start by reviewing the existing jbundle build flow, its jdeps module-detection fallback, and how the CLI handles the proposed build and --jlink-modules paths. Compare the desired trace workflow with the Java agent invocation and exercise modes described here; done means a working jbundle trace command that records runtime modules and feeds them into subsequent builds.

Written by the indexing model from the issue text.

Assessment

Tech stack
clojure, java, rust
Domain
cli, developer-experience, tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.