RuboCop公式のServer Modeを試す
まとめた記事書いた
- Deamonize
- rubocop コマンドを実行するたびに process を起動するのではなく、process を常駐させる
え、なに、ついに公式できちゃったの?オレオレでゴニョゴニョと書いていた人としては試さざるを得ない
公式のドキュメント。
The server mode was introduced in RuboCop 1.31.
とある。1.31というと6/27にリリースされている。結構前じゃん。
該当のPull Requestを見るに、前にぼくが参考にしたrubocop-daemonを、本家でintegrateしたいということだったらしい。まぁそうだよね。
ひとまず、ぱっと思いつく知りたいことは以下
- Docker環境でいい感じにできるか
- Server側(Dockerコンテナ内)とClient側でバージョンが違っても動くか
- ホストマシンにプロジェクトのGemfileのすべてをインストールせずに動かせるか
- vscodeでいい感じに動かせるか
まずは対象のプロジェクトを適当にでっち上げる。
なんかrubyでdocker composeな感じでとりあえずrubocopが入ってればok。
リポジトリ
はい。適当なsinatraなやつ。
version: "3"
services:
app:
build:
context: app
command: bundle exec ruby main.rb -o 0.0.0.0
volumes:
- ./app:/app
- bundle:/usr/local/bundle
ports:
- 4567:4567
volumes:
bundle:
FROM ruby:3.1.2
RUN mkdir /app
WORKDIR /app
COPY Gemfile Gemfile.lock .
RUN bundle install
# frozen_string_literal: true
source "https://rubygems.org"
gem "sinatra"
gem "sinatra-reloader"
gem "puma"
require 'sinatra'
require "sinatra/reloader"
get '/' do
'Hello world!'
end
ちなみに、ruby3系からはWEBrickがruby標準に無い関係で、明示的にthin
なりpuma
なり入れないといけないらしい。知らんかった。
rubocopを追加で入れる。
gem 'rubocop', '1.31'
ホスト側のrubocop version > コンテナ内の(プロジェクト指定の)rubocop version という実際にありそうな状況を作ってみたく、バージョンは固定した。
ではserver modeで適当に遊んで挙動を確認する。
コンテナに入ってちょっと触ってみた
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.
なにもしない場合、ポート番号は固定されないらしい。
いやその前に速度確認しろよw
念のためstop serverしてから、serverなし、serverあり(一回目)、serverあり(二回目)でtimeしてみた。
realだけ抜き出すと下記。
- serverなし: 0.677s
- serverあり(1): 0.645s
- serverあり(2): 0.242s
※原理的に早いはずなのは知っているし、ベンチする気もないので一回ずつしか試してない
以下生ログ。
root@b6d6d4555a18:/app# rubocop --stop-server
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
ところで、 --start-server
したら裏でプロセスを起動するだけしてコマンドは終了してしまう。
ドキュメントを見てもforegroundで起動しっぱなしにするみたいなモードは無いようにみえる。
これdocker環境だとどこにserverプロセスを差し込むか悩むな。
ん?いやごちゃごちゃ考えなくてもdocker compose exec
でやればよいのでは?
host-machine$ time docker compose exec app rubocop --server
RuboCop server starting on 127.0.0.1:44725.
Inspecting 2 files
..
2 files inspected, no offenses detected
________________________________________________________
Executed in 709.86 millis fish external
usr time 46.85 millis 205.00 micros 46.65 millis
sys time 29.34 millis 205.00 micros 29.13 millis
host-machine$ time docker compose exec app rubocop --server
Inspecting 2 files
..
2 files inspected, no offenses detected
________________________________________________________
Executed in 287.50 millis fish external
usr time 21.98 millis 200.00 micros 21.79 millis
sys time 45.12 millis 200.00 micros 44.92 millis
まぁ普通に動きますな...
現実的にチームに入れるならゴチャゴチャ書くよりはシンプルにこれのがいいな...
適当にrubocopのバージョンを1.31以上にして、docker compose execすればそれだけでいいっぽい結論が出かかっているが、一応最初にイメージしてたあたりの挙動も試したい。
適当にdocker composeの中にrubocop server用のコンテナを立てる。
version: "3"
x-app-base: &app-base
build:
context: app
volumes:
- ./app:/app
- bundle:/usr/local/bundle
services:
base:
<<: *app-base
command: bundle install
app:
<<: *app-base
command: bundle exec ruby main.rb -o 0.0.0.0
ports:
- 4567:4567
rubocop-server:
<<: *app-base
command: bash -c 'rubocop --start-server; sleep infinity'
environment:
RUBOCOP_SERVER_HOST: 0.0.0.0
RUBOCOP_SERVER_PORT: 45678
ports:
- 45678:45678
volumes:
bundle:
少々ごちゃついているが、x-app-base
やbase
serviceの構成は、同じRuby環境で複数のコンテナを立ち上げる開発環境のための豆テクです。
volumesで/usr/local/bundle
しつつserviceのコマンドでbundle installすることで、Gemfileの中身が変わった場合の再bundle時に以前のvolumeの中身をcache的に使えるというハック。なのでDockerfileの中ではRUN bundle install
してません。
で、rubocop-server
serviceを適当に作る。単にrubocop --start-server
を起動しただけでは一瞬で終了してしまうので、sleep infinity
で起動しっぱなしに。
で、適当にホストでgem install rubocop
したあとに env RUBOCOP_SERVER_HOST=localhost RUBOCOP_SERVER_PORT=45678 rubocop -- server
などとやってみたが...
Address already in use - bind(2) for "localhost" port 45678 (Errno::EADDRINUSE)
などと、こちらでも別途のserverを起動しようとしてしまう。
そもそも、既存のserverに接続専用としてシンクライアント的にrubocopを起動するオプションは無いっぽい。
一応、本体のコードを読んで仕様を確かめてみる。
まず実行ファイル?的なものの該当コードがこれ。
RuboCop::Server.running?
の定義を追ってみると、こう。
めちゃめちゃcacheとかpidとか見てるがな。同一ホストで動いてる想定しか存在してない。せやな。
というわけで、docker compose execする以外に手段は無い。少なくとも現時点では。
まぁ下手にホスト側から実行できて、dockerを介さない分オーバーヘッドが減って早くなるみたいな結果が出てしまうと歯止めが効かなくなってしまうので、ここで止めてくれて助かったということにしておこう。
一応vscodeもためそう。
結論からすると、以下の準備をすればできる。
bin/rubocop
を用意
プロジェクトにrubocopのwrapperを用意する。
#!/bin/bash
cd $(dirname $0)/..
OPTION=$(test -p /dev/stdin && echo '-T')
docker compose exec $OPTION app rubocop --server $@
docker-compose.ymlがあるディレクトリに移動したあと、execしている。
なお標準入力を使う場合でも使わない場合でもいい感じに動くようにすべく、-T
オプションの有無を制御している。
とはいえ、ホストでshellから起動する場合でもファイルパスはコンテナ内基準で指定する必要がある(あくまでrubocop本体はコンテナ内のファイルを見るため)。モノレポなどでリポジトリルートとコンテナ内のWORKDIRが違う場合は注意が必要。
rubocopのextensionをセットアップする
misogi.ruby-rubocop
拡張を入れ、下記をsettings.jsonに追記する:
"ruby.rubocop.executePath": "./bin/",
"[ruby]": {
"editor.defaultFormatter": "misogi.ruby-rubocop"
},
※lintだけでいいならeditor.defaultFormatter
の設定はいらない
ちなみに、この方法を使う場合は、下記の理由により通常のRuby extension (rebornix.Ruby
) のlinter/formatter設定で使うことはできず、misogi.ruby-rubocop
を使う必要があります。
-
rebornix.Ruby
はrubocopに渡すファイルパスをホスト上の絶対パスで渡す仕様になっている- コンテナ内のファイルパスと食い違ってしまい、うまく動かない
-
misogi.ruby-rubocop
は相対パスで渡すので問題ない -
bin/rubocop
内でsed "s|$PWD/||"
とかで置き換えてもいいけど、そこまでしなくても上記拡張を使うだけで済む
-
rebornix.Ruby
はformatterのパスを指定できず、カスタムのbin/rubocop
を指定できない- linterは指定できるのに
- issueはあるが実装される気配が無い
これだけ準備してしまえばいい感じにrubocopライフが送れる。