Rails ではない場合に gem で定義された Rake Task を呼び出す

まあ、つまり普通に Ruby で書く場合にどうするかってことで、私みたいな Rails とセットでしか Ruby をほとんど使わないような人間でしか疑問に思わないようなことだと思います :sweat_smile: （というか僕が勉強不足なだけなんですが）

## まず Rails で使われる前提だとどうするか？

まさに公式で説明されている通りです。

http://api.rubyonrails.org/classes/Rails/Railtie.html

```rb
class MyRailtie < Rails::Railtie
  rake_tasks do
    load 'path/to/my_railtie.tasks'
  end
end
```

gem 内で Railtie に用意されている rake_tasks メソッドを呼び出して gem 内の Rake ファイルをロードさせるようにします。

例えば `rspec-rails` でも以下のように呼び出していますね。

https://github.com/rspec/rspec-rails/blob/master/lib/rspec-rails.rb#L22-L24

```rb:rspec-rails/lib/rspec-rails.rb
module RSpec
  module Rails
    class Railtie < ::Rails::Railtie
      rake_tasks do
        load "rspec/rails/tasks/rspec.rake"
      end
```

では、ここから Rails がどういう流れでこの `load` を処理しているか確認していきます。

### rake_tasks メソッドは何をしているか？

さっそく `Rails::Railtie` を見てみます。

https://github.com/rails/rails/blob/da23e125f8d755917b08f5cca1f7fe1ff38c8b7e/railties/lib/rails/railtie.rb#L134-L136

```rb:rails/railties/lib/rails/railtie.rb
module Rails
  class Railtie
    class << self
      def rake_tasks(&blk)
        register_block_for(:rake_tasks, &blk)
      end

      private
        # receives an instance variable identifier, set the variable value if is
        # blank and append given block to value, which will be used later in
        # `#each_registered_block(type, &block)`
        def register_block_for(type, &blk)
          var_name = "@#{type}"
          blocks = instance_variable_defined?(var_name) ? instance_variable_get(var_name) : instance_variable_set(var_name, [])
          blocks << blk if blk
          blocks
        end
```

やっていることは簡単で、 `register_block_for` に `:rake_tasks` をキーに `load` が含まれたブロックを渡し、 `blocks` 配列につっこまれていきます。つまり、 __すぐに実行される訳ではありません__ 。

### ではいつ実行されるのか？

https://github.com/rails/rails/blob/da23e125f8d755917b08f5cca1f7fe1ff38c8b7e/railties/lib/rails/railtie.rb#L240-L243

```rb
module Rails
  class Railtie
    protected
      def run_tasks_blocks(app) #:nodoc:
        extend Rake::DSL
        each_registered_block(:rake_tasks) { |block| instance_exec(app, &block) }
      end
```

`register_block_for` と同じクラス内に実行する `run_tasks_blocks` も配置されています。

`Raks::DSL` を `extend` することで Rake の構文を解釈できるようにした上で、登録されていたブロックを `instance_exec` してロードしていきます。

これを実際に呼び出すのは以下の箇所です。

https://github.com/rails/rails/blob/fe1f4b2ad56f010a4e9b93d547d63a15953d9dc2/railties/lib/rails/engine.rb#L455-L459

```rb
module Rails
  class Engine < Railtie
    autoload :Configuration, "rails/engine/configuration"

    def load_tasks(app = self)
      require "rake"
      run_tasks_blocks(app)
      self
    end

    protected
      def run_tasks_blocks(*) #:nodoc:
        super
        paths["lib/tasks"].existent.sort.each { |ext| load(ext) }
      end
```

`lib/tasks` にあるファイルも一緒にロードしているようですね。

### load_tasks を呼び出すのは誰？

これは Rails プロジェクトを new した時に生成される Rake のテンプレートに最初から書いてありました。

https://github.com/rails/rails/blob/92703a9ea5d8b96f30e0b706b801c9185ef14f0e/railties/lib/rails/generators/rails/app/templates/Rakefile

```rb:rails/railties/lib/rails/generators/rails/app/templates/Rakefile
require_relative 'config/application'

Rails.application.load_tasks
```

つまり、Rails プロジェクトで `bin/rake` を使えば、自動的にロードされる仕組みになっています。

## Rails 以外ではどうするか？

### Bundler を利用している場合

この分け方が適切かは自信が無いのですが、`bundle exec rake` で実行する場合には、ここまで Rails で見てきた方法がそのまま適用できます。

ただし Railtie は無いので、 __gem 側で Rake ファイルの load を書くことはできない__ と思います。（それができないか探していたのですが、見つけられませんでした。。）

よって、利用側がそれを記載することになります。 Rails アプリでもテンプレートという形は取っているものの、アプリ側の方で明示的に `Rails.application.load_tasks` を呼んでいたのと同じです。

#### 例

```text:gem側
.
├── Gemfile
├── Rakefile
├── lib
│   ├── sample_gem
│   │   ├── tasks
│   │   │   └── sample.rake
│   │   └── version.rb
│   └── sample_gem.rb
└── sample_gem.gemspec
```

`sample_gem` の中に `sample.rake` があったとします。

これを、利用側のアプリで呼び出そうと思うと以下のような Rakefile を用意するだけです。

```rb:Rakefile
load "sample_gem/tasks/sample.rake"
```

これで `bundle exec rake -T` すれば、 gem 側の `sample.rake` のタスクが表示されます。（description あればですが）

### Bundler を利用していない場合

- [Ruby Rake load tasks from a gem - stackoverflow](http://stackoverflow.com/questions/15446153/ruby-rake-load-tasks-from-a-gem)
- [Sharing rake tasks defined in a gem - Andy Atkinson](http://andyatkinson.com/blog/2014/06/23/sharing-rake-tasks-in-gems#fn1)

上記のサイトに説明有るとおりですが、以下のように実現できます。

```rb:Rakefile
spec = Gem::Specification.find_by_name 'sample_gem'
load "#{spec.gem_dir}/sample_gem/tasks/sample.rake"
```

bundler 利用していないため Gemfile のコンテキストで実行できないので、フルパスが必要になるようです。（まあ、あんまり使うことなさそうな気はします）

## その他

調べてる中でちょっと気になったこと

### `bundle exec` で実行した時、依存 Gem のバージョンファイルだけは読み込まれてる

`bundle console` した時に依存 gem の名前空間が解決できていて「え :flushed: 」となっていたのですが、`gemspec` に `VERSION` を渡していたからかぁと。

`$LOADED_FEATURES` でロードされているファイルに `version.rb` だけ含まれていたので気づきました。

### Rake ファイル同士のインポート

`import 'lib/tasks/hoge.rake'` のような形で記述できるようですが、私は初めてみました。

[複数のRakefileの連携 - Rake](http://www2s.biglobe.ne.jp/~idesaku/sss/tech/rake/#multiple_rakefile)

> - ファイルが読み込まれるのは、ベースのRakefileのタスク定義を解決した後である。
> - importするファイルは、ファイルタスクの依存性解決に組み込める。
 
### bundler の install_tasks

`bundle gem` した場合には `rake release` 等が利用できるようになってますが、これは Rakefile に `require "bundler/gem_tasks"` が追加されているからのようです。

以下の記事がとても詳しいのですが、 `Bundler::GemHelper.install_tasks` で一気に task がロードされているのが面白いです。

そういうアプローチで今回の私のそもそもの課題の回答をくれている人もいました。

[Rake tasks from a Rubygem](https://eng.climate.com/2014/04/18/rake-tasks-from-a-rubygem/)

`gem` 側で install_task 部分を用意しておいて、利用側は Rakefile でそれを require するだけ、それも良さそうな気がします。

### bindir のスクリプトがコピーされるなら、bin 用のスクリプトを書いた方が良い？

ここまで見てきた方法は、どうしても利用側に Rakefile に書いてもらうなど一手間加えてもらう必要があります。

一方 gemspec には `bindir` の指定ができ、そこに配置したスクリプトは gem の利用側ですぐに使う事ができます。 `rspec` のコマンドもそうですね。

その gem の用途にもよりますが、Rake タスクでのコマンドの配布よりもスクリプトとして配布した方が便利なケースもあるのかもしれません。