~/.spring.rb

Railsアプリケーションを開発していてspringにコマンドを追加したいのだけどなあってときにはGemfileに加えずに~/.spring.rbを使う手もあるよっていうのを知ったので試してみたという話。

目標

Gemfilespring-commands-rspecを入れずにbin/spring rspecを実行できるようにする。

ログ

さっそくGemfileからspring-commands-rspecを外し~/.spring.rbを以下の内容で作ってみる。

gem 'spring-commands-rspec'
require 'spring-commands-rspec'

おお、動く。動く。動くんだが。

$ bin/spring rspec --help
WARN: Unresolved specs during Gem::Specification.reset:
      spring (>= 0.9.1)
WARN: Clearing out unresolved specs.
Please report a bug if this causes problems.
[1]
WARN: Unresolved specs during Gem::Specification.reset:
      spring (>= 0.9.1)
WARN: Clearing out unresolved specs.
Please report a bug if this causes problems.
Running via Spring preloader in process 99422
Usage: rspec [options] [files or directories]

    -I PATH                          Specify PATH to add to $LOAD_PATH (may be used more than once).
[...]

~/.spring.rbの内容を以下のように変えるとWARNは消える。 もちろん事前にgem install spring-commands-rspecしておく必要がある。

gem 'spring'
gem 'spring-commands-rspec'
require 'spring-commands-rspec'

なお、この動作はbin/springを使う場合に限る。bundle exec springした場合、springコマンドが動き出す時点でBundlerによる初期化が終っているので

$ bundle exec spring rspec --help
/Users/akira/.spring.rb:3:in `require': cannot load such file -- spring-commands-rspec (LoadError)
        from /Users/akira/.spring.rb:3:in `<top>'

のようになる。

ところでGemfile.lockにあるspringと、インストール済みspringのバージョンに違いがあったらどうなるのだろうか。

Gemfile.lock1.7.0がある一方gem list spring1.7.1, 1.7.0, ...と出てくる状況で実行してみた。

$ bin/spring rspec --help
/Users/akira/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/bundler-1.12.5/lib/bundler/runtime.rb:35:in `block in setup': You have already activated spring 1.7.1, but your Gemfile requires spring 1.7.0. Prepending `bundle exec` to your command may solve this. (Gem::LoadError)
        from /Users/akira/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/bundler-1.12.5/lib/bundler/runtime.rb:20:in `map'

ま、こうなるわな。どうするか。

if Gem.loaded_specs['spring']
  gem 'spring', Gem.loaded_specs['spring'].version.to_s
else
  gem 'spring'
end
gem 'spring-commands-rspec'
require 'spring-commands-rspec'

bin/springでアクティベートされるspringに合わせればいいんだろってことで、RubyGemsのAPIをチェックしながらこうしてみた。うん、うまくいきそう。そう思ってたんだけど。

$ bin/spring rspec --help
/Users/akira/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/bundler-1.12.5/lib/bundler/runtime.rb:35:in `block in setup': You have already activated spring 1.7.1, but your Gemfile requires spring 1.7.0. Prepending `bundle exec` to your command may solve this. (Gem::LoadError)
        from /Users/akira/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/bundler-1.12.5/lib/bundler/runtime.rb:20:in `map'

あれ?

pデバッグしてみるとGem.loaded_specsが実質空だというのがわかる。あれ?

$ ruby -e 'gem "spring"; p Gem.loaded_specs["spring"]'                                                                       
#<:specification:0x3fc439ca0138 spring-1.7.1>

うん、そうだよな。ということはbin/springか。う〜ん、と、コードをながめてみるも、ちら見程度ではよくわからない。

p Gem.loaded_specs

をいろんなところにいれてみたのだけど、やっぱりよくわからない。どこかで消えているのはたしかなんだけど…… というところでふと思い付いて、というか、プロセスが異なるであろうことを思い出して

p $$=>Gem.loaded_specs

に変えてみた。そうすると

$ bin/spring rspec --help
{13100=>{"did_you_mean"=>#<:specification:0x3ff1e0905c60 did_you_mean-1.0.0>, "bundler"=>#<:specification:0x3ff1e086e950 bundler-1.12.5>, "io-console"=>#<:specification:0x3ff1e0928bd4 io-console-0.4.5>, "spring"=>#<:specification:0x3ff1e0998d94 spring-1.7.0>}}
{13100=>{"did_you_mean"=>#<:specification:0x3ff1e0905c60 did_you_mean-1.0.0>, "bundler"=>#<:specification:0x3ff1e086e950 bundler-1.12.5>, "io-console"=>#<:specification:0x3ff1e0928bd4 io-console-0.4.5>, "spring"=>#<:specification:0x3ff1e0998d94 spring-1.7.0>}}
{13131=>{"did_you_mean"=>#<:specification:0x3ff668905d28 did_you_mean-1.0.0>}}
/Users/akira/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/bundler-1.12.5/lib/bundler/runtime.rb:35:in `block in setup': You have already activated spring 1.7.1, but your Gemfile requires spring 1.7.0. Prepending `bundle exec` to your command may solve this. (Gem::LoadError)

一つめの13100bin/springにいれたもの。

  if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^    (?:  )*spring \((.*?)\)$.*?^$/m))
    Gem.paths = { 'GEM_PATH' => [Bundler.bundle_path.to_s, *Gem.path].uniq.join(Gem.path_separator) }
    gem 'spring', match[1]
    p $$=>Gem.loaded_specs
    require 'spring/binstub'

二つめの13100spring/binstubにいれたもの。

lib = File.expand_path("../../lib", __FILE__)
$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib) # enable local development
require 'spring/client'
p $$=>Gem.loaded_specs
Spring::Client.run(ARGV)

そして13131~/.spring.rbにいれたもの。

p $$=>Gem.loaded_specs
if Gem.loaded_specs['spring']
  gem 'spring', Gem.loaded_specs['spring'].version.to_s
else
  gem 'spring'
end
gem 'spring-commands-rspec'
require 'spring-commands-rspec'

で、このときのspringの状態はというとこんな感じ。(ちなみに13100も新たに出てきた13161もプロセスが終了していてもういない。)

$ bin/spring status      
{13161=>{"did_you_mean"=>#<:specification:0x3fcae0c8fca4 did_you_mean-1.0.0>, "bundler"=>#<:specification:0x3fcae086eb0c bundler-1.12.5>, "io-console"=>#<:specification:0x3fcae1018bf0 io-console-0.4.5>, "spring"=>#<:specification:0x3fcae10d4cd8 spring-1.7.0>}}
{13161=>{"did_you_mean"=>#<:specification:0x3fcae0c8fca4 did_you_mean-1.0.0>, "bundler"=>#<:specification:0x3fcae086eb0c bundler-1.12.5>, "io-console"=>#<:specification:0x3fcae1018bf0 io-console-0.4.5>, "spring"=>#<:specification:0x3fcae10d4cd8 spring-1.7.0>}}
Spring is running:

13128 spring server | wm-groundwork | started 4 secs ago    
13131 spring app    | wm-groundwork | started 4 secs ago | test mode      

なるほどなあと、適当にコードを拾い読みしてみるとなかなか興味深い。FD送ったりしてるんだ……というのはいいとして。springの動きがもう少し分からないとなんとも。ということでspringのログを出力させてみる。

$ SPRING_LOG=/dev/tty bin/spring rspec --help
{14343=>{"did_you_mean"=>#<:specification:0x3fc31d505c88 did_you_mean-1.0.0>, "bundler"=>#<:specification:0x3fc31d46ea90 bundler-1.12.5>, "io-console"=>#<:specification:0x3fc31dc18bec io-console-0.4.5>, "spring"=>#<:specification:0x3fc31d5a8b90 spring-1.7.0>}}
[2016-06-25 20:57:20 +0900] [14371] [server] started on /var/folders/25/5gm2mn2w8xlfbc008k8yhkkh0000gn/T/spring-501/a1a74dcf59c99a3b2a195a87ccec9658
[2016-06-25 20:57:20 +0900] [14371] [server] accepted client
[2016-06-25 20:57:20 +0900] [14371] [server] running command rspec
[2016-06-25 20:57:20 +0900] [14371] [application_manager:test] child not running; starting
[2016-06-25 20:57:20 +0900] [14343] [client] sending command
/Users/akira/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/spring-1.7.0/lib/spring/env.rb:84:in `write': Input/output error (Errno::EIO)
        from /Users/akira/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/spring-1.7.0/lib/spring/env.rb:84:in `puts'

ん、ダメか。ここで時間をかけてもなんなのでふつうにファイルを指定する。(あ、特に書いてはないないけど、適当にspring stopしてプロセス群をリフレッシュしている。)

$ SPRING_LOG=/tmp/l bin/spring rspec --help
{15884=>{"did_you_mean"=>#<:specification:0x3ff9d146fc34 did_you_mean-1.0.0>, "bundler"=>#<:specification:0x3ff9d141c958 bundler-1.12.5>, "io-console"=>#<:specification:0x3ff9d186eba0 io-console-0.4.5>, "spring"=>#<:specification:0x3ff9d20a092c spring-1.7.0>}}
{15884=>{"did_you_mean"=>#<:specification:0x3ff9d146fc34 did_you_mean-1.0.0>, "bundler"=>#<:specification:0x3ff9d141c958 bundler-1.12.5>, "io-console"=>#<:specification:0x3ff9d186eba0 io-console-0.4.5>, "spring"=>#<:specification:0x3ff9d20a092c spring-1.7.0>}}
{15913=>{"did_you_mean"=>#<:specification:0x3fc759105be8 did_you_mean-1.0.0>}}
nil
/Users/akira/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/bundler-1.12.5/lib/bundler/runtime.rb:35:in `block in setup': You have already activated spring 1.7.1, but your Gemfile requires spring 1.7.0. Prepending `bundle exec` to your command may solve this. (Gem::LoadError)

$ cat /tmp/l                               
[2016-06-25 21:05:26 +0900] [15912] [server] started on /var/folders/25/5gm2mn2w8xlfbc008k8yhkkh0000gn/T/spring-501/a1a74dcf59c99a3b2a195a87ccec9658
[2016-06-25 21:05:26 +0900] [15912] [server] accepted client
[2016-06-25 21:05:26 +0900] [15912] [server] running command rspec
[2016-06-25 21:05:26 +0900] [15912] [application_manager:test] child not running; starting
[2016-06-25 21:05:26 +0900] [15884] [client] sending command
[2016-06-25 21:05:26 +0900] [15913] [application:test] initialized -> running
[2016-06-25 21:05:26 +0900] [15913] [application:test] got client
[2016-06-25 21:05:26 +0900] [15913] [application:test] preloading app
[2016-06-25 21:05:26 +0900] [15913] [application:test] exception: You have already activated spring 1.7.1, but your Gemfile requires spring 1.7.0. Prepending `bundle exec` to your command may solve this.
[2016-06-25 21:05:26 +0900] [15884] [client] got no pid

$ bin/spring status
{16009=>{"did_you_mean"=>#<:specification:0x3ff668c03c8c did_you_mean-1.0.0>, "bundler"=>#<:specification:0x3ff668c62a98 bundler-1.12.5>, "io-console"=>#<:specification:0x3ff668cb0bf8 io-console-0.4.5>, "spring"=>#<:specification:0x3ff668ca05f0 spring-1.7.0>}}
{16009=>{"did_you_mean"=>#<:specification:0x3ff668c03c8c did_you_mean-1.0.0>, "bundler"=>#<:specification:0x3ff668c62a98 bundler-1.12.5>, "io-console"=>#<:specification:0x3ff668cb0bf8 io-console-0.4.5>, "spring"=>#<:specification:0x3ff668ca05f0 spring-1.7.0>}}
Spring is running:

15912 spring server | wm-groundwork | started 49 secs ago    
15913 spring app    | wm-groundwork | started 49 secs ago | test mode      

これは初回の実行なのでプロセス群の初期化をしているはず。そこでせっかくなのでもう一度実行するとどうなるかも見ておく。

$ bin/spring rspec --help 
{16098=>{"did_you_mean"=>#<:specification:0x3fd6a9025c8c did_you_mean-1.0.0>, "bundler"=>#<:specification:0x3fd6a886e958 bundler-1.12.5>, "io-console"=>#<:specification:0x3fd6a88c8c14 io-console-0.4.5>, "spring"=>#<:specification:0x3fd6a940c4a4 spring-1.7.0>}}
{16098=>{"did_you_mean"=>#<:specification:0x3fd6a9025c8c did_you_mean-1.0.0>, "bundler"=>#<:specification:0x3fd6a886e958 bundler-1.12.5>, "io-console"=>#<:specification:0x3fd6a88c8c14 io-console-0.4.5>, "spring"=>#<:specification:0x3fd6a940c4a4 spring-1.7.0>}}
{16126=>{"did_you_mean"=>#<:specification:0x3fdf2c4dbe58 did_you_mean-1.0.0>}}
/Users/akira/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/bundler-1.12.5/lib/bundler/runtime.rb:35:in `block in setup': You have already activated spring 1.7.1, but your Gemfile requires spring 1.7.0. Prepending `bundle exec` to your command may solve this. (Gem::LoadError)

$ cat /tmp/l             
[...]
[2016-06-25 21:06:58 +0900] [15912] [server] accepted client
[2016-06-25 21:06:58 +0900] [15912] [server] running command rspec
[2016-06-25 21:06:58 +0900] [15913] [application:test] running -> exiting
[2016-06-25 21:06:58 +0900] [15912] [application_manager:test] child dead; starting
[2016-06-25 21:06:58 +0900] [15912] [application_manager:test] child 15913 shutdown
[2016-06-25 21:06:58 +0900] [16126] [application:test] initialized -> running
[2016-06-25 21:06:58 +0900] [16126] [application:test] got client
[2016-06-25 21:06:58 +0900] [16126] [application:test] preloading app
[2016-06-25 21:06:59 +0900] [16126] [application:test] exception: You have already activated spring 1.7.1, but your Gemfile requires spring 1.7.0. Prepending `bundle exec` to your command may solve this.

なるほど、そういうふうに動くのか。まあそれはともかくとして。

アプリケーションの親になるであろうプロセス(application_manager:test; 15912)と、実際にtestモードで動くプロセス(application:test; 1591316126)にはGem.loaded_specsが受け継がれていないと。こうして見てみると、まあ、それはそうなのかなという気もしてくるが……。

~/.spring.rbを読むのはapplication:*であり、読むタイミングはBundlerの初期化前。かつ、ざっとspringのコードをながめた感じではRubyGemsにたよらずにrequireしているように思える。その辺はGem.loaded_specsにも表れているわけだが。

となると、結局はGemからヒントを得ることはできず、つまりbin/springがやっているようにGemfile.lockからヒントを得るか、あるいは

gem 'spring', Spring::VERSION
gem 'spring-commands-rspec'
require 'spring-commands-rspec'

こんなところでお茶をにごすか。いや、というか、これ、すぐに思い付いてもよさそうな内容だ。

というわけで、ここまでの長い試行錯誤は、そもそもの目的からすると「なんだったんだ」という感じだけども、springがなかなか興味深い動きをしているのが分かったので悪くなかった。

まとめ

分かったこと。

  • spring配下でコマンド実行すると~/.spring.rbが読み込まれる(Bundlerの初期化前)
  • Gem.loaded_specsでアクティベートされたgemがわかる
  • springはいくつもプロセスを立ち上げて、FDを送ったりしていてなかなか興味深い
  • 環境変数SPRING_LOGでspringの動きを知ることができる

この話では直接は触れなかったけど、途中で拾ったこと。

  • spring配下でコマンド実行するとconfig/spring.rbが読み込まれる(Bundlerの初期化後)
  • config/spring_client.rbというのもあるらしい(preloading appのあたりで読み込まれるようだ)
  • 環境変数DISABLE_SPRINGでspringの動きをパスできる
  • この辺はspringのREADMEに書いてある
  • 環境変数GEM_SKIPに設定したgemはアクティベートできない

最後のはこんな感じ。

$ ruby -e 'ENV["GEM_SKIP"]="spring:termios"; gem "termios"'
/Users/akira/.rbenv/versions/2.3.1/lib/ruby/2.3.0/rubygems/core_ext/kernel_gem.rb:46:in `gem': skipping termios (Gem::LoadError)
        from -e:1:in `<main>'

補足

実は、古い記憶に「springがログファイルをつかみっぱなしにする」というのがあって、なんとかならないものかと考えたのがきっかけだった。でもちょっと確認したら今はそんなことないというのがすぐにわかった。(もしかすると何か別のことと勘違いしていたかもしれない。)

その裏付けをとる(っぽいことをしてみる)ために少し拾い読みをしていたなかで、そういえばREADMEって読んだことないなと思いいたり、読んでみたところ~/.spring.rbを知った。

So if you have any spring-commands-* gems installed that you want to be available in all projects without having to be added to the project's Gemfile, require them in your ~/.spring.rb.

とあって、ほほうと試してみたのが上のログ。実際にはlog/test.logをローテートするのに使えそうだなと考えていた。(プロジェクトによってはconfig/*に好み通りのコードを入れられないこともあるので。)