Permalink
Browse files

Add Gem::Commands::PatchCommand and Gem::Patcher

This adds the patch command to RubyGems. The patch command helps to patch gems without manually opening and rebuilding them. It opens a given .gem file, extracts it, patches it with system `patch` command, clones its spec, updates the file list and builds the patched gem.
  • Loading branch information...
Josef Strzibny
Josef Strzibny committed Oct 1, 2012
1 parent d41bf95 commit e0331a49c88da18c4e066ecacd80492e75d41544
@@ -77,6 +77,7 @@ def initialize
register_command :mirror
register_command :outdated
register_command :owner
+ register_command :patch
register_command :pristine
register_command :push
register_command :query
@@ -0,0 +1,54 @@
+require "rubygems/command"
+require "rubygems/patcher"
+
+class Gem::Commands::PatchCommand < Gem::Command
+ def initialize
+ super "patch", "Patches the gem with the given patches and generates patched gem.",
+ :output => Dir.pwd, :strip => 0
+
+ # Same as 'patch -pNUMBER' on Linux machines
+ add_option('-pNUMBER', '--strip=NUMBER', 'Set the file name strip count to NUMBER.') do |number, options|
+ options[:strip] = number
+ end
+ end
+
+ def arguments # :nodoc:
+ args = <<-EOF
+ GEMFILE path to the gem file to patch
+ PATCH [PATCH ...] list of patches to apply
+ EOF
+ return args.gsub(/^\s+/, '')
+ end
+
+ def description # :nodoc:
+ <<-EOF
+ The patch command helps to patch gems without manually opening and rebuilding them.
+ It opens a given .gem file, extracts it, patches it with system `patch` command,
+ clones its spec, updates the file list and builds the patched gem.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEMFILE PATCH [PATCH ...]"
+ end
+
+ def execute
+ gemfile = options[:args].shift
+ patches = options[:args]
+
+ # No gem
+ unless gemfile
+ raise Gem::CommandLineError,
+ "Please specify a gem file on the command line (e.g. gem patch foo-0.1.0.gem PATCH [PATCH ...])"
+ end
+
+ # No patches
+ if patches.empty?
+ raise Gem::CommandLineError,
+ "Please specify patches to apply (e.g. gem patch foo-0.1.0.gem foo.patch bar.patch ...)"
+ end
+
+ patcher = Gem::Patcher.new(gemfile, options[:output])
+ patcher.patch_with(patches, options[:strip])
+ end
+end
View
@@ -0,0 +1,124 @@
+require "rbconfig"
+require "tmpdir"
+require "rubygems/package"
+
+class Gem::Patcher
+ include Gem::UserInteraction
+
+ class PatchCommandMissing < StandardError; end
+
+ def initialize(gemfile, output_dir)
+ @gemfile = gemfile
+ @output_dir = output_dir
+
+ # @target_dir is a temporary directory where the gem files live
+ tmpdir = Dir.mktmpdir
+ basename = File.basename(gemfile, '.gem')
+ @target_dir = File.join(tmpdir, basename)
+ end
+
+ ##
+ # Patch the gem, move the new patched gem to the working directory and return the path
+
+ def patch_with(patches, strip_number)
+ check_patch_command_is_installed
+ extract_gem
+
+ # Apply all patches
+ patches.each do |patch|
+ info 'Applying patch ' + patch
+ apply_patch(patch, strip_number)
+ end
+
+ build_patched_gem
+
+ # Move the newly generated gem to the working directory
+ gem_file = IO.read(File.join @target_dir, @package.spec.file_name)
+
+ File.open(File.join(@output_dir, @package.spec.file_name), 'w') do |f|
+ f.write gem_file
+ end
+
+ # Return the path to the patched gem
+ File.join @output_dir, "#{@package.spec.file_name}"
+ end
+
+ def apply_patch(patch, strip_number)
+ patch_path = File.expand_path(patch)
+ info 'Path to the patch to apply: ' + patch_path
+
+ # Apply the patch by calling 'patch -pNUMBER < patch'
+ Dir.chdir @target_dir do
+ if system("patch --verbose -p#{strip_number} < #{patch_path}")
+ info 'Succesfully patched by ' + patch
+ else
+ info 'Error: Unable to patch with ' + patch
+ end
+ end
+ end
+
+ private
+
+ def extract_gem
+ @package = Gem::Package.new @gemfile
+
+ # Unpack
+ info "Unpacking gem '#{@gemfile}' in " + @target_dir
+ @package.extract_files @target_dir
+ end
+
+ def build_patched_gem
+ patched_package = Gem::Package.new @package.spec.file_name
+ patched_package.spec = @package.spec.clone
+ patched_package.spec.files = files_in_gem
+
+ # Change dir and build the patched gem
+ Dir.chdir @target_dir do
+ patched_package.build false
+ end
+ end
+
+ def info(msg)
+ say msg if Gem.configuration.verbose
+ end
+
+ def files_in_gem
+ files = []
+
+ Dir.foreach(@target_dir) do |file|
+ if File.directory? File.join @target_dir, file
+ files += files_in_dir(file) unless /\./.match(file)
+ else
+ files << file
+ end
+ end
+
+ delete_original_files(files)
+ end
+
+ def files_in_dir(dir)
+ files = []
+
+ Dir.foreach(File.join @target_dir, dir) do |file|
+ if File.directory? File.join @target_dir, dir, file
+ files += files_in_dir(File.join dir, file) unless /\./.match(file)
+ else
+ files << File.join(dir, file)
+ end
+ end
+
+ files
+ end
+
+ def delete_original_files(files)
+ files.each do |file|
+ files.delete file if /\.orig/.match(file)
+ end
+ end
+
+ def check_patch_command_is_installed
+ unless system("patch --version")
+ raise PatchCommandMissing, 'Calling `patch` command failed. Do you have it installed?'
+ end
+ end
+end
@@ -0,0 +1,34 @@
+require "rubygems/test_case"
+require "rubygems/commands/patch_command"
+
+class TestGemCommandsPatchCommand < Gem::TestCase
+ def setup
+ super
+
+ @command = Gem::Commands::PatchCommand.new
+ end
+
+ def test_execute_no_gemfile
+ @command.options[:args] = []
+
+ e = assert_raises Gem::CommandLineError do
+ use_ui @ui do
+ @command.execute
+ end
+ end
+
+ assert_match 'Please specify a gem file on the command line (e.g. gem patch foo-0.1.0.gem PATCH [PATCH ...])', e.message
+ end
+
+ def test_execute_no_patch
+ @command.options[:args] = ['Gemfile.gem']
+
+ e = assert_raises Gem::CommandLineError do
+ use_ui @ui do
+ @command.execute
+ end
+ end
+
+ assert_match 'Please specify patches to apply (e.g. gem patch foo-0.1.0.gem foo.patch bar.patch ...)', e.message
+ end
+end
Oops, something went wrong.

0 comments on commit e0331a4

Please sign in to comment.