Rails Boot Sequence (Part 1)

Today, we investigate Rails’ boot sequence by observing what happens when we run rails console. Part 2 will look at rails server. Github links to relevant files are provided as necessary.

Our journey begins inside the rails binary1, which is executed by ruby_executable_hooks2:

#!/usr/bin/env ruby_executable_hooks
# 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"
if ARGV.first
str = ARGV.first
str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding
if str =~ /\A_(.*)_\z/ and Gem::Version.correct?($1) then
version = $1
ARGV.shift
end
end
gem 'railties', version
load Gem.bin_path('railties', 'rails', version)

It calls load Gem.bin_path('railties', 'rails', version), which corresponds to gems/railties-4.X.X/bin/rails.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::AppRailsLoader.exec_app
...

exec_app is in charge of executing the bin/rails inside your Rails application. It will look for it recursively, meaning that you can call rails anywhere in your application directory. In fact, rails server or rails console is equivalent to calling ruby bin/rails server or ruby bin/rails console See the abridged contents of rails/app_loader.rb below:

module Rails
module AppLoader # :nodoc:
extend self
RUBY = Gem.ruby
EXECUTABLES = ['bin/rails', 'script/rails']
def exec_app
original_cwd = Dir.pwd
loop do
(code to check for the executable and execute it if found)
# 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
end
end

Next, we turn our focus temporarily to your Rails application. In bin/rails, two files are required:

rails/commands/commands_tasks.rb is in charge of throwing errors in the case of invalid commands, or delegating valid commands to the respective methods, themselves split into files in the rails/commands directory:

For example, if rails console is run, the console method in commands_tasks.rb requires console.rb and runs the start class method from the Rails::Console class, passing it your application as the first argument (command_tasks.rb is made known of your application by requiring APP_PATH, which you’ve kindly provided previously in bin/rails):

Great success! Now let’s look at the actual start instance method, whose code is relatively self-explanatory:

def start
if RUBY_VERSION < '2.0.0'
require_debugger if debugger?
end
set_environment! if environment?
if sandbox?
puts "Loading #{Rails.env} environment in sandbox (Rails #{Rails.version})"
puts "Any modifications you make will be rolled back on exit"
else
puts "Loading #{Rails.env} environment (Rails #{Rails.version})"
end
if defined?(console::ExtendCommandBundle)
console::ExtendCommandBundle.send :include, Rails::ConsoleMethods
end
console.start
end

Footnotes

As indicated in the comments, this file is auto-generated by RubyGems. How does it know to load Rails, as in the last line (load Gem.bin_path('railties', 'rails', version))? Taking a look in railties.gemspec gives us the answer:

For example, the rake gem has rake as an executable. You don’t specify the full path (as in bin/rake); all application-style files are expected to be found in bindir.

Take a look inside the exe directory - its contents will be very familiar soon :) ↩

The binary is defined by the sha-bang to be executed by ruby_executable hooks, which is a thin wrapper that allows RubyGems to run initialization hooks (Gem::ExecutableHooks.run($0)) before Ruby runs the actual code (eval File.read($0), binding, $0). This is what the actual ruby_executable_hooks binary looks like: