class Gem::Commands::RebuildCommand - Documentation for Ruby 3.3

# class Gem::Commands::RebuildCommand

### Constants

DATE_FORMAT

### Public Class Methods

new() click to toggle source
Calls superclass method `Gem::Command::new`

    # File lib/rubygems/commands/rebuild_command.rb, line 15
    def initialize
      super "rebuild", "Attempt to reproduce a build of a gem."

      add_option "--diff", "If the files don't match, compare them using diffoscope." do |_value, options|
        options[:diff] = true
      end

      add_option "--force", "Skip validation of the spec." do |_value, options|
        options[:force] = true
      end

      add_option "--strict", "Consider warnings as errors when validating the spec." do |_value, options|
        options[:strict] = true
      end

      add_option "--source GEM_SOURCE", "Specify the source to download the gem from." do |value, options|
        options[:source] = value
      end

      add_option "--original GEM_FILE", "Specify a local file to compare against (instead of downloading it)." do |value, options|
        options[:original_gem_file] = value
      end

      add_option "--gemspec GEMSPEC_FILE", "Specify the name of the gemspec file." do |value, options|
        options[:gemspec_file] = value
      end

      add_option "-C PATH", "Run as if gem build was started in <PATH> instead of the current working directory." do |value, options|
        options[:build_path] = value
      end
    end

### Public Instance Methods

execute() click to toggle source

    # File lib/rubygems/commands/rebuild_command.rb, line 74
      def execute
        gem_name, gem_version = get_gem_name_and_version

        old_dir, new_dir = prep_dirs

        gem_filename = "#{gem_name}-#{gem_version}.gem"
        old_file = File.join(old_dir, gem_filename)
        new_file = File.join(new_dir, gem_filename)

        if options[:original_gem_file]
          FileUtils.copy_file(options[:original_gem_file], old_file)
        else
          download_gem(gem_name, gem_version, old_file)
        end

        rg_version = rubygems_version(old_file)
        unless rg_version == Gem::VERSION
          alert_error <<-EOF
    You need to use the same RubyGems version #{gem_name} v#{gem_version} was built with.

    #{gem_name} v#{gem_version} was built using RubyGems v#{rg_version}.
    Gem files include the version of RubyGems used to build them.
    This means in order to reproduce #{gem_filename}, you must also use RubyGems v#{rg_version}.

    You're using RubyGems v#{Gem::VERSION}.

    Please install RubyGems v#{rg_version} and try again.
          EOF
          terminate_interaction 1
        end

        source_date_epoch = get_timestamp(old_file).to_s

        if build_path = options[:build_path]
          Dir.chdir(build_path) { build_gem(gem_name, source_date_epoch, new_file) }
        else
          build_gem(gem_name, source_date_epoch, new_file)
        end

        compare(source_date_epoch, old_file, new_file)
      end

### Private Instance Methods

build_gem(gem_name, source_date_epoch, output_file) click to toggle source

    # File lib/rubygems/commands/rebuild_command.rb, line 192
    def build_gem(gem_name, source_date_epoch, output_file)
      gemspec = options[:gemspec_file] || find_gemspec("#{gem_name}.gemspec")

      if gemspec
        build_package(gemspec, source_date_epoch, output_file)
      else
        alert_error error_message(gem_name)
        terminate_interaction(1)
      end
    end

build_package(gemspec, source_date_epoch, output_file) click to toggle source

    # File lib/rubygems/commands/rebuild_command.rb, line 203
    def build_package(gemspec, source_date_epoch, output_file)
      with_source_date_epoch(source_date_epoch) do
        spec = Gem::Specification.load(gemspec)
        if spec
          Gem::Package.build(
            spec,
            options[:force],
            options[:strict],
            output_file
          )
        else
          alert_error "Error loading gemspec. Aborting."
          terminate_interaction 1
        end
      end
    end

compare(source_date_epoch, old_file, new_file) click to toggle source

    # File lib/rubygems/commands/rebuild_command.rb, line 133
    def compare(source_date_epoch, old_file, new_file)
      date = Time.at(source_date_epoch.to_i).strftime("%F %T %Z")

      old_hash = sha256(old_file)
      new_hash = sha256(new_file)

      say
      say "Built at: #{date} (#{source_date_epoch})"
      say "Original build saved to:   #{old_file}"
      say "Reproduced build saved to: #{new_file}"
      say "Working directory: #{options[:build_path] || Dir.pwd}"
      say
      say "Hash comparison:"
      say "  #{old_hash}\t#{old_file}"
      say "  #{new_hash}\t#{new_file}"
      say

      if old_hash == new_hash
        say "SUCCESS - original and rebuild hashes matched"
      else
        say "FAILURE - original and rebuild hashes did not match"
        say

        if options[:diff]
          if system("diffoscope", old_file, new_file).nil?
            alert_error "error: could not find `diffoscope` executable"
          end
        else
          say "Pass --diff for more details (requires diffoscope to be installed)."
        end

        terminate_interaction 1
      end
    end

download_gem(gem_name, gem_version, old_file) click to toggle source

    # File lib/rubygems/commands/rebuild_command.rb, line 237
    def download_gem(gem_name, gem_version, old_file)
      # This code was based loosely off the `gem fetch` command.
      version = "= #{gem_version}"
      dep = Gem::Dependency.new gem_name, version

      specs_and_sources, errors =
        Gem::SpecFetcher.fetcher.spec_for_dependency dep

      # There should never be more than one item in specs_and_sources,
      # since we search for an exact version.
      spec, source = specs_and_sources[0]

      if spec.nil?
        show_lookup_failure gem_name, version, errors, options[:domain]
        terminate_interaction 1
      end

      download_path = source.download spec

      FileUtils.move(download_path, old_file)

      say "Downloaded #{gem_name} version #{gem_version} as #{old_file}."
    end

error_message(gem_name) click to toggle source

    # File lib/rubygems/commands/rebuild_command.rb, line 229
    def error_message(gem_name)
      if gem_name
        "Couldn't find a gemspec file matching '#{gem_name}' in #{Dir.pwd}"
      else
        "Couldn't find a gemspec file in #{Dir.pwd}"
      end
    end

get_gem_name_and_version() click to toggle source

    # File lib/rubygems/commands/rebuild_command.rb, line 179
    def get_gem_name_and_version
      args = options[:args] || []
      if args.length == 2
        gem_name, gem_version = args
      elsif args.length > 2
        raise Gem::CommandLineError, "Too many arguments"
      else
        raise Gem::CommandLineError, "Expected GEM_NAME and GEM_VERSION arguments (gem rebuild GEM_NAME GEM_VERSION)"
      end

      [gem_name, gem_version]
    end

get_timestamp(file) click to toggle source

    # File lib/rubygems/commands/rebuild_command.rb, line 122
    def get_timestamp(file)
      mtime = nil
      File.open(file, Gem.binary_mode) do |f|
        Gem::Package::TarReader.new(f) do |tar|
          mtime = tar.seek("metadata.gz") {|tf| tf.header.mtime }
        end
      end

      mtime
    end

prep_dirs() click to toggle source

    # File lib/rubygems/commands/rebuild_command.rb, line 168
    def prep_dirs
      rebuild_dir = Dir.mktmpdir("gem_rebuild")
      old_dir = File.join(rebuild_dir, "old")
      new_dir = File.join(rebuild_dir, "new")

      FileUtils.mkdir_p(old_dir)
      FileUtils.mkdir_p(new_dir)

      [old_dir, new_dir]
    end

rubygems_version(gem_file) click to toggle source

    # File lib/rubygems/commands/rebuild_command.rb, line 261
    def rubygems_version(gem_file)
      Gem::Package.new(gem_file).spec.rubygems_version
    end

sha256(file) click to toggle source

    # File lib/rubygems/commands/rebuild_command.rb, line 118
    def sha256(file)
      Digest::SHA256.hexdigest(Gem.read_binary(file))
    end

with_source_date_epoch(source_date_epoch) { || ... } click to toggle source

    # File lib/rubygems/commands/rebuild_command.rb, line 220
    def with_source_date_epoch(source_date_epoch)
      old_sde = ENV["SOURCE_DATE_EPOCH"]
      ENV["SOURCE_DATE_EPOCH"] = source_date_epoch.to_s

      yield
    ensure
      ENV["SOURCE_DATE_EPOCH"] = old_sde
    end