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
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂