概要
Railsアプリケーションを開発していてspringにコマンドを追加したいのだけどなあってときにはGemfile
に加えずに~/.spring.rb
を使う手もあるよっていうのを知ったので試してみたという話。
目標
Gemfile
にspring-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 (required)>'
のようになる。
ところでGemfile.lock
にあるspring
と、インストール済みspring
のバージョンに違いがあったらどうなるのだろうか。
Gemfile.lock
に1.7.0
がある一方gem list spring
で1.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"]'
#<Gem::Specification:0x3fc439ca0138 spring-1.7.1>
うん、そうだよな。ということはbin/spring
か。う〜ん、と、コードをながめてみるも、ちら見程度ではよくわからない。
p Gem.loaded_specs
をいろんなところにいれてみたのだけど、やっぱりよくわからない。どこかで消えているのはたしかなんだけど…… というところでふと思い付いて、というか、プロセスが異なるであろうことを思い出して
p $$=>Gem.loaded_specs
に変えてみた。そうすると
$ bin/spring rspec --help
{13100=>{"did_you_mean"=>#<Gem::Specification:0x3ff1e0905c60 did_you_mean-1.0.0>, "bundler"=>#<Gem::Specification:0x3ff1e086e950 bundler-1.12.5>, "io-console"=>#<Gem::Specification:0x3ff1e0928bd4 io-console-0.4.5>, "spring"=>#<Gem::Specification:0x3ff1e0998d94 spring-1.7.0>}}
{13100=>{"did_you_mean"=>#<Gem::Specification:0x3ff1e0905c60 did_you_mean-1.0.0>, "bundler"=>#<Gem::Specification:0x3ff1e086e950 bundler-1.12.5>, "io-console"=>#<Gem::Specification:0x3ff1e0928bd4 io-console-0.4.5>, "spring"=>#<Gem::Specification:0x3ff1e0998d94 spring-1.7.0>}}
{13131=>{"did_you_mean"=>#<Gem::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)
一つめの13100
はbin/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'
二つめの13100
はspring/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"=>#<Gem::Specification:0x3fcae0c8fca4 did_you_mean-1.0.0>, "bundler"=>#<Gem::Specification:0x3fcae086eb0c bundler-1.12.5>, "io-console"=>#<Gem::Specification:0x3fcae1018bf0 io-console-0.4.5>, "spring"=>#<Gem::Specification:0x3fcae10d4cd8 spring-1.7.0>}}
{13161=>{"did_you_mean"=>#<Gem::Specification:0x3fcae0c8fca4 did_you_mean-1.0.0>, "bundler"=>#<Gem::Specification:0x3fcae086eb0c bundler-1.12.5>, "io-console"=>#<Gem::Specification:0x3fcae1018bf0 io-console-0.4.5>, "spring"=>#<Gem::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"=>#<Gem::Specification:0x3fc31d505c88 did_you_mean-1.0.0>, "bundler"=>#<Gem::Specification:0x3fc31d46ea90 bundler-1.12.5>, "io-console"=>#<Gem::Specification:0x3fc31dc18bec io-console-0.4.5>, "spring"=>#<Gem::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"=>#<Gem::Specification:0x3ff9d146fc34 did_you_mean-1.0.0>, "bundler"=>#<Gem::Specification:0x3ff9d141c958 bundler-1.12.5>, "io-console"=>#<Gem::Specification:0x3ff9d186eba0 io-console-0.4.5>, "spring"=>#<Gem::Specification:0x3ff9d20a092c spring-1.7.0>}}
{15884=>{"did_you_mean"=>#<Gem::Specification:0x3ff9d146fc34 did_you_mean-1.0.0>, "bundler"=>#<Gem::Specification:0x3ff9d141c958 bundler-1.12.5>, "io-console"=>#<Gem::Specification:0x3ff9d186eba0 io-console-0.4.5>, "spring"=>#<Gem::Specification:0x3ff9d20a092c spring-1.7.0>}}
{15913=>{"did_you_mean"=>#<Gem::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"=>#<Gem::Specification:0x3ff668c03c8c did_you_mean-1.0.0>, "bundler"=>#<Gem::Specification:0x3ff668c62a98 bundler-1.12.5>, "io-console"=>#<Gem::Specification:0x3ff668cb0bf8 io-console-0.4.5>, "spring"=>#<Gem::Specification:0x3ff668ca05f0 spring-1.7.0>}}
{16009=>{"did_you_mean"=>#<Gem::Specification:0x3ff668c03c8c did_you_mean-1.0.0>, "bundler"=>#<Gem::Specification:0x3ff668c62a98 bundler-1.12.5>, "io-console"=>#<Gem::Specification:0x3ff668cb0bf8 io-console-0.4.5>, "spring"=>#<Gem::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"=>#<Gem::Specification:0x3fd6a9025c8c did_you_mean-1.0.0>, "bundler"=>#<Gem::Specification:0x3fd6a886e958 bundler-1.12.5>, "io-console"=>#<Gem::Specification:0x3fd6a88c8c14 io-console-0.4.5>, "spring"=>#<Gem::Specification:0x3fd6a940c4a4 spring-1.7.0>}}
{16098=>{"did_you_mean"=>#<Gem::Specification:0x3fd6a9025c8c did_you_mean-1.0.0>, "bundler"=>#<Gem::Specification:0x3fd6a886e958 bundler-1.12.5>, "io-console"=>#<Gem::Specification:0x3fd6a88c8c14 io-console-0.4.5>, "spring"=>#<Gem::Specification:0x3fd6a940c4a4 spring-1.7.0>}}
{16126=>{"did_you_mean"=>#<Gem::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
; 15913
、16126
)には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/*
に好み通りのコードを入れられないこともあるので。)