Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
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...
commit e0331a49c88da18c4e066ecacd80492e75d41544 1 parent d41bf95
Josef Strzibny authored
View
1  lib/rubygems/command_manager.rb
@@ -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
View
54 lib/rubygems/commands/patch_command.rb
@@ -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
124 lib/rubygems/patcher.rb
@@ -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
View
34 test/rubygems/test_gem_commands_patch_command.rb
@@ -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
View
285 test/rubygems/test_gem_patch.rb
@@ -0,0 +1,285 @@
+require "rubygems/test_case"
+require "rubygems/patcher"
+
+class TestGemPatch < Gem::TestCase
+ def setup
+ super
+
+ @gems_dir = File.join @tempdir, 'gems'
+ @lib_dir = File.join @tempdir, 'gems', 'lib'
+ FileUtils.mkdir_p @lib_dir
+ end
+
+ ##
+ # Test changing a file in a gem with -p1 option
+
+ def test_change_file_patch
+ gemfile = bake_testing_gem
+
+ patches = []
+ patches << bake_change_file_patch
+
+ # Creates new patched gem in @gems_dir
+ patcher = Gem::Patcher.new(gemfile, @gems_dir)
+ patched_gem = patcher.patch_with(patches, 1)
+
+ # Unpack
+ package = Gem::Package.new patched_gem
+ package.extract_files @gems_dir
+
+ assert_equal patched_file, file_contents('foo.rb')
+ end
+
+ ##
+ # Test adding a file into a gem with -p0 option
+
+ def test_new_file_patch
+ gemfile = bake_testing_gem
+
+ patches = []
+ patches << bake_new_file_patch
+
+ # Create a new patched gem in @gems_fir
+ patcher = Gem::Patcher.new(gemfile, @gems_dir)
+ patched_gem = patcher.patch_with(patches, 0)
+
+ # Unpack
+ package = Gem::Package.new patched_gem
+ package.extract_files @gems_dir
+
+ assert_equal original_file, file_contents('bar.rb')
+ end
+
+ ##
+ # Test adding and deleting a file in a gem with -p0 option
+
+ def test_delete_file_patch
+ gemfile = bake_testing_gem
+
+ patches = []
+ patches << bake_new_file_patch
+ patches << bake_delete_file_patch
+
+ # Create a new patched gem in @gems_fir
+ patcher = Gem::Patcher.new(gemfile, @gems_dir)
+ patched_gem = patcher.patch_with(patches, 0)
+
+ # Unpack
+ package = Gem::Package.new patched_gem
+ package.extract_files @gems_dir
+
+ # Only foo.rb should stay in /lib, bar.rb should be gone
+ assert_raises(RuntimeError, 'File not found') {
+ file_contents(File.join @lib_dir, 'bar.rb')
+ }
+ end
+
+ ##
+ # Incorrect patch, nothing happens
+
+ def test_gem_should_not_change
+ gemfile = bake_testing_gem
+
+ patches = []
+ patches << bake_incorrect_patch
+
+ # Create a new patched gem in @gems_fir
+ patcher = Gem::Patcher.new(gemfile, @gems_dir)
+ patched_gem = patcher.patch_with(patches, 0)
+
+ # Unpack
+ package = Gem::Package.new patched_gem
+ package.extract_files @gems_dir
+
+ assert_equal original_file, file_contents('foo.rb')
+ assert_equal original_gemspec, current_gemspec
+ end
+
+ def bake_change_file_patch
+ patch_path = File.join(@gems_dir, 'change_file.patch')
+
+ File.open(patch_path, 'w') do |f|
+ f.write change_file_patch
+ end
+
+ patch_path
+ end
+
+ def bake_new_file_patch
+ patch_path = File.join(@gems_dir, 'new_file.patch')
+
+ File.open(patch_path, 'w') do |f|
+ f.write new_file_patch
+ end
+
+ patch_path
+ end
+
+ def bake_delete_file_patch
+ patch_path = File.join(@gems_dir, 'delete_file.patch')
+
+ File.open(patch_path, 'w') do |f|
+ f.write delete_file_patch
+ end
+
+ patch_path
+ end
+
+ def bake_incorrect_patch
+ patch_path = File.join(@gems_dir, 'incorrect.patch')
+
+ File.open(patch_path, 'w') do |f|
+ f.write incorrect_patch
+ end
+
+ patch_path
+ end
+
+ def bake_original_gem_files
+ # Create /lib/foo.rb
+ file_path = File.join(@lib_dir, 'foo.rb')
+
+ File.open(file_path, 'w') do |f|
+ f.write original_file
+ end
+
+ # Create .gemspec file
+ gemspec_path = File.join(@gems_dir, 'foo-0.gemspec')
+
+ File.open(gemspec_path, 'w') do |f|
+ f.write original_gemspec
+ end
+ end
+
+ def bake_testing_gem
+ bake_original_gem_files
+
+ test_package = Gem::Package.new 'foo-0.gem'
+ test_package.spec = Gem::Specification.load(File.join(@gems_dir, 'foo-0.gemspec'))
+
+ # Build
+ Dir.chdir @gems_dir do
+ test_package.build false
+ end
+
+ File.join(@gems_dir, 'foo-0.gem')
+ end
+
+ def current_gemspec
+ gemspec_path = File.join(@gems_dir, 'foo-0.gemspec')
+
+ IO.read(gemspec_path)
+ end
+
+ ##
+ # Get the content of the given file in @lib_dir
+
+ def file_contents(file)
+ file_path = File.join(@lib_dir, file)
+
+ begin
+ file_content = IO.read(file_path)
+ rescue
+ raise RuntimeError, 'File not found'
+ end
+
+ file_content
+ end
+
+ def original_gemspec
+ <<-EOF
+ Gem::Specification.new do |s|
+ s.platform = Gem::Platform::RUBY
+ s.name = 'foo'
+ s.version = 0
+ s.author = 'A User'
+ s.email = 'example@example.com'
+ s.homepage = 'http://example.com'
+ s.summary = "this is a summary"
+ s.description = "This is a test description"
+ s.files = ['lib/foo.rb']
+ end
+ EOF
+ end
+
+ def original_file
+ <<-EOF
+ module Foo
+ def bar
+ 'Original'
+ end
+ end
+ EOF
+ end
+
+ def patched_file
+ <<-EOF
+ module Foo
+ class Bar
+ def foo_bar
+ 'Patched'
+ end
+ end
+ end
+ EOF
+ end
+
+ def change_file_patch
+ <<-EOF
+ diff -u a/lib/foo.rb b/lib/foo.rb
+ --- a/lib/foo.rb
+ +++ b/lib/foo.rb
+ @@ -1,6 +1,8 @@
+ module Foo
+ - def bar
+ - 'Original'
+ + class Bar
+ + def foo_bar
+ + 'Patched'
+ + end
+ end
+ end
+ EOF
+ end
+
+ def new_file_patch
+ <<-EOF
+ diff lib/bar.rb lib/bar.rb
+ --- /dev/null
+ +++ lib/bar.rb
+ @@ -0,0 +1,5 @@
+ + module Foo
+ + def bar
+ + 'Original'
+ + end
+ + end
+ EOF
+ end
+
+ def delete_file_patch
+ <<-EOF
+ diff lib/bar.rb lib/bar.rb
+ --- lib/bar.rb
+ +++ /dev/null
+ @@ -1,5 +0,0 @@
+ - module Foo
+ - def bar
+ - 'Original'
+ - end
+ - end
+ EOF
+ end
+
+ def incorrect_patch
+ <<-EOF
+ diff lib/foo.rb lib/foo.rb
+ --- lib/foo.rb
+ +++ /dev/null
+ - module Foo
+ - def bar
+ - 'Original'
+ - end
+ - end
+ EOF
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.