I just published clojure-clr-starter – A Dockerized CLojureCLR Starter Project, that makes it easy to create a ClojureCLR project, start it and connect a Clojure editor. This article describes how the project is set up. Surprisingly little glue was needed once David Miller had created clr.tools.nrepl (a few weeks ago).
There is no dependency tooling for ClojureCLR yet (it’s being worked on) so we have to solve a few things ourselves. We need to get ClojureCLR to:
- Find clr.tools.nrepl
- Find other libraries you might want to use
- Be able to load clr.tools.nrepl and other libraries
- Start the nREPL server
Now, even if there’s no dependency tooling for ClojureCLR, we can still use Clojure tooling for figuring out the classpath and downloading Clojure dependencies. I used Babashka which packs tools.deps beautifully. And it also gives us civilized scripting.
I didn’t want to install .Net and things on my machine, and I also wanted it to be easy to reproduce the setup, so I put the needed dependencies (including ClojureCLR, Java and Leiningen) in a Docker container, and used Docker Compose to define a service that is easy to deploy on my (or yours) computer.
The main script is cheap-deps.clj
. It really is cheap.
😀 This is the entire script:
#!/usr/bin/env bb
(require '[clojure.string :as string])
(require '[clojure.edn :as edn])
(require '[babashka.deps :as deps])
(require '[babashka.classpath :as classpath])
(require '[babashka.fs :as fs])
(defn process-jar-deps! [classpath]
(->> classpath
classpath/split-classpath
(map (fn [path]
(if (clojure.string/ends-with? path ".jar")
(let [deps-subdir (string/replace (fs/file-name path) #".jar$" "")
deps-dir (str "/app/dependencies/" deps-subdir)]
(fs/create-dirs deps-dir)
(fs/unzip path deps-dir {:replace-existing true})
deps-dir)
path)))
(string/join fs/path-separator)))
(when (= *file* (System/getProperty "babashka.file"))
(deps/add-deps (edn/read-string (slurp "deps.edn")) {:aliases [:dev]})
(-> (classpath/get-classpath)
process-jar-deps!
println))
It:
- Uses
babashka.deps/add-deps
to download any dependencies and to build a classpath. - Uses
babasha.classpath
to get the classpath and split it up in paths - If a path is a
.jar
:- Unpacks it in
./dependencies
(ClojureCLR can’t read the jar file) - Rewrites the classpath, replacing the jar path with a path to the unpacked dependency directory
- Unpacks it in
- Prints the new classpath
The script is used by the ENTRYPOINT
of the Docker
container, entrypoint.sh
:
#!/bin/bash
mkdir -p /app/dependencies
CLOJURE_LOAD_PATH=$(bb /app/docker/cheap-deps.clj)
echo "export CLOJURE_LOAD_PATH=${CLOJURE_LOAD_PATH}" > /app/dependencies/load-path.sh
echo "Exported CLOJURE_LOAD_PATH: ${CLOJURE_LOAD_PATH}" >&2
exec "$@"
and will be run when the container starts. We use it to run
cheap-deps.sh and write export
CLOJURE_LOAD_PATH=<classpath>
to the file
./dependencies/load-path.sh
. This file is then sourced
by:
start-repl.sh
:
#!/bin/bash
source /app/dependencies/load-path.sh
echo "Loaded CLOJURE_LOAD_PATH: ${CLOJURE_LOAD_PATH}" >&2
cleanup() {
rm -f /app/docker/.nrepl-port
}
trap cleanup EXIT
Clojure.Main -e '(require (quote user)) (user/start-nrepl!)'
After having sourced the CLOJURE_LOAD_PATH
, it runs
Clojure.Main
and has it evaluate the code for starting the
nrepl server. It also tries to clean up after the
user/start-nrepl!
functions which creates a
docker/.nrepl-port
file:
(ns user
(:require [clojure.tools.nrepl]))
(defn start-nrepl! []
(clojure.tools.nrepl/start-server! {:host "dotnet-clojure"
:port 1667
:quiet true})
(spit "docker/.nrepl-port" "6667")
(println "Started nREPL server at localhost:6667"))
(comment
(start-nrepl!)
:rcf)
(The cleanup doesn’t quite work if the Docker container is shut down, if you know how to fix that, please file a PR on https://github.com/PEZ/clojure-clr-starter.)
The port written to the file is 6667
, because that’s
where the host machine will find the nREPL server running in the Docker
container. This is hardcoded in docker-compose.yml
. I don’t
know how to make this dynamic, which means that, for now, to run several
ClojureCLR nREPL servers this way, you’ll have to change the port in two
places. (Again, PRs welcome.)
That’s it. The rest is just a regular (tiny) Clojure project
structure. As the name implies cheap-deps.sh
is beyond bare
bones. I don’t know if it will scale, but in theory you should now be
able to develop your ClojureCLR app and add (compatible) Clojure
libraries to the deps.edn
file. You’ll need to restart the
container to load the new libraries.
Let’s call it a decent start? Maybe it’ll have to be good enough while we wait for the ClojureCLR CLI. I suggest you head over to David Miller’s Github Sponsor page to show him you appreciate the work he’s putting into this and to ClojureCLR itself, of course.
Why create clojure-clr-starter?
One of Clojure’s many strengths is its reach. You can use it for web applications, mobile apps, on the JVM, standalone executables, for .Net applications, desktop apps, micro services, server monoliths, lambdas, shell scripting, embedded in hardware, and just about wherever. I appreciate this a lot because since I discovered the language, I want to always use Clojure. Everything else has inferior developer ergonomics.
The main vehicle for giving a Clojure coder reach to .Net is ClojureCLR, a port of Clojure that targets the Common Language Runtime. Language-wise it’s the same wonderful Clojure experience.
However, ClojureCLR has been lacking a bit in development tooling
support. Something that changed dramatically a few weeks ago when clr.tools.nrepl,
an nREPL server, was
released. This should, in theory, give ClojureCLR coders most of the
editor tooling support that Clojure and ClojureScript coders have. CIDER, Cursive, and a lot of other Clojure
editor environments, including Calva,
are nREPL clients. As a Calva maintainer I have had on my todo list to
check this out and figure out what Calva might need to add to support
ClojureCLR well. When I finally found some time for doing this, I
noticed that it was very hard to find information about how to set a
ClojureCLR project up. I searched Github for projects and couldn’t
really figure out how they work. A lot of them had a Leiningen
project.clj file using a plugin named lein-clr
. At first I
thought this meant that I could use Leiningen to start my REPL, but all
my attempts failed. Then I realized that Leiningen can’t do this.
Leiningen is a JVM creature.
I then learned that the creator of ClojureCLR, David Miller, is working on a port of the deps.edn tools and CLI, and that there is no dependency or REPL starter tool for ClojureCLR yet. I guess that most ClojureCLR developers are quite at home in the .Net world and know how to use and build DLL files and such and maybe this is how they do it (this article suggests it used to be something like that). I didn’t really have time to learn all that. Then a clue from David Miller, made me realize that I would have to put clr.tools.nrepl on the classpath (CLOJURE_LOAD_PATH) myself in a way that ClojureCLR can consume it. For the purpose of testing the new nrepl server that would be quite easy. Unpack it somewhere and handcraft a classpath.
However, I did not want the next developer that was curious about ClojureCLR to rediscover all this that took me a while to discover. So I wanted to create an example project to package what I had figured out and to make it easier for the next guy to experiment with ClojureCLR things. This led me to the idea to build a simple dependency tool myself. Little did I realize beforehand, how simple that could be.
In the process I made it super easy and quick to go from zero to hacking on ClojureCLR code the Clojure way with full editor tooling support. Or so I say. But don’t take my word for it. Test it yourself!
Get started with ClojureCLR
Head over to the clojure-clr-starter project. Star it. Then follow the instructions there and you will be evaluating your ClojureCLR program into existence in less then two minutes. (If you have Docker installed, otherwise add the time it takes to install that (which is just a few minutes).
Happy coding! ♥️