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

  • 1
    いいね
  • 0
    コメント

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

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

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

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

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

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

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

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::DSLextend することで Rake の構文を解釈できるようにした上で、登録されていたブロックを instance_exec してロードしていきます。

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

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

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

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 を呼んでいたのと同じです。

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

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

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

Rakefile
load "sample_gem/tasks/sample.rake"

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

Bundler を利用していない場合

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

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: 」となっていたのですが、gemspecVERSION を渡していたからかぁと。

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

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

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

複数のRakefileの連携 - Rake

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

bundler の install_tasks

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

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

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

Rake tasks from a Rubygem

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

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

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

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

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