Clojure + Crema: a native binary that runs full JVM Clojure with fast startup.
Cream uses GraalVM's Crema (RuntimeClassLoading) to enable runtime eval,
require, and library loading in a native binary. It can also run Java source
files directly, as a fast alternative to
JBang.
Warning: Cream is very alpha. It depends on GraalVM Crema (EA) and a custom Clojure fork. Do not use in production. Issues and ideas are welcome though: https://github.com/borkdude/cream/issues
Download from the latest dev release:
# macOS (Apple Silicon)
curl -sL https://github.com/borkdude/cream/releases/download/dev/cream-macos-aarch64.tar.gz | tar xz
# Linux (x86_64)
curl -sL https://github.com/borkdude/cream/releases/download/dev/cream-linux-amd64.tar.gz | tar xz
# Windows (PowerShell)
# Invoke-WebRequest -Uri https://github.com/borkdude/cream/releases/download/dev/cream-windows-amd64.zip -OutFile cream.zip
# Expand-Archive cream.zip -DestinationPath .
sudo mv cream /usr/local/bin/ # macOS/LinuxOr build from source (see Building from source).
$ ./cream -M -e '(+ 1 2 3)'
6Unlike babashka, cream supports definterface, deftype, gen-class, and
other constructs that generate JVM bytecode at runtime:
$ ./cream -M -e '(do (definterface IGreet (greet [name]))
(deftype Greeter [] IGreet (greet [_ name] (str "Hello, " name)))
(.greet (Greeter.) "world"))'
"Hello, world"Use -Sdeps to add dependencies:
./cream -Sdeps '{:deps {org.clojure/data.json {:mvn/version "RELEASE"}}}' \
-M -e '(do (require (quote [clojure.data.json :as json])) (json/write-str {:a 1}))'
;; => "{\"a\":1}"A deps.edn in the current directory is picked up automatically:
$ cat deps.edn
{:paths ["src"] :deps {dev.weavejester/medley {:mvn/version "1.9.0"}}}
$ ./cream -M -m my.appA pom.xml is picked up the same way when there is no deps.edn, so a Maven
project needs no extra configuration. See
examples/java-pom:
./cream src/main/java/com/example/Hello.javaUse -M:alias to add :extra-deps, :extra-paths and :main-opts from an
alias. -A, -X and -T work as in the Clojure CLI:
./cream -M:testUse -Scp to pass a classpath directly. Nothing is resolved and deps.edn is
ignored:
./cream -Scp "$(clojure -Spath)" -M -m my.appDependencies are resolved with deps.clj,
which is built into the binary. Resolving needs java on the PATH or
JAVA_HOME set, reading the cached classpath from .cpcache does not. The
first resolution downloads the Clojure tools jar to ~/.deps.clj.
Cream can run .java source files directly, compiling and caching them
automatically. This makes it a fast alternative to JBang.
public class Hello {
public static void main(String[] args) {
System.out.println("Hello from Java!");
if (args.length > 0) {
System.out.println("Args: " + String.join(", ", args));
}
}
}$ ./cream /tmp/Hello.java
Hello from Java!
$ ./cream /tmp/Hello.java world
Hello from Java!
Args: worldA package declaration is honored, so files in a Maven source tree run as they
are.
Use //DEPS comments (same syntax as JBang) to declare Maven dependencies:
//DEPS commons-codec:commons-codec:1.17.1
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
public class HelloCodec {
public static void main(String[] args) {
String input = args.length > 0 ? args[0] : "hello world";
System.out.println("Input: " + input);
System.out.println("Hex: " + Hex.encodeHexString(input.getBytes()));
System.out.println("SHA-256: " + DigestUtils.sha256Hex(input));
}
}$ time ./cream /tmp/HelloCodec.java
Input: hello world
Hex: 68656c6c6f20776f726c64
SHA-256: b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9
./cream /tmp/HelloCodec.java 0.02s user 0.01s system 93% cpu 0.031 totalDependencies are resolved from Maven Central using
deps.clj, with no external tooling
required. Compiled classes and resolved classpaths are cached under
$XDG_CACHE_HOME/cream/ (defaulting to ~/.cache/cream/) so subsequent runs
skip compilation and dependency resolution.
Clojure code runs from the binary alone, with no JDK on the system. This covers Java interop and every library in Tested libraries, including HTTPS.
Running .java files requires JAVA_HOME pointing to a JDK, since cream
shells out to javac. A JDK is also needed for classes that are neither in the
image nor on the classpath, which Crema loads from lib/modules (JRT
filesystem).
Resolving deps.edn or -Sdeps dependencies needs java on the PATH or
JAVA_HOME set. A cached classpath is read without either.
- Running
.javafiles needs a JDK on the system forjavac. Clojure code does not - Loading a class from
lib/modulesneedsJAVA_HOME. Without it the class is not found - Requires a lightly patched Clojure fork (minor workarounds for Crema
quirks in
RT.javaandVar.java, details) - Java enum support fixed in GraalVM ea17 (oracle/graal#13034)
- Large binary (~208MiB, includes Crema interpreter, Ristretto JIT and preserved packages)
- Crema is EA (GraalVM's RuntimeClassLoading is experimental and only available in EA builds)
See doc/technical.md for the full list of known issues and workarounds.
| Cream | Babashka | |
|---|---|---|
| Clojure | Full JVM Clojure (1.13 fork) | SCI interpreter (subset) |
| Library loading | Any library from JARs at runtime | Any library (with built-in classes, SCI/deftype limitations) |
| Java interop | Full (runtime class loading) | Limited to compiled-in classes |
| Startup | ~7ms | ~10ms |
| Binary size | ~208MiB | ~68MiB |
| Standalone | Yes for Clojure, JDK needed to run .java files |
Yes |
| Loop 10M iterations* | ~21ms | ~173ms |
| Compile time (GitHub Actions, linux-amd64) | ~10min | ~3min |
| Maturity | Experimental | Production-ready |
* (time (loop [i 0] (when (< i 10000000) (recur (inc i)))))
Java interop is faster in cream since it calls methods directly rather than through SCI's reflection layer:
# 100K StringBuilder appends, cream is ~3x faster
$ ./cream -M -e '(time (let [sb (StringBuilder.)] (dotimes [i 100000] (.append sb (str i))) (.length sb)))'
"Elapsed time: 20.593417 msecs"
$ bb -e '(time (let [sb (StringBuilder.)] (dotimes [i 100000] (.append sb (str i))) (.length sb)))'
"Elapsed time: 65.012708 msecs"Babashka still loads pure Clojure code faster. Cream runs it faster once
loaded, since -H:+GraalJITCompileAtRuntime compiles runtime-loaded bytecode
instead of interpreting it:
$ bb -cp "$(clojure -Spath -Sdeps '{:deps {dev.weavejester/medley {:mvn/version "1.9.0"}}}')" -e '(time (require (quote [medley.core :as mc]))) (time (dotimes [i 100000] (mc/greatest 5 2 1 3 4)))'
"Elapsed time: 4.9255 msecs"
"Elapsed time: 62.024166 msecs"
$ ./cream -Scp "$(clojure -Spath -Sdeps '{:deps {dev.weavejester/medley {:mvn/version "1.9.0"}}}')" -M -e '(time (require (quote [medley.core :as mc]))) (time (dotimes [i 100000] (mc/greatest 5 2 1 3 4)))'
Reflection warning, medley/core.cljc:519:25 - call to java.util.ArrayList ctor can't be resolved.
"Elapsed time: 45.207875 msecs"
"Elapsed time: 30.8955 msecs"
$ ./cream -Scp "$(clojure -Spath -Sdeps '{:deps {camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.3"}}}')" -M -e '(time (require (quote [camel-snake-kebab.core :as csk]))) (time (dotimes [i 100000] (csk/->SCREAMING_SNAKE_CASE "I am constant")))'
"Elapsed time: 63.072834 msecs"
"Elapsed time: 246.68125 msecs"
$ bb -cp "$(clojure -Spath -Sdeps '{:deps {camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.3"}}}')" -e '(time (require (quote [camel-snake-kebab.core :as csk]))) (time (dotimes [i 100000] (csk/->SCREAMING_SNAKE_CASE "I am constant")))'
"Elapsed time: 4.244083 msecs"
"Elapsed time: 551.424208 msecs"When cream might make sense: you need full Clojure compatibility, arbitrary library loading, or Java interop beyond what babashka offers.
When babashka is better: scripting, tasks, CI glue, or anything where a standalone binary, fast startup, and a mature ecosystem matter.
Libraries are tested against the cream binary using bb run-lib-tests.
| Library | CI | Status | Notes |
|---|---|---|---|
| data.csv | ✅ | Works | |
| data.json | ✅ | Works | |
| data.xml | Works | ||
| core.async | ✅ | Works | Some test ns skipped (ForkJoinPool segfault) |
| math.combinatorics | ✅ | Works | |
| tools.reader | ✅ | Works | |
| medley | ✅ | Works | |
| camel-snake-kebab | ✅ | Works | |
| hiccup | ✅ | Works | |
| deep-diff2 | ✅ | Works | |
| malli | Works | ||
| meander | Works | ||
| selmer | Works | ||
| specter | Works | ||
| tick | Works | ||
| clj-commons/fs | Works | ||
| prismatic/schema | ✅ | Works | |
| instaparse | ✅ | Works | |
| flatland/useful | Works | ||
| cheshire | ✅ | Works | Enum fix in ea17 |
| clj-yaml | ✅ | Works | Fixed in ea20 (java.lang preserve) |
| nextjournal/markdown | ✅ | Works | Lambda fix in ea20 + java.lang preserve |
| Jsoup | Works | HTML parsing | |
| http-kit | Works | Server + client including HTTPS |
Pure Clojure libraries generally work. Libraries using Java interop work when the relevant packages are preserved.
Requires a GraalVM EA build with RuntimeClassLoading support.
-
Install the custom Clojure fork:
git clone -b crema https://github.com/borkdude/clojure.git /tmp/clojure-fork cd /tmp/clojure-fork && mvn install -Dmaven.test.skip=true
-
Build the native binary:
GRAALVM_HOME=/path/to/graalvm bb build-native
- Bundle JRT metadata in the binary so classes from
lib/modulesresolve without a JDK - Reduce binary size: currently ~208MiB due to preserved packages, the Crema interpreter and the Ristretto JIT
- nREPL support: enable interactive development with editor integration
See doc/technical.md for implementation details, architecture decisions, and known issues.
Distributed under the EPL License. See LICENSE.
This project contains code from:
- Clojure, which is licensed under the same EPL License.