まあ、つまり普通に Ruby で書く場合にどうするかってことで、私みたいな Rails とセットでしか Ruby をほとんど使わないような人間でしか疑問に思わないようなことだと思います (というか僕が勉強不足なだけなんですが)
まず Rails で使われる前提だとどうするか?
まさに公式で説明されている通りです。
class MyRailtie < Rails::Railtie
rake_tasks do
load 'path/to/my_railtie.tasks'
end
end
gem 内で Railtie に用意されている rake_tasks メソッドを呼び出して gem 内の Rake ファイルをロードさせるようにします。
例えば rspec-rails
でも以下のように呼び出していますね。
module RSpec
module Rails
class Railtie < ::Rails::Railtie
rake_tasks do
load "rspec/rails/tasks/rspec.rake"
end
では、ここから Rails がどういう流れでこの load
を処理しているか確認していきます。
rake_tasks メソッドは何をしているか?
さっそく Rails::Railtie
を見てみます。
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
配列につっこまれていきます。つまり、 すぐに実行される訳ではありません 。
ではいつ実行されるのか?
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
してロードしていきます。
これを実際に呼び出すのは以下の箇所です。
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 のテンプレートに最初から書いてありました。
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
を呼んでいたのと同じです。
例
.
├── Gemfile
├── Rakefile
├── lib
│ ├── sample_gem
│ │ ├── tasks
│ │ │ └── sample.rake
│ │ └── version.rb
│ └── sample_gem.rb
└── sample_gem.gemspec
sample_gem
の中に sample.rake
があったとします。
これを、利用側のアプリで呼び出そうと思うと以下のような Rakefile を用意するだけです。
load "sample_gem/tasks/sample.rake"
これで bundle exec rake -T
すれば、 gem 側の sample.rake
のタスクが表示されます。(description あればですが)
Bundler を利用していない場合
上記のサイトに説明有るとおりですが、以下のように実現できます。
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 の名前空間が解決できていて「え 」となっていたのですが、gemspec
に VERSION
を渡していたからかぁと。
$LOADED_FEATURES
でロードされているファイルに version.rb
だけ含まれていたので気づきました。
Rake ファイル同士のインポート
import 'lib/tasks/hoge.rake'
のような形で記述できるようですが、私は初めてみました。
- ファイルが読み込まれるのは、ベースのRakefileのタスク定義を解決した後である。
- importするファイルは、ファイルタスクの依存性解決に組み込める。
bundler の install_tasks
bundle gem
した場合には rake release
等が利用できるようになってますが、これは Rakefile に require "bundler/gem_tasks"
が追加されているからのようです。
以下の記事がとても詳しいのですが、 Bundler::GemHelper.install_tasks
で一気に task がロードされているのが面白いです。
そういうアプローチで今回の私のそもそもの課題の回答をくれている人もいました。
gem
側で install_task 部分を用意しておいて、利用側は Rakefile でそれを require するだけ、それも良さそうな気がします。
bindir のスクリプトがコピーされるなら、bin 用のスクリプトを書いた方が良い?
ここまで見てきた方法は、どうしても利用側に Rakefile に書いてもらうなど一手間加えてもらう必要があります。
一方 gemspec には bindir
の指定ができ、そこに配置したスクリプトは gem の利用側ですぐに使う事ができます。 rspec
のコマンドもそうですね。
その gem の用途にもよりますが、Rake タスクでのコマンドの配布よりもスクリプトとして配布した方が便利なケースもあるのかもしれません。