The rails command and exec(2)

Published on December 20, 2011 by Jesse Storimer

TL;DR: The sole job of the rails command that ships with the rails gem is to exec ./script/rails in your Rails project. Combine that with Aaron's PSA and that can be considerable overhead. You should use ./script/rails instead of rails.


Ah, the rails command. Chances are that if you're writing Ruby code: you're using the rails command all the time.

The rails command is just a thin wrapper around exec(2). If you don't know what exec(2) is or why the rails command uses it then keep reading.

From the top

## File: rails/bin/rails
#!/usr/bin/env ruby

begin
  require "rails/cli"
rescue LoadError
  railties_path = File.expand_path('../../railties/lib', __FILE__)
  $:.unshift(railties_path)
  require "rails/cli"
end

This is the contents of bin/rails from the rails gem. This is the code that gets invoked when you use the rails binary.

You can see that all it really does is require rails/cli from railties. Let's look there next.

## File: railties/lib/rails/cli.rb
require 'rbconfig'
require 'rails/script_rails_loader'

# If we are inside a Rails application this method performs an exec and thus
# the rest of this script is not run.
Rails::ScriptRailsLoader.exec_script_rails!

railties_path = File.expand_path('../../lib', __FILE__)
$:.unshift(railties_path) if File.directory?(railties_path) && !$:.include?(railties_path)

require 'rails/ruby_version_check'
Signal.trap("INT") { puts; exit }

require 'rails/commands/application'

The first line of interest is the one after the requires. The comment mentions that this method may or may not call exec. If it does then the rest of the script won't run.

It's time to step back and look at what exec(2) does. First, exec(2) is a system call: a direct request to the kernel asking for some function that only the kernel can do.

exec(2) transforms the current process into another process.

puts 'Hi from Ruby'
exec 'ls'
puts 'Bye from Ruby'

If you execute that block of code you'll notice that the last line after the exec is never reached. The call to exec actually transformed the Ruby process into an ls process. You'll see the output from ls right after the output from the first line.

In this way exec(2) is a bit like jumping out of a plane. Your process fully commits to becoming another process and there's no way to go back and resume execution. The ls process has no idea that it used to be a Ruby process, and is not programmed to transform itself to another process.

This looks a bit like Kernel#system but works differently. If you shell out using something like system your Ruby process will resume execution after the shell command is finished.

puts 'Hi from Ruby'
system 'ls'
puts 'Bye from Ruby'

In this example the last line will be executed.

This is because Kernel#system uses a combination of fork(2) (create a new process) and exec(2) (transform that process) rather than transforming the current process.

The example above was a bit contrived. Keep reading to see how the rails command uses it.

Down another level.

## File: railties/lib/rails/script_rails_loader.rb
require 'pathname'

module Rails
  module ScriptRailsLoader
    RUBY = File.join(*RbConfig::CONFIG.values_at("bindir", "ruby_install_name")) + RbConfig::CONFIG["EXEEXT"]
    SCRIPT_RAILS = File.join('script', 'rails')

    def self.exec_script_rails!
      cwd = Dir.pwd
      return unless in_rails_application? || in_rails_application_subdirectory?
      exec RUBY, SCRIPT_RAILS, *ARGV if in_rails_application?
      Dir.chdir("..") do
        # Recurse in a chdir block: if the search fails we want to be sure
        # the application is generated in the original working directory.
        exec_script_rails! unless cwd == Dir.pwd
      end 
    rescue SystemCallError
      # could not chdir, no problem just return
    end 

    def self.in_rails_application?
      File.exists?(SCRIPT_RAILS)
    end 

    def self.in_rails_application_subdirectory?(path = Pathname.new(Dir.pwd))
      File.exists?(File.join(path, SCRIPT_RAILS)) || !path.root? && in_rails_application_subdirectory?(path.parent)    
    end
  end   
end

This class is a bit noisy. We'll take it one line at a time.

RUBY = File.join(*RbConfig::CONFIG.values_at("bindir", "ruby_install_name")) + RbConfig::CONFIG["EXEEXT"]
SCRIPT_RAILS = File.join('script', 'rails')

The class sets up some constants for itself. The first is notable as it gets the full path to the Ruby executable for this system. By running it through File.join and adding the extension suffix it is compatible with both Unix and Windows filesystems.

def self.exec_script_rails!
  cwd = Dir.pwd
  return unless in_rails_application? || in_rails_application_subdirectory?
  exec RUBY, SCRIPT_RAILS, *ARGV if in_rails_application?
  Dir.chdir("..") do
    # Recurse in a chdir block: if the search fails we want to be sure
    # the application is generated in the original working directory.
    exec_script_rails! unless cwd == Dir.pwd
  end   
rescue SystemCallError
  # could not chdir, no problem just returnend
end
  
def self.in_rails_application?
  File.exists?(SCRIPT_RAILS)
end 

def self.in_rails_application_subdirectory?(path = Pathname.new(Dir.pwd))
  File.exists?(File.join(path, SCRIPT_RAILS)) || !path.root? && in_rails_application_subdirectory?(path.parent)    
end

The main method here is exec_script_rails!. Notice that it returns unless already in a Rails app directory or subdirectory. In that case exec won't be called and the command will take a different path.

The methods in_rails_application_subdirectory? and in_rails_application? recursively search the filesystem looking for script/rails.

Assuming that we're somewhere in a Rails app directory the following block of code is executed.

  exec RUBY, SCRIPT_RAILS, *ARGV if in_rails_application?
  Dir.chdir("..") do
    # Recurse in a chdir block: if the search fails we want to be sure
    # the application is generated in the original working directory.
    exec_script_rails! unless cwd == Dir.pwd
  end
rescue SystemCallError
  # could not chdir, no problem just return

When already in the root of a Rails app then the exec is done immediately, passing any of the arguments that were passed in on the command line. Remember from our talk about exec that if this happens then none of the rest of the code afterwards will be executed.

The other code path covers the case where the rails command is being used from a Rails app subdirectory. It continues to Dir.chdir to the parent directory until it gets to the root of the Rails app, at which point it does the same call to exec.

The comment about chdir refers to the fact that even though the pwd is being changed with Dir.chdir it will be reset to the original working directory when the block returns.

So...

So, besides being used à la rails new to bootstrap a new app, the whole point of the rails command is just to find ./script/rails and 'become' it. You can do the job of the rails command yourself by just using ./script/rails directly.

rails console
# same as
./script/rails console

Using ./script/rails directly avoids only a little bit of code, but it may provide more of a boost than you think given Aaron Patterson's recent PSA regarding rubygems.

When you use the rails command you pay the penalty of loading every gemspec on your system. Even worse is using bundle exec rails. Since both bundle and rails are rubygems binstubs you pay that penalty twice!

By using ./script/rails you can avoid paying that penalty and execute just a little bit less code from Rails startup. And, as we all know: The best code is no code at all.

Understanding how your tools work is critical to improving your workflow, as well as properly debugging your production apps, find out what you need to know about Unix programming in Working With Unix Processes.


Like what you read?

Join 2,000+ Ruby programmers improving their skills with exclusive content about digging deeper with sockets, processes, threads, and more - delivered to your inbox weekly.

I'll never send spam and you can unsubscribe any time.


comments powered by Disqus