Railsコードリーディング
対象: Ruby 2.6.3 x Rails 5.2
目的: Railsの内部アーキテクチャを理解する
読み方:適当なRailsアプリを作って起動シーケンスを見つつ、必要なファイルを勘で読んでいく
code:shell
$ rbenv local 2.6.3
$ gem install rails -v 5.2
$ rails new codereadingrails --skip-bundle
$ cd codereadingrails
$ bundle
code:shell
$ cat $(bundle exec which rails)
code:ruby
#
# This file was generated by RubyGems.
#
# The application 'railties' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require 'rubygems'
version = ">= 0.a"
str = ARGV.first
if str
if str and Gem::Version.correct?(str)
version = str
ARGV.shift
end
end
if Gem.respond_to?(:activate_bin_path)
load Gem.activate_bin_path('railties', 'rails', version)
else
gem "railties", version
load Gem.bin_path("railties", "rails", version)
end
require 'rubygems'
rubygemsで入れたgemにアクセスするときにかつて必要だったが、今は不要ということらしい
たしかにコメントアウトしても bundle exec rails server は動いた
str.b[/\A_(.*)_\z/, 1]
self の文字エンコーディングを ASCII-8BIT にした文字列の複製を返します。
/\A_(.*)_\z/
\Aは文字列の先頭
\zは文字列の末尾
起動するrailsのバージョンを指定して起動する的な?
load Gem.activate_bin_path('railties', 'rails', version)
loadはrequireみたいなやつなので、activate_bin_pathはファイルパスなのだろうということはわかる
Find the full path to the executable for gem name.
この場合activate_bin_pathは、railties というgemの rails というファイルへのファイルパスを返すようだ
そもそもこのbin/rails自体が binstub というものっぽく、おそらくここまでがテンプレっぽい
binstub
よくわかんないけど、環境を整えてから実行可能ファイルを実行するためのファイルということらしい
load p(Gem.activate_bin_path('railties', 'rails', version)) に修正して実行してみると、以下のように出力された
/path/to/dir/codereadingrails/vendor/bundle/ruby/2.6.0/gems/railties-5.2.4.4/exe/rails
結局の所このファイルは、 railties gem に含まれる exe/rails.rb を起動している。
code:shell
$ cat vendor/bundle/ruby/2.6.0/gems/railties-5.2.4.4/exe/rails
code:ruby
# frozen_string_literal: true
git_path = File.expand_path("../../.git", __dir__)
if File.exist?(git_path)
railties_path = File.expand_path("../lib", __dir__)
$:.unshift(railties_path)
end
require "rails/cli"
__dir__
現在のソースファイル(__FILE__)のあるディレクトリ名を正規化された絶対パスで返します。
File.exist?(git_path)
p してみると git_path は codereadingrails/vendor/bundle/ruby/2.6.0/gems/.git になっていた。
require "rails/cli"
次に見る
code:shell
$ cat vendor/bundle/ruby/2.6.0/gems/railties-5.2.4.4/lib/rails/cli.rb
code:ruby
# frozen_string_literal: true
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) }
require "rails/command"
if ARGV.first == "plugin"
ARGV.shift
Rails::Command.invoke :plugin, ARGV
else
Rails::Command.invoke :application, ARGV
end
require 'rails/app_loeader'
railtieのrails/app_loaderのことと思われる↓
code:ruby
# frozen_string_literal: true
require "pathname"
require "rails/version"
module Rails
module AppLoader # :nodoc:
extend self
RUBY = Gem.ruby
BUNDLER_WARNING = <<EOS
(略)
EOS
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) && 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
end
end
extend self
この文脈においてselfは Rails::AppLoader である
つまり、モジュール内で定義するメソッドを、自身の特異メソッドとして追加するという意味っぽい
実際 Rails::AppLoader.exec_app をRails consoleから実行したところ、No Method Errorとはならなかった
exec_app
bin/rails か script/rails があればそれを「実行」し、なければ何もしないという処理を実現する
今回initializeしたアプリケーションだと、 bin/rails が存在して、その中に APP_PATHというパターンが出てくるので、execでrubyのプロセスを立ち上げて、bin/railsが実行される
code:shell
$ cat bin/rails
code:ruby
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'
話を rails/cli.rb に戻すため、コードを再掲
code:shell
$ cat vendor/bundle/ruby/2.6.0/gems/railties-5.2.4.4/lib/rails/cli.rb
code:ruby
# frozen_string_literal: true
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) }
require "rails/command"
if ARGV.first == "plugin"
ARGV.shift
Rails::Command.invoke :plugin, ARGV
else
Rails::Command.invoke :application, ARGV
end
Rails::AppLoader.exec_app
上で読んだexec_appが実行される
If we are inside a Rails application this method performs an exec and thus the rest of this script is not run. とある。おそらくこれ以降のスクリプトは、rails newするときとかに実行されるんやろなという気がする
余談だが、bundle exec rails s するよりも bin/rails s したほうが正しそうな雰囲気がする。直接起動できるわけだし、bundle exec rails s だと思わぬ挙動をすることがありそう
今回は、Railsの起動シーケンスを追いたいので、このファイルはここで読むのをやめて bin/rails を見に行く
code:shell
$ cat bin/rails
code:ruby
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'
APP_PATH = File.expand_path('../config/application', __dir__)
config/applicationを指す(それはそう)
vendor以下をgrepしてみたところ、 require APP_PATHしてるところがいくつかあったので、そのために定義してるようだ
require_relative '../config/boot'
code:shell
$ cat config/boot
code:ruby
require 'bundler/setup' # Set up gems listed in the Gemfile.
require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
たぶん後続の require 'bundler/setup' が使うGemfileを明示的に指定するために使ってるんじゃないかな。知らんけど。
require 'bundler/setup'
ざっくり言うと、bundle exec的なことをやってくれるっぽい。つまり、bundle execをつけなくても、require 'bundler/setup'すれば、Gemfileに書いたgemがrequireできるようになる
code:shell
$ mkdir hoge
$ cd hoge
$ bundle init
$ vim Gemfile # gem 'sinatra' を追加
$ bundle exec irb # bundle installしてないのでコケる
Could not find gem 'sinatra' in any of the gem sources listed in your Gemfile.
Run bundle install to install missing gems.
$ irb # bundle execをつけなければ起動できる
irb(main):001:0> original_load_path = $:.dup
(略)
irb(main):002:0> $:.size
=> 10
irb(main):003:0> require 'sinatra' # 失敗する
(略)
LoadError (cannot load such file -- sinatra)
irb(main):004:0> require 'bundler/setup' # これによってbundlerが管理するgemがload_pathに追加され、requireできるようになる
=> true
irb(main):005:0> $:.size
=> 16
irb(main):006:0> $: - original_load_path # bundlerとsinatra関連のgemがload_pathに追加されてることがわかる
irb(main):007:0> require 'sinatra' # sinatraがrequireできる
=> true
require 'bootsnap/setup'
rubyの何かをキャッシュして起動を高速化してくれるやつ
余談ですが、定期的に消すなどしないと無限にキャッシュが溜まって死にます
bin/rails に戻るため、再掲
code:shell
$ cat bin/rails
code:ruby
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'
require 'rails/commands'
railties/rails/commands.rb のことっぽい
code:shell
$ cat vendor/bundle/ruby/2.6.0/gems/railties-5.2.4.4/lib/rails/commands.rb
code:ruby
# frozen_string_literal: true
require "rails/command"
aliases = {
"g" => "generate",
"d" => "destroy",
"c" => "console",
"s" => "server",
"db" => "dbconsole",
"r" => "runner",
"t" => "test"
}
command = ARGV.shift
command = aliasescommand || command Rails::Command.invoke command, ARGV
Rails::Command.invoke command, ARGV が本質で、それまでのコードはrequireしたりコマンドのaliasを処理したりしてるだけ
require 'rails/command'
code:shell
$ cat vendor/bundle/ruby/2.6.0/gems/railties-5.2.4.4/lib/rails/command.rb
code:ruby
# frozen_string_literal: true
require "active_support"
require "active_support/dependencies/autoload"
require "active_support/core_ext/enumerable"
require "active_support/core_ext/object/blank"
require "active_support/core_ext/hash/transform_values"
require "thor"
module Rails
module Command
extend ActiveSupport::Autoload
autoload :Behavior
autoload :Base
include Behavior
HELP_MAPPINGS = %w(-h -? --help)
class << self
def hidden_commands # :nodoc:
@hidden_commands ||= []
end
def environment # :nodoc:
end
# Receives a namespace, arguments and the behavior to invoke the command.
def invoke(full_namespace, args = [], **config)
namespace = full_namespace = full_namespace.to_s
if char = namespace =~ /:(\w+)$/
command_name, namespace = $1, namespace.slice(0, char)
else
command_name = namespace
end
command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name)
command_name, namespace = "version", "version" if %w( -v --version ).include?(command_name)
command = find_by_namespace(namespace, command_name)
command.perform(command_name, args, config)
else
find_by_namespace("rake").perform(full_namespace, args, config)
end
end
# Rails finds namespaces similar to Thor, it only adds one rule:
#
# Command names must end with "_command.rb". This is required because Rails
# looks in load paths and loads the command just before it's going to be used.
#
# find_by_namespace :webrat, :rails, :integration
#
# Will search for the following commands:
#
# "rails:webrat", "webrat:integration", "webrat"
#
# Notice that "rails:commands:webrat" could be loaded as well, what
# Rails looks for is the first and last parts of the namespace.
def find_by_namespace(namespace, command_name = nil) # :nodoc:
lookups << "#{namespace}:#{command_name}" if command_name
lookups.concat lookups.map { |lookup| "rails:#{lookup}" }
lookup(lookups)
namespaces = subclasses.index_by(&:namespace)
end
# Returns the root of the Rails engine or app running the command.
def root
if defined?(ENGINE_ROOT)
Pathname.new(ENGINE_ROOT)
elsif defined?(APP_PATH)
Pathname.new(File.expand_path("../..", APP_PATH))
end
end
def print_commands # :nodoc:
sorted_groups.each { |b, n| print_list(b, n) }
end
def sorted_groups # :nodoc:
lookup!
groups = (subclasses - hidden_commands).group_by { |c| c.namespace.split(":").first }
groups.transform_values! { |commands| commands.flat_map(&:printing_commands).sort }
rails = groups.delete("rails")
"rails", rails + groups.sort.to_a
end
private
def command_type # :doc:
@command_type ||= "command"
end
def lookup_paths # :doc:
@lookup_paths ||= %w( rails/commands commands )
end
def file_lookup_paths # :doc:
end
end
end
end
require "active_support"
active_support内の各モジュールをautoloadに登録してるっぽい
require "active_support/dependencies/autoload"
code:shell
$ cat vendor/bundle/ruby/2.6.0/gems/activesupport-5.2.4.4/lib/active_support/dependencies/autoload.rb
code:ruby
# frozen_string_literal: true
require "active_support/inflector/methods"
module ActiveSupport
# Autoload and eager load conveniences for your library.
#
# This module allows you to define autoloads based on
# Rails conventions (i.e. no need to define the path
# it is automatically guessed based on the filename)
# and also define a set of constants that needs to be
# eager loaded:
#
# module MyLib
# extend ActiveSupport::Autoload
#
# autoload :Model
#
# eager_autoload do
# autoload :Cache
# end
# end
#
# Then your library can be eager loaded by simply calling:
#
# MyLib.eager_load!
module Autoload
def self.extended(base) # :nodoc:
base.class_eval do
@_autoloads = {}
@_under_path = nil
@_at_path = nil
@_eager_autoload = false
end
end
def autoload(const_name, path = @_at_path)
unless path
path = Inflector.underscore(full)
end
if @_eager_autoload
end
super const_name, path
end
def autoload_under(path)
@_under_path, old_path = path, @_under_path
yield
ensure
@_under_path = old_path
end
def autoload_at(path)
@_at_path, old_path = path, @_at_path
yield
ensure
@_at_path = old_path
end
def eager_autoload
old_eager, @_eager_autoload = @_eager_autoload, true
yield
ensure
@_eager_autoload = old_eager
end
def eager_load!
@_autoloads.each_value { |file| require file }
end
def autoloads
@_autoloads
end
end
end
いろいろ調べた感じだと、autoload自体はRuby言語自体にある機能で、その定数が参照されるまでrequireを遅延する
ただし、ActiveSupport::Autoloadは、requireするpathをRailsのconventionにしたがって自動的に決定してくれる
Ruby組み込みのautoloadは明示的にpathを指定する必要がある
あと、eager_load!はたぶんRubyには存在しない
他の require は、Rubyの標準クラスの拡張を取得してる模様
core_ext以下のactive_supportは、active_support.rbではautoloadされてないみたい
確かに、既存のクラスを拡張しているわけなので、autoloadできない感じはする
再掲
code:shell
$ cat vendor/bundle/ruby/2.6.0/gems/railties-5.2.4.4/lib/rails/command.rb
code:ruby
# frozen_string_literal: true
require "active_support"
require "active_support/dependencies/autoload"
require "active_support/core_ext/enumerable"
require "active_support/core_ext/object/blank"
require "active_support/core_ext/hash/transform_values"
require "thor"
module Rails
module Command
extend ActiveSupport::Autoload
autoload :Behavior
autoload :Base
include Behavior
HELP_MAPPINGS = %w(-h -? --help)
class << self
def hidden_commands # :nodoc:
@hidden_commands ||= []
end
def environment # :nodoc:
end
# Receives a namespace, arguments and the behavior to invoke the command.
def invoke(full_namespace, args = [], **config)
namespace = full_namespace = full_namespace.to_s
if char = namespace =~ /:(\w+)$/
command_name, namespace = $1, namespace.slice(0, char)
else
command_name = namespace
end
command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name)
command_name, namespace = "version", "version" if %w( -v --version ).include?(command_name)
command = find_by_namespace(namespace, command_name)
command.perform(command_name, args, config)
else
find_by_namespace("rake").perform(full_namespace, args, config)
end
end
# Rails finds namespaces similar to Thor, it only adds one rule:
#
# Command names must end with "_command.rb". This is required because Rails
# looks in load paths and loads the command just before it's going to be used.
#
# find_by_namespace :webrat, :rails, :integration
#
# Will search for the following commands:
#
# "rails:webrat", "webrat:integration", "webrat"
#
# Notice that "rails:commands:webrat" could be loaded as well, what
# Rails looks for is the first and last parts of the namespace.
def find_by_namespace(namespace, command_name = nil) # :nodoc:
lookups << "#{namespace}:#{command_name}" if command_name
lookups.concat lookups.map { |lookup| "rails:#{lookup}" }
lookup(lookups)
namespaces = subclasses.index_by(&:namespace)
end
# Returns the root of the Rails engine or app running the command.
def root
if defined?(ENGINE_ROOT)
Pathname.new(ENGINE_ROOT)
elsif defined?(APP_PATH)
Pathname.new(File.expand_path("../..", APP_PATH))
end
end
def print_commands # :nodoc:
sorted_groups.each { |b, n| print_list(b, n) }
end
def sorted_groups # :nodoc:
lookup!
groups = (subclasses - hidden_commands).group_by { |c| c.namespace.split(":").first }
groups.transform_values! { |commands| commands.flat_map(&:printing_commands).sort }
rails = groups.delete("rails")
"rails", rails + groups.sort.to_a
end
private
def command_type # :doc:
@command_type ||= "command"
end
def lookup_paths # :doc:
@lookup_paths ||= %w( rails/commands commands )
end
def file_lookup_paths # :doc:
end
end
end
end
再掲:Rails::Command.invoke command, ARGVが何をしているのかを調べるのが目的
まずはクラスの「地の文」を見ていく
code:ruby
autoload :Behavior
autoload :Base
include Behavior
Behaviorモジュールの中身を見ていくわよ
code:shell
$ cat vendor/bundle/ruby/2.6.0/gems/railties-5.2.4.4/lib/rails/command/behavior.rb
code:ruby
# frozen_string_literal: true
require "active_support"
module Rails
module Command
extend ActiveSupport::Concern
class_methods do
# Remove the color from output.
def no_color!
Thor::Base.shell = Thor::Shell::Basic
end
# Track all command subclasses.
def subclasses
@subclasses ||= []
end
private
# This code is based directly on the Text gem implementation.
# Copyright (c) 2006-2013 Paul Battley, Michael Neumann, Tim Fletcher.
#
# Returns a value representing the "cost" of transforming str1 into str2.
def levenshtein_distance(str1, str2) # :doc:
s = str1
t = str2
n = s.length
m = t.length
return m if (0 == n)
return n if (0 == m)
d = (0..m).to_a
x = nil
# avoid duplicating an enumerable object in the loop
str2_codepoint_enumerable = str2.each_codepoint
str1.each_codepoint.with_index do |char1, i|
e = i + 1
str2_codepoint_enumerable.with_index do |char2, j|
cost = (char1 == char2) ? 0 : 1
x = [
e + 1, # deletion
].min
e = x
end
end
x
end
# Prints a list of generators.
def print_list(base, namespaces)
return if namespaces.empty?
puts "#{base.camelize}:"
namespaces.each do |namespace|
end
puts
end
# Receives namespaces in an array and tries to find matching generators
# in the load path.
def lookup(namespaces)
paths = namespaces_to_paths(namespaces)
paths.each do |raw_path|
lookup_paths.each do |base|
path = "#{base}/#{raw_path}_#{command_type}"
begin
require path
return
rescue LoadError => e
raise unless e.message =~ /#{Regexp.escape(path)}$/
rescue Exception => e
end
end
end
end
# This will try to load any command in the load path to show in help.
def lookup!
$LOAD_PATH.each do |base|
begin
path = path.sub("#{base}/", "")
require path
rescue Exception
# No problem
end
end
end
end
# Convert namespaces to paths by replacing ":" for "/" and adding
# an extra lookup. For example, "rails:model" should be searched
# in both: "rails/model/model_generator" and "rails/model_generator".
def namespaces_to_paths(namespaces)
paths = []
namespaces.each do |namespace|
pieces = namespace.split(":")
paths << pieces.dup.push(pieces.last).join("/")
paths << pieces.join("/")
end
paths.uniq!
paths
end
end
end
end
end
まず extend ActiveSupport::Concern を見る必要があるな
code:shell
$ cat vendor/bundle/ruby/2.6.0/gems/activesupport-5.2.4.4/lib/active_support/concern.rb
code:ruby
# frozen_string_literal: true
module ActiveSupport
# A typical module looks like this:
#
# module M
# def self.included(base)
# base.extend ClassMethods
# base.class_eval do
# scope :disabled, -> { where(disabled: true) }
# end
# end
#
# module ClassMethods
# ...
# end
# end
#
# By using <tt>ActiveSupport::Concern</tt> the above module could instead be
# written as:
#
# require 'active_support/concern'
#
# module M
# extend ActiveSupport::Concern
#
# included do
# scope :disabled, -> { where(disabled: true) }
# end
#
# class_methods do
# ...
# end
# end
#
# Moreover, it gracefully handles module dependencies. Given a +Foo+ module
# and a +Bar+ module which depends on the former, we would typically write the
# following:
#
# module Foo
# def self.included(base)
# base.class_eval do
# def self.method_injected_by_foo
# ...
# end
# end
# end
# end
#
# module Bar
# def self.included(base)
# base.method_injected_by_foo
# end
# end
#
# class Host
# include Foo # We need to include this dependency for Bar
# include Bar # Bar is the module that Host really needs
# end
#
# But why should +Host+ care about +Bar+'s dependencies, namely +Foo+? We
# could try to hide these from +Host+ directly including +Foo+ in +Bar+:
#
# module Bar
# include Foo
# def self.included(base)
# base.method_injected_by_foo
# end
# end
#
# class Host
# include Bar
# end
#
# Unfortunately this won't work, since when +Foo+ is included, its <tt>base</tt>
# is the +Bar+ module, not the +Host+ class. With <tt>ActiveSupport::Concern</tt>,
# module dependencies are properly resolved:
#
# require 'active_support/concern'
#
# module Foo
# extend ActiveSupport::Concern
# included do
# def self.method_injected_by_foo
# ...
# end
# end
# end
#
# module Bar
# extend ActiveSupport::Concern
# include Foo
#
# included do
# self.method_injected_by_foo
# end
# end
#
# class Host
# include Bar # It works, now Bar takes care of its dependencies
# end
module Concern
class MultipleIncludedBlocks < StandardError #:nodoc: def initialize
super "Cannot define multiple 'included' blocks for a Concern"
end
end
base.instance_variable_set(:@_dependencies, [])
end
def append_features(base)
if base.instance_variable_defined?(:@_dependencies)
base.instance_variable_get(:@_dependencies) << self
false
else
return false if base < self
@_dependencies.each { |dep| base.include(dep) }
super
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
end
end
def included(base = nil, &block)
if base.nil?
if instance_variable_defined?(:@_included_block)
if @_included_block.source_location != block.source_location
raise MultipleIncludedBlocks
end
else
@_included_block = block
end
else
super
end
end
def class_methods(&class_methods_module_definition)
mod = const_defined?(:ClassMethods, false) ?
const_get(:ClassMethods) :
const_set(:ClassMethods, Module.new)
mod.module_eval(&class_methods_module_definition)
end
end
end
append_features
つまり、class AAA; include M; endと書いたときに何が起きるかというと、以下が実行される
code:ruby
M.append_features(AAA)
M.included(AAA)
ActiveSupport::Concernはappend_featuresをoverrideしている
全体の流れ
HostがBarに依存し、BarがFooに依存しているケース
code:ruby
module Foo
extend ActiveSupport::Concern
included do
def self.method_injected_by_foo
...
end
end
end
module Bar
extend ActiveSupport::Concern
include Foo
included do
self.method_injected_by_foo
end
end
class Host
include Bar # It works, now Bar takes care of its dependencies
end
このとき、以下のように動く
extend ActiveSupport::ConcernによってFooに@_dependenciesが定義される(空配列)
included do ~~~ endによってFooに@_included_block が定義される(includedに指定されたブロック)
extend ActiveSupport::ConcernによってBarに@_dependenciesが定義される(空配列)
include Fooによって、Foo.append_featuresが起動し、Barの@_dependenciesにFooが追加される
included do ~~~ endによってBarに@_included_blockが定義される
include Barによって、Bar.append_featuresが起動し、
HostにFooがincludeされ、Foo.append_featuresが起動し、base.class_eval(&@_included_block) が実行され、HostにHost.method_injected_by_fooが定義される
base.class_eval(&@_included_block)が実行され、Host.method_injected_by_fooが実行される
つまり、includeされたときに実行される append_features をoverrideして、依存するモジュールがあればそれをincludeし、クラスメソッドが定義されてればそれを定義するし、includedが定義されてればそれを実行する、という処理をする。そのために、@_dependenciesとか@_included_blockとかClassMethodとかが保持されている
もっと雑に言うと(コメントにもあるが)、class_methodとかincludedでクラスメソッドとかインスタンスメソッドを定義できるし、モジュールがモジュールに依存してても、includeしたときに全部引っ張ってこれますということらしい
いつまで経ってもRailsの起動にたどり着けないので、飛ばし気味に行きます
code:ruby
# railties/lib/rails/command.rb から抜粋
module Rails
module Command
class << self
# Receives a namespace, arguments and the behavior to invoke the command.
def invoke(full_namespace, args = [], **config)
namespace = full_namespace = full_namespace.to_s
if char = namespace =~ /:(\w+)$/
command_name, namespace = $1, namespace.slice(0, char)
else
command_name = namespace
end
command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name)
command_name, namespace = "version", "version" if %w( -v --version ).include?(command_name)
command = find_by_namespace(namespace, command_name)
command.perform(command_name, args, config)
else
find_by_namespace("rake").perform(full_namespace, args, config)
end
end
end
end
end
if char = namespace =~ /:(\w+)$/ ~~~ else ~~~ end は、 db:resetみたいな引数を command_name (reset)とnamespace(db)に分離する
serverの場合は、command_name = 'server' となる
find_by_namespace でコマンドオブジェクトを取得して、command.performで実行する
include Behavior が生やす lookupというメソッドに依存しているが、これは実質的にはrequireをしているだけです
紆余曲折の末 require 'rails/command/server_command' が実行される
これによって、Rails::Command.subclassesにRails::Command::ServerCommandが追加されます
bundle exec rails server が実行された時、rails/command/server_command.rbにあるCommand::ServerCommand.performが実行されます
command.perform(command_name, args, config)
command_name == 'server'
args == ARGV であり、['server']になってると思う
configは空ハッシュ
Command::ServerCommand.performはCommand::Base.performを指します
code:ruby
def perform(command, args, config) # :nodoc:
if Rails::Command::HELP_MAPPINGS.include?(args.first)
command, args = "help", []
end
dispatch(command, args.dup, nil, config)
end
このdispatchはThorのメソッドであり、Thorの挙動をRails::Command::Baseが書き換えていることからいろいろなことが起こったりするのだが、最終的に以下のメソッドが実行される
code:shell
$ cat vendor/bundle/ruby/2.6.0/gems/railties-5.2.4.4/lib/rails/commands/server/server_command.rb
code:ruby
module Rails
module Commands
class ServerCommand
(略)
def perform
set_application_directory!
prepare_restart
Rails::Server.new(server_options).tap do |server|
# Require application after server sets environment to propagate
# the --environment option.
require APP_PATH
Dir.chdir(Rails.application.root)
server.start
end
end
end
end
end
この Rails::Server は以下で定義される
code:shell
$ cat vendor/bundle/ruby/2.6.0/gems/railties-5.2.4.4/lib/rails/commands/server/server_command.rb # 上記と同じファイル
code:ruby
module Rails
class Server < ::Rack::Server
class Options
def parse!(args)
Rails::Command::ServerCommand.new([], args).server_options
end
end
def initialize(options = nil)
@default_options = options || {}
super(@default_options)
set_environment
end
def app
@app ||= begin
app = super
if app.is_a?(Class)
ActiveSupport::Deprecation.warn(<<-MSG.squish)
Using Rails::Application subclass to start the server is deprecated and will be removed in Rails 6.0.
Please change run #{app} to run Rails.application in config.ru.
MSG
end
app.respond_to?(:to_app) ? app.to_app : app
end
end
def opt_parser
Options.new
end
def set_environment
end
def start
print_boot_information
trap(:INT) { exit }
create_tmp_directories
setup_dev_caching
super
ensure
# The '-h' option calls exit before @options is set.
# If we call 'options' with it unset, we get double help banners.
puts "Exiting" unless @options && options:daemonize end
def middleware
Hash.new([])
end
def default_options
super.merge(@default_options)
end
private
def setup_dev_caching
Rails::DevCaching.enable_by_argument(options:caching) end
end
def print_boot_information
puts "=> Run rails server -h for more startup options"
end
def create_tmp_directories
%w(cache pids sockets).each do |dir_to_make|
FileUtils.mkdir_p(File.join(Rails.root, "tmp", dir_to_make))
end
end
def log_to_stdout
wrapped_app # touch the app so the logger is set up
console = ActiveSupport::Logger.new(STDOUT)
console.formatter = Rails.logger.formatter
console.level = Rails.logger.level
unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDOUT)
Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
end
end
def restart_command
end
def use_puma?
server.to_s == "Rack::Handler::Puma"
end
end
end
よく考えたら、 bundle exec rails serverではなくbundle exec puma から起動シーケンスを追うべきだった気がしてきた。config.ruがあるので...