Roswell 環境下でのローカル・プロジェクト管理入門

目次

この記事は 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 ディレクティヴを指定すると既存の設定が上書きされる.

パス部分は以下のいずれかで記述します.

  1. 文字列によるパス.絶対パスで指定した方が安全.

  2. :home, :root はそれぞれ,ホームディレクトリとルートディレクトリに展開される.

  3. ([上記2つのうちいずれか] 相対パス) で1つ目で指定されたパスと相対パスが結合されたパスを表す.

実際の記述例をコード 1 に示します.

コード 1. source-registry DSL 記述例
(: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 に渡します.

コード 2. Lisp コードからのロードパス設定例
(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 があるディレクトリの絶対パスに展開されます.

コード 3. ~/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 つの処理を行います.

  1. ダウンロード済みの tar アーカイブと解凍済みのプロジェクトディレクトリを削除する.

  2. 管理用の metadata ファイルを更新する.

  3. 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 も使えることがわかります.

コード 4. Roswell 環境下の Quicklisp のローカル・プロジェクト・ディレクトリ
> 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))),で再コンパイルしてロードする例です.

コード 5. REPL 上での手動コンパイルとロード
> (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 上に置いてあります.

ローカル・プロジェクトを使うケースは,

  1. 開発作業中の REPL に独自ライブラリをコンパイル・ロードしたい,

  2. 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)から構成されています.

コード 6. プロジェクト全体像
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 への検索パスが自動的に設定されます.

コード 7. hello-builder/hello-builder.asd
(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 の様な処理を実行します.

コード 8. 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 に配置するのがベスト・プラクティスでしょう.

コード 9. ~/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 ファイルを作成して,サブ・プロジェクトへのパスを通します.

コード 10. 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 だけを記述します.

コード 11. hello/hello.asd
(defsystem "hello"
  :version "0.0.1"
  :author "wshito"
  :depends-on (:date
;	       :dummy
	       )
  :components ((:module "src"
		:components
		((:file "hello")))))

コード 12 が hello プロジェクトのコード本体です.hello は単に標準出力にメッセージと日付を表示するだけのプログラムです.

コード 12. 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 プロジェクトを作成します.コード 13date のシステム定義です.

コード 13. date-v1.0/date.asd
(defsystem "date"
  :version "1.0"
  :author "wshito"
  :components ((:module "src"
		:components
		((:file "date")))))

最後は コード 14 に掲げた date プロジェクトのプログラム本体です.日付の文字列を返す today 関数を提供します.

コード 14. 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 の検索パスの設定,ビルド,プログラム起動までの実行結果を示します.

コード 15. 実行結果
> (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 ディレクトリ内に作成します.まず,スクリプトの雛形を作成します.

コード 16. hello.ros の雛形生成
$ mkdir roswell; cd roswell
$ ros init hello
Successfully generated: hello.ros

生成された hello.ros ファイルを コード 17 の様に書き換えます.重要なポイントは 3 か所です.まず,コード 17 の 16—​19 行目は コード 7hello-builder.asd 内で設定した source-registry と全く同じになります.これでプロジェクト・ルートの asd.conf が読み込まれます.2 点目が 22 行目のプロジェクトのロードです.3 点目は 26 行目でプログラムを起動しています.

コード 17. hello.ros スクリプト
#!/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 を実行することができます.

コード 18. Ros スクリプトの実行結果
$ cd ~/program/blog-example
$ ros lisp/package-management/hello-project/roswell/hello.ros
Hello world! 2017年12月2日(土)19時9分25秒