A few words from Agical

How to create a really simple ClojureCLR dependency tool

I just published clojure-clr-starterA 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:

  1. Find clr.tools.nrepl
  2. Find other libraries you might want to use
  3. Be able to load clr.tools.nrepl and other libraries
  4. 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:

  1. Uses babashka.deps/add-deps to download any dependencies and to build a classpath.
  2. Uses babasha.classpath to get the classpath and split it up in paths
  3. If a path is a .jar:
    1. Unpacks it in ./dependencies (ClojureCLR can’t read the jar file)
    2. Rewrites the classpath, replacing the jar path with a path to the unpacked dependency directory
  4. 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! ♥️