如何配置一个Common Lisp 集成开发环境 - 使用 Roswell 在 Ubuntu 中快速开始 Common Lisp 编程
如何配置一个Common Lisp IDE - 使用 Roswell 在 Ubuntu 中快速开始 Common Lisp 编程
[原文]https://towardsdatascience.com/how-to-set-up-common-lisp-ide-in-2021-5be70d88975b
如果走错了路,配置一个 Common Lisp 开发环境会既乏味又耗时。
它涉及以下的安装和配置项:
-
一个 Common Lisp 实现 —— 通常是
sbcl
; -
emacs
—— Common Lisp 开发中首选的非商业编辑器; -
quicklisp
—— Common Lisp 包管理器的金标准; -
slime
——emacs
和 CL 实现(sbcl)之间的桥梁。
这个过程充满了陷阱,可能会令人不知所措。尤其是对于初学者而言。幸运的是,现在有了 roswell
!
roswell
是一个现代的 Common Lisp 虚拟环境和包管理器,它不仅允许将 Common Lisp 的不同实现(sbcl, ecl, allegro 等)安装到单独的虚拟环境中,还可以将它们的不同版本安装到单独的虚拟环境中。Roswell 允许在不同的实现和版本之间切换,这对于 Common Lisp 包的开发人员来说必不可少。因此,Roswell 应该是任何严肃的 Common Lisp 程序员的好伙伴。
此外,Roswell 允许设置测试环境并集成 CI(参见此处)。
在 Ubuntu 20.04 LTS 中安装 Roswell
# Following instructions in github of roswell:
# 安装到系统侧
sudo apt-get -y install git build-essential automake libcurl4-openssl-dev zlib1g-dev
git clone -b release https://github.com/roswell/roswell.git
cd roswell
sh bootstrap
./configure
make
sudo make install
ros setup
# 安装到用户侧
git clone -b release https://github.com/roswell/roswell.git
cd roswell
sh bootstrap
./configure --prefix=$HOME/.local
make
make install
echo 'PATH=$HOME/.local/bin:$PATH' >> ~/.profile
PATH=$HOME/.local/bin:$PATH
ros setup
或者,您也可以按照以下步骤操作:
# install dependencies for ubuntu
$ sudo apt install libcurl4-openssl-dev automake
# download Roswell installation script
$ curl -L https://github.com/roswell/roswell/releases/download/v19.08.10.101/roswell_19.08.10.101-1_amd64.deb --output roswell.deb
# run the installation
$ sudo dpkg -i roswell.deb
# add roswell to PATH to your ~/.bashrc
# (important for scripts to run correctly!)
export PATH="$HOME/.roswell/bin:$PATH"
# don't forget to source after adding to ~/.bashrc if youare still
# in the same session and
# if you don't want to restart your computer:
$ source ~/.bashrcalso
设置 Roswell 后,我们可以通过以下方式检查可用的 Common Lisp 实现:
$ ros install
# this prints currently:
Usage:
To install a new Lisp implementaion:
ros install impl [options]
or a system from the GitHub:
ros install fukamachi/prove/v2.0.0 [repository... ]
or an asdf system from quicklisp:
ros install quicklisp-system [system... ]
or a local script:
ros install ./some/path/to/script.ros [path... ]
or a local system:
ros install ./some/path/to/system.asd [path... ]
For more details on impl specific options, type:
ros help install implCandidates impls for installation are:
abcl-bin
allegro
ccl-bin
clasp-bin
clasp
clisp
cmu-bin
ecl
mkcl
sbcl-bin
sbcl
sbcl-source
安装不同的 Common Lisp 实现和不同的版本
# these are examples how one can install specific implementations and versions of them:
$ ros install sbcl-bin # 默认的 sbcl
$ ros install sbcl # 最新发布的 sbcl
$ ros install ccl-bin # 预编译的 ccl
$ ros install sbcl/1.2.0 # 安装指定的 sbcl 版本
我建议安装最新的 sbcl, sbcl-bin 似乎不支持多线程。
ros install sbcl
要列出已安装的实现和版本,请执行以下操作:
ros list installed
在不同的实现和版本之间切换
检查当前使用的实现/版本:
$ ros run -- --version
SBCL 1.2.15
切换一另一个实现/版本:
$ ros use sbcl/2.1.7
启动 REPL
$ ros run
# 或者使用 rlwrap 包裹运行
$ rlwrap ros run
rlwrap
对sbcl
非常有用,因为“裸”的 sbcl REPL不允许在行内跳转或其它有用的编辑命令,这是使用 Ubuntu shell 形成的习惯。
使用 Roswell 安装 Common Lisp 的包
任何可以使用 quicklisp
的 (ql:quickload ...)
安装的包,现在都可以用 roswell 从命令行安装进当前激活的实现中,方法是:
# 从 quicklisp 的仓库安装
$ ros install rove
# 直接从 github 安装
$ ros install fukamachi/rove
# 更新已安装的包
$ ros update rove
安装 emacs 和 slime 并将它们与 Roswell 连接
A.简单方式(目前好像不行了)
通常,可以通过 Roswell 中的一条命令安装和设置 emacs,其中 quicklisp 连接并运行 lem ,这是日本开发人员 cxxxr 开发的 CL 特殊模式。
$ ros install cxxxr/lem
# 启动 lem
$ lem
不幸的是,目前 lem 安装存在问题。
因此,必须手动安装 emacs 并手动将其与 Roswell 连接。
B.手动方式
- 全局安装 emacs
$ sudo apt install emacs
# 后台运行 emacs
$ emacs &
- 通过 Roswell 安装 slime
$ ros install slime
看来这还不够,还需要通过在 emacs 中安装 slime: M-x install-package RET
slime RET
- 在 emacs 的配置文件
~/.emacs.d/init.el
, 通过 slime 连接 Roswell。
在 emacs 中,通过按 C-x C-f 打开 ~/.emacs.d/init.el
Emacs 快捷键符号:
- C 代表 Ctrl
- M 代表 Alt
- S 代表 Shift
- SPC 代表空格键
- RET 代表回车键
- 组合键表示按住一个键的同时按下另外一个键,例如:先按下 Ctrl 键,不要放开,再按下 x, 这个组合键写成
C-x
C-x C-f
表示按下 Ctrl
的同时按下 x
, 放开,然后,按下 Ctrl
的同时再按下 f
,再放开。
在 emacs 中打开~/emacs.d/init.el
后,我们按照 Roswell wiki 中的说明写了下面的内容:
;; 初始化并启用 emacs 的包管理器
(require 'package)
(setq package-enable-at-startup nil)
(setq package-archives '())
;; 使用清华的镜像源
(setq package-archives
'(("gnu" . "http://mirrors.tuna.tsinghua.edu.cn/elpa/gnu/")
("melpa" . "http://mirrors.tuna.tsinghua.edu.cn/elpa/melpa/")))
;; 初始化包列表
(package-initialize)
(package-refresh-contents)
;; 确保 `use-package` 已经安装,如果就有的话就安装
(unless (package-installed-p 'use-package)
(package-refresh-contents)
(package-install 'use-package))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; slime for common-lisp
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; to connect emacs with roswell
(load (expand-file-name "~/.roswell/helper.el"))
;; for connecting slime with current roswell Common Lisp implementation
(setq inferior-lisp-program "ros -Q run")
;; for slime;; and for fancier look I personally add:
(setq slime-contribs '(slime-fancy))
;; ensure correct indentation e.g. of `loop` form
(add-to-list 'slime-contribs 'slime-cl-indent)
;; don't use tabs
(setq-default indent-tabs-mode nil)
;; set memory of sbcl to your machine's RAM size for sbcl and clisp
;; (but for others - I didn't used them yet)
(defun linux-system-ram-size ()
(string-to-number (shell-command-to-string
"free --mega | awk 'FNR == 2 {print $2}'")))
(setq slime-lisp-implementations
`(("sbcl" ("sbcl" "--dynamic-space-size"
,(number-to-string (linux-system-ram-size))))
("clisp" ("clisp" "-m"
,(number-to-string (linux-system-ram-size))
"MB"))
("ecl" ("ecl"))
("cmucl" ("cmucl"))))
保存并关闭
- C-x C-s (保存当前缓冲区/文件)
- C-x C-c (关闭 emacs)
-
重新打开 emacs
emacs &
要尝试 emacs 和 Common Lisp 实现(Roswell 中当前激活的实现),请创建一个 test.lisp
文件:
- 在 emacs 中打开/创建文件: C-x C-f 然后输入:
test.lisp
,敲回车
如果原先就存在test.lisp
文件,emacs 会打开它,如果不存在就新建一个。
- 在 emacs 中运行 slime: M-x slime RET
在当前缓冲区的下面会出现一个新的缓冲区,你应该可以在里面看到 slime REPL 的提示符:
; SLIME 2.26.1
CL-USER>
最好的事情是,现在,test.lisp
与 Roswell 中当前的 CL 实现连接(通过 slime
)。 您可以将光标移动到 test.lisp
文件中任意 lisp 表达式的末尾,然后按 C-x C-c :emacs 会将这个表达式转发到连接的 Common Lisp 实现(在我们的例子中是 sbcl)并执行该表达式。
要通过 vim 或 atom 而不是 emacs 进行连接,请查看 Roswell GitHub 站点的 wiki
在下文中,我们将会了解如何创建 Common Lisp 包(或项目),如何设置测试,尤其是 Travis CI 的自动化测试并监控测试的代码覆盖率。
使用 cl-project 开始一个 Common Lisp 包/项目
可以通过 cl-project
自动生成一个包的骨架。通过 Roswell 安装:
$ ros install fukamachi/cl-project
进入 Roswell 的 local-projects
文件夹,因为我们要先将包保存在本地机器上。
$ cd ~/.roswell/local-projects
现在创建您的项目主干——让我们将项目称为 my-project
并假设它依赖于包 alexandria
和 cl-xlsx
:
$ make-project my-project --depends-on alexandria cl-xlsx
用tree
命令查看目录结构:
$ tree my-project
my-project
├── my-project.asd
├── README.markdown
├── README.org
├── src
│ └── main.lisp
└── tests
└── main.lisp
2 directories, 5 files
由 Common Lisp 标准的包系统 ASDF(Another System Definition Facility)构建的元信息项目文件 my-project/my-project.asd
的内容为:
(defsystem "my-project"
:version "0.1.0"
:author ""
:license ""
:depends-on ("alexandria"
"cl-xlsx")
:components ((:module "src"
:components
((:file "main"))))
:description ""
:in-order-to ((test-op (test-op "my-project/tests"))))
(defsystem "my-project/tests"
:author ""
:license ""
:depends-on ("my-project"
"rove")
:components ((:module "tests"
:components
((:file "main"))))
:description "Test system for my-project"
:perform (test-op (op c) (symbol-call :rove :run c)))
Common Lisp 中的测试被组织为单独的包——因此,.asd
文件将项目的测试定义为单独的包,并添加了 :rove
作为其依赖项之一。
填写作者(:author
)、许可(:license
)和描述(:description
)部分。
在你的项目中,当引入新的包时,它们应该首先包含在两个包系统的 :depends-on
列表中。
:components
部分列出了属于该系统的所有文件和文件夹(列出的文件名没有扩展名 .lisp
!)。 因此,无论何时添加新文件或文件夹,都应更新此文件中的两个 defsystem
!
my-project/src/main.lisp
的内容是:
(defpackage my-project
(:use :cl))
(in-package :my-project)
;; blah blah blah.
用你的代码替换掉 ;; blah blah blah
为了避免在引用其它包的符号时加上包前缀,可以将其它包添加到 use
列表中,在当前的情况下,(:use :cl :alexandria :cl-xlsx)
是合理的。
或者,如果您想从这些包中导入单独的函数,则仅通过以下方式显式导入这些函数:
(defpackage my-project
(:use #:cl
#:cl-xlsx) ;; this package's entire content is imported
(:import-from #:alexandria ;; package name
#:alist-hash-table) ;; a function from :alexandria
(:export #:my-new-function))
在 :export
子句中,您可以声明哪些函数应该被导出并公开——从这个文件外部可见。
设置用 Roswell 和 rove 测试
Common Lisp 中的测试通常使用 FiveAM
或 Prove
来完成。
rove
是Prove
的接班人,和 Roswell 配合得很好。
安装:
$ ros install fukamachi/rove
在你的 REPL 中首次载入它:
(ql:quickload :rove)
进入它的命名空间:
(in-package :rove)
测试的语法很简单
肯定和否定测试
通常,您在执行测试时检查真或假。rove
为您提供了 ok
和 ng
(否定)函数进行检查。
;; expect true
(ok (= (+ 1 2) 3))
;; expect false
(ng (eq 'a 'a))
;; add error message
(ok (= 1 1) "equality should be true")
(ng (< 3 2) "3 and 2 should not be equal")
您可以检查错误、标准输出和宏展开:
;; tests for a specific, expected error
(ok (signals (/ 1 0) 'division-by-zero))
;; tests for error occurrence itself without exact specification
(ok (signals (/ 1 0)))
输出到控制台:
(ok (outputs (format t "hello") "hello"))
下面是有些蠢的宏展开示例
(defmacro my-sum (&rest args) `(+ ,@args))
(ok (expands '(my-sum 1 2 3) '(+ 1 2 3)))
;; which is:
(ok (expands '<macro-call> '<expected macroexpand-1 result>))
将几个测试分组,并命名和注释
一个人应该写下为什么/为什么要做测试。 有人说“测试是最精确的文档形式”。 ——所以它们也应该有可读性。
(deftest <test-name>
(testing "what the test is for"
(ok <expr>)
(ok <expr>)
(ng <expr>))
(testing "another subpoint"
(ok <expr>)
(ng <expr>)
(ng <expr>)))
这是一个示例函数及其测试:
(defun my-absolute (x) (if (< x 0) (* -1 x) x))
这是可能的测试:
(deftest my-absolute
(testing "negative input should give positive output"
(ok (eq (my-absolute -3) 3))
(ng (eq (my-absolute -3) -3)))
(testing "positive input should give positive output"
(ok (eq (my-absolute 3.1) +3.1))
(ng (eq (my-absolute 2.3452) + 2.4352)))
(testing "zero should always give zero"
(ok (zerop (my-absolute 0))
(zerop (my-absolute 0.0000000))))
从控制台运行测试
(run-test 'my-absolute)
将一项或多项测试组合成测试包(测试套件)
想象一下,您创建了一个名为 my-package
的项目或包。
在包/项目文件夹中应该有一个tests
文件夹,其中包含该包的测试文件(测试套件)。
cl-project
已经自动创建了测试的骨架,在我们的例子中是 tests/main.lisp
。
它自动生成的内容是:
(defpackage my-project/tests/main
(:use :cl
:my-project
:rove))
(in-package :my-project/tests/main)
;; NOTE: To run this test file, execute `(asdf:test-system :my-project)' in your Lisp.
(deftest test-target-1
(testing "should (= 1 1) to be true"
(ok (= 1 1))))
每当我们需要包作为新的依赖项或它们的单个函数时,我们应该修改这个 defpackage
定义,类似于我们在 my-project/src/main.lisp
的 defpackage
表达式所做的那样。
如果我们创建多个测试文件/套装,每个文件都必须在 .asd
文件的 defsystem
的 :components
部分中注册。 这样 (asdf:test-system :my-project)
就包含了所有的测试套件。
添加准备和收尾过程
setup
函数在测试包之前求值其列出的命令一次。 这很有用,例如 用于建立数据库或创建临时目录。
测试运行后,可以使用 teardown
命令清理临时目录或其他生成的文件。
;; after the defpackage and in-package declarations:
(setup
(ensure-directories-exist *tmp-directory*))
;; all the tests of the package
(teardown
(uiop:delete-directory-tree *tmp-directory* validate t
:if-does-not-exist :ignore))
对于应该在每次测试之前或之后运行的命令,请使用:
(defhook :before ...)
(defhook :after ...)
为了让测试具有不同的风格,您可以指定全局变量:
(setf *rove-default-reporter* :dot) ;; other: :spec :none
(setf *rove-debug-on-error* t)
运行测试套件
(asdf:test-system :my-project)
(rove:run :my-project/tests)
;; you can modify their style when calling to run
(rove:run :my-project/tests :style :spec) ;; other :dot :none
另一个测试套装 FiveAM
也非常类似,
使用 Travis-CI 自动化测试
借助 https://raw.githubusercontent.com/roswell/roswell/release/scripts/install-for-ci.sh 中提供的脚本,Travis CI 的设置大大简化,该脚本负责在 Roswell 中安装所有可用的实现。
首先,必须在 Travis CI 中注册一个帐户。
这里和这里说明了将 Travis CI 与 GitHub 或 Bitbucket 或 GitLab 连接。
总而言之:必须单击个人资料图片并单击设置,然后选择您希望 Travis CI 监控的存储库。 (在我们的例子中,这将是包含 my-project
的 GitHub 存储库)。
下一步要做的就是创建一个名为 .travis.yml
的文件并将其放在项目根文件夹中(由 git 运行)。 一旦你 git 添加新的 .travis.yml
文件并 git commit
并 push
它,Travis CI 将开始“照管”你的存储库。
对于仅使用最新的 SBCL 二进制文件进行测试,一个非常简短、简约的 .travis.yml
就足够了:
language: common-lisp
sudo: required
install:
- curl -L https://raw.githubusercontent.com/roswell/roswell/release/scripts/install-for-ci.sh | sh
script:
- ros -s prove -e '(or (rove:run :my-project/tests) (uiop:quit -1))'
通过将 sudo: 字段设置为 false,可以激活 Travis CI 以使用新的基于容器的基础架构(测试的启动时间比旧系统更快)。
下面的 .travis.yml
脚本让 Roswell 在每次提交和 git push 代码时通过两个实现运行 rove
测试。
language: common-lisp
sudo: false
env:
global:
- PATH=~/.roswell/bin:$PATH
- ROSWELL_INSTALL_DIR=$HOME/.roswell
matrix:
- LISP=sbcl-bin
- LISP=ccl-bin
install:
- curl -L https://raw.githubusercontent.com/roswell/roswell/release/scripts/install-for-ci.sh | sh
- ros install rove
- ros install gwangjinkim/cl-xlsx
script:
- ros -s rove -e '(or (rove:run :my-project/tests :style :dots) (uiop:quit -1))'
env:
下的 matrix:
部分列出了要测试的 Common Lisp 的不同版本和实现。
这是一个完整的 .travis.yml
文件,用于测试更多的 Common Lisp 实现。
language: common-lisp
sudo: false
addons:
apt:
packages:
- libc6-i386
- openjdk-7-jre
env:
global:
- PATH=~/.roswell/bin:$PATH
- ROSWELL_INSTALL_DIR=$HOME/.roswell
matrix:
- LISP=sbcl-bin
- LISP=ccl-bin
- LISP=abcl
- LISP=clisp
- LISP=ecl
- LISP=cmucl
- LISP=alisp
matrix:
allow_failures:
- env: LISP=clisp
- env: LISP=abcl
- env: LISP=ecl
- env: LISP=cmucl
- env: LISP=alisp
install:
- curl -L https://raw.githubusercontent.com/roswell/roswell/release/scripts/install-for-ci.sh | sh
- ros install fukamachi/rove
- ros install gwangjinkim/cl-xlsx
cache:
directories:
- $HOME/.roswell
- $HOME/.config/common-lisp
script:
- ros -s rove -e '(or (rove:run :my-project/tests :style :dots)
(uiop:quit -1))'
addons: apt:
部分必须存在,因为 clisp
、abcl
、alisp
和/或 cmucl
需要 openjdk-7-jre
,而 clisp
需要必须使用 apt
安装的 libc-i386
。
cache:
下列出的目录被缓存(为了更快地启动)。
install:
部分列出了当缺少其中一个组件时要为您的系统完成的安装——按照这个给定的顺序。
这些实现由 https://raw.githubusercontent.com/roswell/roswell/release/scripts/install-for-ci.sh 脚本准备,因此单个实现的安装不必在安装下列出: 明确地。 如果没有 Roswell,测试不同的实现需要做更多的工作。 Roswell 使用最新版本的实现进行安装和测试。
如果您的测试包本身有一些更简单的命令行命令可用,则 script:
命令看起来会更简单。
查看 Travis CI 文档以获取更多信息(在不同操作系统上进行测试等)
工作完成后,让 Travis CI 通过电子邮件或 telegram 通知您
将下面的内容添加到 .travis.yml
notifications:
email:
- my-email@gmail.com
每次运行作业时都会通知您。
[略]
使用 Coveralls 将代码覆盖率添加到 Travis
为此,我们遵循 Roswell 的文档或 Boretti 的博客, 使用 Roswell 独立脚本和 FiveAMAs 脚本作为测试系统。
和 Travis CI 类似,你需要先创建一个 Coveralls)帐号
然后,您在 .travis.yml
文件中标记应评估其代码覆盖率的实现:
env:
matrix:
- LISP=sbcl COVERALLS=true
- LISP=ccl
并且还应指定安装 coveralls 的命令:
install:
# Coveralls support
- ros install fukamachi/cl-coveralls
最后,脚本的调用必须修改为:
- ros -s rove
-s cl-coveralls
-e '(or (coveralls:with-coveralls (:exclude (list "t"))
(rove:run :quri-test))
(uiop:quit -1))'
结束 —— 这才是开始
现在,我们已经为 2021 年构建了一个完整的 Common Lisp IDE 系统。
请享受 Lisp Hacking!
(欢迎在评论区提出建议和反应!——关于设置 Roswell 和 cl-project
以及使用 rove
进行测试的部分,我从我的日本朋友那里了解到,他们用日语写了一本书:SURVIVAL COMMON LISP——作者是:Masatoshi Sano、Hiroki Noguchi、Eitaro Fukamachi、gos-k、Satoshi Imai、cxxxr、Takafumi Harashima。但是,许多信息可在包和工具的文档中找到。)