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:
#!/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"In gems/railties-4.X.X/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::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
endNext, we turn our focus temporarily to your Rails application. In bin/rails, two files are required:
#!/usr/bin/env ruby
### The below part will be present if you use spring
# begin
# load File.expand_path("../spring", __FILE__)
# rescue LoadError
# end
APP_PATH = File.expand_path('../../config/application', __FILE__)
require_relative '../config/boot'
require 'rails/commands'../config/boot (in your app directory) determines the location of the Gemfile and allows Bundler to configure the load path for your Gemfile’s dependencies.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
require 'bundler/setup' # Set up gems listed in the Gemfile.rails/commands parses options passed in as command line arguments, including alias mapping (c for console, g for generate, etc.)
ARGV << '--help' if ARGV.empty?
aliases = {
"g" => "generate",
"d" => "destroy",
"c" => "console",
"s" => "server",
"db" => "dbconsole",
"r" => "runner"
}
command = ARGV.shift
command = aliases[command] || command
require 'rails/commands/commands_tasks'
Rails::CommandsTasks.new(ARGV).run_command!(command)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:
$ ls
application.rb console.rb destroy.rb plugin.rb server.rb
commands_tasks.rb dbconsole.rb generate.rb runner.rb
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):
def console
require_command!("console")
options = Rails::Console.parse_arguments(argv)
# RAILS_ENV needs to be set before config/application is required
ENV['RAILS_ENV'] = options[:environment] if options[:environment]
# shift ARGV so IRB doesn't freak
shift_argv!
require_application_and_environment!
Rails::Console.start(Rails.application, options)
end
# some ways down
private
def require_command!(command)
require "rails/commands/#{command}"
end
def require_application_and_environment!
require APP_PATH
Rails.application.require_environment!
endIn rails/commands/console.rb, you can see the start class method instantiating itself and calling the new instance’s start instance method:
class << self # old idiom for defining class methods, equivalent to def self.start
def start(*args)
new(*args).start
endAs it is instantiated, @app is set as your Rails application, and @console is set to app.config.console if present, or defaults to IRB:
def initialize(app, options={})
@app = app
@options = options
app.sandbox = sandbox?
app.load_console
@console = app.config.console || IRB
endLet’s see if the above code actually works by setting your application config to use Pry as the console instead:
# don't forget to add gem 'pry' to your Gemfile and bundle
# in coolrailsapp/config/application.rb
module CoolRailsApp
class Application < Rails::Application
...
config.console = Pry
end
end$ rails c
Loading development environment (Rails 4.2.3)
[1] pry(main)>
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
endFinally, console.start boots the console3.
Next, we’ll look at the code path taken by rails server.
#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 inrailties.gemspecgives us the answer:s.bindir = 'exe' s.executables = ['rails']What does the above mean? RubyGem’s documentation:
↩EXECUTABLES
Executables included in the gem.
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 inbindir. ^ Take a look inside theexedirectory - 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 actualruby_executable_hooksbinary looks like:↩#!/usr/bin/env ruby title = "ruby #{ARGV*" "}" $0 = ARGV.shift Process.setproctitle(title) if Process.methods.include?(:setproctitle) require 'rubygems' begin require 'executable-hooks/hooks' Gem::ExecutableHooks.run($0) rescue LoadError warn "unable to load executable-hooks/hooks" if ENV.key?('ExecutableHooks_DEBUG') end eval File.read($0), binding, $0 -
You can test this out for yourself with just 3 lines of code. Create a file with the following:
require 'irb' x = IRB x.startRun it and see what happens:
↩$ ruby ~/test.rb >>