Common Lisp Dependency Vendoring with Submodules

By Artyom Bologov 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.

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

These mostly fall into two categories:

  1. Package/project managers fetching things from the Internet right into the running Lisp image and not touching the project itself.
  2. 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")'
Makefile for submodule vendoring approach
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.

Leave feedback! (via email)