使用 ASDF 创建项目

Table of Contents

Common Lisp 中,术语 system 就是“库”的意思,一个 system 就是由多个源文件或者第三方组件构成,定义一个 system,就是为了解决文件和组件之间的依赖关系。通常“项目(project)”的最小单位就是 system。

而在 Common Lisp 标准中,最小单位的术语叫”modules“,主要涉及全局变量 *modules* 和 provide、require 两个函数。require 在多数 Common Lisp 实现中已经被替换成其他,比如 ASDF。

目前 ASDF 已成为 Common Lisp“公认标准”,多数 Common Lisp 实现已自带 ASDF 模块。

1 使用 ASDF 手动构建项目

1.1 ASDF 结构

ASDF 定义的 system 里通常有一个 .asd 的文件,里面定义了这个 system 的名字、文件依赖以及三方库的依赖等信息。

以 restas(一个WEB框架)为例,解压后的 restas 目录里有一个 restas.asd 文件,如下:

(defsystem #:restas
    :depends-on (#:cffi #:hunchentoot #:bordeaux-threads
                        #:routes #:alexandria #:data-sift)
    :pathname "src"
    :components ((:file "packages")
                 (:file "special" :depends-on ("packages"))
                 (:file "declarations" :depends-on ("packages"))
                 (:file "errors" :depends-on ("special"))
                 (:file "render" :depends-on ("special"))
                 (:file "context" :depends-on ("special"))
                 (:file "module" :depends-on ("context" "declarations"))
                 (:file "route" :depends-on ("module" "render"))
                 (:file "decorators" :depends-on ("route"))
                 (:file "vhost" :depends-on ("special"))
                 (:file "hunchentoot" :depends-on ("vhost" "module" "errors"))
                 (:file "policy" :depends-on ("packages"))))

这里有几个参数需要说明:

defsystem 函数的第一个参数就是 system 的名字。

depends-on 定义了这个 system 外部三方组建依赖,这里是一个列表,列举了所有依赖的组件,这些组件首先被加载。

pathname 指定了源码存放路径,此处指定了在 src 目录里。

components 定义了各个文件的依赖,这里是一个嵌套列表,关键字 :file 定义了要加载的文件名,注意不需要文件后缀名。然后 ASDF 会依照列表的顺序加载文件。这里首先加载 src/packages.lisp;然后再加载 src/special.lisp,并且指明了 special.lisp 依赖 packages.lisp。

1.2 手动创建 .asd

本节我们先手动创建一个 .asd 来管理依赖。先找个位置创建个文件夹来管理 system,本例中我创建了一个文件夹在 /tmp/hello 里。

然后新建三个文件:

$ touch hello.asd
$ touch hello.lisp
$ touch package.lisp

hello.asd 用来定义 system 信息,主要代码写在 hello.lisp 里,而 package.lisp 是用来定义包名字的,通常都会把包定义单独放置在一个文件里来管理,这样方便其他文件中调用。

hello.asd的内容:

(defsystem #:hello
    :author "lu4nx"  ; 定义作者名字
    :version "1.0"   ; 定义版本号
    :license "MIT"   ; 定义开源协议
    :components ((:file "package")
                 (:file "hello" :depends-on ("package"))))

按上面说过的,components 参数决定了加载哪些文件、加载顺序以及依赖关系。

package.lisp 的内容如下:

(defpackage #:hello-world
  (:use :cl)
  ;; 对外导出 say-hello 函数
  (:export :say-hello))

这里定义了包名叫 hello-world,注意 package 的名字和 system 的名字之间区别,system 名字是用于加载,而加载之后的命名空间由 package 决定,两者可以不是同名的,我这里定义的 system 名字叫 hello,而 package 的名字是 hello-world。

hello.lisp 代码如下:

(in-package #:hello-world)

(defun say-hello ()
  (print "hello world"))

1.3 加载 system

现在定义好了 system,要加载它得先让 ASDF 能找到它,关于 ASDF 的搜索过程请见文档: http://common-lisp.net/project/asdf/asdf.html#Controlling-where-ASDF-searches-for-systems

在 SBCL 中,require 函数默认已经被 ASDF 的加载实现替换了,所以可以直接用:

(require 模块名)

有些 CL 实现可能需要显示用 ASDF 的加载函数:

(asdf:load-system 模块名)

system 加载路径:

  • 从固定路径中加载: ~/common-lisp 或者 ~/.local/share/common-lisp/source/
  • 配置 /etc/common-lisp/source-registry.conf

如果 system 分散在磁盘中多个位置,就可以配置 source-registry.conf.d。

比如有些 system 是实际使用的,还有些 system 处于开发阶段,放在一个开发目录中(如:/my/mycode/common-lisp),但同时希望 ASDF 能加载这两个位置中的 system。

  • 早期风格:asdf:*central-registry*

早期配置全局变量 *central-registry* 来定义加载位置,新版本的 ASDF 中不推荐继续使用。

1.3.1 示例

最简单的方法是在 ~/quicklisp/local-projects 下创建一个 hello.asd 的链接,链接到 /tmp/hello/hello.asd:

$ cd ~/quicklisp/local-projects
$ ln -s /tmp/hello/hello.asd .

然后用 Quickload 加载:

CL-USER> (ql:quickload :hello)
To load "hello":
  Load 1 ASDF system:
    hello
; Loading "hello"
[package hello-world]
(:HELLO)

尝试下调用 say-hello 函数:

CL-USER> (hello-world:say-hello)

"hello world"
"hello world"

1.4 解决附加文件问题

比如你的项目中某段代码需要加载某一个文件是用 Common Lisp 自带的文件函数来加载,在用 Quicklisp 加载你的这个项目时,会出现找不到该附加文件的错误。

ASDF 编译 system 后,加载的话,*load-truename* 和 *load-pathname* 会被改变,但是 ASDF 提供了 system-relative-pathname 函数可用:

(defun get-tld-data-path ()
  (asdf:system-relative-pathname 'cl-etcdomain "tld.dat"))

这样就会在原始目录加载 tld.dat 文件了。

2 QuickProject

我们其实可以用 QuickProject 这个库来自动创建项目,它会自动帮助我们创建 .asd 文件。

首先用 QuickLoad 加载 QuickProject,如果系统里没有安装 QuickProject 会自动下载并加载:

(ql:quickload :quickproject)

然后新建一个 hello1 项目:

(quickproject:make-project #p"/tmp/hello1" :author "lu4nx")

QuickProject 会自动在 /tmp 目录下创建一个叫 hello1 的文件夹,之前我们手动定义的内容它全部帮我们完成了:

  ├── hello1.asd
  ├── hello1.lisp
  ├── package.lisp
  └── README.txt

剩下的过程就跟上节的一样了,写代码,然后加载。