If I want to use Clojure proper for command line tools, instead of say, babashka, does there exist a semi straightforward way to AOT compile something that has the expected trade-offs?
what are "the expected trade-offs"
compile can create class files, and tools.build has a compile-clj function. I'm pretty sure leiningen has the same thing (might be behind the uberjar command, but I'm sure there are other ways to get there as well). <https://clojure.org/guides/tools_build#_compiled_uberjar_application_build> has an example of building an uberjar via tools.build, if that's what you're in search of
The run-time tradeoffs I meant, e.g. that it would startup faster but maybe not end up being the best performing over heavy calculations or long-running tasks
• you can create standalone executables with babashka too • you can use native-image to create binaries from clojure
My concepts are a bit outdated. I may be (even falsely) remembering a time where there were some inroads into being able to use GraalVM to create AOT fast-starting programs
Yes. You can use GraalVM to native compile your own binary, but it's not that straightforward how
<https://github.com/clj-easy/graal-docs> is about compiling clojure code using graal, but as mentioned, "straightforward" is maybe not the most apt description for it
Is it better to change my mindset and use different environments for different use cases as Michiel is suggesting? Trying to understanding the headspace of Clojurism in 2026, not fight against it or ruffle any feathers
It depends on what you're building. If you want absolute startup and runtime performance, say you are implementing your own alternative to grep for example, then native-image makes sense. Compiling with native-image is less straightforward because you need to compile on each architecture (macOS for macOS, windows for windows, Linux for Linux, x86 + arm64, etc.). And also because not all valid Java and Clojure code is supported, anything too dynamic might require special config and it's not always obvious what those need to be to make it work.
But if you just want fast startup, and python-like runtime performance, babashka works great and is simpler. Now babashka also has a more limited set of libraries, so that depends too.
Clojure and Babashka are very very close to each other, including the Java interrop and all that. So it's not like some of the other dialects like say Glojure, ClojureScript or Jank. Those ones have different hosts and so there's more of a learning curve and more things that differ from Clojure. But babashka is way similar to Clojure in so many ways. So I really recommend trying it out first and if it doesn't fit the bill, then try going native-image.
I'd say try Babashka first. Either ship a script with bbin, or package a standalone executable with Babashka. If you then decide you need stuff that you can't do with Babashka, reach for GraalVM native image. (Second option because Babashka is usually way more ergonomic, and this is the epicenter of the use cases Babashka was built for.)
Joining the choir by recommending Babashka until you hit obvious limitations.
Thank you all so much for the well-reasoned options and trade-offs
I built a CLI tool to use at work, where we don't typically use Clojure or Java, so I opted for Clojurescript for a node target. This works for me as everyone has a node.js install. I considered graalvm's native build, but the size of the artifact put me off.
#clojuredart has a nice experience if you want native binaries as well: https://github.com/Tensegritics/ClojureDart/blob/main/doc/quick-start.md But as pointed out above, library use is very different from clojure/babashka
I've ended up going with Babashka! Thank you all who recommended and thank you @borkdude for creating and maintaining it. Deeply appreciate understanding all these options for the future as well