Develop and Build a Common Lisp Project using Nix


Coding
Oct 27, 2025

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 (or slime) in Emacs, and load the asd file by sly-compile-and-load-file.
  • Execute (asdf:load-system "proj-name") in the sly repl.
  • 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.