I built a tool to solve a problem I kept hitting: deploying Clojure apps without requiring Java on the target machine.23:20:00 [3/101]
The usual answer is GraalVM native-image, but in practice it means dealing with reflection configs, library incompatibilities, long build times, and a complex toolchain. For many projects it's more friction than it's worth.
clj-pack takes a different approach: it bundles a minimal JVM runtime (via jlink) with your uberjar into a single executable. The result is a binary that runs anywhere with zero external dependencies and full JVM compatibility — no reflection configs, no unsupported libraries, your app runs exactly as it does in development.
clj-pack build --input ./my-project --output ./dist/my-app
./dist/my-app # no Java needed
How it works:
Detects your build system (deps.edn or project.clj)
Compiles the uberjar
Downloads a JDK from Adoptium (cached locally)
Uses jdeps + jlink to create a minimal runtime (~30-50 MB)
Packs everything into a single binary
The binary extracts on first run (cached by content hash), subsequent runs are instant.
Trade-off is honest: binaries are slightly larger than GraalVM output (~30-50 MB vs ~20-40 MB), and first execution has extraction overhead. But you get full compatibility and a simple build process in return.
Written in Rust, supports Linux and macOS (x64/aarch64).
whats the advantage over just manually making a uberjar and using jlink/jpackage?
Do you have the ability to crosscompile to other architectures/OS?
Do you have the ability to generate a plain executable? the jlink/jpackage route ends up generating an "installers" for each system, which i find hard/annoying to test and people are reluctant to install a program you send them
In the past ive ended up distributing an uberjar bc i didnt have the setup to test all the resulting bundles (esp MacOS which requires buying a separate machine). I also found JavaFX to be a bit inconsistent.. though its been a few years and maybe the situation has improved
The main pain point jbundle solves is that jpackage generates installers (.deb, .rpm, .dmg, .msi), not plain executables. jbundle produces a single self-contained binary — just a shell stub concatenated with a compressed payload. You chmod +x it, distribute it, and the user runs ./app. No installation step, no system-level changes.
It also automates the full pipeline (detect build system → build uberjar → download JDK → jdeps → jlink → pack) so you don't need a JDK installed on the build machine — it fetches the exact version from Adoptium. Plus it includes startup optimizations like AppCDS (auto-created on first run, JDK 19+), CRaC checkpoints, and profile-tuned JVM flags for CLI vs server workloads.
Cross-compilation:
Yes — jbundle build --target linux-x64 (or linux-aarch64, macos-x64, macos-aarch64). Since the JAR is platform-independent, it just downloads the appropriate JDK runtime for the target OS/arch from Adoptium and bundles it. You can build a Linux binary from macOS and vice-versa.
Plain executable (not an installer):
That's exactly what jbundle produces. The output is a single file you can scp to a server or hand to someone. On first run it extracts the runtime and jar to ~/.jbundle/cache/ (keyed by content hash), so subsequent runs are instant. No .deb, no .dmg, no "install this first" — just a binary.
For the macOS testing concern: since it's a CLI binary (not a .app bundle), it doesn't require signing/notarization to run. And with --target macos-aarch64 you can build it from a Linux CI without needing a Mac.
When we think of Git, the first association we make is with code version control. However, Git’s internal mechanisms are so well designed that they can be leveraged for purposes beyond traditional version control. A particularly interesting use is as a database, especially for data that benefits from historical and chronological tracking. In this post, we’ll explore Git’s internal structure and how it can be adapted to function as an efficient database.
> The impact of a high number of inodes and the limitations of the number of inodes in Unix-like systems. A very valid point. For which I created 4 million directories, under ...
"FAMF" is horrible for large volumes of writing, it might work well for KPM, but I wouldn't trade DB (even if it's SQLite) for FAMF, probably because I'm a software engineer (as you mentioned in your text)
Discover how tech-enabled companies like Airbnb, Uber and Amazon are innovating and scaling rapidly, as we explore success beyond technology in startups
I turned a Clojure database into a .so that runs embedded in Rust, Python, or anything that loads FFI
Zero JVM at runtime. GraalVM Native Image did the heavy lifting