Releases¶
Changelog for Rig.
Unreleased¶
v0.3.1 — 2026-10-07¶
Fixed¶
Managed JDK and GraalVM installs on macOS¶
Temurin and GraalVM CE ship their macOS archives in the bundle
layout, with the JDK under Contents/Home. The managed stores
verified installs by stat'ing bin/java (and bin/native-image)
at the install root, so a complete macOS install never validated:
every command touching it failed with corrupt install (missing
bin/java) — rig jvm install, jvm path, and anything downstream.
Installs now resolve the home inside the bundle, and rig jvm path
prints the correct macOS JAVA_HOME.
v0.3.0 — 2026-10-07¶
New¶
Kernel checkpointing (CRaC)¶
On Linux, Rig restores the kernel JVM from a checkpoint of a warmed
run instead of forking a cold one: ~0.3 s instead of 1–3 s per lock,
check, tree — and every restored run re-dumps the checkpoint, so it
stays current with use. Everything stays opportunistic: without a
CRaC-capable JVM every command behaves exactly as before.
rig crac install fetches the Zulu CRaC JDK pinned by this release
(sha256-verified, into the state dir); rig crac status and
rig crac clean inspect and prune it. Image chains are keyed to the
kernel jar bytes, the JVM build and the CPU features — a rotated
kernel or JDK starts a fresh chain instead of restoring stale state,
and old-kernel chains are swept automatically by a daily boot-time
pass.
Config that rides the JVM environment in cold runs
(RIG_REPO_TOKENS, RIG_PROXY_REPOS) rides the request file for
restored runs and is authoritative there: a restored kernel never
inherits the bootstrap run's tokens. See
Configuration — Kernel checkpointing.
Maven coordinates in built artifacts¶
A module with :rig/lib and a version now carries
META-INF/maven/<group>/<artifact>/pom.{xml,properties} in everything
it builds — jar, uberjar and native image. The pom is the deployed
one: same coordinates, same lock-pinned dependencies, as
rig publish deploys them. It is the convention every jar scanner
attributes through, and the classic version-self-report
(io/resource on pom.properties): an artifact built by Rig now
reports about itself the coordinates it deploys under.
rig jvm path and rig graalvm path¶
rig jvm path <major> and rig graalvm path <major> print the
JAVA_HOME / GRAALVM_HOME of the newest installed managed JDK or
GraalVM satisfying the major — a bare path on stdout, for scripts and
CI. The state dir is read only: the commands never install, never
download, and need no network; when nothing matches they fail (exit 2)
with the install hint. setup-rig uses them to expose the pinned
JVM's homes as action outputs.
Fixed¶
- Ubers carried no classes their AOT bytecode linked against.
Source-only dependencies (
org.clojure/tools.namespace,rewrite-clj) were loaded by the build's preloads without ever emitting classes: the build went green while the uber died at first use with aClassNotFoundException. Uber preloads now run with the compile path at the working class dir, so source-only dependencies — and their transitive requires — ride into the uber as classes, with no declaration.:rig/ns-compileentries now compile unconditionally, even when a preload already loaded them: asking for a compile means it. - Temurin's legacy JDK 8 asset names (
jdk8u492-b09) did not parse as a release version, so the managed-JDK resolver could not match its own installed JDK 8. They now parse as8.0.492+09.
v0.2.1 -- 2026-10-07¶
Fixed¶
v0.2.0 — 2026-10-06¶
Everything merged since the first release.
New¶
rig launch — the production entrypoint¶
rig launch runs the built artifact with the production JVM flag set of
Rig: G1 (AlwaysPreTouch), exit on out-of-memory (with heap dump), and
loopback-only JMX on port 10101. You can override these per module via
:rig/launch-opts and per deployment via the RIG_LAUNCH_OPTS env var
(appended last, same override rules). It replaces its own process with
the JVM (process exec) — in a container the app is PID 1 and signals
reach the JVM directly. rig build bakes the launch plan (main, launch
opts, build JVM) into the jar of a module that declares a main, as
META-INF/rig/launch.json (uberjars pick it up too). So
rig launch <jar> works standalone, outside the workspace — for example
as a container ENTRYPOINT. It runs what Rig already built and cached.
The major version of the launch JVM must match the major version of the
build JVM. See
Configuration — Production launch
and Docker.
Native-image builds¶
rig build --native compiles the locked classpath into a GraalVM
native-image binary (default target/<lib>, :rig/native-file, extra
arguments :rig/native-opts; the entry point is :rig/main). Rig
derives the GraalVM version from the :rig/jvm pin of the workspace
and records it in the lock ({vendor, requested}). Rig manages GraalVM
community JDKs itself, one per major version — rig graalvm install /
list / uninstall / update, downloaded from the
graalvm/graalvm-ce-builds GitHub releases, sha256-verified, into the
state dir. The build never downloads: when Rig has installed no GraalVM
for the major, --native fails with a rig graalvm install <major>
hint. See
Configuration — Native images.
Leiningen migration¶
rig migrate now converts a Leiningen workspace in place (--dry-run
first): project.clj → root and per-module deps.edn with :rig/*
keys. Rig materializes :managed-dependencies into the :deps of the
modules that inherit them. A declared version or source key wins over
the pool entry, and each win is a per-dep warning. Rig decomposes
:sub monorepos into per-module manifests with :local/root sibling
deps. Rig merges :provided into the base (Leiningen has it active by
default). rig migrate reports unexpressible keys and blocking
problems (nested :sub, :sign-releases true, an s3p:// deploy
repository). After you migrate, run rig lock and rig check. See
Migrating from Leiningen for the full key
mapping.
Prep libraries and native javac¶
Rig now honors the standard tools.deps :deps/prep-lib key. Before
rig build, rig test, rig run and rig repl, Rig runs the prep
function of the target module and of every local dependency module that
declares one, in dependency order. Rig checks each prep for staleness
(manifest hash, source digests, locked dependencies, function name),
and the clean step of the build never deletes what the prep :ensures.
If the prep is "javac my own sources", Rig does it natively. Rig itself
compiles modules that declare :rig/java-src-dirs, as part of prep, on
their locked classpath. javac runs before the prep function and before
the Clojure compile, so module code can reference its own Java classes.
A module declaring both runs the javac first, then the function, each
with its own staleness stamp. See Java sources.
AOT-compile in rig check and rig build¶
Stage 2 of rig check now AOT-compiles the namespaces of each module in
dependency order, and it preloads the non-project requires first.
rig build compiles AOT the same way. Rig does not compile a namespace
again when a preload already loaded it, so its top level never runs
twice — except the namespaces you list in :rig/ns-compile, which
compile unconditionally: asking for a compile means it. Code that
references a namespace it does not require — a bare ns/var, or a class
only produced by AOT-compiling a sibling namespace — previously failed
with a ClassNotFoundException. Both paths now compile it. New key
:rig/ns-compile lists extra namespaces to AOT-compile alongside the
sources of the module itself. An uber build additionally loads its
preloads through the working class dir, so dependencies that ship only
sources and no classes (org.clojure/tools.namespace, rewrite-clj)
land in the uber as classes: without them the module's AOT bytecode
references interfaces that exist nowhere on the artifact's classpath,
and the uber dies with a ClassNotFoundException when that code first
loads.
New configuration¶
| Key | Declared in | Effect |
|---|---|---|
:rig/compile-jvm-opts |
workspace root | JVM flags for the build and check JVMs (the AOT build and the check load/AOT-compile). Version-sensitive flags like --enable-preview require a :rig/jvm pin; a lock with the flag and no pin is rejected. |
:rig/launch-opts |
module | JVM flags for rig launch; baked into the artifact's launch descriptor. |
:rig/javac-opts |
module | javac options; when the workspace pins :rig/jvm, --release <n> is prepended unless the opts already set the source level. |
:rig/native? / :rig/native-file / :rig/native-opts |
module | Native-image build: enable, output path, extra arguments. |
:rig/ns-compile |
module | Extra namespaces AOT-compiled alongside the module's own sources. |
:rig/timestamp-string |
module | Fixed zip entry timestamp (entries written name-sorted) — with a pinned :rig/jvm, the build output is byte-reproducible. Pre-1980 values are refused. |
Rig now documents and honors the standard tools.deps :jvm-opts key.
It applies to dev execution — rig run, rig repl, rig exec, rig
test — and to the prep function of the module. It never applies to
rig build, rig check or rig launch
(JVM flags).
rig build now derives the directories it packages from the :paths of
the module, when :rig/artifact-dirs is absent. Previously it fell back
to the hardcoded ["src" "resources"], so a module with non-default
source directories built an empty jar. Rig renames :rig/src-dirs to
:rig/artifact-dirs, and the lock schema bumps to v2. Lock-consuming
commands reject v1 locks with an unsupported-version error. rig lock,
rig add, rig remove and rig update migrate a v1 lock in place. See
Lockfile — anatomy.
Changed¶
- JVM version inputs are major-version only.
:rig/jvmand every JVM version input (rig jvm install,rig graalvm install) accept a major (feature) version —"21", not"21.0.10+7". The major is what matters for compatibility, and Rig manages the exact release itself. Thejvm/graalvmblocks indeps.lockno longer record an exactversion; lock files written by an earlier build keep theirs, which Rig now ignores — re-lock to rewrite them. - The rig-managed JDK takes precedence. When the lock pins a JVM,
Rig uses the rig-managed JDK for the major from the state dir, even
when the system
javahas the same feature version. A matching system JDK (JAVA_HOME, thenPATH) serves only when Rig installed no managed JDK. With no matching JDK anywhere, online Rig installs the newest matching release on demand; offline it fails with an install hint. - One JDK per major version.
rig jvm installno longer upgrades: when the store already holds a JDK for the major, it exits without changing anything.rig jvm updateandrig graalvm updateare now store-level commands: they move the rig-managed install for the pinned major of the workspace to the newest release, and they replace it. They install the newest release when the store holds none. They no longer writedeps.lock, and--frozenno longer applies to them. - The shared pool force-pins. A
:rig/depsentry now pins the coordinate's version workspace-wide: a module that consumes it transitively gets the pool version, like Leiningen's managed pool. A pool entry nothing consumed no longer holdsrig checkred. - A hung test run now dies.
rig testkills the runner JVM when an exec-fn has not finished in 30 minutes and fails the run —--timeoutconfigures it,0disables it. - Stricter CLI.
rig lock --frozenis refused (writing the lock isrig lock's job), and a repeated-p/--pathis refused — both usage errors (exit 2). Before, both quietly did something else. - The GitHub API lookup uses a token.
rig graalvm installandrig self-updateresolve releases through the GitHub API, which allows 60 requests per hour per IP address. Shared CI runners exhaust that pool. Rig sends the bearer token ofGH_TOKENorGITHUB_TOKEN(theghCLI's names, in that order) on the API request, and sends it to no other host. GitHub Actions exportsGITHUB_TOKENfor every step, so CI needs no configuration. Downloads and their.sha256sidecars carry no token. - In a container, the declared JDK in the image base is the build JVM.
The Production launch
image builds on the
eclipse-temurinbase with Rig copied in — noRUN rig jvm installstep. The runtime stage takes only therigbinary, andrig launchnever touches the resolver kernel or the runner.
Fixed¶
- The AOT compile JVM. The kernel forks one JVM to compile the
namespaces of the module.
tools.buildchose that JVM from the environment:JAVA_CMD, then thePATHsearch, thenJAVA_HOME. The host JVM compiled the code, while the kernel itself ran on the JVM Rig used for the workspace. A green build said nothing about the pinned JVM. The kernel now starts the fork on its own JVM — the JVM Rig launched for the workspace.JAVA_CMDandJAVA_HOMEno longer select the fork's JVM. The build's kernel JVM also getsJAVA_HOMEof the managed JDK, as the run and test JVMs already did. Code that starts a JVM at build time now sees the same JDK as inrig runandrig test. - Bytecode floor. The kernel jar of v0.1.0 compiled without a
--releasepin and shipped a Java 21-targetedMain.class— it failed to load on any:rig/jvmpin below 21. Rig now compiles the kernel with--release 8. After every assembly, Rig scans the release jars: any class above the floor fails the build (multi-release jar entries only where the JVM can load them). With a:rig/jvmpin,rig buildlikewise scans every class in the jar or uberjar it produces — dependency classes included — against the pinned JVM, and fails listing the offending entries. (javac does not receive--releaseon Java < 9 hosts, where the flag does not exist.) - Sources in built jars. The drop-sources step ran before the
compile and wiped every
.clj/.cljcfrom the class dir, so a source shipped as payload — a clj-kondo hook export, adeps-newtemplate — vanished from the published jar. It now runs after the compile and drops only the sources paired with their AOT__init.class— the load-time recompile hazard (two class identities,ClassCastException) it exists to kill. Everything else ships as the data it is. The build never drops prep output under an:ensurepath — it is an input of the build, not its output. - What the AOT plan compiles. Every
.cljunder the:pathsroots was treated as a namespace. A data source (a config map without annsform), a file nested under a resources root, or a namespace requiring itself killedrig checkandrig buildoutright. The plan now keeps only the files whose declared namespace matches their path, ignores self-requires, and reports what it left out —rig checkshows aplan-skipwarning instead of dying. - Java modules ran without their own classes. Rig compiled a module
declaring
:rig/java-src-dirsinto its class dir, then kept that dir off the dev classpath unless:pathsnamed it too:rig check,rig testandrig rundied with aClassNotFoundExceptionon the module's own Java classes. The class dir now follows the module's sources onto the classpath. - Class-dir clean. The clean step of the build now deletes only
class-dir content that is not under a declared
:ensurepath. A module whose prep owned the class dir previously lost the whole dir on every build. The jar shipped without the classes of the prep, and dependent builds failed withClassNotFoundException. The staleness check did not notice this. - Lock resolver identity. The CLI now stamps the
resolverblock of the lock at lock time — the kernel pin (lib/version/git sha) plus the sha256 of the kernel jar it actually launched. Before, the kernel self-reported from a build-info resource baked into the jar, which fell back to a placeholder (v0.1.0 + zeroed sha) whenever the resource was absent.rig infoshows the short jar sha when present. Older locks load unchanged. - Non-module
:local/rootdeps. A:local/rootdep can point at a directory with its owndeps.ednthat is not a declared workspace module (a dev/test overlay). Rig now locks such a dep as its own module entry and follows nested refs — its sources reach the classpath. Previously the lock absorbed it into the longest-prefix module, andrig testfailed withexec-fn not found. An overlay is never a build/test/publish target. - Local module beats published coordinate. When the same library
enters a basis as both a
:local/rootand a:mvn/versionrequirement, the local module now wins in either direction. This case typically arises when a transitive POM references the published artifact of the module itself. Vanilla tools.deps cannot order the pair and fails. The lock records the local module, and the published coordinate never enters it. rig check:- a dependency declared with a classifier, for example
org.apache.kafka/kafka-clientswith a$testclassifier, no longer reports a spuriousstale-lock … lock has no pin. The pin key now includes the classifier. - unqualified dependency names (
aleph) normalize to the qualified form the lock pins (aleph/aleph), with a$classifiersuffix preserved. - Stage 2 now walks
.cljcsources — previously it walked only.clj, so an unloadable.cljcnamespace reported a vacuous ok. rig checkno longer requiresdata_readers.clj(a top-level map at a source root — theclojure.mainclasspath convention) as a namespace.rig checkreports a module that pinsorg.clojure/clojurebelow 1.8.0 asclojure-floor, before it launches any JVM (the same gate applies torig test).- with no lock at all, one message, exit 3 (the lock-problem code) — previously one stale-lock problem per module, exit 1.
- Cache entries carry their extension. An artifact sat in the cache
at
<sha256>with no suffix, so the JDK handed consumersfile:URLs for classpath jars and resource copying out of them broke (a kaocha-cljs run died onENOENT). Cache names now end in the lock's extension; existing caches rename into the layout on first hit. rig testprints like the REPL. Runner code saw*print-namespace-maps*false where the same classpath underclojure -Mor lein sees true — the runner now runs underclojure.main's bindings.- Publishing. Rig drops direct-to-S3 publishing (
s3p://repositories): publishing targets http/https Maven repositories only (S3 is what Pier provides).rig migratereports a deploy repository with ans3p://URL as a blocking problem. - Manifest parsing. The compact macro for namespaced maps
(
{ns/…}) now parses;rig migratenormalizes it to the non-compact form. rig versionno longer prints a blank line (exit 0) when the lock records an empty version — it reports "no version found" (exit 1).:rig/version-fn :git-count-revstrims the version template's trailing newline out of the version — it used to name the artifactapp-1.0.1<newline>.jar.- Leiningen migration, continued. A bare exclusion symbol
(
:exclusions [foo]) now migrates tofoo/foo, like Leiningen reads it — verbatim, tools.deps silently excluded nothing. Dropped Leiningen keys are warned about on the profile that carries them, and an entry appended to a manifest lands on its own line. rig buildwarns when:rig/uber-opts :excludeexcludes nothing.tools.buildmatches patterns withre-matches(full match), so a prefix-style^clojure/silently shipped everything; onlydir/.*actually excludes a dir. Every pattern that matches no entry, and every:rig/uber-optskey Rig does not consume, now gets named in a warning.
Docs¶
- New pages: Migrating from Leiningen and
Java sources (native javac,
:deps/prep-lib,:rig/javac-opts). New reference sections: JVM flags, Native images and Production launch. The lockfile anatomy page now covers v2. The Docker entrypoint pattern usesrig launch. - Messaging rework across the site.
Under the hood¶
- Hot commands (
rig test,rig checkstage 2) now run on a separate pinned runner jar (rig-runner), an install-time artifact shipped next to the kernel jar. The Docker image pre-seeds it, sotestandcheckwork on read-only stores. When the runner fails, Rig fails: exit 1 forrig testandrig check— any other runner exit code becomes a plain Rig error; the dev commands (run,repl,exec) pass the child code through verbatim. - CI now builds the kernel and runs its test suite on Temurin 8 — the floor gate that keeps the kernel jar loadable on the oldest supported JVM. CI fixed the tag-trigger behavior.
- The release workflow now propagates the released version into the
README and docs examples (install one-liner,
self-update --versionexamples), replacing the hand-pinning that had left stalev0.2.0examples in the docs. - Rig removes
rig release(breaking). A release is now a version commit plusrig publishplus a tag — the version history and the tag are yours, and Rig does the publish. See Build, publish, release. rig migratedrops the legacy:exoscale.project/extra-clean-targetswith a warning: Rig has no:rig/clean-dirskey,rig cleanremoves the target dirs and nothing else.
v0.1.0 — 2026-09-25¶
GitHub release. First release of Rig — build and run Clojure projects the obvious way: one binary, one manifest, one lockfile, one way to do each thing.
- One binary, one manifest namespace, one lockfile. All tool
configuration lives under
:rig/*in thedeps.ednfiles you already have; standard tools.deps keys, aliases and CI keep working as-is.deps.lockpins every artifact in the resolved dependency tree by sha256. Classpaths reference only the lock (the closed build), and Rig hash-verifies every artifact before it enters a classpath. - Security at the adoption moment. A cooldown window refuses
freshly published versions (default 48h, per-repo overrides,
--force), andrig verifyis the CI security gate. Rig supports repositories behind an OIDC gate (:auth :oidc,rig auth get). - The full command set. Locking and dependency changes:
lock,update,add,remove. Consistency and execution:verify,check(lock-vs-manifest consistency plus namespace load per module),test,run,repl,exec. Artifacts:build [--uber],install,publish,release. Inspection:outdated,tree,info,version. Hygiene:clean,lint,fmt. Project creation:new,new-module. Legacy conversion:migrate, for:exoscale.*/:slipset.*workspaces. - JVM management.
:rig/jvmpins the JVM of the project (Temurin). The lock records the exact release, and Rig installs missing JDKs into its state dir, hash-verified. - Installation. A pinned one-liner installs from the GitHub release
artifacts (sha256-verified against the
SHA256SUMSof the release). Theghcr.io/brutasse/rigDocker image ships multi-arch, with the kernel pre-baked and a Temurin 21 base.rig self-updatechecks for a release at most once per 24h.