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
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
  1. 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:

    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 in bindir.

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

  2. 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:

    #!/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
    

  3. 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
    >>