EmacsとCommon Lispとの相性は抜群に良い.
SLIMEというプラグインを使うと,Emacs上でCommon Lisp処理系を立ち上げて接続したり,既に起動している処理系に接続したりすることで,REPLや補完を動作させられる.このEmacsとCommon Lisp処理系との接続は,処理系上でSWANKサーバというのを起動することで取り持ってもらっている.
ローカルで動作している処理系にSLIMEで接続することはよく行われるし,そのためのドキュメントも充実しているようだが,しかしdocker,とくに現代的なdocker-compose環境でCommon Lispプロダクトを開発する際に,Emacsを処理系に接続し効果的な開発を行う方法についてのドキュメントはあまりみあたらなかった.
そこでこのエントリでは,docker-composeで起動した典型的なCommon Lispアプリケーション(Caveman 2)にEmacsを接続し,SWANKを介して実行中のコードに触れ,動作に干渉してみることにする.また,Emacsでの基本的なSLIMEの設定はもう済んでいるものとする.
動機
自分が細々といじっているソフトウェアがあり,これはdocker化されていてdocker-composeで起動もする.しかし開発環境のうち「エディタまわり」はdocker化されていなかった. どういうことかというと,このソフトウェアを編集するEmacsでは,シンタックスハイライトや補完のためにSLIMEを起動して処理系と接続していたが,接続していたのはdocker-compose上で動作する処理系ではなく,ローカルで起動させた処理系であった. このため思い通りに補完が動作しなかったり,ライブラリがうまく読み込まれないなど,開発者にとっては開発のためにDockerとローカルとで二重の負担を強いられる状況となっていた.
今回はこの不便を脱出するべく,コーディング環境とサーバの動作環境とを一本化し,まさに動作しているコードで開発できる環境を整えようと思う.
環境
cat /proc/version
Linux version 4.4.104-39-default (geeko@buildhost) (gcc version 4.8.5 (SUSE Linux) ) #1 SMP Thu Jan 4 08:11:03 UTC 2018 (7db1912)
lsb_release -a
- LSB Version: n/a
- Distributor ID: openSUSE project
- Description: openSUSE Leap 42.3
- Release: 42.3
- Codename: n/a
ros version
build with gcc (SUSE Linux) 4.8.5 libcurl=7.37.0 Quicklisp=2017-03-06 Dist=2017-08-30 lispdir='/usr/local/etc/roswell/' homedir='/home/windymelt/.roswell/' configdir='/home/windymelt/.roswell/'
ros run -- -version
SBCL 1.4.0
サンプルアプリケーション
サンプルアプリケーションを用意した.手順に従うだけでSWANKで接続できるようになっている.
docker-compose
にSWANKサーバのポートを露出させる
Emacsと処理系とが通信するためには,SWANKサーバが処理系で起動している必要がある. まずはその下準備として,SWANKサーバが使うポートをホストに露出し,Emacsから接続できるようにする.
ここで,処理系が動作するサービスはapp
であり,後述のDockerfile
を用いてコンテナをビルドする.
また,Webアプリケーション(caveman 2)が5000
番を使用するのでホストで見られるように露出させる.
そしてSWANKサーバは6005
番ポートを使うことにする.これはEmacsから接続する際に指定できるので,自由に設定してよい.
# docker-compose.yml version: '3' services: app: build: . ports: - "6005:6005" # For development; SWANK port. cf. Dockerfile - "5000:5000"
Dockerfile
Dockerfile
では,アプリケーションサーバの起動に先行してSWANKサーバを起動させるべくCMD
を記述する.
Dockerfileの大半は今回の話題と無関係であり,長いのでここではその末尾を抜粋する.
CMD [ \ "qlot", "exec", "ros", \ "-e", "(ql:quickload :swank)", \ "-e", "(setf swank::*loopback-interface* \"0.0.0.0\")", \ "-e", "(swank:create-server :port 6005 :dont-close t :style :spawn)", \ "-l", "bundle-libs/bundle.lisp", \ "-S", ".", "~/.roswell/bin/clackup", "--server", ":woo", "--address", "0.0.0.0", "--port", "5000", "app.lisp" \ ]
まずライブラリローカライザであるqlot
を起動させる.これにより,quicklisp/
以下のライブラリが読み込まれるようになる.
qlot
の仕事は,ローカルに保存してあるライブラリの使うための設定を適切に行い,処理系に渡すことだ.ここで処理系はros
によって起動される.CMD
上ではパラメータが長々とぶら下がっているが,ros
はこれを順に処理していく.一緒に見ていこう.
まず-e (ql:quickload :swank)
によってswank
システムが読み込まれる.これはサーバを起動するために必要な措置だ.
今思えば,qlot
を使っているので依存関係をqlfile
に記載して,ここでは-e (asdf:load-system :swank)
とすべきだったかもしれない.
次に-e (setf swank::*loopback-interface* "0.0.0.0")
によってSWANKがリッスンするアドレスを書き換える.Docker環境だとIPアドレスが変化しがちなため,ここで0.0.0.0
に固定することで確実に接続できるようにする.
ちなみにSWANKに認証機構は備わっていないため, ソケットにアクセスできる誰からでも接続を許してしまう.これが心配ならばsshなどを経由する必要があるが,このエントリではdocker-composeを開発目的でのみ使っているので問題なしとしている.
次に-e (swank:create-server :port 6005 :dont-close t :style :spawn)
によってSWANKサーバを6005
番ポートで起動する.:dont-close t
は勝手に接続を閉じないための設定だが,もしかすると不要かもしれない.:style :spawn
は接続ごとにプロセスをspawnして複数の接続ができるようにするためのものである.1つの接続しかしないのであれば削除してよさそうだ.
ここからはSWANKとは無関係な処理である.
-l bundle-libs/bundle.lisp
は,依存ライブラリを読み込むためのものだ.qlot
は,qlot bundle
コマンドで依存ライブラリをすべてローカルのbundle-libs
に落とすことができる.これらのライブラリ全てを読み込むブートストラップがbundle.lisp
で,ros
の-l
オプションを使って読み込む.-l
は,指示したファイルを単にロードして実行するだけのオプションである.
-S .
は,ASDFのシステムに対するデフォルト読み込み地点(asdf:*central-registry*
)をカレントディレクトリ(ここではプロジェクトルート)に設定するものである.ここで指定したディレクトリにASDファイルを置いておくと,ASDFはそのシステムを発見できるようになる.
最後に ~/.roswell/bin/clackup --server :woo --address 0.0.0.0 --port 5000 app.lisp
としているのは,clackup
(clack
パッケージが提供)コマンドによってWebアプリケーションを起動するためのものだ.app.lisp
がClackアプリケーションと呼ばれるもので,他の言語におけるapp.psgi
とかにあたると考えてよいだろう.このファイルはCaveman2がプロジェクトを作成したときに自動的に作成される.
Emacsから接続する
これで準備が整った.docker-compose
を使ってプロジェクトを起動し,Emacsから接続してみよう.
docker-compose up
M-x slime-connect
すると,Host:
というプロンプトが表示されるのでlocalhost
と入力する.次にPort:
というプロンプトが表示されるので,先程設定した6005
番ポートを指定する.すると以下のような表示がミニバッファに出力されて,REPLウィンドウが表示され,操作できるようになっているはずだ.
Connecting to Swank on port 6005.. Connected. Take this REPL, brother, and may it serve you well.
すでにコードが読み込まれているので,コード上のパッケージに移動できる.
;; hogeapplicationというcaveman2プロジェクトがあるという想定 CL-USER> (in-package :hogeapplication.web) #<PACKAGE "HOGEAPPLICATION.WEB"> HOGEAPPLICATION.WEB> *web* #<<WEB> {12345678}> HOGEAPPLICATION.WEB> (render #P"index.html") ;; ---hogeapplication.webで同様の記述をしたときと全く同じようにテンプレートがレンダされる--- HOGEAPPLICATION.WEB> (clear-routing-rules *web*) ;; --ルーティング情報が全て消え,実行中のアプリケーションが404しか出さなくなる.試しにやってみよう--
まとめ
Dockerfile
上でSWANKサーバを起動させ,適切に設定を行いポートを露出させることで,
ホスト上のEmacsからdocker-compose上で動作するコンテナの処理系へと接続し,
動作中のアプリケーションのシンボルを読んだり,動作中のコードを変更できるようになった.
SWANKから得られる情報をもとにSLIMEが補完機能を提供するので,実際に動作するアプリケーションに忠実な補完が利用できるようになった.