OSS updates July and August 2026

In this post I'll give updates about open source I worked on during July and August 2026.

To see previous OSS updates, go here.

Sponsors

I'd like to thank all the sponsors and contributors who make this work possible. Without you, the projects below would not be as mature or would not exist or be maintained at all! So a sincere thank you to everyone who contributes to the sustainability of these projects.

gratitude

Current top tier sponsors:

Open the details section for more info about sponsoring.

Sponsor info

If you want to ensure that the projects I work on are sustainably maintained, you can sponsor this work in the following ways. If you work for a company that uses my OSS, please ask your employer, that would be even better. Thank you!

Updates

In the past two months it was summertime in Europe. Due to a couple of heatwaves, it was the perfect time to spend inside and enjoy my new air conditioning, while coding ;-).

The first half of July was mostly spent on improving performance and compatibility of SCI on CLJS. SCI now JIT-compiles interpreted function bodies to JavaScript at runtime, which closes a lot of the gap with compiled ClojureScript: a tight numeric loop went from ~175ms to ~7ms, over 20 times faster than the interpreter. Implementing core protocols on custom types now also works. There's hardly anything you can't do in SCI that you can do in compiled CLJS. I released new versions of scittle and nbb that take full advantage of this.

Also in the middle of July, clj-kondo got a pretty cool enhancement. It infers types of function arguments from how they are used. E.g. when you write (defn foo [x] (inc x)) we can infer that foo is a function that takes a number. I took this principle as far as I could while preventing false positives. Of course, clj-kondo supports the latest Clojure 1.13 destructuring changes too.

In the second half of July I spent significant time on improving babashka tasks with automatic help and completions, backed by babashka.cli. You can read all about that in this blog post: Babashka tasks with automatic help and completions

In August I had the pleasure of giving a talk about Reagami at Func Prog Sweden. In the talk I gave an interactive demo of how to use Reagami in a Squint project through a REPL. I also went into detail on the algorithm that powers the fast DOM diffing. While preparing for the talk, I added SSR to Reagami too. You can view the talk on YouTube:

The last few weeks of August I created babashka.ffi, a new namespace in babashka to call C libraries. See my previous blog post: Babashka 1.13.220 gets FFI. To validate the design I wrote four libraries with it: babashka.sqlite, babashka.duckdb, babashka.postgres and filewatcher. Each one exercised a different corner of the API, along with some examples based on raylib. PacMan is particularly cool:

pac-man running in babashka through babashka.ffi and raylib

Right now I'm looking forward to giving a babashka workshop at the Clojure/conj together with Rahul Dé. We're still polishing the workshop material behind the scenes and I'm excited to see how it's turning out. I'm sure it'll be a lot of fun and hope to catch many of you there.

In between all of this, I also worked on squint. It now supports the core protocols, so you can plug in your own collections and use them with core functions. E.g. you can use Immutable.js with Squint. I'm thinking about lightweight immutable persistent data structures for squint, but so far I haven't had much need for them, outside of Advent of Code puzzles.

The above was all about making existing projects better. But I also had a few new creative ideas:

  • Choq: Cherry hosted on QuickJS, nREPL included.
  • Buzz: a cross client-server framework that lets you write web-apps on the JVM or babashka without any JS tooling, while still having full JS expressivity via Squint. I wrote tube-pod and multi-snake with it.
  • Cljbang.el: A Clojure-like language that runs as Emacs Lisp

Here are some highlights per project. See each project's CHANGELOG.md for the full list.

  • Babashka: native, fast-starting Clojure interpreter for scripting.

    • 1.13.220: Add experimental babashka.ffi: call C functions in shared libraries straight from babashka and JVM Clojure! See the guide
    • 1.13.220: On Linux, the install script installs the dynamic binary by default. It installs the static binary on musl systems and on systems with glibc older than 2.17. The --static and --dynamic options override the automatic selection
    • 1.13.220: :exec-args can sit directly on a task, not only under :cli, the way (exec ...) already reads it. Before, it was ignored on an :exec-fn or :cmd task
    • 1.13.220: A task's :cli spec adds to the runner-level :tasks {:cli {:spec ...}} instead of replacing it. An option from the runner level keeps its coercion and default, and --help lists it under Inherited options
    • 1.13.220: A task with :exec-fn runs when another task :depends on it. Before, it did nothing
    • 1.13.220: Options declared by an :exec-fn task named in :depends also parse for the CLI task that runs, with their coercion and default. --help lists them under Inherited options
    • 1.13.220: :cmd can be a symbol naming a var that holds the command tree, like :cli. Its namespace loads on demand
    • 1.13.220: Shell completion offers inherited options (via :depends) too
    • 1.13.220: SCI: call site caching for instance and static methods, constructors and fields. Interop calls are up to 5x faster
    • 1.13.219: Tasks get automatic --help and shell completions, through the new :exec-fn and :cmd keys. See the blog post! These task keys should be considered experimental and may change in a future version of babashka, depending on feedback from the community
    • 1.13.219: Clojure 1.13 map destructuring: :keys!, :syms!, :strs!, & inside a directive, :select, :all and :defaults. Adds req! and some-vals to clojure.core
    • #1321: support implementing the clojure.core/Inst protocol on records, types and reify, and with extend-protocol and extend-type
    • #2054: a proxy of java.io.Writer supports the one-argument write and append, so binding *out* to it works
    • #1918: fall back to $HOME when the OS does not supply a home directory, e.g. for LDAP users in the static binary
    • #1994: fix :eval and :print options of clojure.main/repl being ignored in the interactive REPL (@jeroenvandijk)
    • Bump jline to 4.4.0: security hardening, a rewritten signal path for the FFM terminal, Kitty keyboard protocol
    • #2021: bump http-kit to 2.9.0-beta4, which fixes four security advisories
    • Class additions by @weavejester (#1985, #1986, #1987, #1988), @paintparty (#1982) and @christoph-frick (#2003)
    • Full changelog
  • babashka.ffi: call C functions in shared libraries from Clojure. New library, also usable from JVM Clojure. See the guide and the examples. The API is experimental

  • babashka.sqlite: SQLite for babashka through babashka.ffi

    • Uses the SQLite shared library that macOS, Linux and Windows already ship with, so there is nothing to install
    • with-conn, queries, aggregates, transactions, last-insert-rowid, interrupt, and create-function! for defining a Clojure function callable from SQL
    • CI green on three operating systems
  • babashka.duckdb: DuckDB for babashka through babashka.ffi

    • Query CSV files directly with SQL, results as Clojure data
    • HoneySQL support, thread safety, prepared statement cleanup
  • babashka.postgres: PostgreSQL for babashka through babashka.ffi and libpq

    • connect, close!, with-conn, query, execute!, with-transaction, in-transaction?, cancel!, json, jsonb, version, server-version
    • Vectors map to arrays in both directions, maps map to JSON. Bring your own JSON library through :read-json and :write-json
    • clj-kondo export with a with-conn hook, CI on three operating systems
  • filewatcher: watch files and directories from babashka

    • Built on babashka.ffi: FSEvents on macOS, inotify on Linux, ReadDirectoryChangesW on Windows, and polling everywhere
    • The same event types on all three platforms, modeled after chokidar
    • A watcher keeps the process alive until close
  • SCI: Configurable Clojure/Script interpreter suitable for scripting

    • ClojureScript JIT compilation. SCI on CLJS compiles interpreted function bodies to JavaScript at runtime via js/Function. This is enabled by default and needs no configuration. When JIT is enabled, loops and numerical computations become much faster (and, in unrestricted contexts, JS interop too)
    • When eval is unavailable (e.g. under a Content Security Policy) SCI falls back to the interpreter. Results, error messages and error locations should be identical. And of course, it works under :advanced compilation
    • You can turn JIT off at runtime with js/globalThis.SCI_DISABLE_JIT = true before loading SCI, or in your Google Closure compile settings with :closure-defines {sci.core/disable-jit true}
    • More CLJS JIT performance improvements. Up to 20x on arithmetic-dense code for >2 arity. Keyword lookups, instance? and js globals no longer fall back to the interpreter
    • ClojureScript: native protocol support (#639). SCI code can implement CLJS protocols on deftype, defrecord and reify, and host code calling protocol methods on such instances dispatches into the sci implementations. Works under :advanced compilation
    • #1063: CLJS: deftype and defrecord fields are JS accessors on the type's prototype: (.-field x) works on instances, (set! (.-field x) v) mutates deftype fields
    • New :unrestricted option on init and eval-string: when true, evaluated code may mutate built-in vars and CLJS instance interop skips :classes checks. The option applies only to the context it was passed to
    • BREAKING: enable-unrestricted-access! now throws. Use the :unrestricted option instead. The old function set a process-global flag that leaked into nested contexts
    • Support async functions by adding :async true in the attr map of defn
    • Caches resolved JVM instance methods per call site for performance
    • Fix babashka#2030: aset on a primitive array was reflective and 170x slower than aset-double
    • Errors thrown inside a loop now report a located stack frame for the loop form instead of a frame without location (all platforms, including babashka)
    • Full changelog
  • clj-kondo: static analyzer and linter for Clojure code that sparks joy.

    • Type checker: infer the type of a function param from how it is used in the body. E.g. (defn f [s] (subs s 1)) (f 42) will warn, since the evidence (subs s 1) tells us that s should be a string.
    • Type checker: infer the value type of a destructured map key from how it is used in the body. E.g. (defn f [{:keys [x]}] (inc x)) (f {:x "foo"}) will warn. A key whose use rejects nil and that has no :or default is required.
    • Type checker: a destructured binding gets the value type of its key when the map's type is known, including through function return maps. E.g. (defn cfg [] {:port "8080"}) (let [{:keys [port]} (cfg)] (inc port)) will warn.
    • Type checker: a key missing from a map literal is provably nil, also through destructuring, keyword access chains and function return maps. E.g. (inc (:y {})) will warn.
    • Type checker: narrow the type of a local in the then-branch of if or the body of when when it is guarded by a known predicate. E.g. (if (string? x) (inc x) ...) will warn.
    • Built-in analysis now uses Clojure 1.13.0-alpha4. Param type inference over the core sources grows the arg type coverage of clojure.core from 23 to 150 vars. E.g. (interleave 1 [2]) and (mod "a" 2) will warn.
    • #721: NEW linter: :constant-condition: warn on a condition whose truthiness is the same on every run. On by default. Replaces :condition-always-true, whose config and ignores still apply to always-true conditions, and takes over the cond catch-all warning from :unreachable-code
    • Clojure 1.13 CLJ-2961: infer required keys from :keys!, :syms! and :strs! and report them at call sites
    • #2874: Clojure 1.13 CLJ-2964: support :select in map destructuring. The bound map's keys are known to the type checker
    • Clojure 1.13 CLJ-2966: support :defaults in map destructuring, error when used without :or
    • #2943: Type checker: when an :analyze-call hook rewrites a call, clj-kondo checks the arity of the original function but not its parameter types.
    • #2900: :discouraged-var: new per-var :positions option (a set or vector of :call and/or :value) to limit the warning to call position or value position. A var passed to a higher-order function such as map counts as :value.
    • #2851: NEW linter: :seq-rest: suggest using (next x) over (seq (rest x)). Defaults to :off (@tomdl89)
    • #1882: built-in support for clojure.test.check.clojure-test/defspec
    • #2877: warn when #_ before an unmatched reader conditional discards the next form. E.g. [#_#?(:cljs 1) 2] reads as [] in :clj and will warn.
    • Vars defined in comment forms no longer count for :shadowed-var, :unused-private-var and :inline-def.
    • Performance: use a record for var usages: 13.5% less allocation, ~5-10% faster linting. More performance work by @alexander-yakushev
    • The minimum Clojure version to run clj-kondo on the JVM is now 1.11.
    • Full changelog
  • babashka CLI: Turn Clojure functions into CLIs!

    • #197: :positional spec marker: positional args get their own Arguments: help section and may not be passed as options
    • #197: :restrict-args: error on positional args not consumed by :args->opts
    • #219: :cmd-aliases on a table entry or tree node gives a command one or more alternative names.
    • A short option that declares a non-boolean :coerce takes the rest of its token as its value, like getopt: -J-Dfoo=bar binds "-Dfoo=bar", -p80 binds 80. Flag letters may precede the valued option in a cluster: with :b a flag and :a valued, -ba x parses as -b -a x
    • #216: in a cluster of flags, where no letter takes a value, an interior hyphen is an error instead of silently ending option parsing.
    • Help: show the dispatch-level :spec options under Inherited options:. The parser always accepted these options, but help did not show them
    • Help: format-command-help accepts :spec, the dispatch-level spec, so a standalone call shows the same options as dispatch
    • dispatch: the command named on the command line wins over the :exec-args of its ancestors. A value the user typed at an ancestor level still wins over both
    • Add ordered :enum values for validation, help and completion
    • Support :doc and :epilog as a vector of lines, joined with newlines
    • #198: :cmd may be a vector of [name command] pairs, preserving command order without :cmd-order
    • #199: fix hang on variadic arguments that weren't "collected" (e.g. (repeat :k))
    • #203: parse-opts* resolves :spec so its :coerce/:collect entries steer parsing like in parse-opts
    • Completion: the fish snippet registers with --keep-order, so fish offers options in the order they are emitted, long option before its short alias, rather than sorting short options first
    • zsh completion: offer a command's options without typing a dash first, by opting the registered program names out of zsh's prefix-needed style
    • Thanks to @lread for continued documentation review and maintenance
    • Full changelog
  • Squint: CLJS syntax to JS compiler

    • Preparatory release before adding immutable + persistent collections in squint.immutable. Added a lot of protocols and made sure core functions work properly with them
    • Add the ILookup, IAssociative, IMap, ICounted, IKVReduce, ICollection, IEmptyableCollection and IEquiv protocols. get, assoc, contains?, find, dissoc, count, reduce-kv, conj, empty and = dispatch to them on custom types. Plain objects and arrays keep their fast paths
    • Add the IStack, IIndexed, IVector, IWriter and IPrintWithWriter protocols, write-all, and an ITransientVector -pop! slot; nth, peek, pop, pop!, subvec, vec, vector?, sequential?, set?, map?, seq, = and printing dispatch to custom collection types
    • Add equiv, hash, hash-ordered-coll, hash-unordered-coll and the IHash protocol. hash follows equiv: plain mutable objects and arrays hash by reference
    • Add the IMeta and IWithMeta protocols; meta and with-meta dispatch through them and the internal meta symbol property is gone
    • clojure.set dispatches through the collection protocols: results keep the input's type, membership tests against a protocol set are value-based, and rename-keys/map-invert no longer mutate a record
    • Add defrecord, record? and the IRecord marker protocol. Records store their fields as own string-keyed properties and implement the map-facing protocols, so keyword lookup, keys, seq, assoc, conj and = work through the regular core functions. assoc keeps the record type, dissoc of a basis field gives a plain map, printing gives #TypeName{:a 1}
    • Clojure 1.13 destructuring: :keys!/:syms!/:strs! for required keys, & inside them for keys required but not bound, :select, :all, :defaults, and :or by key
    • Fix #975: & {:keys [...]} now destructures a map instead of the raw rest args, and a seq destructured as a map is read as kwargs
    • Fix #977: recur inside try no longer emits an illegal continue
    • Support :as-alias in ns :require like CLJS: no runtime import, only a compile-time alias so a namespaced keyword such as ::alias/x resolves
    • Add :require-global and :refer-global to ns, binding globals loaded via a script tag to consts without emitting an import
    • Add :squint/compile-time opt-in mechanism for macro/compile-time namespaces. See doc/compile-time.md
    • A defmacro is compile-time only: no longer emitted to the runtime module, and :refering a macro no longer emits a runtime import for it, matching CLJS
    • The CLI reports the file, line and column of a compile error and exits non-zero, instead of dumping the raw exception
    • Fix #957: vite HMR: support ^:dev/after-load + ^:dev/before-load hooks similar to shadow-cljs
    • .indexOf on a lazy seq now uses reference equality like a JS array, not value equality. This diverges from CLJS but keeps = out of any bundle that only builds lazy seqs, shrinking a conj bundle from 3801 to 2215 bytes
    • Use Symbol.for for protocol method dispatch, so pulling in multiple copies of squint.core (e.g. via http://esm.sh/) does not break protocol dispatch
    • Full changelog
  • Cherry: Experimental ClojureScript to ES6 module compiler

    • Add cherry.test with clojure.test-compatible testing API, requirable as cljs.test or clojure.test
    • cherry.test/report is a multimethod dispatching on [*current-reporter* type] like cljs.test, so reporting can be extended with defmethod
    • Add a vite plugin with browser REPL over nREPL and ^:dev/after-load / ^:dev/before-load hot-reload hooks, sharing squint's implementation: import cherry from 'cherry-cljs/vite.js'
    • Add reify, defmulti/defmethod and the vswap! macro. #'foo emits foo's value, like squint
    • Dynamic vars compile to squint's box scheme, so set! and binding work across ESM modules. cljs.core dynamic vars are exported as accessor boxes proxying the real var
    • defprotocol :extend-via-metadata impls resolve under the fully qualified method symbol, so replicant's mutation-log renderer works: replicant's own test suite passes under cherry
    • Fix deftype implementing cljs.core protocols such as Inst, IIterable and IAtom: their marker properties were Closure-renamed in the precompiled core and missing from the emitter's core protocol set. The externs list and the set are now generated from cljs.core's protocols (bb gen-externs) and the build fails on drift
    • Fix #190: share PROTOCOL_SENTINEL with coexisting CLJS runtimes in the same JS realm
    • Share the macro scan and macro lookup with squint. Namespaces flagged {:squint/compile-time true} load only their compile-time part into the macro environment, like squint
    • CLI: --help/-h, argument validation and error messages via babashka.cli's dispatch, like squint. Adds watch and nrepl-server commands, shell tab completion, and reads options from cherry.edn instead of squint.edn
    • Fix emitted import specifiers on Windows: backslashes are normalized via the path resolution now shared with squint
    • Full changelog
  • Choq: a ~5 MB binary running the cherry compiler on embedded QuickJS

    • New project. Runs cherry inside quickjs-ng via rquickjs
    • No JIT, so hot code is slower than Node.js, Bun or Deno, but the binary is small, startup is fast and memory use stays low. A Hono app serves around 30k requests per second locally, using less memory than the same app on Node.js or Bun
    • An install script for macOS, Linux and Windows, and dev release binaries
    • Clojure git and Maven deps, a module table covering url and util, @babashka/fs, and a test runner
    • Experimental
  • Buzz: write a web application with the JVM or babashka only

    • New project. Server state is watched and updated from client code. The UI compiles through squint and renders with Reagami, so no ClojureScript toolchain and no Node.js
    • Rendering is asynchronous by default and coalesces at 20ms, and a failing render is contained to its own connection
    • Examples: a whiteboard, a tap viewer, and a Datalevin browser with a CodeMirror query editor
    • Highly experimental, the API will change
  • tube-pod: turn YouTube videos into a private podcast

    • New project, written with Buzz. Add a link in the browser, tube-pod downloads the audio with yt-dlp, writes an RSS feed and serves both
    • Rsyncs the audio and the feed to a remote after each change, since a laptop is asleep when you want to listen
  • multi-snake: snake for as many players as show up

  • Reagami: A minimal zero-deps Reagent-like for Squint and CLJS

    • Add reagami.ssr to render hiccup to an HTML string on the JVM, Babashka, Squint and CLJS. See Server-side rendering
    • reagami.core/render (the regular render function) now hydrates a server-rendered page. It adopts the existing DOM instead of clearing the root
    • Add create-reagami-app. Run npm create reagami-app my-app to create a Vite project with hot reload and a browser nREPL
    • Breaking: :on-render now takes a map: (fn [{:keys [node lifecycle state save]}]). Call save with a value to keep it for the next call, and read it back as state. In previous versions, the hook took three arguments and its return value became the state
    • Move reordered nodes with moveBefore where the browser has it, so a moved subtree keeps its iframe state, animations, focus and selection (#54)
    • Set value, checked, selected and disabled on a tag with a hyphen as attributes, not as JS properties. A custom element observes attributes, so a property never had any effect. Native elements still handle them as properties
    • Custom events, e.g. :on-rated, now reach the element through addEventListener, because a browser only wires an on* property for standard events
    • Add web component example. A <todo-list> custom element, used from Squint, from JavaScript with and without Reagami
    • Fix memory leak with :on-render nodes and other :on-render improvements
    • I gave a talk about Reagami at Func Prog Sweden
  • cljbang: a Clojure-like language that runs as Emacs Lisp

    • Compiles Clojure forms to Emacs Lisp forms and evaluates them in the running Emacs. No subprocess and no transpiled text, following the same approach as squint
    • Namespaces with per-namespace aliases, multiple arities in fn and defn, loop/recur with a tail position check, try/throw/ex-info, case, atoms, syntax quote including nesting, &form and &env in macros, regex and set literals, #_, edn/read-string, slurp and spit
    • el! for calling Emacs Lisp names that are not valid Clojure symbols
  • nbb: Scripting in Clojure on Node.js using SCI

    • ClojureScript JIT compilation. Nbb now bundles a SCI that compiles interpreted function bodies to JavaScript at runtime via js/Function. This is enabled by default. This makes loops, numerical computations and JS interop much faster
    • Nbb now ships babashka.fs as a built-in library. The full file system API (glob, copy, move, create-dirs, delete-tree, with-temp-dir, path helpers and more) is available via (require '[babashka.fs :as fs]), matching Babashka
    • Support implementing CLJS protocols (e.g. ILookup, etc) on deftype and defrecord
    • Support editscript: CLJS deftype/defrecord field interop, set! on ^:unsynchronized-mutable fields, add cljs.core type classes like PersistentHashMap, write-all and goog.math.Long
    • #416: Fix problem with prn in nREPL
    • SCI now covers most CLJS capabilities, so nbb should run existing CLJS libraries unless they rely on very specific macros that require the JVM. If you have anything that does not run, please report it in #nbb!
  • Scittle: Execute Clojure(Script) directly from browser script tags via SCI

    • ClojureScript JIT compilation. Scittle now bundles a SCI that compiles interpreted function bodies to JavaScript at runtime via js/Function. This is enabled by default
    • Include helitorus demo to show improved JIT
    • Bump reagent to 1.2.0, re-frame to 1.4.7, replicant to 2026.06.2 and shadow-cljs to 3.4.11
  • squint-inline: write squint functions and inline expressions in a ClojureScript project

    • New project. Squint operates on JavaScript objects and arrays, so assoc, update-in and select-keys work on those without js->clj and clj->js
    • Squint core is tree-shaken through :js-provider :import, and each function's tree-shaken size is recorded
    • Squint functions can call each other across namespaces, and JS module references work inside squint bodies
  • Edamame: configurable EDN and Clojure parser with location metadata and more

    • Speed up parsing by holding parse context in record fields instead of the extmap: ~10% faster on JVM, ~4% on ClojureScript
    • Respect :refer + rename in :auto-resolve-ns
    • With :auto-resolve-ns, qualify syntax-quoted imported classes (e.g. `Date with (:import [java.util Date])) with the full classname
    • With :auto-resolve-ns, leave method, constructor and dotted syntax-quoted symbols (`.toString, `Bar., `foo.bar) as-is, matching Clojure
    • Do not resolve function literal params in a syntax quote
    • ClojureDart: fix parsing zero literals and make plain readers non-indexing, matching tools.reader (#144)
  • fs: file system utility library for Clojure

    • Released 0.5.34, which ships the Node.js support mentioned in the previous update as the @babashka/fs npm package
  • http-client: HTTP client for Clojure and babashka

    • #80: accept a function of the request URI in :proxy to select a proxy per request (@jeeger)
  • http-server: serve static assets

    • Range requests: inclusive Content-Range last-pos per RFC 9110, suffix ranges (bytes=-N, previously a 500), clamping last-pos beyond EOF, reading the full range, and a test suite (@slagyr)
  • Cream: Clojure + GraalVM Crema native binary

    • Reduced the core.async virtual thread memory corruption I reported upstream to a pure Java repro. GraalVM 25.0.3-ea.04 fixes it, and the pipeline test is back on now that the compile NPE is gone too
    • Enable the Ristretto JIT for runtime-loaded bytecode, and update the benchmarks for it
    • Clojure code runs without a JDK present, with the boot class loader warning suppressed
    • Pick up pom.xml when there is no deps.edn, and recompile Java sources when a dependency changed
  • graaljs-cherry: a native-image cherry REPL on GraalJS

    • New prototype. Compiles cherry expressions on the JVM and evaluates the resulting JS in an embedded GraalJS context
    • Two variants: a default Truffle JIT build, and a 49MB --small build without it
  • clj-kondo-browser: a static Clojure source browser built from clj-kondo analysis

    • New prototype. Renders a codebase as a static HTML page where every symbol links to its definition and usages, scope-aware, so a local is linked only within its scope
    • Runs clj-kondo as a pod and gets the classpath from deps.clj
  • grasp: Grep Clojure code using clojure.spec regexes

    • Babashka compatibility (#34)
  • deps.clj: a faithful port of the Clojure CLI Bash script to Clojure

    • As always, catching up with the most recent Clojure CLI versions
  • lein-clj-kondo and clj-kondo-bb: released alongside each clj-kondo release

Other projects

These are some other projects I'm involved with, but little to no activity happened in the past two months.

Click for more details

Published: 2026-09-02

Tagged: clojure oss updates

Babashka 1.13.220 gets FFI

Today babashka 1.13.220 is released, with a new babashka.ffi namespace for calling C libraries directly from Babashka scripts. The babashka.ffi library is also available as a standalone library for JVM Clojure, so you can use it in your Clojure projects as well. Note that the API is still experimental, although no changes are currently planned. It just needs more exposure and your feedback :). Here's a small demo.

Calling C

This example loads libz from your system and requests the version.

(require '[babashka.ffi :as ffi :refer [defcfn]])

(def zlib (ffi/load-system-library "z"))
(def zlib-version (ffi/cfn zlib "zlibVersion" [] :string))

(zlib-version)
;;=> "1.3.1"

This example loads an OS-specific library for doing math:

(ffi/load-library
 {:mac "libm.dylib"
  :linux "libm.so.6"
  :windows "ucrtbase.dll"})

(defcfn cos "cos" [:double] :double)
(defcfn pow "pow" [:double :double] :double)

(cos 0.0)      ;;=> 1.0
(pow 2.0 10.0) ;;=> 1024.0

To get a feeling for how to use it in larger, non-trivial projects, read the library guide. Some of the API decisions like defcfn are clearly inspired by coffi, so I want to thank Joshua Suskalo for leading the way with his excellent library. But babashka.ffi is not simply a copy of coffi. It does a few things differently. You can provide an explicit library (or a function or delay that resolves to one) to defcfn for example. Also it has a place concept (inspired by Specter's paths) that efficiently lets you read from and write to structs and unions. Like coffi, babashka.ffi builds on java.lang.foreign and makes you manage memory explicitly through arenas. One benefit of this is that you'll get exceptions rather than segfaults that tear down your REPL and you can use with-open to release allocated memory.

Install

To use babashka.ffi and libraries that build on it, you have to use a dynamically linked version of babashka. On Mac and Windows this was always the default. On Linux, the static binary was preferred historically since it did not depend on your system's libc and zlib. In this release we flip this default to a mostly-static binary: all the shared C libraries that babashka needs are statically linked, and glibc is the only dynamically linked part. The aarch64 binary, although it carries -static in its name, was already built this way. Babashka on Linux is built in a container that pins the glibc version to the lowest one possible so it should work on all mainstream LTS versions of Linux today. If you still prefer the fully static binary, you can use the install script with the --static flag. If you use a package manager or a GitHub Action to install babashka, it may not yet be up to date with this new policy. If that is the case, feel free to open an issue at the babashka GitHub repo and I'll reach out to get this fixed. Meanwhile you can install babashka using the installer script on GitHub to a temporary directory to get a second installation of babashka with FFI enabled:

$ curl -sLO https://raw.githubusercontent.com/babashka/babashka/master/install
$ bash install --dir /tmp/bb-test
$ /tmp/bb-test/bb -e "(require '[babashka.ffi :as ffi]) (ffi/load-system-library \"z\")"

The installer script probes your system for the supported glibc version and falls back to the fully static version when necessary.

Demos

To validate the design of babashka.ffi, I built a few shiny demos:

  • pacman.clj: pac-man with the classic ghost personalities (requires raylib)
  • doom.clj: a raycaster with textures and sprites (requires raylib)
  • helitorus.clj: a helix around a torus (requires raylib)
  • gtk4.clj: a native GTK 4 window rendering from an atom
  • portaudio.clj: an arpeggio through a realtime audio callback
  • python.clj: embedded CPython calling back into Clojure
pac-man running in babashka through babashka.ffi and raylib

A one-liner to try these demos:

$ bb -e '(load-string (slurp "https://raw.githubusercontent.com/babashka/ffi/main/examples/pacman.clj"))'

FFI-based libraries

To validate the design of babashka.ffi even more, a couple of new libraries were born. These libraries mostly resemble existing pods but now use FFI to fulfill similar use cases.

One cool thing you could not do with a pod before is defining a Clojure function in SQLite:

(require '[babashka.sqlite :as sq])

(sq/with-conn [db nil]
  (sq/create-function! db "initials"
    (fn [s] (apply str (map first (clojure.string/split s #" ")))))
  (sq/query db ["select initials(?) i" "gerald jay sussman"]))
;;=> [{:i "gjs"}]

Tasks: :exec-fn composition

This release also has some really nice task improvements: :exec-fn tasks now compose through :depends. A task can depend on another CLI task, and the dependency's options parse, coerce and show up in --help and shell completion:

{:tasks
 {compile {:exec-fn build/compile-sources
           :cli {:spec {:release {:coerce :boolean}}}}
  jar     {:depends [compile]
           :exec-fn build/jar}}}
$ bb jar --help
...
Inherited options:
  --release

Also you can now directly provide :exec-args on a task:

{:tasks
 {deploy {:exec-fn deploy/run
          :exec-args {:env "staging"}}}}

A :cmd tree can now be provided through a var, whose namespace is loaded on demand:

{:tasks
 {cli {:cmd my.project.cli/commands}}}

AI disclosure

While developing FFI and while validating the design through examples and writing libraries, I have made use of LLM assistance.

Wrapping up

Hope you'll like these new features!

The full changelog can be found here.

Published: 2026-08-31

Tagged: clojure tasks babashka ffi

Babashka tasks with automatic help and completions

Babashka tasks is a project task manager that is part of babashka since May 2021. It's a practical way to manage a Clojure or other software project. Just invoking bb tasks gives you an overview of everything that is important to developers of this project. E.g. launching a REPL, bumping and releasing new versions, etc.

Example bb.edn:

{:tasks
 {dev   {:doc "Start the dev system"
         :task (clojure "-M:dev")}

  test  {:doc "Run the tests"
         :task (clojure "-M:test")}

  build {:doc "Build an uberjar"
         :depends [test]
         :task (clojure "-T:build uber")}}}
$ bb tasks
The following tasks are available:

dev    Start the dev system
test   Run the tests
build  Build an uberjar

The bb build task runs test first because it :depends on it.

Before: exec and -x

Babashka CLI is a command line parsing library that comes bundled with babashka. Babashka tasks already had some integration with it before, through the exec function. The exec function auto-resolve a var symbol and that var can carry a :org.babashka/cli specification.

bb.edn:

{:paths ["bb"]
 :tasks {dev {:doc "Start the dev system"
              :task (exec 'tasks/dev)}}}

bb/tasks.clj:

(ns tasks
  (:require [babashka.tasks :refer [clojure]]))

(defn dev
  {:org.babashka/cli {:spec {:port {:coerce :int
                                    :default 8080}}}}
  [opts]
  (clojure "-X:dev" opts))
$ bb dev
dev system on port 8080

$ bb dev --port 3000
dev system on port 3000

Babashka's -x flag already invoked that function by its qualified name:

$ bb -x tasks/dev
dev system on port 8080

$ bb -x tasks/dev --port 3000
dev system on port 3000

Since a couple of months, Babashka CLI provides automatic help and completions. Babashka tasks now uses those when configured with either :exec-fn or :cmd.

Opting in with :exec-fn

To opt-in to that new behavior, the dev task becomes:

bb.edn:

{:paths ["bb"]
 :tasks {dev {:exec-fn tasks/dev}}}

bb/tasks.clj:

(ns tasks
  (:require [babashka.tasks :refer [clojure]]))

(defn dev
  "Start the dev system"
  {:org.babashka/cli {:spec {:port {:coerce :int
                                    :default 8080
                                    :desc "HTTP port"}}}}
  [opts]
  (clojure "-X:dev" opts))
$ bb dev --help
Usage: bb dev [options]

Start the dev system

Options:
      --port  HTTP port (default: 8080)
  -h, --help  Show this help

The function's docstring also becomes the task description in bb tasks:

$ bb tasks
The following tasks are available:

dev  Start the dev system

Task explosion

One thing I noticed in some projects is that during their lifecycle, tasks tended to get copied when they need another option.

$ bb tasks
The following tasks are available:

dev                              Start the dev system
dev:with-transactor              Start the dev system with a transactor
dev:with-transactor:with-nrepl   Start the dev system with a transactor and nREPL

I now think that this is a "task smell" and this should be only one task with command line options:

bb/tasks.clj:

(defn dev
  "Start the dev system"
  {:org.babashka/cli {:spec {:port       {:coerce :int
                                          :default 8080
                                          :desc "HTTP port"}
                             :transactor {:coerce :boolean
                                          :desc "Start a transactor"}
                             :nrepl      {:coerce :boolean
                                          :desc "Start an nREPL server"}}}}
  [opts]
  (clojure "-X:dev" opts))
$ bb dev --help
Usage: bb dev [options]

Start the dev system

Options:
      --port        HTTP port (default: 8080)
      --transactor  Start a transactor
      --nrepl       Start an nREPL server
  -h, --help        Show this help

$ bb dev --transactor
dev system on port 8080 transactor=true nrepl=false

Shell completions

To enable completions, you can use bb org.babashka.cli/completions snippet --shell <shell> for your specific shell.

E.g. for zsh, we do this by adding this to ~/.zshrc after compinit:

source <(bb org.babashka.cli/completions snippet --shell zsh)

For Bash, fish, PowerShell, and Nushell, see Completions in the Babashka CLI README.

After doing that, task name completion includes descriptions on auto-complete:

$ bb <TAB>
dev  -- Start the dev system

Task specific option completions includes descriptions:

$ bb dev <TAB>
--help  -h  -- Show this help
--port      -- HTTP port

You can call the completions command directly to inspect its output without a shell, e.g. for debugging:

$ bb org.babashka.cli/completions complete --shell zsh -- dev ''
--port	HTTP port
--help	Show this help
-h	Show this help

Restricting values with :enum

Use :enum to restrict an option to a fixed set of values:

(defn dev
  "Start the dev system"
  {:org.babashka/cli {:spec {:port {:coerce :int
                                    :default 8080
                                    :desc "HTTP port"}
                             :env  {:desc "Environment"
                                    :enum ["dev" "staging" "prod"]
                                    :default "dev"}}}}
  [opts]
  (clojure "-X:dev" opts))

The help output lists the allowed values:

$ bb dev --help
Usage: bb dev [options]

Start the dev system

Options:
      --port  HTTP port (default: 8080)
      --env   Environment (one of: dev, staging, prod) (default: dev)
  -h, --help  Show this help

The value is also validated:

$ bb dev --env qa
Error: Invalid value for option --env: qa. Expected one of: dev, staging, prod

Usage: bb dev [options]

Run "bb dev --help" for more information.

Shell completion also uses the :enum values:

$ bb dev --env <TAB>
dev  prod  staging

Commands

Add commands to a task with :cmd:

bb.edn:

{:paths ["bb"]
 :tasks {dev {:exec-fn tasks/dev}

         db  {:doc "Manage the database"
              :cmd {"migrate" {:exec-fn tasks/db-migrate}
                    "seed"    {:exec-fn tasks/db-seed}}}}}

Each leaf uses :exec-fn to point to a function:

bb/tasks.clj:

(defn db-migrate
  "Run pending migrations"
  {:org.babashka/cli {:spec {:env {:desc "Environment"
                                   :enum envs
                                   :default "dev"}}}}
  [{:keys [env]}]
  (println "migrating" env))

(defn db-seed
  "Seed the database with fixtures"
  {:org.babashka/cli {:spec {:env {:desc "Environment"
                                   :enum envs
                                   :default "dev"}}}}
  [{:keys [env]}]
  (println "seeding" env))

A top-level :exec-fn can handle bb db. When no command is specified:

$ bb db
No command given.

Automatic help lists the commands and provides separate help for each:

$ bb db --help
Usage: bb db [options] <command>

Manage the database

Commands:
  migrate  Run pending migrations
  seed     Seed the database with fixtures

Options:
  -h, --help  Show this help

Run "bb db <command> --help" for more information on a command.

Shell completion includes commands and option values:

$ bb db <TAB>
migrate  -- Run pending migrations
seed     -- Seed the database with fixtures

$ bb db migrate --env <TAB>
dev  prod  staging

CLI settings with :cli

Use :cli for Babashka CLI settings, such as a help epilog:

bb.edn:

{:paths ["bb"]
 :tasks {dev {:exec-fn tasks/dev}

         db  {:doc "Manage the database"
              :cli {:epilog "Migration code is in resources/migrations."}
              :cmd {"migrate" {:exec-fn tasks/db-migrate}
                    "seed"    {:exec-fn tasks/db-seed}}}}}
$ bb db --help
Usage: bb db [options] <command>

Manage the database

Commands:
  migrate  Run pending migrations
  seed     Seed the database with fixtures

Options:
  -h, --help  Show this help

Run "bb db <command> --help" for more information on a command.

Migration code is in resources/migrations.

For tasks that require code outside of bb.edn, :cli may contain a fully qualified var symbol:

bb.edn:

{:paths ["bb"]
 :tasks {dev {:exec-fn tasks/dev}

         db  {:doc "Manage the database"
              :cli tasks/db-cli
              :cmd {"migrate" {:exec-fn tasks/db-migrate}
                    "seed"    {:exec-fn tasks/db-seed}}}}}

bb/tasks.clj:

(defn- report-error
  [{:keys [msg]}]
  (binding [*out* *err*]
    (println "db:" msg))
  (System/exit 1))

(def db-cli
  {:epilog "Migration code is in resources/migrations."
   :error-fn report-error})
$ bb db migrate --env qa
db: Invalid value for option --env: qa. Expected one of: dev, staging, prod

Top-level :cli options in :tasks apply to every CLI task:

{:paths ["bb"]
 :tasks {:cli tasks/db-cli
         ...}}

Multi-line docs

A :doc value may be a vector of lines:

bb.edn:

db {:doc ["Manage the database"
          "Migrations are applied in order and are idempotent."]
    :cmd {"migrate" {:exec-fn tasks/db-migrate}
          "seed"    {:exec-fn tasks/db-seed}}}
$ bb db --help
Usage: bb db [options] <command>

Manage the database
Migrations are applied in order and are idempotent.

Commands:
  migrate  Run pending migrations
  seed     Seed the database with fixtures

Options:
  -h, --help  Show this help

Run "bb db <command> --help" for more information on a command.

The bb tasks overview prints only the first line:

$ bb tasks
The following tasks are available:

dev  Start the dev system
db   Manage the database

Availability

The task integration is available in babashka 1.13.219. The Babashka CLI features it builds on are in 0.12.85:

org.babashka/cli {:mvn/version "0.12.85"}

Closing remarks

I hope you'll enjoy these new additions to bb tasks! The new task keys should be considered experimental and may change in a future version of babashka, depending on feedback from the community.

Published: 2026-07-27

Tagged: clojure tasks cli babashka

Archive