Submodules give you the flexibility to fetch the dependencies, or not.\ And they enable more granular reproducible builds.\ Use submodules! assets/cl-submodules.png A bright thumbnail with handwritten digital text on it. \ In the center, there’s a composition of four schematic boxes of differing sizes spread around. \ Some have lambdas (Greek letter, a symbol of functional programming and Lisp) drawn on them. \ A bright “SUBMODULE” is written over them. \ In the corners, attributions to “aartaka.me” and “Artyom Bologov” are written. IMAGE_ALT

So there are half a dozen of Common Lisp package/system/project managers:

These mostly fall into two categories:

    Package/project managers fetching things from the Internet right into the running Lisp image and not touching the project itself.
  1. Project-local package managers that include all the dependencies into the project tree.

Project-local solutions bloat the checkout size and slow down repository cloning. Online ones require network connection. I’m not satisfied with either of the trade-offs, and I think I have a solution to that.

Okay, I’ll need a more involved problem statement than that. The features I’m looking for in my dependency management journey are:

Submodules

Git submodules are this kind of solution.

So my setup is:

Stripped-down project Makefile (yes, I use Make for Lisp projects) then looks like:

LISP ?= sbcl
LISP_FLAGS ?= ...

export CL_SOURCE_REGISTRY ?= $(PWD)//

executable:
	$(LISP) $(LISP_FLAGS) --eval '(require "asdf")' --eval '(asdf:make "system")' --eval '(uiop:quit)'

.PHONY: tests
tests:
	$(LISP) $(LISP_FLAGS) --eval '(require "asdf")' --eval '(asdf:test-system "system")' --eval '(uiop:quit)'

.PHONY: repl
repl:
	$(LISP) $(LISP_FLAGS) --eval '(require "asdf")' --eval '(asdf:load-system "system")'
Things to note:
exported/shared CL_SOURCE_REGISTRY with double slash
That’s a vital hack mentioned by Alexander Artemenko on Reddit. This double slash means a given directory is an ASDF “tree” to be searched for systems recursively. Here’s the syntax explanation in ASDF manual.
--eval '(require "asdf")' --eval '(asdf:load-system "system")'
All the bootstrapping is basically two expressions, all reliant on ASDF. Given that CL_SOURCE_REGISTRY is set up properly, all the dependencies are there. Build, test, and launch REPLs all you want. No Quicklisp needed.
repl
REPL is the same call to $LISP, but without the --eval '(quit)' It’s both rlwrap compatible and has all the necessary libraries accessible.
Swank/Slynk integration
Easy to support by adding two more ASDF subsystems and running them from the Makefile.
A thin strip of an image with lots of boxes, evidently written by an amateur. Some boxes have text on them: “deps”, “CI”, “REPL”, “resilience” (overflowing the box,) and “compat”.

This setup gives me:

What not to like about it?

The Problem With ~/common-lisp

Thanks to Konrad Hinsen for making me think about that! One scenario where this submodule-driven workflow might break is when you clone the repo into ~/common-lisp. And then there’s another version of one of the deps in there, provoking a version clash.

Cloning into ~/common-lisp is a bad practice that should be eradicated in favor of properly compartmentalized registries. But alas. Anyway, if you clone into ~/common-lisp or ~/.local/share/common-lisp/source/, then do a non-recursive clone instead. If you cloned the library there, you likely have all the dependencies available already. In the very same ~/common-lisp, no doubt. So no need for submodules.

But better not use ~/common-lisp at all, global state bad.


To see how the whole setup around submodules looks, you can check out my Quickproject template and its makefile and .asd file.