Common Lisp environment with Nix

It is very difficult to find a working example of common lisp environment with Nix. I hope this article helps folks to start off the common lisp project utilizing Nix.

Make the nix-shell environment

First, take a look at how to create shell environments with shell.nix.

A basic shell.nix file for the lisp(sbcl) environment

Create a shell.nix file in the fresh directory.

let
  nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-24.05";
  pkgs = import nixpkgs { config = {}; overlays = []; };
  sbcl' = pkgs.sbcl.withPackages ( ps: with ps; [ ]);
in
pkgs.mkShellNoCC {
  packages = [ sbcl' ];
}
Code Snippet 1: A basic shell.nix file

Enter the environment by running nix-shell in this project directory. nix-shell looks first for shell.nix in the current directory.

nix-shell

Now you are able to invoke sbcl in the project directory. Since there is no dependency in the environment, you can do almost nothing in there for now. Dependencies will be added in the later section of this article.

Automatic activation of the shell enviroment

You hove to invoke nix-shell everytime you change to this directory. This is very cumbersome. By using direnv, you can automate this.

Prepare .envrc. with a following content.

use nix
Code Snippet 2: .envrc

Activate direnv by issuing following command.

direnv allow

Now, every time you change directory to the project directory, the shell environment becomes automatically available.

Packaging a minimal code

Now that you have the lisp development environment, let’s build a web server as an example.

Define cl-tutorial system and prepare a server source code

Make cl-tutorial.asd as following. Notice that the first part of the file name is cl-tutorial, which is same as the package name called “cl-tutorial”, and a file name extension must be .asd, not .asdf.

As an web server library, hunghentoot is used.

(defsystem "cl-tutorial"
  :version "0.0.1"
  :author "antarcticafalls"
  :license "MIT"
  :description "a minimal web server as a tutorial"
  :depends-on (:hunchentoot)
  :components ((:module "./."
            :components
            ((:file "main")))))

A fake server code. you are going to revise later as needed.

(in-package :cl-user)
(defpackage :cl-tutorial
  (:use :cl :hunchentoot)
  (:export
   :main))
(in-package :cl-tutorial)

(defun main ()
  (loop))
Code Snippet 3: main.lisp

Load cl-tutorial system

Invoke sbcl in the environment and type following to load cl-tutorial.

(load (sb-ext:posix-getenv "ASDF"))
(pushnew (truename "./.") asdf:*central-registry*)
(asdf:load-system "cl-tutorial")

Then you would see:

debugger invoked on a ASDF/FIND-COMPONENT:MISSING-DEPENDENCY in thread
#<THREAD tid=2056565 "main thread" RUNNING {10003F0143}>:
  Component :HUNCHENTOOT not found, required by #<SYSTEM "cl-tutorial">

Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL.

restarts (invokable by number or by possibly-abbreviated name):
  0: [RETRY                        ] Retry loading HUNCHENTOOT.
  1:                                 Retry ASDF operation.
  2: [CLEAR-CONFIGURATION-AND-RETRY] Retry ASDF operation after resetting the
                                     configuration.
  3: [ABORT                        ] Exit debugger, returning to top level.

(ASDF/FIND-COMPONENT:RESOLVE-DEPENDENCY-NAME #<ASDF/SYSTEM:SYSTEM "cl-tutorial"> :HUNCHENTOOT NIL)
   error finding frame source: Bogus form-number: the source file has probably
                               changed too much to cope with.
   source: NIL
0]

Load fails because you don’t have hunchgentoot in the shell environment.

Add a hunchentoot dependency in the shell environment

In order to make hunchentoot available in the nix shell environment, you need adjust shell.nix.

let
  nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-24.05";
  pkgs = import nixpkgs { config = {}; overlays = []; };
  sbcl' = pkgs.sbcl.withPackages (ps: with ps; [ hunchentoot ]);
in
  pkgs.mkShellNoCC {
    packages = [ sbcl' ];
}
Code Snippet 4: shell.nix revised

After reloading an environment by direnv reload, again you type following on sbcl repl.

(load (sb-ext:posix-getenv "ASDF"))
(pushnew (truename "./.") asdf:*central-registry*)
(asdf:load-system "cl-tutorial")
Code Snippet 5: Load cl-tutorial system

WARNING: System definition file #P"/nix/store/db65yvmfx3zscw71kfkqz2rnnmj5kngw-sbcl-hunchentoot-v1.3.0/hunchentoot.asd" contains definition for system "hunchentoot-test". Please only define "hunchentoot" and secondary systems with a name starting with "hunchentoot/" (e.g. "hunchentoot/test") in that file.
WARNING: System definition file #P"/nix/store/db65yvmfx3zscw71kfkqz2rnnmj5kngw-sbcl-hunchentoot-v1.3.0/hunchentoot.asd" contains definition for system "hunchentoot-dev". Please only define "hunchentoot" and secondary systems with a name starting with "hunchentoot/" (e.g. "hunchentoot/test") in that file.
; compiling file "/home/c/cl-tutorial/main.lisp" (written 16 NOV 2024 10:19:26 PM):

; wrote /home/c/.cache/common-lisp/sbcl-2.4.4-linux-arm64/home/c/cl-tutorial/main-tmpGHU3ALSV.fasl
; compilation finished in 0:00:00.011
T

This should bring you to the cl-tutorial system. Try

(in-package :cl-tutorial)

Implement a server code

Instead of a fake code, implemented a very minimal server code into mail.lisp

(in-package :cl-user)
(defpackage :cl-tutorial
  (:use :cl)
  (:export
   :main))
(in-package :cl-tutorial)

(hunchentoot:define-easy-handler (root-route :uri "/") (name)
  (format nil "Hey~@[ ~A~]!" name))
(defvar *server* (make-instance 'hunchentoot:easy-acceptor :port 6789))

(defun main ()
  (hunchentoot:start *server*)
  (loop))
Code Snippet 6: main.lisp revised

If you load cl-tutorial system, you should see a response from the server.

curl -v http://localhost:6789/
* Host localhost:6789 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:6789...
* connect to ::1 port 6789 from ::1 port 49520 failed: Connection refused
*   Trying 127.0.0.1:6789...
* Connected to localhost (127.0.0.1) port 6789
> GET / HTTP/1.1
> Host: localhost:6789
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Content-Length: 4
< Date: Sun, 17 Nov 2024 11:47:47 GMT
< Server: Hunchentoot 1.3.0
< Content-Type: text/html; charset=utf-8
<
* Connection #0 to host localhost left intact
Hey!⏎

Appendix

The directory structure is following.

.
├── .envrc
├── cl-tutorial.asd
├── main.lisp
└── shell.nix
Code Snippet 7: The directory structure