この記事は Lisp Advent Calendar 2017 の4日目の記事として書かれました.コメント,質問等は @waterloo_jp まで.
「 Lisp のパッケージ管理入門.Quicklisp,ASDF,Roswell の違いなど 」では,Quicklisp,ASDF,Roswell の違いを説明し,Roswell の導入部分を解説しました.ここでは,もう少し踏み込んで,自分で開発するプロジェクト(ローカル・プロジェクト)を Roswell の環境下で管理する方法を説明します.本稿の最後では,ハンズオン・チュートリアル で実際のプロジェクト設定例を示します.
Roswell は背後で Quicklisp と ASDF を使用してライブラリをビルドしロードしています.ローカル・プロジェクトを Roswell で管理・使用するには,Quicklisp と ASDF の理解が欠かせません.Quicklisp はネット上のリポジトリからパッケージをダウンロードするだけで,コンパイルとロードは ASDF が担っています.そこで,ローカル・プロジェクトを ASDF に認識させるための設定から見ていきましょう.
ASDF にローカル・プロジェクトを認識させる方法
Roswell で実装系をインストールした場合,ASDF 3 がインストールされているはずですが一応 ASDF のバージョンを確認しておきます.
> (asdf:asdf-version)
"3.1.5"
手っ取り早く ASDF に認識させるにはローカル・プロジェクトを ~/common-lisp/
以下に配置します.これで (ql:quickload :foo)
,(require :foo)
,(asdf:load-system :foo)
のいずれを使ってもプロジェクトをロードできます.
しかし,複数プロジェクトで構成されるプログラムを配布する際には,インストール先を独自に設定する必要があるかもしれません.ここでは,ASDF 3 から採用された source-registry を使って,任意ディレクトリにロードパスを通す方法を解説します.source-registry は Lisp コードからも設定できるため,Ros スクリプト内で使うこともできます.
source-registry の公式マニュアルは ASDFマニュアル Sec 8 です.
SOURCE-REGISTRY
source-registry は,ASDF 3 から採用されたロードパスの設定方法です.旧バージョンでは *central-rgistry*
変数を使って設定していました.後方互換性を維持するため *central-registry*
に設定されたパスは source-registry の設定より前に検索されます.Roswell 環境下では旧バージョン用の *central-registry*
が以下のように設定されています.
> asdf:*central-registry*
(#P"/Users/wshito/.roswell/lisp/quicklisp/quicklisp/")
source-registryは,設定ファイルに記述する方法と,Lisp コードで設定する方法の2種類あります.どちらの方法でも ASDFマニュアル Sec 8.5で定義されている DSL 文法にしたがって記述します.
SOURCE-REGISTRY 設定用 DSL
source-registry 設定用の DSL は (:source-registory …)
で始まります.…
の箇所にはディレクティヴが続きます.主に使うディレクティヴは下記の 4 つです.
-
(:tree パス)
ディレクティヴはパス以下を再帰的に検索する. -
(:directory パス)
ディレクティヴは指定されたパスのみを 再帰せずに 検索する. -
:inherit-configuration
ディレクティヴは既に設定済みの source-registry を挿入する. -
:ignore-inherited-configuration
ディレクティヴを指定すると既存の設定が上書きされる.
パス部分は以下のいずれかで記述します.
-
文字列によるパス.絶対パスで指定した方が安全.
-
:home
,:root
はそれぞれ,ホームディレクトリとルートディレクトリに展開される. -
([上記2つのうちいずれか] 相対パス)
で1つ目で指定されたパスと相対パスが結合されたパスを表す.
実際の記述例をコード 1 に示します.
(:source-registry
(:tree (:home "lisp/project1")) ; /Users/home/wshito/lisp/project1 に展開される
(:directory "/usr/local/lib/project2")
:inherit-configuration)
SOURCE-REGISTRY 設定方法
ここでは前節で説明した DSL を使って,Lisp コードと設定ファイルを併用してロードパスを設定する方法を解説します.
Lisp コードから設定する場合は,コード 1 の記述をクォートして asdf:initialize-source-registry
に渡します.
(asdf:initialize-source-registry
'(:source-registry
(:tree (:home "lisp/project1"))
(:directory "/usr/local/lib/project2")
:inherit-configuration))
コード 2 の 3 行目で ~/lisp/project1
以下を再帰的に検索するように設定しています.プロジェクトフォルダ以下に Lisp 以外のディレクトリが多くある場合,全てのディレクトリを再帰的に検索するのは非効率です.例えば ~/lisp/project1
以下で検索が必要なのは,src/lisp
以下の全ディレクトリと,src/test/lisp
ディレクトリ直下だけだとします.この場合,プロジェクトフォルダに asdf.conf
ファイルを配置し,その中で :here
ディレクティヴを使った設定を書くことによって,ロードパスをさらに細かくコントロールすることができます.この例の場合,~/lisp/project1/asdf.conf
ファイル内にコード 3 を記述すると所望の結果が得られます.:here
ディレクティヴは asd.conf
があるディレクトリの絶対パスに展開されます.
~/lisp/project1/asdf.conf
の設定内容(:source-registry
(:tree (:here "src/lisp")) ; /Users/home/wshito/lisp/project1/src/lisp
(:directory (:here "src/test/lisp"))) ; /Users/home/wshito/lisp/project1/src/test/lisp
最終的な source-registry の設定は以下の変数で確認出来ます.ただし,この変数を直接変更してはいけません.
> asdf:*source-registry-parameter*
ライブラリのロード
ここまでの設定が完了していれば,以下のいずれかでライブラリをロードできます.
> (asdf:load-system ライブラリ名) ;; ASDFを使ってロード
> (ql:quickload ライブラリ名) ;; Quicklispを使ってロード
> (require ライブラリ名) ;; 処理系がASDF経由でロード
ハンズオン・チュートリアル では実際にプロジェクトを使って設定手順を示します.
ASDF 3 は,source-registry で設定したロードパス内のシステム情報をキャッシュします.ファイル名やディレクトリ構成を変更した場合,(asdf:clear-source-registry)
を実行するとキャッシュをクリアできます.
ASDF の検索パスと fasl 保存ディレクトリの違い
これまで説明してきたロードパスは,システム定義が書かれた ASD ファイルを ASDF が探すパスです.ASDF によってコンパイルされた fasl ファイルは,~/.cache/common-lisp/[処理系名]/path-to-src-file/src-file.fasl
に保存されます(保存先は手動コンパイル時に画面に表示される).例えば sbcl-1.4.1 でコンパイルされた /Users/wshito/.roswell/lisp/quicklisp/dists/quicklisp/software/lack-20170830-git/src/lack.lisp
ファイルは,~/.cache/common-lisp/sbcl-1.4.1-macosx-x64/Users/wshito/.roswell/lisp/quicklisp/dists/quicklisp/software/lack-20170830-git/src/lack.fasl
に保存されます.
当然ですが,~/.cache/common-lisp/
以下に fasl が存在しても,ASD ファイルがロードパス上になければfaslファイルはロードされません.
Quicklisp を使ったローカル・プロジェクト管理
Quicklisp はライブラリのコンパイルとロードを ASDF に委譲します.ASDF を使ったここまでの設定で (ql:quickload :my-project)
を実行すると my-project
をロードできます.一方で,Quicklisp にもローカル・プロジェクトを認識させる設定方法があるため,ASDF の source-registry を使わず,Quicklisp 側の設定だけでローカル・プロジェクトを認識させることもできます.ここでは,Quicklisp を中心としたローカル・プロジェクトの管理方法を説明します.
Quicklisp の基本
Quicklisp 側の設定に入る前に Quicklisp の基本的な使い方をおさらいしておきましょう.まずは Quicklisp 自体のアップデート方法です.すでに最新バージョンの時はダウンロードせずバージョンを表示するので,バージョン確認にも使えます.
> (ql:update-client)
Quicklisp はライブラリのダウンロード,インストール,コンパイル,ロードを ql:quickload
関数で行います.引数に :verbose t
を設定すると,コンパイル時のメッセージなど詳しい出力が得られます.
> (ql:quickload :lack)
ダウンロード済みのライブラリを最新のバージョンにアップデートするには ql:update-all-dists
を実行します.
> (ql:update-all-dists)
Quicklisp によってダウンロードされたライブラリの保存場所は以下で調べられます.
> (ql:where-is-system :clack)
#P"/Users/wshito/.roswell/lisp/quicklisp/dists/quicklisp/software/clack-20161204-git/"
インストール(ダウンロード)済みライブラリの一覧を得たい時には ql:system-list
を使います.膨大な量になるので下の例では結果を all
に保持しています.
> (setf all (ql:system-list))
> (length all)
3491
名前を指定して特定のライブラリがインストールされているかを調べるには ql:system-apropos
を使います.
(ql:system-apropos "woo") ;; 部分文字列にマッチする.
アンインストールは ql:uninstall
を使います.アンインストールする前に,依存関係を調べておきましょう.
(ql:who-depends-on "woo")
問題がなければ以下でアンインストールできます.
(ql:uninstall "woo")
アンインストールは以下の 3 つの処理を行います.
-
ダウンロード済みの tar アーカイブと解凍済みのプロジェクトディレクトリを削除する.
-
管理用の metadata ファイルを更新する.
-
asdf:clear-system
を実行し ASDF のキャッシュをクリアする.
ただし,現在の Lisp セッションに既にロードされたライブラリは,セッションを終了するまでセッション内で生き続けます.
Quicklisp のローカル・プロジェクト
ここからは Quicklisp にローカル・プロジェクトを認識させる方法を説明します.
Quicklisp がデフォルトで読み込むローカル・プロジェクトの場所は,*quicklisp-home*
に設定されたディレクトリ下の local-projects
ディレクトリです.
> ql:*quicklisp-home*
#P"/Users/wshito/.roswell/lisp/quicklisp/"
Roswell 環境下の *quicklisp-home*
は上記のとおり ~/.roswell/lisp/quicklisp
なので,~/.roswell/lisp/quicklisp/local-projects
にローカル・プロジェクトを配置します.
Quicklisp が認識するローカル・プロジェクトのディレクトリは ql:*local-project-directories*
変数で変更することができます.コード 4 の通り,Roswell 環境下ではデフォルトの *quicklisp-home*
以下のディレクトリ以外に ~/.roswell/local-projects
も使えることがわかります.
> ql:*local-project-directories*
(#P"/Users/wshito/.roswell/local-projects/"
#P"/Users/wshito/.roswell/lisp/quicklisp/local-projects/")
local-projects
ディレクトリにプロジェクトを配置したら,Quicklisp に登録する必要があります.登録するには以下を実行します.
(ql:register-local-projects)
これで local-projects
ディレクトリ以下に system-index.txt
という metadata ファイルが作成されます.
ASDF を使った手動コンパイルとロード
開発時にはデバッガを使ってプログラムをステップ実行したい場合,依存ライブラリをデバッグ用の設定で再コンパイルする必要があります.コード 5 は,hunchentoot,clack,lack をデバグ用設定,(declaim (optimize (debug 3)))
,で再コンパイルしてロードする例です.
> (declaim (optimize (debug 3)))
> (asdf:compile-system :hunchentoot :force t)
> (asdf:compile-system :clack :force t)
> (asdf:compile-system :lack :force t)
> (asdf:load-system :clack)
> (asdf:load-system :lack)
:force t
は依存ライブラリも再コンパイルするディレクティヴです.
ハンズオン・チュートリアル
以上の解説を踏まえて,ここでは実際にローカル・プロジェクトを作成し,ビルド,実行まで行ってみましょう.コードは GitHub 上に置いてあります.
ローカル・プロジェクトを使うケースは,
-
開発作業中の REPL に独自ライブラリをコンパイル・ロードしたい,
-
Ros スクリプトからプログラムを起動する際にローカル・プロジェクトをロードしたい,
という 2 つのケースが考えられます.
まず 例1 で,REPL からの開発作業を前提とした設定例を示します.例2 では,Ros スクリプトを使ってコマンドラインから Lisp プログラムを起動する設定例を示します.どちらも基本は同じで ASDF の source-registry を使って設定します.
例 1: source-registry を設定し REPL から起動
主な開発手順は以下の通りです.
-
プロジェクト本体は自由な場所に置く.
-
プロジェクト本体へのロードパスを設定する開発者用ツールを
~/common-lisp
に配備する. -
ローカル・プロジェクトの開発を始める前に開発者用ツールをロードする.
-
ローカル・プロジェクトをビルド,ロードする.
プロジェクトの構成は コード 6 の通りです.hello-project は 3 つのサブ・プロジェクト(hello,date-v1.0,dummy)から構成されています.
hello-project/
+- asd.conf
+- hello/
| `- hello.asd
| `- src/
| `- hello.lisp
+- date-v1.0/
| `- date.asd
| `- src/
| `- date.lisp
+- dummy/
+- hello-builder ;; 開発者用ツール.~/common-lisp 以下にインストールする
`- hello-builder.asd
`- builder.lisp
hello-builder は開発者用ツールのプロジェクトです.開発者用ツールを使って,親プロジェクトである hello-project 以下を再帰的に検索するようにロードパスを設定します.hello-project 内のロードパスは asd.conf
でさらに細かくコントロールします.dummy は検索パスに含めないプロジェクト例として置いているだけで中身は空です.
開発者用ツールの作成
まずは,開発者用ツールである hello-builder
ディレクトリから見ていきましょう.この中の hello-builder.asd
で hello-project の source-registry を設定します.builder.lisp
内にはコンパイル,ロードを自動化するコードを記述します.
hello-builder.asd
の中身は コード 7 の通りです.asd
ファイルでは,パッケージ定義などをせず単一の defsystem
のみを記述することが ASDF マニュアルで推奨されています.一方で,asd
ファイルの中身は通常の Lisp コードとしてリーダーで読まれ eval
されるため Lisp コードを含めることもできます.ここでは,コード 7 の 14—17 行目で source-registry の設定コードを含めています.これで hello-builder
開発ツールをロードすると,hello-project
への検索パスが自動的に設定されます.
(in-package :cl-user)
(defpackage hello-builder-asd
(:use :cl :asdf))
(in-package :hello-builder-asd)
(defsystem "hello-builder"
:version "1.0"
:author "wshito"
:components ((:file "builder")))
;; Set up source-registry
(asdf:initialize-source-registry
'(:source-registry
(:tree (:home "program/blog-examples/lisp/package-management/hello-project")) (1)
:inherit-configuration))
1 | `:home`を使うことで絶対パスに変換される. |
コード 7 の 14—17 行目を手動で REPL で評価してもパスが通りますが,hello-builder
開発ツールをシステムとして定義し ~/common-lisp
以下に配置することで,開発ツールを (require :hello-builder)
一発でロードでき,source-registry も同時に設定できます.プロジェクト全体は任意の場所に配置し,開発者用ツールが入ったディレクトリだけ ~/common-lisp
に配置するのです.
hello-project 全体のコンパイルやロードは,開発ツールの hello-builder/builder.lisp
内で定義されている make
マクロで行います(コード 8).ASDF 3.1 からは asdf:make
が利用可能になっていますが,まだベータ実装ということなのでここでは使用していません.make
マクロは debug
キーワード引数に応じて コード 5 の様な処理を実行します.
hello-builder/builder.lisp
(in-package :cl-user)
(defpackage hello-builder
(:use :cl :asdf)
(:export :make))
(in-package :hello-builder)
(defmacro make (&key (debug nil))
(let ((dec (if debug
'(declaim (optimize (debug 3)))
'(declaim (optimize (speed 3)))))
(comp-op (append '(asdf:compile-system :hello :force) (list debug))))
`(progn
,dec
,comp-op
(asdf:load-system :hello))))
以上,2 つのファイルから成る hello-builder
ディレクトリを ASDF のデフォルト検索パスに含まれる ~/common-lisp
に配置します.
本稿の前半で説明した通り,Roswell のデフォルト設定では,ASDF は ~/.roswell/lisp/quicklisp/quicklisp/
以下を,Quicklisp は ~/.roswell/local-projects/
,~/.roswell/lisp/quicklisp/local-projects/
以下のローカル・プロジェクトを認識します.しかし,~/.roswell
以下にローカル・プロジェクトを配備するのはお勧めしません.なぜなら,Rowswell をアプグレードし,Quicklisp 等システム関連の初期化スクリプトが更新された場合,~/.roswell
以下を削除してもう一度,ros setup
を実行し,~/.roswell
以下を刷新する必要が生じることがあるからです.
プロジェクトの source-registry を設定するプログラムは ~/common-lisp
に配置するのがベスト・プラクティスでしょう.
~/common-lisp
以下に build ツールへのリンクを設置$ cd ~/common-lisp
$ ln -s ~/program/blog-examples/lisp/package-management/hello-project/hello-builder
$ ls
hello-builder
プロジェクト本体の作成
開発者用ツールが出来たので,いよいよプロジェクト本体を作成しましょう.hello-project は2つの独立したサブ・プロジェクトから構成されています.まず,source-registry で設定したプロジェクトルートに asd.conf
ファイルを作成して,サブ・プロジェクトへのパスを通します.
asd.conf
;;
;; ASDF source-registry configuration file
;; ignores the 'dummy' project
;;
(:source-registry
(:directory (:here "hello"))
(:directory (:here "date-v1.0")))
hello プロジェクトのシステム定義は コード 11 の通りです.ここでは ASDF マニュアルの推奨通り asd
ファイルには単一の defsystem
だけを記述します.
hello/hello.asd
(defsystem "hello"
:version "0.0.1"
:author "wshito"
:depends-on (:date
; :dummy
)
:components ((:module "src"
:components
((:file "hello")))))
コード 12 が hello プロジェクトのコード本体です.hello
は単に標準出力にメッセージと日付を表示するだけのプログラムです.
hello/src/hello.lisp
(in-package :cl)
(defpackage :hello
(:use :cl)
(:export :hello))
(in-package :hello)
(defun hello ()
(format t "Hello world! ~A" (date:today)))
次は,hello プロジェクトが依存する date プロジェクトを作成します.コード 13 が date
のシステム定義です.
date-v1.0/date.asd
(defsystem "date"
:version "1.0"
:author "wshito"
:components ((:module "src"
:components
((:file "date")))))
最後は コード 14 に掲げた date プロジェクトのプログラム本体です.日付の文字列を返す today
関数を提供します.
date-v1.0/src/date.lisp
(in-package :cl)
(defpackage :date
(:use :cl)
(:export :today))
(in-package :date)
(defmacro with-utime-decoding ((utime &optional zone) &body body)
`(multiple-value-bind (sec min h date month year day-of-week daylight-p zone)
(decode-universal-time ,utime ,@(if zone (list zone)))
(declare (ignorable sec min h date month year day-of-week daylight-p zone))
,@body))
(defun today ()
(with-utime-decoding ((get-universal-time))
(format nil "~A年~A月~A日(~[月~;火~;水~;木~;金~;土~;日~])~A時~A分~A秒"
year month date day-of-week h min sec)))
以上で準備完了です.コード 15 に,ASDF の検索パスの設定,ビルド,プログラム起動までの実行結果を示します.
> (require :hello-builder) ;; source-registryの設定とhello-builder:makeのロード
> (hello-builder:make) ;; helloプロジェクトをビルドしロード
> (hello:hello) ;; helloプロジェクトを実行してみる.
Hello world! 2017年12月2日(土)18時36分47秒
例 1 のまとめ
-
プロジェクト本体は自由な場所に設置する.
-
サブ・プロジェクトへの細かな検索パスの設定を,プロジェクト・トップに配置した
asd.conf
で行う. -
プロジェクト・トップへの検索パスは開発者用ツール内の ASDF ファイルで設定する.
-
開発者用ツールへのリンクを
~/common-lisp
に配置する. -
開発者用ツールをロードすることでプロジェクト本体への検索パスを通す.
例 2: Ros スクリプトからの起動設定
ここでは,例 1 のプロジェクトを Ros スクリプトから起動する方法を説明します.Ros スクリプトから起動する場合も,ASD ファイルで設定した source-registry をそのまま利用することができます.
Ros スクリプトはプロジェクト・ルートの roswell ディレクトリ内に作成します.まず,スクリプトの雛形を作成します.
$ mkdir roswell; cd roswell
$ ros init hello
Successfully generated: hello.ros
生成された hello.ros
ファイルを コード 17 の様に書き換えます.重要なポイントは 3 か所です.まず,コード 17 の 16—19 行目は コード 7 の hello-builder.asd
内で設定した source-registry と全く同じになります.これでプロジェクト・ルートの asd.conf
が読み込まれます.2 点目が 22 行目のプロジェクトのロードです.3 点目は 26 行目でプログラムを起動しています.
#!/bin/sh
#|-*- mode:lisp -*-|#
#| <Put a one-line description here>
exec ros -Q -- $0 "$@"
|#
(progn ;;init forms
(ros:ensure-asdf)
;;#+quicklisp (ql:quickload '() :silent t)
)
(defpackage :ros.script.hello.3720592857
(:use :cl))
(in-package :ros.script.hello.3720592857)
;; Set up source-registry
(asdf:initialize-source-registry
'(:source-registry
(:tree (:home "program/blog-examples/lisp/package-management/hello-project")) (1)
:inherit-configuration))
;; load the application
(require :hello)
(defun main (&rest argv)
(declare (ignorable argv))
(hello:hello))
1 | :home を使うことで絶対パスに変換される. |
実行結果は以下の通りです.source-registry でプロジェクトルートへの絶対パスを設定しているので,どのディレクトリから Ros スクリプトを実行しても hello
を実行することができます.
$ cd ~/program/blog-example
$ ros lisp/package-management/hello-project/roswell/hello.ros
Hello world! 2017年12月2日(土)19時9分25秒