Develop and Build a Common Lisp Project using Nix
I recently tried to build a Common Lisp project using Nix and found it is quite easy with just the buildASDFSystem function in nixpkgs and ASDF alone. I will share how I have done it here in case someone else finds it useful.
Simple Configuration When Developing
On the Nix we will have something like this:
# flake.nix
{
description = "";
inputs.flake-utils.url = "github:numtide/flake-utils";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = { self, flake-utils, nixpkgs }:
flake-utils.lib.eachDefaultSystem (system:
let pkgs = nixpkgs.legacyPackages.${system};
in rec {
packages.default = pkgs.sbcl.buildASDFSystem {
pname = "proj-name";
version = "0.1";
src = self;
lispLibs = with pkgs.sbclPackages; [
alexandria
# lisp packages in nixpkgs go here
];
nativeLibs = [
# native libs go here
];
};
}
);
}
On the Lisp side we will have an asd file
;; proj-name.asd
(require 'asdf)
(asdf:defsystem "proj-name"
:depends-on (#:alexandria)
:components ((:file "main"))) ;; we will have a main.lisp file
With these two files we can then start developing the project with the following steps
- Enter nix develop environment by executing
nix develop .in the directory the project. - Launch Emacs inside the developing environment.
- Launch
sly(orslime) in Emacs, and load theasdfile bysly-compile-and-load-file. - Execute
(asdf:load-system "proj-name")in theslyrepl. - Enjoy!
Building an Executable
To build an executable we need a few more snippets on the Nix side
#...
packages.default = pkgs.sbcl.buildASDFSystem {
# ...
# other code is omitted here
lispLibs = with pkgs.sbclPackages; [
alexandria
# lisp packages in nixpkgs go here
];
nativeBuildInputs = [
pkgs.makeWrapper
];
buildScript = pkgs.writeText "build-awesomes" ''
(require 'asdf)
(asdf:load-system "proj-name")
(sb-ext:save-lisp-and-die
"proj-name"
:executable t
#+sb-core-compression :compression
#+sb-core-compression t
;; assume we have the `main' function defined and exported in the main.lisp file
:toplevel #'main:main)
'';
installPhase = ''
runHook preInstall
mkdir -p $out/bin
cp proj-name $out/bin
# make sure the executable can find dynamically linked libraries
wrapProgram $out/bin/proj-name \
--prefix LD_LIBRARY_PATH : $LD_LIBRARY_PATH
runHook postInstall
'';
};
When we execute nix build . in the project directory, lisp scripts in buildScript will be executed and generates a binary file proj-name. The binary file will then be copied into $out/bin directory by the scripts inside installPhase. After the nix build . finished, we can find the executable in the result/bin/ directory
We can also use nix run . to run the result executable directly.
Limitations of This Method and Other Notes
The biggest limitation of this solution may be you need to restart the REPL (or even the Emacs launched inside the develop environment) every time you add a package into the flake.nix.
You can find the lisp section of the nixpkgs documentation here. packages.nix in the nixpkgs also has plenty lisp project definitions we can take references from.
I have a little project built this way and it runs on github actions simply by using nix run ., you can also check that.