Skip to content

Why cl-nix-forge

There are two established ways to build a Common Lisp system with Nix: nixpkgs' own lisp-modules (pkgs.sbcl.buildASDFSystem) and cl-nix-lite. Both are real, usable options, and this project owes its build primitive to both.

cl-nix-forge exists because of gaps neither closes.

Point by point

nixpkgs lisp-modules cl-nix-lite cl-nix-forge
One derivation per ASDF system, sharing prebuilt fasls yes yes yes
One repo exporting several systems that depend on each other, deduplicated no yes (ancestryWalker), documented as an advanced API yes, same mechanism, promoted to a documented, supported path
Lisp dependency vs. Nix build input conflated conflated (buildInputs = ... ++ ancestry.deps) separated: lispDependencies never touches buildInputs
Native library visible to a transitive (2+ hop) consumer no no (one hop, and only if the consumer directly depends on a package literally named cffi) yes, propagated through the same dependency graph as everything else
Declarative multi-Lisp-implementation test matrix no no (hand-maintained meta.broken predicate lists) yes (mkCheckMatrix)
.asd :version extraction no no ("no QuickLisp database nor .asd file introspection is done whatsoever") yes (fromAsdSystem), fails loudly on an unrecognized shape
Cross-platform program-op executable delivery — — yes — the same asdf:program-op path on every declared platform, including aarch64-darwin with SBCL (see below)
Consume a nixpkgs or foreign-flake package as a dependency — — yes (fromDerivation/fromNixpkgsLisp)

lib/core/*.nix carries the implementation and the reasoning behind each of these, in doc comments next to the code they document.

The rows that need more than a cell

Multi-system repositories

A single source tree can export several ASDF systems that depend on each other — and, transitively, on systems from other source trees that depend back on the first tree's other systems. That is a same-repository dependency cycle, and it appears the moment you stop treating "system" and "repository" as the same granularity.

Building each exported system as an independent derivation would recompile the shared source tree once per system, and worse, ASDF would try to rewrite fasls a sibling derivation already produced from the same files.

ancestryWalker walks the dependency graph and collapses any two derivations resolving to the same key — normally the store path of the source tree they were built from — into one. cl-nix-lite reached the same mechanism, and then warned its users away from it. Here it is the documented path, with one addition: when two merged systems disagree about a non-dependency build argument, that is an evaluation error naming the arguments and both systems, never a silent pick of whichever side the dependency walk folded last.

Lisp dependency versus Nix build input

They are different things. A Lisp-level dependency has to be findable by ASDF; a Nix-level build input has to be on PATH or in the linker's view. Conflating them, as both prior options do, means every transitive Lisp dependency silently joins your derivation's rebuild-trigger set even when nothing native changed.

lispDependencies resolve purely into CL_SOURCE_REGISTRY. They are still derivation inputs — their string context is preserved, so a dependency change still rebuilds its consumer — but they never enter buildInputs.

Native library propagation

A Lisp dependency that also builds a native .so/.dylib (via CFFI, say) has, in prior art, no way to make that fact visible more than one hop away. Every transitive consumer has to know by name which of its dependencies-of-dependencies produced the library, and hand-copy an LD_LIBRARY_PATH line for it.

Here nativeLibraries folds into the same dependency walk as lispDependencies, so a consumer three hops from the producer still finds the library — at build time, in a dev shell, and inside a delivered executable's wrapper. See Dependencies and native libraries.

Darwin executable delivery

mkExecutable drives asdf:program-op, which on SBCL embeds the Lisp image directly into the executable — the same path on aarch64-darwin as on Linux. An earlier revision fell back to a plain .core via save-lisp-and-die wrapped in sbcl --core on Darwin with SBCL, on the strength of one observation that program-op had not produced a binary within five minutes. That observation did not reproduce: rebuilt in isolation against a current nixpkgs SBCL, the same invocation this module uses completed in seconds and produced a working, already ad-hoc-signed Mach-O executable. See Platform coverage for what is and is not verified about this on aarch64-darwin.

What this does not claim

Non-goals lists what cl-nix-forge does not do and the reason for each boundary.