Lambdaカクテル

集団への盲従を激しく嫌う

SWANKサーバによるEmacs SLIMEとdocker-composeとの連携 -- 現代的なCommon Lisp開発

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で接続できるようになっている.

github.com

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が補完機能を提供するので,実際に動作するアプリケーションに忠実な補完が利用できるようになった.