# Introduction

Package JVM applications into self-contained binaries. No JVM installation required to run the output.

## What is jbundle?

**jbundle** transforms JVM applications (Clojure, Java, Kotlin, Scala, Groovy) into self-contained binaries. Think of it as the Go/Rust distribution experience for the JVM world.

```bash
# Build your app
jbundle build --input ./my-app --output ./dist/my-app

# Run anywhere (no Java required)
./dist/my-app
```

## Why jbundle?

The conventional deployment approach requires both a JAR and a JVM on the target machine. GraalVM native-image is an alternative, but presents challenges: slow compilations, complex reflection configuration, and library incompatibilities.

**jbundle offers a practical solution:** bundle a minimal JVM runtime with your uberjar into a single executable. The result is a single file, no external dependencies, with full JVM compatibility.

## Key Features

| Feature                | Description                                   |
| ---------------------- | --------------------------------------------- |
| **Single binary**      | One file to distribute, like Go or Rust       |
| **No JVM required**    | Runtime is bundled inside the binary          |
| **Fast startup**       | AppCDS + profiles achieve \~200-350ms startup |
| **Full compatibility** | Everything that works on JVM works here       |
| **Multiple languages** | Clojure, Java, Kotlin, Scala, Groovy          |
| **Smart caching**      | Layers cached independently by content hash   |

## How It Works

```
detect → build uberjar → download JDK → jdeps → jlink → pack
```

1. **Detect** — Identifies your build system (deps.edn, project.clj, pom.xml, build.gradle)
2. **Build** — Runs the appropriate build command to create an uberjar
3. **Download JDK** — Fetches JDK from Adoptium (cached locally)
4. **Analyze** — Uses jdeps to detect required modules
5. **Minimize** — Creates minimal runtime with jlink (\~30-50 MB)
6. **Package** — Bundles everything into a single executable

## Quick Comparison

| Aspect            | jbundle              | GraalVM native-image            |
| ----------------- | -------------------- | ------------------------------- |
| **Compatibility** | 100% JVM compatible  | Requires reflection config      |
| **Build time**    | Fast                 | Slow (AOT compilation)          |
| **Startup**       | \~200-350ms (AppCDS) | \~10-50ms                       |
| **Setup**         | Just `jbundle`       | GraalVM + native-image + config |

## Next Steps

* [Installation](/getting-started/installation) — Get jbundle on your system
* [Quick Start](/getting-started/quick-start) — Build your first binary
* [Configuration](/user-guide/configuration) — Customize with `jbundle.toml`


# Installation

## Install Script (Recommended)

### macOS / Linux

```bash
curl -sSL https://raw.githubusercontent.com/avelino/jbundle/main/install.sh | sh
```

### Windows (PowerShell)

> **Help wanted:** Windows support is best-effort. Contributions welcome — see [open issues](https://github.com/avelino/jbundle/issues).

```powershell
irm https://raw.githubusercontent.com/avelino/jbundle/main/install.ps1 | iex
```

Installs to `%USERPROFILE%\.jbundle\bin` and adds it to your PATH.

### Custom Install Directory

```bash
# macOS / Linux
JBUNDLE_INSTALL_DIR=~/.local/bin curl -sSL https://raw.githubusercontent.com/avelino/jbundle/main/install.sh | sh

# Windows
$env:JBUNDLE_INSTALL_DIR = "C:\tools" ; irm https://raw.githubusercontent.com/avelino/jbundle/main/install.ps1 | iex
```

### Install a Specific Version

```bash
# macOS / Linux
JBUNDLE_VERSION=v0.2.0 curl -sSL https://raw.githubusercontent.com/avelino/jbundle/main/install.sh | sh

# Windows
$env:JBUNDLE_VERSION = "v0.2.0" ; irm https://raw.githubusercontent.com/avelino/jbundle/main/install.ps1 | iex
```

## Homebrew

```bash
brew tap avelino/jbundle
brew install jbundle
```

Works on macOS (Intel and Apple Silicon) and Linux via Linuxbrew.

## Pre-compiled Binaries

Download from [GitHub Releases](https://github.com/avelino/jbundle/releases):

| Platform        | Binary                          |
| --------------- | ------------------------------- |
| Linux x86\_64   | `jbundle-linux-x86_64.tar.gz`   |
| Linux ARM64     | `jbundle-linux-aarch64.tar.gz`  |
| macOS x86\_64   | `jbundle-darwin-x86_64.tar.gz`  |
| macOS ARM64     | `jbundle-darwin-aarch64.tar.gz` |
| Windows x86\_64 | `jbundle-windows-x86_64.zip`    |

```bash
# Example: manual install on Linux x86_64
curl -sSL https://github.com/avelino/jbundle/releases/latest/download/jbundle-linux-x86_64.tar.gz | tar xz
sudo mv jbundle /usr/local/bin/
```

```powershell
# Example: manual install on Windows
Invoke-WebRequest https://github.com/avelino/jbundle/releases/latest/download/jbundle-windows-x86_64.zip -OutFile jbundle.zip
Expand-Archive jbundle.zip -DestinationPath .
Move-Item jbundle.exe C:\Windows\
```

## From Source

Build from source using Cargo (Rust's package manager).

### Prerequisites

* [Rust toolchain](https://rustup.rs/) (1.70+)
* Git
* SSL development libraries
  * Debian/Ubuntu: `sudo apt update && sudo apt install libssl-dev`

### Steps

```bash
git clone https://github.com/avelino/jbundle.git
cd jbundle
cargo install --path .
```

## Verify Installation

```bash
jbundle --version
```

## Requirements

jbundle itself has no runtime dependencies. However, to **build** applications, you need the appropriate build tools:

| Build System | Required Tool                                             |
| ------------ | --------------------------------------------------------- |
| deps.edn     | [Clojure CLI](https://clojure.org/guides/install_clojure) |
| project.clj  | [Leiningen](https://leiningen.org/)                       |
| pom.xml      | [Maven](https://maven.apache.org/)                        |
| build.gradle | [Gradle](https://gradle.org/)                             |

The **output binary** has no dependencies — it includes everything needed to run.


# Quick Start

Build your first self-contained binary in under 5 minutes.

## Basic Usage

```bash
# Build from project directory
jbundle build --input ./my-app --output ./dist/my-app

# Run the binary (no Java required)
./dist/my-app
```

## By Build System

### Clojure (deps.edn)

```bash
jbundle build --input ./my-clojure-app --output ./dist/app
```

Requires a `:build` alias with `tools.build` configured to produce an uberjar.

### Clojure (Leiningen)

```bash
jbundle build --input ./my-lein-app --output ./dist/app
```

Runs `lein uberjar` internally.

### Java (Maven)

```bash
jbundle build --input ./my-java-app --output ./dist/app
```

Runs `mvn package -DskipTests` internally. Requires a configured shade/assembly plugin for uberjar.

### Java (Gradle)

```bash
jbundle build --input ./my-gradle-app --output ./dist/app
```

Runs `gradle build -x test` internally. Requires a configured [ShadowJar](https://github.com/GradleUp/shadow)/fatJar task to create an uberjar.

```kotlin
plugins {
    id("com.gradleup.shadow") version "8.3.0"
}

tasks.withType<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>().configureEach {
    mergeServiceFiles()
    duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
```

For larger projects `isZip64 = true` needs to be added.

### From Pre-built JAR

If you already have an uberjar:

```bash
jbundle build --input ./target/app.jar --output ./dist/app
```

This skips the build step and goes straight to packaging.

## What Happens

On first run of the output binary:

1. **Extract** — Runtime and app are extracted to `~/.jbundle/cache/`
2. **Generate AppCDS** — JVM creates shared class archive (one-time, \~2-5s)
3. **Run** — Application starts

On subsequent runs:

1. **Cache hit** — Everything already extracted
2. **Load AppCDS** — JVM loads pre-processed metadata
3. **Run** — Application starts fast (\~200-350ms)

## Common Options

```bash
# Specify Java version
jbundle build --input . --output ./dist/app --java-version 21

# Use CLI profile (optimized for fast startup)
jbundle build --input . --output ./dist/app --profile cli

# Cross-compile for Linux
jbundle build --input . --output ./dist/app --target linux-x64

# Pass JVM arguments
jbundle build --input . --output ./dist/app --jvm-args "-Xmx512m"
```

## Next Steps

* [Configuration](/user-guide/configuration) — Use `jbundle.toml` to avoid repeating flags
* [JVM Profiles](/user-guide/profiles) — Understand `cli` vs `server` profiles
* [CLI Reference](/reference/cli) — Full command documentation


# Configuration

Avoid repeating flags by creating a `jbundle.toml` in your project root.

## Configuration File

```toml
# jbundle.toml

java_version = 21
target = "linux-x64"
jvm_args = ["-Xmx512m", "-XX:+UseZGC"]
build_args = ["-PeaJdkBuild=false"]
profile = "cli"
shrink = true
appcds = true
crac = false
compact_banner = false

# Gradle multi-project options
gradle_project = "app"
modules = ["java.base", "java.sql"]
java_home = "/usr/lib/jvm/java-21"
jlink_runtime = "./build/jlink"
```

All fields are optional.

## Options

| Field            | Type    | Default          | Description                                                                            |
| ---------------- | ------- | ---------------- | -------------------------------------------------------------------------------------- |
| `java_version`   | integer | `21`             | JDK version to bundle                                                                  |
| `target`         | string  | current platform | Target platform (`linux-x64`, `macos-aarch64`, etc.)                                   |
| `jvm_args`       | array   | `[]`             | JVM arguments passed at runtime                                                        |
| `build_args`     | array   | `[]`             | Extra arguments passed to the build tool                                               |
| `profile`        | string  | `"server"`       | JVM profile (`"cli"` or `"server"`)                                                    |
| `shrink`         | boolean | `false`          | Shrink uberjar by removing non-essential files                                         |
| `appcds`         | boolean | `true`           | Enable AppCDS for faster startup                                                       |
| `crac`           | boolean | `false`          | Enable CRaC checkpoint (Linux only)                                                    |
| `compact_banner` | boolean | `false`          | Use a compact banner in the wrapper                                                    |
| `gradle_project` | string  | —                | Gradle subproject to build (for multi-project)                                         |
| `modules`        | array   | —                | Manual module list (bypasses jdeps detection)                                          |
| `java_home`      | string  | —                | Path to existing JDK installation (skips download). Falls back to `JAVA_HOME` env var. |
| `jlink_runtime`  | string  | —                | Path to existing jlink runtime to reuse                                                |

## Precedence

Configuration values are resolved in this order (highest to lowest):

1. **CLI flags** — `--java-version 17` overrides everything
2. **jbundle.toml** — Project-level defaults
3. **Internal defaults** — Built-in values

## Examples

### CLI Tool

Optimized for fast startup:

```toml
# jbundle.toml
java_version = 21
profile = "cli"
jvm_args = ["-Xmx256m"]
```

### Microservice

Standard server configuration with custom GC:

```toml
# jbundle.toml
java_version = 21
profile = "server"  # Important: use "server" when specifying custom GC
jvm_args = ["-Xmx1g", "-XX:+UseZGC"]
```

> **Note:** When using a custom garbage collector like ZGC, always use `profile = "server"`. The `"cli"` profile includes `-XX:+UseSerialGC`, and the JVM cannot use multiple GCs simultaneously. jbundle will detect this conflict and fail with a helpful error message.

### Cross-Platform Build

Targeting Linux from macOS:

```toml
# jbundle.toml
java_version = 21
target = "linux-x64"
```

### Maximum Performance (Linux)

With CRaC for instant startup:

```toml
# jbundle.toml
java_version = 21
profile = "cli"
crac = true
```

### Gradle Multi-Project

For complex projects like JabRef:

```toml
# jbundle.toml
gradle_project = "jabkit"
java_version = 21
profile = "cli"
jvm_args = ["-Xmx1g"]
```

### With Custom Modules

When jdeps detection is insufficient:

```toml
# jbundle.toml
modules = ["java.base", "java.sql", "java.desktop", "jdk.incubator.vector"]
```

### Custom Build Arguments

Pass extra flags to the build tool (e.g., Gradle project properties):

```toml
# jbundle.toml
gradle_project = "app"
build_args = ["-PeaJdkBuild=false", "-PprojVersion=1.2.3"]
```

Works with any build system. Arguments are appended to the build command.

### Reusing Existing Runtime

Skip jlink if you have a pre-built runtime:

```toml
# jbundle.toml
jlink_runtime = "./build/jlink"
```

### Reusing Local JDK

Skip JDK download by reusing an existing installation:

```toml
# jbundle.toml
java_home = "/usr/lib/jvm/java-21"
```

If `java_home` is not set in the config or CLI, jbundle automatically checks the `JAVA_HOME` environment variable.

## Environment Variables

| Variable    | Description                                                                                    |
| ----------- | ---------------------------------------------------------------------------------------------- |
| `JAVA_HOME` | Path to existing JDK installation (used when `--java-home` and `java_home` config are not set) |
| `RUST_LOG`  | Logging level (`error`, `warn`, `info`, `debug`, `trace`)                                      |

For debugging, set `RUST_LOG` to control jbundle's logging:

```bash
# Show debug output
RUST_LOG=debug jbundle build --input . --output ./dist/app

# Show only warnings
RUST_LOG=warn jbundle build --input . --output ./dist/app
```


# JVM Profiles

The `--profile` flag selects a set of optimized JVM flags for your workload.

## Available Profiles

### server (default)

Standard HotSpot behavior. No additional flags.

**Best for:**

* Long-running services
* Web servers
* Applications where throughput matters more than startup

**Characteristics:**

* Full tiered compilation (C1 → C2)
* G1GC (default garbage collector)
* Higher memory overhead
* Peak performance after warmup

### cli

Optimized for short-lived processes and CLI tools.

**Best for:**

* Command-line tools
* Scripts
* One-shot utilities
* Serverless functions

**Characteristics:**

* Tiered compilation with C1 only (skip C2)
* SerialGC (simpler, faster startup)
* Reduced code cache
* \~200-350ms startup (with AppCDS)

## Usage

```bash
# CLI profile
jbundle build --input . --output ./dist/app --profile cli

# Server profile (default)
jbundle build --input . --output ./dist/app --profile server
```

Or in `jbundle.toml`:

```toml
profile = "cli"
```

## JVM Flags

Each profile injects specific JVM flags into the generated binary:

### cli

```
-XX:+TieredCompilation
-XX:TieredStopAtLevel=1
-XX:+UseSerialGC
```

* **TieredStopAtLevel=1**: Uses only C1 compiler (fast compilation, no C2 optimization)
* **UseSerialGC**: Simple single-threaded GC, minimal overhead

### server

No additional flags. Uses JVM defaults:

* Full tiered compilation (C1 → C2)
* G1GC garbage collector

> **Note:** Because `server` adds no GC flags, it's the right choice when you want to specify a custom garbage collector via `jvm_args`.

## Performance Comparison

| Metric           | cli          | server        |
| ---------------- | ------------ | ------------- |
| Startup (cold)   | \~800-1500ms | \~1000-2000ms |
| Startup (warm)   | \~200-350ms  | \~400-600ms   |
| Peak throughput  | Lower        | Higher        |
| Memory footprint | Lower        | Higher        |
| Warmup time      | Faster       | Slower        |

## When to Use Each

```
cli profile:
  ✓ Run once and exit
  ✓ Interactive commands
  ✓ Scripts and automation
  ✓ Startup time is critical

server profile:
  ✓ Long-running processes
  ✓ Web servers
  ✓ Background services
  ✓ Throughput is critical
```

## GC Conflict Detection

jbundle automatically detects conflicts between the profile's garbage collector and custom `jvm_args`.

The `cli` profile uses `-XX:+UseSerialGC`. If your `jvm_args` specifies a different GC (like `-XX:+UseZGC` or `-XX:+UseG1GC`), jbundle will emit a warning:

```
WARN GC conflict: profile 'cli' uses -XX:+UseSerialGC but jvm_args contains -XX:+UseZGC. The JVM cannot use multiple garbage collectors. Consider using profile = "server" or removing -XX:+UseZGC from jvm_args.
```

The build continues, but the JVM will likely fail at runtime. To fix this, use `profile = "server"` which doesn't set any GC flags:

```toml
# jbundle.toml - using ZGC with server profile
profile = "server"
jvm_args = ["-XX:+UseZGC", "-XX:+UnlockExperimentalVMOptions"]
```

## Combining with AppCDS and CRaC

Profiles work alongside other optimizations:

```bash
# CLI + AppCDS (default) → ~200-350ms
jbundle build --input . --output ./app --profile cli

# CLI + CRaC → ~10-50ms (Linux only)
jbundle build --input . --output ./app --profile cli --crac

# Server + no AppCDS → standard JVM startup
jbundle build --input . --output ./app --profile server --no-appcds
```


# Caching & Performance

jbundle uses a layered caching system to optimize both build time and runtime performance.

## How Caching Works

The output binary contains independent layers:

```
[stub script] [runtime.tar.gz] [app.jar.gz] [crac.tar.gz?]
```

Each layer is cached by content hash at `~/.jbundle/cache/`:

```
~/.jbundle/cache/
├── jdk-21-linux-x64/     # Downloaded JDK (reused across builds)
├── rt-abc123/            # Extracted runtime
├── app-def456/           # Extracted app + app.jsa
└── crac-ghi789/          # CRaC checkpoint (if enabled)
```

## Why This Matters

**Build time:** Changing only application code doesn't re-download the JDK or re-create the runtime.

**Run time:** Updating your app doesn't re-extract the runtime layer. Only the app layer is replaced.

**CI/CD:** Multiple builds with the same JDK version share the cached download.

## Startup Performance

### First Run vs Subsequent Runs

| Metric               | First Run                        | Subsequent Runs |
| -------------------- | -------------------------------- | --------------- |
| **What happens**     | Extract layers + generate AppCDS | Load from cache |
| **Overhead**         | +2-5s                            | None            |
| **Startup (cli)**    | \~800-1500ms                     | \~200-350ms     |
| **Startup (server)** | \~1000-2000ms                    | \~400-600ms     |

### Why First Run is Slower

1. **Extraction** — Compressed layers are decompressed to cache
2. **AppCDS generation** — JVM creates `.jsa` file with pre-processed classes

This is a one-time cost per app version.

### Why Subsequent Runs are Faster

1. **Cache hit** — Everything already extracted
2. **AppCDS loaded** — JVM skips parsing and verification
3. **Profile flags** — Optimized JVM configuration

## AppCDS (Class Data Sharing)

Enabled by default on JDK 19+. The JVM automatically generates a shared archive on first run:

```
~/.jbundle/cache/app-<hash>/app.jsa
```

This file contains:

* Pre-parsed class metadata
* Pre-verified bytecode
* Pre-computed class layouts

Result: 60-75% faster startup on subsequent runs.

### Disabling AppCDS

```bash
jbundle build --input . --output ./app --no-appcds
```

Useful if you observe issues with specific libraries.

## CRaC (Coordinated Restore at Checkpoint)

Optional feature for near-instant startup (\~10-50ms).

### How It Works

1. Application starts and warms up
2. Checkpoint is created (memory snapshot)
3. Checkpoint is bundled in the binary
4. Subsequent runs restore from checkpoint

### Requirements

* Linux only
* JDK with CRaC support (e.g., Azul Zulu with CRaC)

### Usage

```bash
jbundle build --input . --output ./app --crac
```

Falls back to AppCDS if restore fails.

## Cache Management

### View Cache Info

```bash
jbundle info
```

Shows cached JDKs, runtimes, and apps.

### Clean Cache

```bash
jbundle clean
```

Removes all cached data.

## Binary Size Optimization

jbundle uses single-pass compression to minimize binary size:

* **jlink runtime** — Created with no internal compression, then compressed once via `tar.gz` at maximum level. This avoids double-compression overhead (compressing already-compressed data). The `--compress` flag is probed from `jlink --help`, since the accepted spelling (`zip-0` vs numeric `0`) varies across JDK builds.
* **Application JAR** — Compressed once with gzip at maximum level. When `--shrink` runs, it repacks the JAR with entries stored uncompressed so the gzip pass is the only compression step (Stored entries also load faster in the JVM).

## Performance Tips

1. **Use `--profile cli`** for command-line tools
2. **Keep AppCDS enabled** (default) for best startup
3. **Consider CRaC** for Linux deployments where startup is critical
4. **Pre-warm in CI** by running the binary once to generate AppCDS


# Build Systems

jbundle automatically detects your build system and runs the appropriate commands.

## Supported Build Systems

| Build System          | Detection File       | Build Command             |
| --------------------- | -------------------- | ------------------------- |
| Clojure (tools.build) | `deps.edn`           | `clojure -T:build uber`   |
| Leiningen             | `project.clj`        | `lein uberjar`            |
| Maven                 | `pom.xml`            | `mvn package -DskipTests` |
| Gradle                | `build.gradle(.kts)` | `gradle build -x test`    |

## Clojure (deps.edn)

### Requirements

Your `deps.edn` must have a `:build` alias with `tools.build`:

```clojure
{:deps {...}
 :aliases
 {:build
  {:deps {io.github.clojure/tools.build {:mvn/version "0.10.5"}}
   :ns-default build}}}
```

And a `build.clj` with an `uber` function:

```clojure
(ns build
  (:require [clojure.tools.build.api :as b]))

(def lib 'com.example/my-app)
(def version "1.0.0")
(def class-dir "target/classes")
(def uber-file (format "target/%s-%s.jar" (name lib) version))

(defn uber [_]
  (b/copy-dir {:src-dirs ["src" "resources"]
               :target-dir class-dir})
  (b/compile-clj {:basis (b/create-basis {:project "deps.edn"})
                  :class-dir class-dir})
  (b/uber {:class-dir class-dir
           :uber-file uber-file
           :basis (b/create-basis {:project "deps.edn"})
           :main 'com.example.main}))
```

### What jbundle Does

```bash
clojure -T:build uber
# Then looks for JAR in target/
```

## Clojure (Leiningen)

### Requirements

Your `project.clj` should specify a `:main` namespace:

```clojure
(defproject my-app "1.0.0"
  :dependencies [[org.clojure/clojure "1.11.1"]]
  :main my-app.core
  :aot :all)
```

### What jbundle Does

```bash
lein uberjar
# Then looks for *-standalone.jar in target/
```

## Java (Maven)

### Requirements

Configure the Maven Shade Plugin for uberjar creation:

```xml
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-shade-plugin</artifactId>
      <version>3.5.1</version>
      <executions>
        <execution>
          <phase>package</phase>
          <goals><goal>shade</goal></goals>
          <configuration>
            <transformers>
              <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                <mainClass>com.example.Main</mainClass>
              </transformer>
            </transformers>
          </configuration>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>
```

### What jbundle Does

```bash
mvn package -DskipTests
# Then looks for JAR in target/
```

## Java (Gradle)

### Requirements

Use the Shadow plugin for uberjar creation:

```kotlin
// build.gradle.kts
plugins {
    application
    id("com.github.johnrengelman.shadow") version "8.1.1"
}

application {
    mainClass.set("com.example.Main")
}
```

Or with Groovy DSL:

```groovy
// build.gradle
plugins {
    id 'application'
    id 'com.github.johnrengelman.shadow' version '8.1.1'
}

application {
    mainClass = 'com.example.Main'
}
```

### What jbundle Does

```bash
gradle build -x test
# Then looks for *-all.jar in build/libs/
```

### Custom Build Arguments

Pass extra flags to Gradle (or any build system) with `--build-args`:

```bash
jbundle build --input . --output ./app --build-args "-PeaJdkBuild=false" --build-args "-PprojVersion=xyz"
```

Or in `jbundle.toml`:

```toml
build_args = ["-PeaJdkBuild=false", "-PprojVersion=xyz"]
```

Arguments are appended after jbundle's default flags (e.g., after `-x test` for Gradle).

### Multi-Project Builds

For Gradle projects with multiple subprojects, jbundle auto-detects those with the `application` plugin. See [Gradle Multi-Project](/user-guide/gradle-multi-project) for details.

Quick example:

```bash
# Build specific subproject
jbundle build --input . --output ./dist/app --gradle-project app

# Build all application subprojects
jbundle build --input . --output ./dist --all
```

## From Pre-built JAR

Skip the build step entirely:

```bash
jbundle build --input ./target/app.jar --output ./dist/app
```

Useful when:

* You have a custom build process
* The JAR is built by CI
* You're testing with an existing artifact

## Detection Priority

If multiple build files exist, jbundle uses this priority:

1. `deps.edn` (Clojure tools.build)
2. `project.clj` (Leiningen)
3. `pom.xml` (Maven)
4. `build.gradle` or `build.gradle.kts` (Gradle)

## Troubleshooting

### "No build system detected"

Ensure one of the supported build files is in the root of `--input` directory.

### "JAR not found after build"

Check that your build produces an uberjar (not just a thin JAR). The JAR must include all dependencies.

### "Main class not found"

Ensure your JAR's `MANIFEST.MF` specifies `Main-Class`.


# Gradle Multi-Project

jbundle automatically detects and handles Gradle multi-project builds with multiple application subprojects.

## How It Works

When jbundle detects `settings.gradle.kts` (or `.gradle`), it:

1. Parses included subprojects
2. Scans each for the `application` plugin
3. Extracts `mainClass`, `mainModule`, and `addModules` configuration
4. Offers selection or builds all with `--all`

## Detection

jbundle looks for these patterns in `build.gradle.kts`:

```kotlin
plugins {
    id("application")
}

application {
    mainClass.set("com.example.Main")
}
```

And module configuration:

```kotlin
javaModulePackaging {
    addModules.add("jdk.incubator.vector")
}
```

## Single Subproject

### Interactive Selection

When multiple application subprojects exist:

```bash
jbundle build --output ./dist/app
```

```
Multiple application subprojects found:
  [1] app - com.example.App
  [2] cli - com.example.Cli
  [3] server - com.example.Server

Tip: Add 'gradle_project = "app"' to jbundle.toml to skip this prompt

Select subproject [1-3]:
```

### CLI Flag

Skip the prompt with `--gradle-project`:

```bash
jbundle build --output ./dist/app --gradle-project app
```

### Configuration File

Set default in `jbundle.toml`:

```toml
gradle_project = "app"
```

## All Subprojects

Build every application subproject with `--all`:

```bash
jbundle build --output ./dist --all
```

Each binary is placed in `{output}/{subproject-name}`:

```
./dist/
├── app
├── cli
└── server
```

## Module Handling

### Automatic Detection

jbundle combines:

1. **jdeps analysis** — Scans JAR for required modules
2. **Gradle config** — Extracts `addModules.add(...)` from build files

Both are merged and deduplicated for the jlink runtime.

### Manual Override

Bypass automatic detection with `--modules`:

```bash
jbundle build --output ./dist/app \
  --gradle-project app \
  --modules java.base,java.sql,jdk.incubator.vector
```

Or in configuration:

```toml
# jbundle.toml
gradle_project = "app"
modules = ["java.base", "java.sql", "jdk.incubator.vector"]
```

## Reusing Existing Runtime

If Gradle already built a jlink image, skip the jlink step:

```bash
jbundle build --output ./dist/app \
  --gradle-project app \
  --jlink-runtime ./app/build/jlink
```

Common locations jbundle checks:

* `{subproject}/build/jlink/`
* `{subproject}/build/image/`
* `{subproject}/build/jpackage/images/app-image/`

## Build Process

For subprojects, jbundle runs:

1. `:{subproject}:shadowJar` (preferred, produces fat JAR)
2. Falls back to `:{subproject}:build` if shadowJar unavailable

Then looks for JAR in:

* `{subproject}/build/libs/*-all.jar`
* `{subproject}/build/libs/*-uber.jar`
* `{subproject}/build/libs/*.jar`

## Configuration Reference

New options for multi-project builds:

### CLI Flags

| Flag                      | Description                                         |
| ------------------------- | --------------------------------------------------- |
| `--gradle-project <NAME>` | Build specific subproject                           |
| `--all`                   | Build all application subprojects                   |
| `--build-args <ARGS>`     | Extra arguments passed to Gradle (e.g., `-P` flags) |
| `--modules <LIST>`        | Manual module list (comma-separated)                |
| `--jlink-runtime <PATH>`  | Reuse existing jlink runtime                        |

### jbundle.toml

```toml
# Subproject to build by default
gradle_project = "app"

# Manual module override
modules = ["java.base", "java.sql"]

# Extra build arguments
build_args = ["-PeaJdkBuild=false"]

# Reuse existing runtime
jlink_runtime = "./build/jlink"
```

## Examples

### Simple Multi-Project

```bash
# Interactive selection
jbundle build --output ./dist/app

# Specific subproject
jbundle build --output ./dist/cli --gradle-project cli

# All at once
jbundle build --output ./dist --all
```

### With Custom Modules

```bash
jbundle build \
  --output ./dist/app \
  --gradle-project app \
  --modules java.base,java.desktop,java.sql
```

### Reusing Gradle's jlink

```bash
# First, run Gradle's jlink task
./gradlew :app:jlink

# Then use it with jbundle
jbundle build \
  --output ./dist/app \
  --gradle-project app \
  --jlink-runtime ./app/build/image
```

## Troubleshooting

### "No application subproject found"

Check that subprojects have the `application` plugin applied:

```kotlin
plugins {
    id("application")
}
```

### "Subproject 'x' not found"

Verify the subproject name matches what's in `settings.gradle.kts`:

```kotlin
include("app")  // Use "app", not ":app"
```

### Module errors at runtime

If the app fails with `java.lang.module` errors, modules are missing. Use `--modules` to add them:

```bash
jbundle build --modules java.base,java.sql,java.logging,...
```

### shadowJar not available

If your project doesn't use Shadow plugin, ensure the regular build produces a fat JAR with all dependencies bundled.


# GitHub Actions

Build self-contained JVM binaries in your CI/CD pipeline.

## Quick Start

```yaml
name: Build Binary

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 21

      - uses: avelino/jbundle@main
        with:
          input: .
          output: ./dist/myapp

      - uses: actions/upload-artifact@v4
        with:
          name: myapp-linux-x64
          path: ./dist/myapp
```

That's it. Replace `myapp` with your app name.

## Action Reference

### Inputs

| Input          | Required | Default  | Description                                          |
| -------------- | -------- | -------- | ---------------------------------------------------- |
| `version`      | no       | `latest` | jbundle version to install (e.g., `v0.2.0`)          |
| `input`        | no       | —        | Path to project directory or JAR file                |
| `output`       | no       | —        | Output binary path                                   |
| `java-version` | no       | —        | JDK version to bundle (11, 17, 21, etc.)             |
| `target`       | no       | —        | Target platform (`linux-x64`, `macos-aarch64`, etc.) |
| `profile`      | no       | —        | JVM profile (`cli` or `server`)                      |
| `shrink`       | no       | —        | Shrink uberjar (`true` to enable)                    |
| `args`         | no       | —        | Additional arguments passed to `jbundle build`       |
| `install-only` | no       | `false`  | Only install jbundle, don't run build                |

### Outputs

| Output    | Description               |
| --------- | ------------------------- |
| `binary`  | Path to the output binary |
| `version` | Installed jbundle version |

## Examples

### CLI Tool with Fast Startup

```yaml
- uses: avelino/jbundle@main
  with:
    input: .
    output: ./dist/mycli
    profile: cli
    shrink: true
```

### Specific Java Version

```yaml
- uses: avelino/jbundle@main
  with:
    input: .
    output: ./dist/myapp
    java-version: 17
```

### From Pre-built JAR

```yaml
- name: Build JAR
  run: ./gradlew shadowJar

- uses: avelino/jbundle@main
  with:
    input: ./build/libs/app-all.jar
    output: ./dist/myapp
```

### Install Only (Custom Build Commands)

```yaml
- uses: avelino/jbundle@main
  with:
    install-only: true

- name: Build with custom flags
  run: |
    jbundle build \
      --input . \
      --output ./dist/myapp \
      --modules java.base,java.sql,java.desktop \
      --jvm-args "-Xmx1g"
```

### Extra Arguments

```yaml
- uses: avelino/jbundle@main
  with:
    input: .
    output: ./dist/myapp
    args: "--gradle-project app --build-args '-PeaJdkBuild=false'"
```

### Pin to a Specific Version

```yaml
- uses: avelino/jbundle@v0.2.0
  with:
    input: .
    output: ./dist/myapp
```

## Cross-Platform Builds

Build for multiple platforms using a matrix:

```yaml
jobs:
  build:
    strategy:
      matrix:
        include:
          - os: ubuntu-latest
            target: linux-x64
          - os: macos-14
            target: macos-aarch64
          - os: macos-13
            target: macos-x64

    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 21

      - uses: avelino/jbundle@main
        with:
          input: .
          output: ./dist/myapp-${{ matrix.target }}
          target: ${{ matrix.target }}

      - uses: actions/upload-artifact@v4
        with:
          name: myapp-${{ matrix.target }}
          path: ./dist/myapp-${{ matrix.target }}
```

## Windows CI

jbundle works on `windows-latest` runners. Build binaries for all platforms from Windows:

```yaml
jobs:
  build:
    strategy:
      matrix:
        include:
          - os: ubuntu-latest
            target: linux-x64
          - os: macos-14
            target: macos-aarch64
          - os: windows-latest
            target: linux-x64

    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 21

      - uses: avelino/jbundle@main
        with:
          input: .
          output: ./dist/myapp-${{ matrix.target }}
          target: ${{ matrix.target }}

      - uses: actions/upload-artifact@v4
        with:
          name: myapp-${{ matrix.os }}-${{ matrix.target }}
          path: ./dist/myapp-${{ matrix.target }}
```

> **Note:** The output binary uses a Unix shell stub, so `--target` must be a Linux or macOS platform. Windows is supported as a build host, not an output target.

## Gradle Multi-Project

For projects with multiple subprojects (like JabRef):

```yaml
- uses: avelino/jbundle@main
  with:
    input: .
    output: ./dist/myapp
    args: "--gradle-project app"
```

Or with a pre-built jlink runtime:

```yaml
- name: Build with Gradle
  run: ./gradlew :jabgui:jlinkZip

- uses: avelino/jbundle@main
  with:
    input: .
    output: ./dist/jabgui
    args: "--gradle-project jabgui --jlink-runtime jabgui/build/packages"
```

## Caching

Speed up builds by caching the jbundle JDK cache:

```yaml
- uses: actions/cache@v4
  with:
    path: ~/.jbundle/cache
    key: jbundle-${{ runner.os }}-${{ hashFiles('**/jbundle.toml') }}
    restore-keys: |
      jbundle-${{ runner.os }}-
```

## Reusing the CI JDK

`setup-java` sets `JAVA_HOME` automatically. jbundle detects it and skips the Adoptium download:

```yaml
- uses: actions/setup-java@v4
  with:
    distribution: temurin
    java-version: 21

# jbundle reuses JAVA_HOME — no extra download
- uses: avelino/jbundle@main
  with:
    input: .
    output: ./dist/myapp
```

## Using jbundle.toml

Instead of passing inputs, use a config file in your repo:

```toml
# jbundle.toml
java_version = 21
profile = "cli"
jvm_args = ["-Xmx512m"]
```

Then your workflow simplifies to:

```yaml
- uses: avelino/jbundle@main
  with:
    input: .
    output: ./dist/myapp
```

## Release Workflow

Create releases with binaries for all platforms:

```yaml
name: Release

on:
  push:
    tags: ["v*"]

jobs:
  build:
    strategy:
      matrix:
        include:
          - os: ubuntu-latest
            target: linux-x64
          - os: macos-14
            target: macos-aarch64

    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 21

      - uses: avelino/jbundle@main
        with:
          input: .
          output: ./myapp-${{ matrix.target }}
          target: ${{ matrix.target }}

      - uses: softprops/action-gh-release@v1
        with:
          files: ./myapp-${{ matrix.target }}
```

## Debug

Enable verbose logging:

```yaml
- uses: avelino/jbundle@main
  with:
    input: .
    output: ./dist/myapp
    args: "--verbose"
  env:
    RUST_LOG: debug
```

Or use dry-run to preview the build plan:

```yaml
- uses: avelino/jbundle@main
  with:
    input: .
    output: ./dist/myapp
    args: "--dry-run"
```

## Troubleshooting

### Build hangs at "Detecting build system"

Gradle downloading dependencies. Add caching:

```yaml
- uses: actions/cache@v4
  with:
    path: |
      ~/.gradle/caches
      ~/.gradle/wrapper
    key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
```

### Out of memory

```yaml
- uses: avelino/jbundle@main
  with:
    input: .
    output: ./dist/myapp
    args: "--jvm-args '-Xmx2g'"
```


# Dry-Run Mode

Preview what jbundle will do without executing anything.

## Usage

```bash
jbundle build --input ./my-app --output ./dist/app --dry-run
```

## What It Shows

```
[dry-run] Build plan for ./my-app → ./dist/app

  Build system:  DepsEdn (clojure -T:build uber)
  Target:        Target { os: Linux, arch: X86_64 }
  JDK version:   21
  JDK source:    cached ✓
  Modules:       auto-detect (jdeps)
  Runtime:       create via jlink
  Profile:       cli
  Shrink:        enabled
  AppCDS:        enabled
  CRaC:          disabled
  JVM args:      -Xmx512m

  No actions taken.
```

| Field                     | Description                                                         |
| ------------------------- | ------------------------------------------------------------------- |
| **Build system**          | Detected build tool and command that would run                      |
| **Target**                | OS and architecture of the output binary                            |
| **Cross-compile**         | Shown when target differs from host platform                        |
| **JDK version**           | Which JDK version will be used                                      |
| **JDK source**            | Whether the JDK is cached, local (`--java-home`), or needs download |
| **Modules**               | Auto-detect via jdeps or manual override                            |
| **Runtime**               | New via jlink or reusing existing (`--jlink-runtime`)               |
| **Profile**               | `cli` (fast startup) or `server` (throughput)                       |
| **Shrink/AppCDS/CRaC**    | Feature toggles                                                     |
| **JVM args / Build args** | Extra arguments, if any                                             |

## When to Use

**Validating config before a long build:**

```bash
# Check that jbundle.toml is picking up the right settings
jbundle build --input . --output ./app --dry-run
```

**Verifying cross-compilation setup:**

```bash
# Confirm host + target JDK strategy
jbundle build --input . --output ./app --target linux-x64 --dry-run
```

**Debugging CI:**

```bash
# Add to your CI pipeline before the real build
jbundle build --input . --output ./dist/app --dry-run
jbundle build --input . --output ./dist/app
```

**Checking build system detection:**

```bash
# Repos with multiple build files (deps.edn + project.clj)
jbundle build --input ./my-project --output ./app --dry-run
```

## Configuration

Dry-run is CLI-only — there's no `jbundle.toml` equivalent. All other config options (`--java-version`, `--profile`, `--target`, etc.) work normally with `--dry-run` to preview their effect.


# Error Diagnostics

When a build fails, jbundle displays structured diagnostics with source context.

## Diagnostic Format

jbundle parses build errors and presents them in a familiar format, similar to `rustc`:

```
error: Unable to resolve symbol: prntln
 --> src/example/core.clj:9:5
   |
 7 | (defn process-data [data]
 8 |   (let [result (map inc data)]
 9 |     (prntln "Processing:" result)
   |     ^^^^^^^ symbol not found
10 |     (reduce + result)))
```

## What You Get

* **Error type** — What went wrong
* **Location** — File, line, and column
* **Source context** — Surrounding code with the error highlighted
* **Explanation** — When available, what the error means

## Supported Build Systems

Diagnostics work with all supported build systems:

* **Clojure** — Compiler errors, syntax errors, unresolved symbols
* **Java/Maven** — Compilation errors, missing dependencies
* **Gradle** — Build failures, task errors

## Fallback Behavior

If jbundle cannot parse the error format:

* Full raw output is displayed
* No information is lost
* You see exactly what the underlying tool reported

## Examples

### Clojure Syntax Error

```
error: Unmatched delimiter: )
 --> src/myapp/core.clj:15:1
   |
13 | (defn calculate [x y]
14 |   (+ x y)
15 | ))
   | ^ unexpected closing paren
```

### Java Compilation Error

```
error: cannot find symbol
 --> src/main/java/com/example/App.java:12:9
   |
10 | public void process() {
11 |     List<String> items = new ArrayList<>();
12 |     items.add(123);
   |           ^^^ incompatible types: int cannot be converted to String
13 | }
```

### Missing Dependency

```
error: package org.apache.commons.lang3 does not exist
 --> src/main/java/com/example/Utils.java:3:1
   |
 1 | package com.example;
 2 |
 3 | import org.apache.commons.lang3.StringUtils;
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
```

## Debugging Tips

### Enable Verbose Logging

```bash
RUST_LOG=debug jbundle build --input . --output ./app
```

Shows detailed information about each build step.

### Check Build Tool Output

jbundle runs standard build commands. You can run them manually to debug:

```bash
# Clojure
clojure -T:build uber

# Leiningen
lein uberjar

# Maven
mvn package -DskipTests

# Gradle
gradle build -x test
```

### jdeps and jlink Errors

When `jdeps` fails to analyze module dependencies, jbundle shows the error and falls back to a common module set:

```
  ⚠ jdeps failed, falling back to common modules
    command: /path/to/jdeps --print-module-deps --ignore-missing-deps ...
    Error: Missing or corrupt class files
```

When `jlink` fails to create the runtime, jbundle shows the full command, exit code, and stderr:

```
Error: jlink failed: exit code 1
  command: /path/to/jlink --add-modules java.base --strip-debug ...
  stderr:
    Error: Module java.desktop not found
```

Use `--dry-run` to verify your configuration before running a full build:

```bash
jbundle build --input . --output ./app --dry-run
```

### Common Issues

| Error                                  | Likely Cause                                                |
| -------------------------------------- | ----------------------------------------------------------- |
| "No build system detected"             | Missing deps.edn/project.clj/pom.xml/build.gradle           |
| "JAR not found"                        | Build succeeded but no uberjar was created                  |
| "Main class not found"                 | MANIFEST.MF missing Main-Class entry                        |
| "Module not found"                     | jdeps detected a module that jlink can't resolve            |
| "cannot execute binary file"           | Cross-compiling but using target JDK tools — update jbundle |
| "target JDK jmods directory not found" | Target JDK missing `jmods/` directory                       |


# JAR Analysis

Inspect your JAR before packaging to understand what's inside, find optimization opportunities, and catch potential issues.

## Usage

```bash
# Analyze current project (builds uberjar first)
jbundle analyze

# Analyze a pre-built JAR directly
jbundle analyze --input ./target/app-standalone.jar
```

## What It Reports

### Category Breakdown

Every entry in the JAR is classified into one of:

| Category        | Matches                                |
| --------------- | -------------------------------------- |
| Classes         | `*.class`                              |
| Clojure sources | `*.clj`, `*.cljc`, `*.cljs`            |
| Java sources    | `*.java`                               |
| Native libs     | `*.so`, `*.dylib`, `*.dll`, `*.jnilib` |
| Metadata        | `META-INF/*` (non-class)               |
| Resources       | Everything else                        |

### Top Packages

Entries are grouped by the first 3 path segments (matching Maven groupId convention). For example, `org/apache/commons/lang3/StringUtils.class` maps to `org.apache.commons`.

### Clojure Namespaces

Detected from `__init.class` entries. For example, `myapp/core__init.class` maps to namespace `myapp.core`.

### Shrink Estimate

Shows how much space `--shrink` would save by removing non-essential files (Maven metadata, JAR signatures, Java source files, build tool artifacts).

### Potential Issues

* **Duplicate classes** — Same class path appearing multiple times (common in uberjars with dependency conflicts)
* **Large resources** — Files over 1 MB that may be worth reviewing (embedded models, datasets, etc.)

## Example Output

```
JAR: target/app-standalone.jar (87.3 MB)
Entries: 12,345

Category              Size       %    Files
────────────────────────────────────────────────
Classes           42.1 MB    48%    8,432
Resources         38.7 MB    44%    1,203
Native libs        5.2 MB     6%       12
Metadata           1.3 MB     2%      806

Top packages by size:
  org.apache.poi                       28.4 MB  1,322 files
  com.google.guava                      3.1 MB    456 files
  org.clojure                           2.8 MB    342 files

Clojure namespaces:
  clojure.core                          2.1 MB    342 files
  myapp.handlers                        0.5 MB     28 files

Estimated --shrink savings: 12.4 MB (14%) — 892 removable files

Potential issues:
  Duplicate class: javax/servlet/Servlet.class (3 occurrences)
  Large resource: data/model.bin (8.5 MB)
```

## When to Use

* **Before first build** — Understand your JAR composition and spot bloat
* **Evaluating --shrink** — See the savings estimate before enabling it
* **Debugging binary size** — Find which dependencies are largest
* **Dependency conflicts** — Detect duplicate classes from overlapping dependencies


# JabRef (Complex Gradle)

[JabRef](https://github.com/JabRef/jabref) is a complex Gradle multi-project with multiple application subprojects. This guide shows how to package it with jbundle.

## Project Structure

JabRef uses a multi-module Gradle setup:

```
jabref/
├── settings.gradle.kts
├── build.gradle.kts
├── jabkit/
│   └── build.gradle.kts      # CLI tool (application plugin)
├── jabgui/
│   └── build.gradle.kts      # GUI application (application plugin)
├── jabsrv-cli/
│   └── build.gradle.kts      # Server CLI (application plugin)
├── jabls-cli/
│   └── build.gradle.kts      # Language server (application plugin)
└── jablib/
    └── build.gradle.kts      # Shared library (no application)
```

## Auto-Detection

jbundle automatically detects multi-project builds by parsing `settings.gradle.kts` and scanning subprojects for the `application` plugin.

```bash
cd jabref
jbundle build --output ./dist/app
```

When multiple application subprojects are found, jbundle prompts for selection:

```
Multiple application subprojects found:
  [1] jabkit - org.jabref.cli.JabKit
  [2] jabgui - org.jabref.gui.JabRefGUI
  [3] jabsrv-cli - org.jabref.http.server.Server
  [4] jabls-cli - org.jabref.language.JabLS

Tip: Add 'gradle_project = "jabkit"' to jbundle.toml to skip this prompt

Select subproject [1-4]:
```

## Building a Single Subproject

### Using CLI Flag

```bash
jbundle build --input . --output ./dist/jabkit --gradle-project jabkit
```

### Using Configuration File

```toml
# jbundle.toml
gradle_project = "jabkit"
java_version = 21
profile = "cli"
```

Then simply:

```bash
jbundle build --output ./dist/jabkit
```

## Building All Subprojects

Use `--all` to build every application subproject at once:

```bash
jbundle build --input . --output ./dist --all
```

Output:

```
Building 4 application subprojects:
  - jabkit (org.jabref.cli.JabKit)
  - jabgui (org.jabref.gui.JabRefGUI)
  - jabsrv-cli (org.jabref.http.server.Server)
  - jabls-cli (org.jabref.language.JabLS)

━━━ Building jabkit ━━━
[1/6] Detecting build system.............. Gradle multi-project (jabkit)
[2/6] Building uberjar.................... jabkit-all.jar
[3/6] Downloading JDK 21.................. ready
[4/6] Analyzing module dependencies....... 42 modules
[5/6] Creating minimal runtime............ done
[6/6] Packing binary...................... ./dist/jabkit (48.2 MB)

━━━ Building jabgui ━━━
...

━━━ Build complete ━━━
Built 4 binaries:
  - ./dist/jabkit
  - ./dist/jabgui
  - ./dist/jabsrv-cli
  - ./dist/jabls-cli
```

## Module Detection

JabRef uses Java modules with incubator features. jbundle extracts `addModules` from `build.gradle.kts`:

```kotlin
// jabkit/build.gradle.kts
javaModulePackaging {
    addModules.add("jdk.incubator.vector")
}
```

These modules are automatically included in the jlink runtime, combined with jdeps analysis.

### Manual Module Override

If auto-detection misses modules or you want full control:

```bash
jbundle build --output ./dist/jabkit \
  --gradle-project jabkit \
  --modules java.base,java.sql,java.xml,jdk.incubator.vector
```

Or in configuration:

```toml
# jbundle.toml
gradle_project = "jabkit"
modules = ["java.base", "java.sql", "java.xml", "jdk.incubator.vector"]
```

## Reusing Existing jlink Runtime

If JabRef's Gradle build already created a jlink image, reuse it:

```bash
jbundle build --output ./dist/jabkit \
  --gradle-project jabkit \
  --jlink-runtime ./jabkit/build/jlink
```

This skips the JDK download and jlink steps, using the pre-built runtime.

## Complete Configuration

Full `jbundle.toml` for JabRef:

```toml
# jbundle.toml

# Build jabkit by default
gradle_project = "jabkit"

# Use Java 21 (required by JabRef)
java_version = 21

# CLI profile for fast startup
profile = "cli"

# JVM settings for JabRef
jvm_args = ["-Xmx1g", "--enable-preview"]

# Shrink the JAR
shrink = true
```

## CI/CD Integration

### GitHub Actions

```yaml
name: Build Binaries

on:
  release:
    types: [created]

jobs:
  build:
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest]
        include:
          - os: ubuntu-latest
            target: linux-x64
          - os: macos-latest
            target: macos-aarch64

    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v4

      - name: Install jbundle
        run: cargo install jbundle

      - name: Build all applications
        run: |
          jbundle build \
            --input . \
            --output ./dist \
            --target ${{ matrix.target }} \
            --all

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: binaries-${{ matrix.target }}
          path: ./dist/*
```

## Troubleshooting

### "No application subproject found"

Ensure subprojects have the `application` plugin:

```kotlin
plugins {
    id("application")
}

application {
    mainClass.set("com.example.Main")
}
```

### "shadowJar task not found"

jbundle tries `:subproject:shadowJar` first, then falls back to `:subproject:build`. If you're not using Shadow plugin, ensure regular build produces a fat JAR.

### Module resolution errors

If jdeps fails to detect all required modules, use `--modules` to specify them manually:

```bash
jbundle build --modules java.base,java.desktop,java.sql,...
```

### Build takes too long

For development iteration, consider:

1. Using `--jlink-runtime` with a pre-built runtime
2. Building only the subproject you're working on (not `--all`)


# CLI Commands

Complete reference for jbundle command-line interface.

## jbundle build

Build a self-contained binary from a JVM project or JAR.

```bash
jbundle build [OPTIONS] --input <PATH> --output <PATH>
```

### Required Arguments

| Argument          | Description                   |
| ----------------- | ----------------------------- |
| `--input <PATH>`  | Project directory or JAR file |
| `--output <PATH>` | Output binary path            |

### Options

| Option                    | Default  | Description                                                                                |
| ------------------------- | -------- | ------------------------------------------------------------------------------------------ |
| `--java-version <N>`      | `21`     | JDK version to bundle (11, 17, 21, 22, 23, 24, 25)                                         |
| `--target <TARGET>`       | current  | Target platform (see [Platforms](/reference/platforms))                                    |
| `--profile <PROFILE>`     | `server` | JVM profile (`cli` or `server`)                                                            |
| `--jvm-args <ARGS>`       | —        | JVM arguments (e.g., `-Xmx512m`)                                                           |
| `--build-args <ARGS>`     | —        | Extra arguments passed to the build tool (e.g., Gradle `-P` flags)                         |
| `--shrink [true\|false]`  | `false`  | Shrink uberjar by removing non-essential files                                             |
| `--no-appcds`             | —        | Disable AppCDS generation                                                                  |
| `--crac`                  | —        | Enable CRaC checkpoint (Linux only)                                                        |
| `--compact-banner`        | —        | Use a compact banner in the wrapper                                                        |
| `--gradle-project <NAME>` | —        | Gradle subproject to build (multi-project)                                                 |
| `--all`                   | —        | Build all application subprojects (Gradle)                                                 |
| `--modules <LIST>`        | —        | Manual module list, comma-separated                                                        |
| `--java-home <PATH>`      | —        | Path to existing JDK installation (skips JDK download). Falls back to `JAVA_HOME` env var. |
| `--jlink-runtime <PATH>`  | —        | Path to existing jlink runtime to reuse (must contain `bin/java`)                          |
| `--dry-run`               | —        | Show build plan without executing anything                                                 |
| `-v, --verbose`           | —        | Enable verbose output                                                                      |

### Examples

```bash
# Basic build
jbundle build --input ./my-app --output ./dist/app

# With Java 17
jbundle build --input . --output ./app --java-version 17

# CLI profile for fast startup
jbundle build --input . --output ./app --profile cli

# Cross-compile for Linux
jbundle build --input . --output ./app --target linux-x64

# Multiple JVM arguments (use server profile with custom GC)
jbundle build --input . --output ./app --profile server --jvm-args "-Xmx512m -XX:+UseZGC"

# Shrink the uberjar (remove non-essential files)
jbundle build --input . --output ./app --shrink

# Explicitly disable shrinking
jbundle build --input . --output ./app --shrink false

# From pre-built JAR
jbundle build --input ./target/app.jar --output ./dist/app

# With CRaC (Linux)
jbundle build --input . --output ./app --crac

# Gradle multi-project: specific subproject
jbundle build --input . --output ./dist/app --gradle-project app

# Gradle multi-project: build all
jbundle build --input . --output ./dist --all

# Manual module specification
jbundle build --input . --output ./app --modules java.base,java.sql,java.logging

# Use existing JDK (skip download)
jbundle build --input . --output ./app --java-home /usr/lib/jvm/java-21

# Reuse existing jlink runtime
jbundle build --input . --output ./app --jlink-runtime ./build/jlink

# Pass extra arguments to Gradle
jbundle build --input . --output ./app --build-args "-PeaJdkBuild=false" --build-args "-PprojVersion=xyz"

# Preview build plan without executing
jbundle build --input . --output ./app --dry-run

# Dry-run with specific target to verify cross-compilation setup
jbundle build --input . --output ./app --target linux-x64 --dry-run
```

## jbundle analyze

Analyze a JAR or project and report size breakdown, top dependencies, and potential issues.

```bash
jbundle analyze [OPTIONS]
```

### Options

| Option           | Default | Description                             |
| ---------------- | ------- | --------------------------------------- |
| `--input <PATH>` | `.`     | Project directory or pre-built JAR file |

When given a project directory, jbundle detects the build system, builds the uberjar, then analyzes it. When given a JAR file directly, it skips the build step.

### Output

The report includes:

* **Category breakdown** — Classes, Resources, Native libs, Metadata, Clojure/Java sources with size and file count
* **Top packages by size** — Grouped by first 3 path segments (e.g., `org.apache.commons`)
* **Clojure namespaces** — Detected from `__init.class` entries
* **Shrink estimate** — How much `--shrink` would save
* **Potential issues** — Duplicate classes, large resources (> 1 MB)

### Examples

```bash
# Analyze current project
jbundle analyze

# Analyze a specific project
jbundle analyze --input ./my-app

# Analyze a pre-built JAR
jbundle analyze --input ./target/app-standalone.jar
```

## jbundle info

Display cache information.

```bash
jbundle info
```

Shows:

* Cached JDK downloads
* Extracted runtimes
* Application caches
* Total cache size

## jbundle clean

Remove all cached data.

```bash
jbundle clean
```

Removes everything in `~/.jbundle/cache/`.

## jbundle --version

Print version information.

```bash
jbundle --version
```

## jbundle --help

Print help message.

```bash
jbundle --help
jbundle build --help
```

## Exit Codes

| Code | Meaning                                                |
| ---- | ------------------------------------------------------ |
| `0`  | Success                                                |
| `1`  | Build error (compilation failed, JAR not found, etc.)  |
| `2`  | Configuration error (invalid arguments, missing input) |

## Environment Variables

| Variable    | Description                                                                  |
| ----------- | ---------------------------------------------------------------------------- |
| `JAVA_HOME` | Path to existing JDK installation (used when `--java-home` is not specified) |
| `RUST_LOG`  | Logging level (`error`, `warn`, `info`, `debug`, `trace`)                    |

### Logging Examples

```bash
# Show debug output
RUST_LOG=debug jbundle build --input . --output ./app

# Show only errors
RUST_LOG=error jbundle build --input . --output ./app

# Verbose trace logging
RUST_LOG=trace jbundle build --input . --output ./app
```


# Supported Platforms

jbundle can create binaries for multiple platforms.

## Available Targets

| Target          | Architecture          | OS    |
| --------------- | --------------------- | ----- |
| `linux-x64`     | x86\_64               | Linux |
| `linux-aarch64` | ARM64                 | Linux |
| `macos-x64`     | x86\_64               | macOS |
| `macos-aarch64` | ARM64 (Apple Silicon) | macOS |

## Usage

```bash
# Build for current platform (default)
jbundle build --input . --output ./app

# Build for Linux x64
jbundle build --input . --output ./app --target linux-x64

# Build for Linux ARM64
jbundle build --input . --output ./app --target linux-aarch64

# Build for macOS Intel
jbundle build --input . --output ./app --target macos-x64

# Build for macOS Apple Silicon
jbundle build --input . --output ./app --target macos-aarch64
```

Or in `jbundle.toml`:

```toml
target = "linux-x64"
```

## Cross-Compilation

jbundle supports cross-compilation. You can build Linux binaries from macOS:

```bash
# On macOS, build for Linux
jbundle build --input . --output ./app-linux --target linux-x64

# Preview what cross-compilation will do
jbundle build --input . --output ./app-linux --target linux-x64 --dry-run
```

When cross-compiling, jbundle downloads **two** JDKs:

1. **Host JDK** — Used to run `jdeps` (module detection) and `jlink` (runtime creation)
2. **Target JDK** — Provides the `jmods` directory for the target platform

This ensures that `jdeps` and `jlink` can execute on your machine while producing a runtime for the target platform.

## Platform Detection

When no `--target` is specified, jbundle detects the current platform:

| Host OS | Host Arch | Default Target  |
| ------- | --------- | --------------- |
| macOS   | ARM64     | `macos-aarch64` |
| macOS   | x86\_64   | `macos-x64`     |
| Linux   | x86\_64   | `linux-x64`     |
| Linux   | ARM64     | `linux-aarch64` |
| Windows | x86\_64   | `windows-x64`   |

## CI/CD Example

Build for multiple platforms in GitHub Actions:

```yaml
jobs:
  build:
    strategy:
      matrix:
        target: [linux-x64, linux-aarch64, macos-x64, macos-aarch64]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build
        run: jbundle build --input . --output ./dist/app-${{ matrix.target }} --target ${{ matrix.target }}
      - uses: actions/upload-artifact@v4
        with:
          name: app-${{ matrix.target }}
          path: ./dist/app-${{ matrix.target }}
```

## Windows Support

> **Help wanted:** Windows support is maintained on a best-effort basis. If you use jbundle on Windows and want to help keep it working, we'd love your contributions — see [open issues](https://github.com/avelino/jbundle/issues).

jbundle runs on Windows as a **build host** — pre-compiled binaries are available for Windows x86\_64. You can use jbundle on Windows to build Linux and macOS binaries via cross-compilation.

**What works on Windows:**

* Installing and running jbundle (`jbundle.exe`)
* Building uberjars from JVM projects
* Cross-compiling to Linux/macOS targets
* Using `--java-home` or `JAVA_HOME` to reuse a local JDK

**Limitation:** The output binary uses a Unix shell stub (`/bin/sh`), so Windows is not supported as an **output target**. The binaries jbundle produces run on Linux and macOS.

### Install on Windows

```powershell
irm https://raw.githubusercontent.com/avelino/jbundle/main/install.ps1 | iex
```

### Example: Build Linux binary from Windows

```powershell
jbundle build --input . --output ./dist/myapp --target linux-x64
```

## Notes

* **CRaC** is Linux-only (checkpoint/restore requires Linux kernel features)
* **Binary format** differs between platforms (ELF on Linux, Mach-O on macOS)
* **Shell stub** uses `/bin/sh` which is available on all Unix-like systems
* **Windows** is supported as a build host but not as an output target


# JDK Versions

jbundle downloads JDK runtimes from [Adoptium](https://adoptium.net/).

## Supported Versions

| Version | Type | Status      | Notes       |
| ------- | ---- | ----------- | ----------- |
| `11`    | LTS  | Supported   |             |
| `17`    | LTS  | Supported   |             |
| `21`    | LTS  | **Default** | Recommended |
| `22`    | STS  | Supported   |             |
| `23`    | STS  | Supported   |             |
| `24`    | STS  | Supported   |             |
| `25`    | LTS  | Supported   |             |

**LTS** = Long Term Support (recommended for production) **STS** = Short Term Support

## Usage

```bash
# Use default (21)
jbundle build --input . --output ./app

# Specify version
jbundle build --input . --output ./app --java-version 17
jbundle build --input . --output ./app --java-version 25
```

Or in `jbundle.toml`:

```toml
java_version = 21
```

## Why Not Java 8?

jbundle requires `jlink` and `jdeps`, which were introduced in Java 9. These tools are essential for:

* **jdeps** — Analyzing module dependencies
* **jlink** — Creating minimal custom runtimes

Java 8 predates the module system (JPMS) and doesn't include these tools.

## Recommendations

### Production

Use **LTS versions** (11, 17, 21, 25):

* Longer support lifecycle
* More stable
* Security updates for years

### New Projects

Use **Java 21** (current LTS):

* Modern features (virtual threads, pattern matching)
* Long-term support until 2029+
* Best balance of features and stability

### Specific Features

| If you need...              | Minimum version |
| --------------------------- | --------------- |
| Virtual threads             | 21              |
| Pattern matching for switch | 21              |
| Records                     | 16              |
| Text blocks                 | 15              |
| `var` keyword               | 10              |

## AppCDS Compatibility

AppCDS (automatic shared archive) requires JDK 19+. On older JDKs:

* JDK 11-18: Manual CDS configuration (not automatic)
* JDK 19+: Automatic AppCDS via `-XX:+AutoCreateSharedArchive`

For best startup performance, use JDK 21 or newer.

## JDK Download & Cache

JDKs are downloaded from the Adoptium API and cached locally:

```
~/.jbundle/cache/jdk-21-linux-x64/
~/.jbundle/cache/jdk-21-macos-aarch64/
```

Downloads are verified with SHA256 checksums. Re-running builds with the same JDK version reuses the cached download.

## Adoptium vs Other JDKs

jbundle uses [Eclipse Temurin](https://adoptium.net/temurin/) (Adoptium's distribution) because:

* Open source
* Free for commercial use
* Reliable API for automated downloads
* Available for all supported platforms
* TCK certified (passes Java compatibility tests)

Custom JDK distributions (Oracle, Azul, Amazon Corretto) are not currently supported.

### Exception: CRaC

For CRaC support (`--crac`), you need a JDK with CRaC patches. Currently, this means [Azul Zulu with CRaC](https://www.azul.com/products/components/crac/). CRaC support in Adoptium/Temurin is planned but not yet available.


# jbundle vs jpackage

They solve different problems.

## Quick Summary

| Tool         | Purpose                 | Output                       |
| ------------ | ----------------------- | ---------------------------- |
| **jbundle**  | Native-feeling binaries | Single executable            |
| **jpackage** | Traditional installers  | .exe, .msi, .dmg, .deb, .rpm |

## jpackage: Distribution via Installers

**jpackage** is included in JDK 14+. It creates platform-specific installers:

* Windows: `.exe`, `.msi`
* macOS: `.dmg`, `.pkg`
* Linux: `.deb`, `.rpm`

The workflow is traditional:

```
User downloads installer → Runs installer → App installed to system → Launch from menu
```

jpackage bundles a full JVM runtime, but that's where optimization ends. No startup improvements, no runtime tuning. It's essentially "take your JAR, wrap it with a JVM, generate an installer."

**Best for:** Desktop applications where users expect traditional installation.

## jbundle: Native-Feeling Binaries

**jbundle** creates a single executable binary. The workflow mirrors Go or Rust:

```
Download → chmod +x → Run
```

No installer. No installation step. No system directories. The binary is self-contained — move it anywhere, copy to a server, distribute via curl.

**Best for:** CLI tools, microservices, serverless functions.

## Detailed Comparison

| Aspect                   | jbundle                       | jpackage                     |
| ------------------------ | ----------------------------- | ---------------------------- |
| **Output**               | Single executable             | Platform installers          |
| **User experience**      | Download → run                | Download → install → run     |
| **JVM size**             | Minimal (\~30-50MB via jlink) | Full JDK (\~300MB)           |
| **Startup optimization** | AppCDS, CRaC, profiles        | None                         |
| **Distribution**         | curl, cp, scp                 | App stores, package managers |
| **System integration**   | None                          | File associations, shortcuts |
| **Target audience**      | Developers, DevOps            | End users                    |

## Why Startup Time Matters

The JVM is notorious for slow startup. A simple "hello world" can take 500ms+. For CLI tools or short-lived processes, this is unacceptable.

jbundle attacks this from multiple angles:

### 1. Minimal Runtime (jlink)

Instead of the full JDK (\~300MB), jbundle uses `jdeps` to detect which modules your app uses, then `jlink` to create a minimal runtime (\~30-50MB). The runtime is created without internal compression and then compressed once at maximum level in the final binary — avoiding double-compression overhead. Less code to load = faster startup.

### 2. AppCDS (Class Data Sharing)

On first run, the JVM generates a shared archive with pre-parsed class metadata. Subsequent runs load this cache directly, cutting startup by 60-75%.

### 3. Profile-Specific JVM Flags

The `--profile cli` option configures the JVM for short-lived processes:

* C1-only compilation (skip C2)
* SerialGC (simpler, faster startup)
* Reduced code cache

Result: \~200-350ms startup for CLI tools.

### 4. CRaC (Coordinated Restore at Checkpoint)

On supported JDKs, jbundle can create a checkpoint of your warmed-up app. Subsequent runs restore in 10-50ms — essentially native binary territory.

## When to Use What

### Use jpackage when:

* Building desktop applications with GUIs
* Users expect traditional installation
* You need system integration (file associations, shortcuts)
* Distributing through app stores

### Use jbundle when:

* Building CLI tools
* Building microservices or serverless functions
* You want Go/Rust-style distribution
* Startup time matters
* Deploying to servers or containers

## tl;dr

* **jpackage** = installers for desktop apps
* **jbundle** = native-feeling binaries with optimized startup


# jbundle vs GraalVM

Both aim to simplify JVM application distribution. They take fundamentally different approaches.

## Quick Summary

| Tool        | Approach                 | Trade-off                           |
| ----------- | ------------------------ | ----------------------------------- |
| **jbundle** | Bundle minimal JVM + JAR | Full compatibility, good startup    |
| **GraalVM** | Compile to native code   | Fast startup, limited compatibility |

## GraalVM native-image

GraalVM compiles JVM bytecode to native machine code ahead-of-time (AOT). The result is a true native executable — no JVM at runtime.

**Pros:**

* Instant startup (\~10-50ms)
* Small binaries (\~20-40MB)
* Lower memory footprint

**Cons:**

* Long build times (minutes)
* Compatibility issues (reflection, dynamic proxies)
* Complex configuration required
* Not everything works

## jbundle

jbundle bundles a minimal JVM runtime with your application. The JVM runs at runtime, but optimized for startup.

**Pros:**

* 100% JVM compatible
* Fast builds (seconds)
* No configuration required
* Everything that works on JVM works here

**Cons:**

* Larger binaries (\~30-50MB)
* Slower startup than true native (\~200-350ms)

## Detailed Comparison

| Aspect             | jbundle                  | GraalVM native-image            |
| ------------------ | ------------------------ | ------------------------------- |
| **Compatibility**  | 100% JVM                 | Requires reflection config      |
| **Build time**     | Fast (jlink + packaging) | Slow (AOT compilation)          |
| **Binary size**    | \~30-50 MB               | \~20-40 MB                      |
| **Startup (warm)** | \~200-350ms (AppCDS)     | \~10-50ms                       |
| **Setup**          | Just `jbundle`           | GraalVM + native-image + config |
| **Debug**          | Standard JVM tools       | Limited                         |

## The Compatibility Problem

GraalVM uses closed-world analysis — it must know every class at compile time. This breaks:

* **Reflection** — Requires manual `reflect-config.json`
* **Dynamic proxies** — Requires `proxy-config.json`
* **Runtime class loading** — Not supported
* **Many libraries** — Especially older ones

You'll spend hours writing configuration files, debugging `ClassNotFoundException`, and discovering that library X doesn't support native-image.

### Example: Reflection Configuration

```json
// reflect-config.json
[
  {
    "name": "com.example.MyClass",
    "allDeclaredFields": true,
    "allDeclaredMethods": true,
    "allDeclaredConstructors": true
  },
  {
    "name": "com.fasterxml.jackson.databind.ObjectMapper",
    "allDeclaredMethods": true
  }
]
```

This must be maintained as your code changes. Miss one class? Runtime failure.

## jbundle's Approach

jbundle keeps the full JVM, optimizing startup through:

1. **Minimal runtime** — Only modules your app needs
2. **AppCDS** — Pre-parsed class metadata
3. **JVM profiles** — Tuned flags for CLI vs server
4. **CRaC** — Checkpoint/restore for near-native startup

Everything works. No configuration. No compatibility matrix.

## Performance Comparison

### Startup Time

| Scenario        | jbundle (cli) | GraalVM   |
| --------------- | ------------- | --------- |
| First run       | \~800-1500ms  | \~10-50ms |
| Subsequent runs | \~200-350ms   | \~10-50ms |
| With CRaC       | \~10-50ms     | \~10-50ms |

### Build Time

| Project Size | jbundle | GraalVM    |
| ------------ | ------- | ---------- |
| Small        | \~5s    | \~30s      |
| Medium       | \~10s   | \~2-5min   |
| Large        | \~20s   | \~10-20min |

## When to Use What

### Use GraalVM native-image when:

* Startup time is critical (serverless, CLI)
* Your dependencies are native-image compatible
* You have time to maintain reflection configs
* Binary size matters

### Use jbundle when:

* You need full JVM compatibility
* Build time matters (CI/CD)
* You don't want configuration overhead
* Libraries don't support native-image
* You want "it just works"

### Use both?

Some teams use:

* **jbundle** for development and testing (fast builds, full compat)
* **GraalVM** for production (optimal startup)

This works if your app is GraalVM-compatible.

## tl;dr

* **GraalVM** = true native compilation (when it works)
* **jbundle** = JVM with optimized startup (always works)

Choose based on your compatibility requirements and how much configuration you're willing to maintain.


# Contributing

Contributions are welcome!

## Getting Started

### Prerequisites

* [Rust toolchain](https://rustup.rs/) (1.70+)
* Git

### Setup

```bash
# Clone
git clone https://github.com/avelino/jbundle.git
cd jbundle

# Build
cargo build

# Run tests
cargo test

# Lint
cargo clippy -- -D warnings

# Format
cargo fmt
```

## Testing Changes

Run against example projects:

```bash
# Clojure (deps.edn)
cargo run -- build --input ./example/clojure-deps --output ./dist/app

# Clojure (Leiningen)
cargo run -- build --input ./example/clojure-lein --output ./dist/app

# Java (Maven)
cargo run -- build --input ./example/java-pom --output ./dist/app

# Java (Gradle)
cargo run -- build --input ./example/java-gradle --output ./dist/app

# Run the generated binary
./dist/app
```

With verbose logging:

```bash
RUST_LOG=debug cargo run -- build --input ./example/clojure-deps --output ./dist/app
```

## Pull Request Process

1. **Fork** the repository
2. **Create a branch** for your change (`git checkout -b feature/my-feature`)
3. **Make changes** with tests if applicable
4. **Run checks** (`cargo test && cargo clippy && cargo fmt --check`)
5. **Open a pull request**

### PR Guidelines

* Keep changes focused — one feature/fix per PR
* Include tests for new functionality
* Update documentation if behavior changes
* Follow existing code style

## Contribution Ideas

Looking for something to work on? Here are some areas that need help:

### Features

* Windows support
* Custom `jlink` module list override
* Compression options (zstd, xz)
* Homebrew formula
* Pre-built binary releases

### Documentation

* More language-specific examples (Kotlin, Scala)
* CI/CD integration guides
* Troubleshooting guide expansion

### Testing

* More integration tests
* Platform-specific test coverage
* Performance benchmarks

## Code Structure

```
src/
├── main.rs          # CLI and pipeline orchestration
├── config.rs        # BuildConfig, Target types
├── detect.rs        # Build system detection
├── build.rs         # JAR building
├── jlink.rs         # Runtime creation
├── error.rs         # PackError enum
├── jvm/
│   ├── adoptium.rs  # Adoptium API client
│   ├── download.rs  # HTTP download
│   └── cache.rs     # JDK extraction and caching
└── pack/
    ├── archive.rs   # tar.gz creation
    ├── stub.rs      # Shell stub generation
    └── mod.rs       # Final binary assembly
```

## Questions?

* Open an issue for bugs or feature requests
* Discussions welcome in pull requests

## License

By contributing, you agree that your contributions will be licensed under MIT.


