borkdude/cream

Fast starting Clojure runtime built with GraalVM native-image + Crema

Clojure

168

90 commits

updated Jul 27, 2026

See the code

README

Cream

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

Install

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/Linux

Or build from source (see Building from source).

Quick start

$ ./cream -M -e '(+ 1 2 3)'
6

Runtime type creation

Unlike 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"

Loading libraries at runtime

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.app

A 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.java

Use -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:test

Use -Scp to pass a classpath directly. Nothing is resolved and deps.edn is ignored:

./cream -Scp "$(clojure -Spath)" -M -m my.app

Dependencies 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.

Running Java files

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: world

A package declaration is honored, so files in a Maven source tree run as they are.

Dependencies

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 total

Dependencies 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.

Requirements

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.

Known limitations

  • Running .java files needs a JDK on the system for javac. Clojure code does not
  • Loading a class from lib/modules needs JAVA_HOME. Without it the class is not found
  • Requires a lightly patched Clojure fork (minor workarounds for Crema quirks in RT.java and Var.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 vs Babashka

CreamBabashka
ClojureFull JVM Clojure (1.13 fork)SCI interpreter (subset)
Library loadingAny library from JARs at runtimeAny library (with built-in classes, SCI/deftype limitations)
Java interopFull (runtime class loading)Limited to compiled-in classes
Startup~7ms~10ms
Binary size~208MiB~68MiB
StandaloneYes for Clojure, JDK needed to run .java filesYes
Loop 10M iterations*~21ms~173ms
Compile time (GitHub Actions, linux-amd64)~10min~3min
MaturityExperimentalProduction-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.

Tested libraries

Libraries are tested against the cream binary using bb run-lib-tests.

LibraryCIStatusNotes
data.csv✅Works
data.json✅Works
data.xmlWorks
core.async✅WorksSome test ns skipped (ForkJoinPool segfault)
math.combinatorics✅Works
tools.reader✅Works
medley✅Works
camel-snake-kebab✅Works
hiccup✅Works
deep-diff2✅Works
malliWorks
meanderWorks
selmerWorks
specterWorks
tickWorks
clj-commons/fsWorks
prismatic/schema✅Works
instaparse✅Works
flatland/usefulWorks
cheshire✅WorksEnum fix in ea17
clj-yaml✅WorksFixed in ea20 (java.lang preserve)
nextjournal/markdown✅WorksLambda fix in ea20 + java.lang preserve
JsoupWorksHTML parsing
http-kitWorksServer + client including HTTPS

Pure Clojure libraries generally work. Libraries using Java interop work when the relevant packages are preserved.

Building from source

Requires a GraalVM EA build with RuntimeClassLoading support.

  1. 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
    
  2. Build the native binary:

    GRAALVM_HOME=/path/to/graalvm bb build-native
    

Future work

  • Bundle JRT metadata in the binary so classes from lib/modules resolve 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

Documentation

See doc/technical.md for implementation details, architecture decisions, and known issues.

License

Distributed under the EPL License. See LICENSE.

This project contains code from:

  • Clojure, which is licensed under the same EPL License.

borkdude/cream

Fast starting Clojure runtime built with GraalVM native-image + Crema

Clojure

168

90 commits

updated Jul 27, 2026

See the code

README

Cream

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

Install

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/Linux

Or build from source (see Building from source).

Quick start

$ ./cream -M -e '(+ 1 2 3)'
6

Runtime type creation

Unlike 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"

Loading libraries at runtime

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.app

A 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.java

Use -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:test

Use -Scp to pass a classpath directly. Nothing is resolved and deps.edn is ignored:

./cream -Scp "$(clojure -Spath)" -M -m my.app

Dependencies 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.

Running Java files

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: world

A package declaration is honored, so files in a Maven source tree run as they are.

Dependencies

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 total

Dependencies 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.

Requirements

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.

Known limitations

  • Running .java files needs a JDK on the system for javac. Clojure code does not
  • Loading a class from lib/modules needs JAVA_HOME. Without it the class is not found
  • Requires a lightly patched Clojure fork (minor workarounds for Crema quirks in RT.java and Var.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 vs Babashka

CreamBabashka
ClojureFull JVM Clojure (1.13 fork)SCI interpreter (subset)
Library loadingAny library from JARs at runtimeAny library (with built-in classes, SCI/deftype limitations)
Java interopFull (runtime class loading)Limited to compiled-in classes
Startup~7ms~10ms
Binary size~208MiB~68MiB
StandaloneYes for Clojure, JDK needed to run .java filesYes
Loop 10M iterations*~21ms~173ms
Compile time (GitHub Actions, linux-amd64)~10min~3min
MaturityExperimentalProduction-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.

Tested libraries

Libraries are tested against the cream binary using bb run-lib-tests.

LibraryCIStatusNotes
data.csv✅Works
data.json✅Works
data.xmlWorks
core.async✅WorksSome test ns skipped (ForkJoinPool segfault)
math.combinatorics✅Works
tools.reader✅Works
medley✅Works
camel-snake-kebab✅Works
hiccup✅Works
deep-diff2✅Works
malliWorks
meanderWorks
selmerWorks
specterWorks
tickWorks
clj-commons/fsWorks
prismatic/schema✅Works
instaparse✅Works
flatland/usefulWorks
cheshire✅WorksEnum fix in ea17
clj-yaml✅WorksFixed in ea20 (java.lang preserve)
nextjournal/markdown✅WorksLambda fix in ea20 + java.lang preserve
JsoupWorksHTML parsing
http-kitWorksServer + client including HTTPS

Pure Clojure libraries generally work. Libraries using Java interop work when the relevant packages are preserved.

Building from source

Requires a GraalVM EA build with RuntimeClassLoading support.

  1. 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
    
  2. Build the native binary:

    GRAALVM_HOME=/path/to/graalvm bb build-native
    

Future work

  • Bundle JRT metadata in the binary so classes from lib/modules resolve 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

Documentation

See doc/technical.md for implementation details, architecture decisions, and known issues.

License

Distributed under the EPL License. See LICENSE.

This project contains code from:

  • Clojure, which is licensed under the same EPL License.

Significant stargazers

Lee Read

55 followers · starred Mar 2026

Stephan Renatus

136 followers · starred Feb 2026

Ondra Pelech

61 followers · starred Mar 2026

zachcp

83 followers · starred Feb 2026