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_hooks
2:
#!/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
end
Next, 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!
end
In 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
end
As 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
end
Let’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
end
Finally, 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.gemspec
gives 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 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 actualruby_executable_hooks
binary 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.start
Run it and see what happens:
$ ruby ~/test.rb >>