RuboCopのServer Modeで高速lint/formatする with Docker
以前、rubocop-daemonをDocker環境で動かしてvscodeで高速にformat on saveする記事を書きました。
この記事ではサードパーティのライブラリのfork + 自前のクライアントスクリプトで実現していましたが、2022年6月末にリリースされたRuboCop 1.31より、同じことを実現するServer Modeが公式に実装されたようです。
というわけで、こちらでも同様にDocker環境 + vscodeで動かしてみました。
挙動や仕様を探ってみる
まずはServer Modeを軽く動かしてみて挙動や仕様を確認してみます。
最低限rubocopが動くRuby on Dockerな開発環境を作り、そこで動作確認をしたあとにvscodeに組み込んでいく流れを想定しています。なお完成品のリポジトリがこちらです。sinatraのサンプルコードにrubocopを入れただけのコンテナをdocker composeで起動する構成です。
通常のユースケースを試す
まずはコンテナ/ホストを意識しない通常のユースケースで動かしてみます。通常の開発をしている想定でdocker composeでコンテナを起動し、そのコンテナの中に入って各種コマンドを試してみます。
ドキュメントによると、--server
オプションをつけてrubocopを実行した場合、サーバプロセスがなければ起動して実行、既にサーバがあればそれを使って実行するというような挙動になるようです。
ということで、serverなし・serverあり(一回目)・serverあり(二回目)の3パターンをtimeコマンドで時間を見ながら実行します。[1]
root@b6d6d4555a18:/app# time rubocop .
Inspecting 2 files
..
2 files inspected, no offenses detected
real 0m0.677s
user 0m0.617s
sys 0m0.132s
root@b6d6d4555a18:/app# time rubocop --server
RuboCop server starting on 127.0.0.1:46801.
Inspecting 2 files
..
2 files inspected, no offenses detected
real 0m0.645s
user 0m0.616s
sys 0m0.102s
root@b6d6d4555a18:/app# time rubocop --server
Inspecting 2 files
..
2 files inspected, no offenses detected
real 0m0.242s
user 0m0.059s
sys 0m0.019s
realだけ取り出してサマリすると以下となります。
- serverなし: 0.677s
- serverあり(1): 0.645s
- serverあり(2): 0.242s
serverあり(2)
が早くなっており、想定通りの挙動をしているように見えます。今回は実験環境が非常にミニマルなためserverなしでもそこまで遅くありませんが、実際のプロジェクトではより遅いはずなので、serverありで得られる恩恵はより大きいでしょう。
コンテナ外からの実行を考える
以前書いた記事では、あらかじめrubocopのdaemonをコンテナとして常時起動しておき、それに対してホストマシンからシンクライアント的にコマンドを実行するという仕組みにしていました。そのため、Server Modeでも似た仕組みを想定しつつ挙動を見ていきます。
ドキュメントによると--start-server
や--stop-server
オプションでサーバの起動や停止だけを実行できるようなので、試してみます。
root@b6d6d4555a18:/app# rubocop --start-server
RuboCop server starting on 127.0.0.1:41393.
root@b6d6d4555a18:/app# rubocop --start-server
RuboCop server (27) is already running.
root@b6d6d4555a18:/app# rubocop --stop-server
root@b6d6d4555a18:/app# rubocop --start-server
RuboCop server starting on 127.0.0.1:42167.
デフォルトではポート番号は特に固定されておらず、起動ごとに変更されるようです。ここは$RUBOCOP_SERVER_HOST
や$RUBOCOP_SERVER_PORT
環境変数で指定できるとドキュメントに記載されています。
また--start-server
はサーバプロセスを起動してすぐ終了する仕様で、ドキュメントを見てもサーバをforegroundで起動するようなオプションは見当たりませんでした。サーバを単独で起動させるコンテナを立てるには少々工夫が要りそうです。
ともあれ、適当なポートをホストにbindしたコンテナでサーバを実行し、ホスト側からそこに繋いでrubocopしてみます。
docker-compose.yml
に以下のようなサービスを追加し、
rubocop-server:
...
command: bash -c 'rubocop --start-server; sleep infinity'
environment:
RUBOCOP_SERVER_HOST: 0.0.0.0
RUBOCOP_SERVER_PORT: 45678
ports:
- 45678:45678
これを起動した状態でホスト側でenv RUBOCOP_SERVER_HOST=localhost RUBOCOP_SERVER_PORT=45678 rubocop --server
と実行すると
Address already in use - bind(2) for "localhost" port 45678 (Errno::EADDRINUSE)
新しくサーバを起動しようとしてエラーになってしまいました。どうも別ホストに明示的に起動したサーバに接続しにいくようなユースケースは想定されていないようです。
コードからこの挙動を追ってみた記録
念のため、rubocop本体のコードを確認して上記仕様を確かめてみます。
まずエントリーポイント的なファイルの該当コードがこれです。
ここから、RuboCop::Server.running?
の定義を追ってみると、下記のようになっていました。
これ以上は掘り下げていませんが、見るからにキャッシュ(ファイルシステムと思われる)やpidを参照しており、あくまでサーバもクライアントも同一ホストで動作させる想定になっているようです。
仕方ないのでこの方針は諦めます。そこで通常のユースケースに則り、rubocop用のコンテナを立てるようなことはせず、アプリケーションコンテナに対してdocker compose exec
でrubocop --server
を実行してみます。
sbox_rubocop-server-on-docker$ time docker compose exec app rubocop --server
RuboCop server starting on 127.0.0.1:34289.
Inspecting 2 files
..
2 files inspected, no offenses detected
real 0m0.873s
user 0m0.039s
sys 0m0.030s
sbox_rubocop-server-on-docker$ time docker compose exec app rubocop --server
Inspecting 2 files
..
2 files inspected, no offenses detected
real 0m0.238s
user 0m0.043s
sys 0m0.026s
2回目が早くなっており、単にこれを実行させれば良さそうということがわかります。
こちらのほうがシンプルに実現できそうであり、かつそもそも他の方法は無いことから、この方向で進めます。
vscodeに組み込む
Server Modeの挙動や仕様とDocker環境での実現方針は決まったので、vscode上で動かせるように設定していきます。なお、vscode想定とはいえ用意するラッパースクリプトは汎用なので、vscode以外でも使えるはずです。
前提となる環境条件ですが、cop対象のコードがあるコンテナがdocker-composeで動作しており、そのプロジェクト内にあるrubocopが1.31
以上のバージョンになっていればokです。特に追加のライブラリやコンテナ設定などは不要です。
まずホスト環境でrubocop
コマンドとして振る舞うラッパースクリプトを用意します。ここではbin/rubocop
として下記のようなスクリプトを作成します。
#!/bin/bash
cd $(dirname $0)/.. # docker-compose.ymlがあるディレクトリに移動
OPTION=$(test -p /dev/stdin && echo '-T') # 標準入力を受ける場合もそうでない場合もいい感じに動くよう、`-T`オプションの有無を制御
docker compose exec $OPTION app rubocop --server $@ # --serverオプションを入れつつ、他の引数をそのまま渡す
これでbin/rubocop
が実質コンテナ内でのrubocopコマンドとして動作します。ただしファイルパスはコンテナ内のWORKDIR基準なので注意が必要です。
次にvscode用に拡張機能と設定を用意します。rubocopを動作させる拡張としてmisogi.ruby-rubocop
を導入し、また下記設定を追加します。
"ruby.rubocop.executePath": "./bin/",
// formatはせずlintだけでよいなら下記設定は不要
"[ruby]": {
"editor.defaultFormatter": "misogi.ruby-rubocop"
},
ここまでできれば、Server Modeなrubocopがいい感じにlintやformatを実行してくれるようになります。
導入のために追加したファイルは上記2つのみで、特にプロジェクトの既存の設定やコードに手を入れておらず、コンテナを足したりもしていません。つまり、rubocopのバージョンさえ上がっていれば既存のプロジェクトにも大きな変更を入れずに導入することができます。
そのrubocopのバージョンを上げるのがどのくらい現実的かというのはプロジェクトによってまちまちかもしれませんが、追加のライブラリを足すのと違い、rubocop本体は遅かれ早かれ上げるものだと思います。せっかくなので、このタイミングで上げてみるのも良いでしょう。
まとめ
RuboCopが公式にServer Modeを実装したことにより、高速なlintやformatがかなり簡単に導入できるようになりました。
これまでの環境への変更が多い手順に抵抗があった方も、これを機に導入してみてはいかがでしょうか。
-
シンタックスハイライトが想定と違うように見えていますが、ハイライトなしよりは見やすいと思うのでこのままにしています ↩︎
Discussion