使用 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 .

加载、调用:

(ql:quickload :hello)
(hello-world:say-hello)                 ; => "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

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