gemlock   [plain text]


#!/usr/bin/env ruby
# -*- ruby -*-
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++


# gemlock -- Generate a lockdown list of a set of gems
#
# Usage:   gemlock  gem_name-version...
#
# gemlock will generate a list of +gem+ statements that will
# lock down the versions for the gem given in the command line.  It
# will specify exact versions in the requirements list to ensure that
# the gems loaded will always be consistent.  A full recursive search
# of all effected gems will be generated.
#
# Example:
#
#   gemlock rails-1.0.0 >lockdown.rb
#
# will produce in lockdown.rb:
#
#   require "rubygems"
#   gem 'rails', '= 1.0.0'
#   gem 'rake', '= 0.7.0.1'
#   gem 'activesupport', '= 1.2.5'
#   gem 'activerecord', '= 1.13.2'
#   gem 'actionpack', '= 1.11.2'
#   gem 'actionmailer', '= 1.1.5'
#   gem 'actionwebservice', '= 1.0.0'
#
# Just load lockdown.rb from your application to ensure that the
# current versions are loaded.  Make sure that lockdown.rb is loaded
# *before* any other require statements.
#
# Notice that rails 1.0.0 only requires that rake 0.6.2 or better be
# used.  Rake-0.7.0.1 is the most recent version installed that
# satisfies that, so we lock it down to the exact version.

require 'rubygems'
require 'ostruct'
require 'optparse'
require 'yaml'

Gem.manage_gems

def handle_options(args)
  options = OpenStruct.new
  options.verbose = false
  options.strict = false
  opts = OptionParser.new do |opts|
    opts.banner = "Usage: #$0 [options] GEM_NAME-VERSION..."
    opts.separator ""
    opts.separator "Where options are:"
    
    opts.on('--verbose', '-v', 'Verbose output') do |value|
      options.verbose = value
    end

    opts.on('--[no-]strict', '-s',
      'Fail if unable to satisfy a dependency') do |value|
      options.strict = value
    end

    opts.on_tail('--help', '-h', 'Show this message') do
      puts opts
      exit
    end 
    
    opts.on_tail('--version', '-V', 'Show version') do
      puts "gemlock (#{Gem::RubyGemsVersion})"
      exit
    end 
  end 
  options.usage = opts

  opts.parse!(args)
  options
end 

def spec_path(gem_full_name)
  File.join(Gem.path, "specifications", gem_full_name + ".gemspec")
end

def complain(message, options)
  if options.strict
    fail message
  else
    puts "# #{message}"
  end
end

def lock_down(pending, options)
  puts 'require "rubygems"'
  locked = {}
  while ! pending.empty?
    full_name = pending.shift
    spec = Gem::SourceIndex.load_specification(spec_path(full_name))
    puts "gem '#{spec.name}', '= #{spec.version}'" unless locked[spec.name]
    locked[spec.name] = true
    spec.dependencies.each do |dep|
      next if locked[dep.name]
      candidates = Gem.source_index.search(dep.name, dep.requirement_list)
      if candidates.empty?
        complain(
          "Unable to satisfy '#{dep}' from currently installed gems.",
          options)
      else
        pending << candidates.last.full_name
      end
    end
  end
end

if __FILE__ == $0 then
  options = handle_options(ARGV)
  if ARGV.empty?
    puts options.usage
    exit(1)
  else
    lock_down(ARGV, options)
  end
end