'rails -v'コマンドは何をしているのか

rails -v'コマンドは何をやっているのか

rails -vコマンドがどのような仕組みになっているか気になったので調べてみる。RailsのバージョンはRails 5.0.1

which railsコマンドで実行ファイルの場所を取得する。

$ which rails
~/.rbenv/shims/rails

rbenvを使用している場合には上記が実行ファイルとなるが、上記のファイルはただのラッパーのようである。ここでは詳細は追わないが、以下のリンクが後で理解するときに参考になりそう。

memo.sugyan.com

実際の実行ファイルを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/railsRubyスクリプトとして実行される。bin/rails../config/bootrails/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以外から実行した場合と同じになる。