Common Lisp traditionally produces standalone executables by dumping an entire heap image, including the runtime, compiler, debugger, and all loaded libraries. While suitable for applications, this approach leads to large binaries and slow startup times—especially problematic for lightweight scripting tasks.
A more efficient strategy borrows from busybox: bundle multiple scripts into a single executable image and use symbolic links to dispatch behavior based on the invoked name. Tools like cl-launch support this pattern across multiple Common Lisp implementations, making CL viable for scripting.
Project Structure
All scripts reside in one ASDF system named clbox, located in ~/common-lisp/clbox/:
$ ls
bar.lisp clbox.asd foo.lisp
Each script defines its own package and a main function that accepts command-line arguments as a list:
;;; bar.lisp
(defpackage :bar (:use :cl))
(in-package :bar)
(defun main (args)
(declare (ignore args))
(format t "Hello world from bar~%"))
;;; foo.lisp
(defpackage :foo (:use :cl))
(in-package :foo)
(defun main (args)
(declare (ignore args))
(format t "Hello world from foo~%"))
The ASDF system definition includes both files:
;;; clbox.asd
(asdf:defsystem #:clbox
:description "CL scripts demo"
:components ((:file "foo")
(:file "bar")))
Building the Unified Executable
Use cl-launch to generate a single binary that embeds all scripts:
$ cl-launch -o clbox \
-d ! \
-s clbox \
-p foo -DE foo \
-p bar -DE bar
Create symbolic links for each script name pointing to the unified binary:
$ ln -s clbox foo
$ ln -s clbox bar
Now each link behaves as its own script:
$ ./foo
Hello world from foo
$ ./bar
Hello world from bar
Automated Build Script
To simplify maintenance, use a build script (build.lisp) that regenerates the image and updates symlinks:
#!/usr/bin/cl -l ccl -E main
(defparameter *bindir* "~/.local/bin/")
(defparameter *image* "clbox")
(defparameter *lisp* "ccl")
(defparameter *script-list* '("foo" "bar"))
(defun make-args ()
(apply #'append
(mapcar (lambda (s)
(list "--package" s "-DE" s))
*script-list*)))
(defun symlink-exists (src target)
(and (uiop:probe-file* target)
(equal (uiop:probe-file* src :truename t)
(uiop:probe-file* target :truename t))))
(defun make-symlinks ()
(dolist (s *script-list*)
(unless (symlink-exists *image* s)
(format t "~&Create symlink for ~A~%" s)
(uiop:run-program `("ln" "-s" ,*image* ,s)
:output t :error-output t))))
(defun make-cmd ()
`("cl-launch" "--output" ,*image*
"--dump" "!"
"--lisp" ,*lisp*
"--system" "clbox"
,@(make-args)))
(defun main (args)
(declare (ignore args))
(when *bindir*
(unless (uiop:probe-file* *bindir*)
(format t "~&Creating directory ~A~%" *bindir*)
(ensure-directories-exist (uiop:native-namestring *bindir*)))
(uiop:chdir *bindir*))
(uiop:run-program (make-cmd) :output t :error-output t)
(format t "~&Heap image ~A updated~%" *image*)
(make-symlinks)
(format t "~&All done~%"))
Note: Avoid running this build script with SBCL due to symlink detection isues; CCL or CLISP are recommended. The target Lisp implementation for the final image (set via *lisp*) can be any supported by cl-launch.
Adding New Scripts
To add a new script:
- Add the source file (e.g.,
newscript.lisp) defining package:newscriptand functionmain. - Include it in
clbox.asd. - Add
"newscript"to*script-list*inbuild.lisp. - Run
./build.lisp.
Integrating Quicklisp Libraries
To allow ASDF to locate Quicklisp-installed packages during builds, configure the source registry:
mkdir -p ~/.config/common-lisp/source-registry.conf.d
echo '(:tree (:home "quicklisp/dists/quicklisp/software/"))' > \
~/.config/common-lisp/source-registry.conf.d/include-quicklisp.conf