Common Lisp 脚本化实践
[2024/01/21] 更新:这个方法我已经弃用了。现在有了更好的解决方案,就是使用 roswell。
Common Lisp 脚本化实践
背景
Common Lisp 生成的可执行文件实际上是内存堆的映像,包含了完整的运行时,库(标准库及所有载入的第三方库),编译器,调试器等等。通常体积巨大。对于“产品”的交付和部署这倒不是什么大问题,但是对于脚本,特别是个人脚本就显得有些尴尬了。不经过的编译的 CL 脚本通常会有比较明显的启动延迟,载入的库越多延迟越久。而经过编译的每一个 CL 脚本都携带着一坨完整的Lisp环境,就好像这个世界上不存在动态链接这种技术,想像一下,每一个 C 程序都是静态编译的...
本文使用的技术来自于 busybox。busybox 将很多小的命令行程序集成进同一个可执行文件,然后创建不同名字的软链接。busybox 通过用户运行的命令名执行不同的程序逻辑。
目前 buildapp 和 cl-launch 都支持类似的技术来生成单一的堆映像,不同的是 buildapp 仅支持 ccl 和 sbcl 两个实现,所以本文选择了 cl-launch 来做实验。实验的结果证实,CL 确实可以作为一个合格的脚本语言来用。
一些约束
-
整个“脚本环境”都容纳在一个单独的 ASDF “系统”中;
-
每个脚本创建了一个独立的包(package),在命名空间上和其它脚本隔离;
-
每个脚本的入口命名为
main
函数,和 C 一样; 命令行传递的所有参数都以一个列表的形式传递给main
函数。 -
脚本名 = 包名 = 文件名
这些约束并非强制性的,仅仅是我自己的做法。不采用同样的策略当然是可行的,但是某些后面的步骤也需要相应地修改。
内容
我的系统名叫clbox
,整个系统放在 ~/common-lisp/
目录中,这是 ASDF 默认的源注册目录之一。
$ cd ~/common-lisp/clbox
$ ls
bar.lisp clbox.asd foo.lisp
$ cat bar.lisp
(defpackage :bar
(:use :cl))
(in-package :bar)
(defun main (args)
(declare (ignore args))
(format t "Hello world from bar~%"))
$ cat foo.lisp
(defpackage :foo
(:use :cl))
(in-package :foo)
(defun main (args)
(declare (ignore args))
(format t "Hello world from foo~%"))
$ cat clbox.asd
(asdf:defsystem #:clbox
:description "CL scripts demo"
:components ((:file "foo")
(:file "bar")))
构建
$ cl-launch -o clbox \
> -d ! \
> -s clbox \
> -p foo -DE foo \
> -p bar -DE bar
$ ls
bar.lisp clbox clbox.asd foo.lisp
$ ln -s clbox foo
$ ln -s clbox bar
$ ls -l
lrwxrwxrwx 1 fm fm 5 2月 19 20:29 bar -> clbox
-rw-rw-r-- 1 fm fm 135 2月 18 18:48 bar.lisp
-rwxr-xr-x 1 fm fm 42672688 2月 19 20:28 clbox
-rw-rw-r-- 1 fm fm 117 2月 18 18:47 clbox.asd
lrwxrwxrwx 1 fm fm 5 2月 19 20:29 foo -> clbox
-rw-rw-r-- 1 fm fm 135 2月 18 18:49 foo.lisp
$ ./foo
Hello world from foo
$ ./bar
Hello world from bar
自动化构建
其实作为概念演示,上面的内容就够了。下面的脚本build.lisp
用于自动化构建并自动生成软链接,以降低维护工作量。
#!/usr/bin/cl -l ccl -E main
;;; Directory to save the output
;;; Don't forget the last slash, or the last part of the path will be treated
;;; as a file rather than a directory
(defparameter *bindir* "~/.local/bin/")
;;; File name of the executable heap image
(defparameter *image* "clbox")
;;; Specify the LISP implementation
(defparameter *lisp* "ccl")
;;; Names of all scripts
(defparameter *script-list* '("script-1"
"script-2"
"script-3"
"more-scripts-add-to-here"
))
(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*)
(when (not (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*
(let ((dir-exist? (uiop:probe-file* *bindir*)))
(when (not dir-exist?)
(format t "~&The specified directory ~A does not exists
try to create it~%" *bindir*)
(let ((mkdir-status (ensure-directories-exist
(uiop:native-namestring *bindir*))))
(when (not mkdir-status)
(error "~&Directory ~A creation failed, cannot continue~%" *bindir*)
(uiop:quit)))))
(uiop:chdir *bindir*))
(let ((cmdline (make-cmd)))
(uiop:run-program cmdline :output t :error-output t)
(format t "~&heap image ~A updated~%" *image*)
(make-symlinks)
(format t "~&All done~%")))
脚本将输出目录设置为 ~/.local/bin
,在我的系统中,这个目录会自动添加到环境变量PATH
中。
构建完毕后会自动在相同的目录里面建立软链接。不过这里有个问题,当用 sbcl
运行此脚本的时候,用于检测链接是否已经存在的判断逻辑会失效,而使用ccl
或clisp
就没有这个问题。所以,请避免使用 sbcl
来运行构建脚本。至于用于生成可执行文件的 Lisp 实现,倒是没什么问题,全局变量*lisp*
可以设为任何你喜欢,而且受cl-launch支持,而且可以导出可执行映像的 CL 实现。
现在,要新加入一个脚本,其工作量除了编写脚本本身外,只剩下:
-
修改
clbox.asd
将新脚本列为系统组件; -
将新脚本的名字添加到
*script-list*
中; -
执行
./build.lisp
-
收工
补充:让ASDF可以找到Quicklisp安装的库
quicklisp 是一个“重量级”的工具,在REPL中使用没什么问题,但是在脚本中就没必要了。可以将 quicklisp 的目录添加到 ASDF 的源注册目录中,在构建时 ASDF 就可以找到并 load 已经安装的第三方包。
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