How ruby gem executable works?

Have you ever thought why ‘rake’ is available in the shell or windows cmd, after "gem install rake"? What if you want write your who gem that could let you say hello to he world from console? Let’s better ask rubygems itself 🙂

But, before we dive into the code, there is a little warm up. The gem specification, which contains the meta date of the gem, have some entries related the executables:

Executables   A list of files in the package that are applications.

Bindir   The directory containing the application files, if any.

For example:
   1: spec.executables << 'rake'
   2: spec.bindir = 'bin'

Is tell us to looking up a file named rake, as executable, in ‘bin’ folder. Just as above, we could find rake file(or rake.bat if you are on windows), which start with #!/usr/bin/ruby, in you bin folder after you install rake gem. It told us all this is done by rubygems, or to be more specific, by gem install.

So let’s try to find out if there is a file called something like install in the rubygems package. Lucky enough, there is truly a installer.rb there!

   1: def install
   2:     # If we're forcing the install then disable security unless the security
   3:     # policy says that we only install singed gems.
   4:     @security_policy = nil if @force and @security_policy and
   5:                               not @security_policy.only_signed
   6:  
   7:     unless @force then
   8:       if rrv = @spec.required_ruby_version then
   9:         unless rrv.satisfied_by? Gem::Version.new(RUBY_VERSION) then
  10:           raise Gem::InstallError, "#{@spec.name} requires Ruby version #{rrv}"
  11:         end
  12:       end
  13:  
  14:       if rrgv = @spec.required_rubygems_version then
  15:         unless rrgv.satisfied_by? Gem::Version.new(Gem::RubyGemsVersion) then
  16:           raise Gem::InstallError,
  17:                 "#{@spec.name} requires RubyGems version #{rrgv}"
  18:         end
  19:       end
  20:  
  21:       unless @ignore_dependencies then
  22:         @spec.dependencies.each do |dep_gem|
  23:           ensure_dependency @spec, dep_gem
  24:         end
  25:       end
  26:     end
  27:  
  28:     FileUtils.mkdir_p @gem_home unless File.directory? @gem_home
  29:     raise Gem::FilePermissionError, @gem_home unless File.writable? @gem_home
  30:  
  31:     Gem.ensure_gem_subdirectories @gem_home
  32:  
  33:     FileUtils.mkdir_p @gem_dir
  34:  
  35:     extract_files
  36:     generate_bin
  37:     build_extensions
  38:     write_spec
  39:  
  40:     write_require_paths_file_if_needed
  41:  
  42:     # HACK remove?  Isn't this done in multiple places?
  43:     cached_gem = File.join @gem_home, "cache", @gem.split(///).pop
  44:     unless File.exist? cached_gem then
  45:       FileUtils.cp @gem, File.join(@gem_home, "cache")
  46:     end
  47:  
  48:     say @spec.post_install_message unless @spec.post_install_message.nil?
  49:  
  50:     @spec.loaded_from = File.join(@gem_home, 'specifications',
  51:                                   "#{@spec.full_name}.gemspec")
  52:  
  53:     return @spec
  54:   rescue Zlib::GzipFile::Error
  55:     raise Gem::InstallError, "gzip error installing #{@gem}"
  56:   end

I think the author is kind enough to show all the gem installation logic in this single method. Here we only need to focus on line36, oh yes, it is rubygems responsibility to generate executable and put it in the bindir. Of cause, there are much more magic happens behind this simple line of "generate_bin", I bet you will be pleased to learn it by your self:)

Advertisements

what is Rakefile?

Most people treat Rakefile as the rake task definition, but it’s rarely documented, as far as I know. Let once again ask the source code for confirmation 🙂

Rake start with run method, which is quite simple

   1: def run
   2:   standard_exception_handling do
   3:     init
   4:     load_rakefile
   5:     top_level
   6:   end
   7: end

Obviously, load_rakefile is suspicious, which is a method basically invoke raw_load_rakefile:

   1: def raw_load_rakefile # :nodoc:
   2:   rakefile, location = find_rakefile_location
   3:   if (! options.ignore_system) &&
   4:       (options.load_system || rakefile.nil?) &&
   5:       system_dir && File.directory?(system_dir)
   6:     puts "(in #{Dir.pwd})" unless options.silent
   7:     glob("#{system_dir}/*.rake") do |name|
   8:       add_import name
   9:     end
  10:   else
  11:     fail "No Rakefile found (looking for: #{@rakefiles.join(', ')})" if
  12:       rakefile.nil?
  13:     @rakefile = rakefile
  14:     Dir.chdir(location)
  15:     puts "(in #{Dir.pwd})" unless options.silent
  16:     $rakefile = @rakefile if options.classic_namespace
  17:     load File.expand_path(@rakefile) if @rakefile && @rakefile != ''
  18:     options.rakelib.each do |rlib|
  19:       glob("#{rlib}/*.rake") do |name|
  20:         add_import name
  21:       end
  22:     end
  23:   end
  24:   load_imports
  25: end

Although it is a pretty long method, we only need to focus the first line for our goal:

   1: rakefile, location = find_rakefile_location

And let’s see what find_rakefile_location could explain:

   1: def find_rakefile_location
   2:     here = Dir.pwd
   3:     while ! (fn = have_rakefile)
   4:         Dir.chdir("..")
   5:         if Dir.pwd == here || options.nosearch
   6:             return nil
   7:         end
   8:         here = Dir.pwd
   9:     end
  10:     [fn, here]
  11: ensure
  12:     Dir.chdir(Rake.original_dir)
  13: end

find_rakefile_location find rake definition files from the current directory to all it parent directories. Not so hard to understand, right?

have_rakefile is the method for detecting if the rake definition file is exist in the current path from a candidate list @rakefiles:

   1: def have_rakefile
   2:   @rakefiles.each do |fn|
   3:     if File.exist?(fn) || fn == ''
   4:       return fn
   5:     end
   6:   end
   7:   return nil
   8: end

Finally, we found our rake definition files: @rakefiles, which is an array:

   1: [rakefile, Rakefile, rakefile.rb, Rakefile.rb]

Haha , we got our answer from the source code:) It turns out we are not limited with only Rakefile, but also other three forms. It also explains why the Rakefile always located in the root path of a project, because rake will eventually reach definition files all the way up from all directories under the project path in which we execute the rake method.


Freeze rails

Freeze rails version for you rails app is a very common job:
rake rails:freeze:gems    #Freeze to the system rails gem version on your machine
rake rails:freeze:edge    #Freeze to the lasted rails version from web
rake rails:freeze:edge TAG=rel_1-1-6   
rake rails:unfreeze 
To better understand, read the source code in railsrailtieslibtasksframework.rake   
   1: namespace :rails do
   2:   namespace :freeze do
   3:     desc "Lock this application to the current gems (by unpacking them into vendor/rails)"
   4:     task :gems do
   5:       deps = %w(actionpack activerecord actionmailer activesupport actionwebservice)
   6:       require 'rubygems'
   7:       Gem.manage_gems
   8:  
   9:       rails = (version = ENV['VERSION']) ?
  10:         Gem.cache.find_name('rails', "= #{version}").first :
  11:         Gem.cache.find_name('rails').sort_by { |g| g.version }.last
  12:  
  13:       version ||= rails.version
  14:  
  15:       unless rails
  16:         puts "No rails gem #{version} is installed.  Do 'gem list rails' to see what you have available."
  17:         exit
  18:       end
  19:  
  20:       puts "Freezing to the gems for Rails #{rails.version}"
  21:       rm_rf   "vendor/rails"
  22:       mkdir_p "vendor/rails"
  23:  
  24:       chdir("vendor/rails") do
  25:         rails.dependencies.select { |g| deps.include? g.name }.each do |g|
  26:           Gem::GemRunner.new.run(["unpack", "-v", "#{g.version_requirements}", "#{g.name}"])
  27:           mv(Dir.glob("#{g.name}*").first, g.name)
  28:         end
  29:  
  30:         Gem::GemRunner.new.run(["unpack", "-v", "=#{version}", "rails"])
  31:         FileUtils.mv(Dir.glob("rails*").first, "railties")
  32:       end
  33:     end
  34:  
  35:     desc "Lock to latest Edge Rails or a specific revision with REVISION=X (ex: REVISION=4021) or a tag with TAG=Y (ex: TAG=rel_1-1-0)"
  36:     task :edge do
  37:       $verbose = false
  38:       `svn --version` rescue nil
  39:       unless !$?.nil? && $?.success?
  40:         $stderr.puts "ERROR: Must have subversion (svn) available in the PATH to lock this application to Edge Rails"
  41:         exit 1
  42:       end
  43:  
  44:       rm_rf   "vendor/rails"
  45:       mkdir_p "vendor/rails"
  46:  
  47:       svn_root = "http://dev.rubyonrails.org/svn/rails/"
  48:  
  49:       if ENV['TAG']
  50:         rails_svn = "#{svn_root}/tags/#{ENV['TAG']}"
  51:         touch "vendor/rails/TAG_#{ENV['TAG']}"
  52:       else
  53:         rails_svn = "#{svn_root}/trunk"
  54:  
  55:         if ENV['REVISION'].nil?
  56:           ENV['REVISION'] = /^r(d+)/.match(%x{svn -qr HEAD log #{svn_root}})[1]
  57:           puts "REVISION not set. Using HEAD, which is revision #{ENV['REVISION']}."
  58:         end
  59:  
  60:         touch "vendor/rails/REVISION_#{ENV['REVISION']}"
  61:       end
  62:  
  63:       for framework in %w(railties actionpack activerecord actionmailer activesupport activeresource)
  64:         system "svn export #{rails_svn}/#{framework} vendor/rails/#{framework}" + (ENV['REVISION'] ? " -r #{ENV['REVISION']}" : "")
  65:       end
  66:     end
  67:   end
  68:  
  69:   desc "Unlock this application from freeze of gems or edge and return to a fluid use of system gems"
  70:   task :unfreeze do
  71:     rm_rf "vendor/rails"
  72:   end
  73:  
  74:   desc "Update both configs, scripts and public/javascripts from Rails"
  75:   task :update => [ "update:scripts", "update:javascripts", "update:configs" ]
  76:  
  77:   namespace :update do
  78:     desc "Add new scripts to the application script/ directory"
  79:     task :scripts do
  80:       local_base = "script"
  81:       edge_base  = "#{File.dirname(__FILE__)}/../../bin"
  82:  
  83:       local = Dir["#{local_base}/**/*"].reject { |path| File.directory?(path) }
  84:       edge  = Dir["#{edge_base}/**/*"].reject { |path| File.directory?(path) }
  85:  
  86:       edge.each do |script|
  87:         base_name = script[(edge_base.length+1)..-1]
  88:         next if base_name == "rails"
  89:         next if local.detect { |path| base_name == path[(local_base.length+1)..-1] }
  90:         if !File.directory?("#{local_base}/#{File.dirname(base_name)}")
  91:           mkdir_p "#{local_base}/#{File.dirname(base_name)}"
  92:         end
  93:         install script, "#{local_base}/#{base_name}", :mode => 0755
  94:       end
  95:     end
  96:     ....