'rails -v'コマンドは何をしているのか
‘rails -v'コマンドは何をやっているのか
rails -v
コマンドがどのような仕組みになっているか気になったので調べてみる。RailsのバージョンはRails 5.0.1
。
which rails
コマンドで実行ファイルの場所を取得する。
$ which rails ~/.rbenv/shims/rails
rbenv
を使用している場合には上記が実行ファイルとなるが、上記のファイルはただのラッパーのようである。ここでは詳細は追わないが、以下のリンクが後で理解するときに参考になりそう。
実際の実行ファイルをrbenv which rails
コマンドで取得する。
$ rbenv which rails ~/.rbenv/versions/2.3.1/bin/rails
~/.rbenv/versions/2.3.1/bin/rails
の中身を見ると、
(省略) gem 'railties', version load Gem.bin_path('railties', 'rails', version) #[1] pry(main)> Gem.bin_path('railties', 'rails') #=> "~/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/railties-5.0.1/exe/rails"
となっており、railties
というgemのrails
という実行ファイルを実行している。
/exe/rails
を見てみる。
#!/usr/bin/env ruby git_path = File.expand_path('../../../.git', __FILE__) if File.exist?(git_path) railties_path = File.expand_path('../../lib', __FILE__) $:.unshift(railties_path) end require "rails/cli"
上位のフォルダに.git
があれば、自分自身のlib
が探索されるように探索パス($:
)にパスを追加している。
そしてrequire "rails/cli"
でcli.rb
を実行している。/lib/rails/cli.rb
が読み込まれるはずだが、念のためにrequire "rails/cli"
の上の行にputs $:
を差し込んで~/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/railties-5.0.1/lib
が含まれているかどうか確認した。ちゃんとあったので多分これが読み込まれているはず。
/lib/rails/cli.rb
を見てみる。
require 'rails/app_loader' # If we are inside a Rails application this method performs an exec and thus # the rest of this script is not run. Rails::AppLoader.exec_app require 'rails/ruby_version_check' Signal.trap("INT") { puts; exit(1) } if ARGV.first == 'plugin' ARGV.shift require 'rails/commands/plugin' else require 'rails/commands/application' end
Rails::AppLoader.exec_app
を詳しく見るため、/lib/rails/app_loader.rb
を見る。
require 'pathname' require 'rails/version' module Rails module AppLoader # :nodoc: extend self RUBY = Gem.ruby EXECUTABLES = ['bin/rails', 'script/rails'] (省略) def exec_app original_cwd = Dir.pwd loop do if exe = find_executable contents = File.read(exe) if contents =~ /(APP|ENGINE)_PATH/ exec RUBY, exe, *ARGV break # non reachable, hack to be able to stub exec in the test suite elsif exe.end_with?('bin/rails') && contents.include?('This file was generated by Bundler') $stderr.puts(BUNDLER_WARNING) Object.const_set(:APP_PATH, File.expand_path('config/application', Dir.pwd)) require File.expand_path('../boot', APP_PATH) require 'rails/commands' break end end # If we exhaust the search there is no executable, this could be a # call to generate a new application, so restore the original cwd. Dir.chdir(original_cwd) and return if Pathname.new(Dir.pwd).root? # Otherwise keep moving upwards in search of an executable. Dir.chdir('..') end end def find_executable EXECUTABLES.find { |exe| File.file?(exe) } end (省略)
if exe = find_executable
は現在自分がRails::Root
にいるならtrue
、そうでないならfalse
を返す。
カレントディレクトリがRails::Root
以外
ここでは、一旦homeディレクトリにいるとして続きを見ていく。
cli.rb
に戻ると、今回のコマンドの引数は-v
なのでrequire 'rails/commands/application'
が実行されることがわかる。
/lib/rails/commands/application.rb
では、args = Rails::Generators::ARGVScrubber.new(ARGV).prepare!
が実行されている。
require 'rails/generators' require 'rails/generators/rails/app/app_generator' module Rails module Generators class AppGenerator # :nodoc: # We want to exit on failure to be kind to other libraries # This is only when accessing via CLI def self.exit_on_failure? true end end end end args = Rails::Generators::ARGVScrubber.new(ARGV).prepare! Rails::Generators::AppGenerator.start args
Rails::Generators::ARGVScrubber.new(ARGV).prepare!
を詳しく見るために、/lib/rails/generators/rails/app/app_generator.rb
を見る。
# This class handles preparation of the arguments before the AppGenerator is # called. The class provides version or help information if they were # requested, and also constructs the railsrc file (used for extra configuration # options). # # This class should be called before the AppGenerator is required and started # since it configures and mutates ARGV correctly. class ARGVScrubber # :nodoc: def initialize(argv = ARGV) @argv = argv end def prepare! handle_version_request!(@argv.first) handle_invalid_command!(@argv.first, @argv) do handle_rails_rc!(@argv.drop(1)) end end (中略) private def handle_version_request!(argument) if ['--version', '-v'].include?(argument) require 'rails/version' puts "Rails #{Rails::VERSION::STRING}" exit(0) end end
prepare!
メソッド内で呼び出しているhandle_version_request!
でRailsのバージョンを表示していることがわかった。
これでRails::Root
以外の場所からrails -v
コマンドを打った時の流れがわかった。
カレントディレクトリがRails::Root
次にRails::Root
にいる場合を見ていきます。Rails::AppLoader.exec_app
を再び見ていくと、bin/rails
をreadしている。
#!/usr/bin/env ruby APP_PATH = File.expand_path('../../config/application', __FILE__) require_relative '../config/boot' require 'rails/commands'
APP_PATH
という文字列が含まれているので、Rails::AppLoader.exec_app
の
if contents =~ /(APP|ENGINE)_PATH/ exec RUBY, exe, *ARGV
により、bin/rails
はRubyのスクリプトとして実行される。bin/rails
は../config/boot
とrails/commands
を実行する。
# ../config/boot ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) require 'bundler/setup' # Set up gems listed in the Gemfile.
詳細までは追いませんが、gemを使用可能にできるようロードパスを更新してるみたい。
Bundler: The best way to manage a Ruby application's gems
# rails/commands ARGV << '--help' if ARGV.empty? aliases = { "g" => "generate", "d" => "destroy", "c" => "console", "s" => "server", "db" => "dbconsole", "r" => "runner", "t" => "test" } command = ARGV.shift command = aliases[command] || command require 'rails/commands/commands_tasks' Rails::CommandsTasks.new(ARGV).run_command!(command)
rails
コマンドに続けて入力した引数をここで変数として取り扱っている。rails s
といったエイリアスもここで定義されている。そして、rails/commands/commands_tasks
をrequireしている。rails/commands/commands_tasks
では、
require 'rails/commands/rake_proxy' module Rails # This is a class which takes in a rails command and initiates the appropriate # initiation sequence. # # Warning: This class mutates ARGV because some commands require manipulating # it before they are run. class CommandsTasks # :nodoc: include Rails::RakeProxy attr_reader :argv (中略) def initialize(argv) @argv = argv end def run_command!(command) command = parse_command(command) if COMMAND_WHITELIST.include?(command) send(command) else run_rake_task(command) end end (中略) def version argv.unshift '--version' require_command!("application") end (中略) private (中略) def require_command!(command) require "rails/commands/#{command}" end (中略) def parse_command(command) case command when '--version', '-v' 'version' when '--help', '-h' 'help' else command end end end end
となっており、Rails::CommandsTasks
を定義している。
Rails::CommandsTasks.new(ARGV).run_command!(command)
でRails::CommandsTasks
のインスタンスを作り、run_command!
メソッドを実行している。run_command!
では、まずparse_command
でエイリアスを取り扱い、version
コマンドとしてまとめて扱うようにしている。そして、version
メソッド→require_command!
メソッドの順に実行され、最終的にrails/commands/application
を実行する。
ここから先は、Rails::Root
以外から実行した場合と同じになる。