class Gem::Package

Example using a Gem::Package

Builds a .gem file given a Gem::Specification. A .gem file is a tarball which contains a data.tar.gz, metadata.gz, checksums.yaml.gz and possibly signatures.

require 'rubygems'
require 'rubygems/package'

spec = Gem::Specification.new do |s|
  s.summary = "Ruby based make-like utility."
  s.name = 'rake'
  s.version = PKG_VERSION
  s.requirements << 'none'
  s.files = PKG_FILES
  s.description = <<-EOF
Rake is a Make-like program implemented in Ruby. Tasks
and dependencies are specified in standard Ruby syntax.
  EOF
end

Gem::Package.build spec

Reads a .gem file.

require 'rubygems'
require 'rubygems/package'

the_gem = Gem::Package.new(path_to_dot_gem)
the_gem.contents # get the files in the gem
the_gem.extract_files destination_directory # extract the gem into a directory
the_gem.spec # get the spec out of the gem
the_gem.verify # check the gem is OK (contains valid gem specification, contains a not corrupt contents archive)

files are the files in the .gem tar file, not the Ruby files in the gem extract_files and contents automatically call verify

Constants

MINIMUM_RUBYGEMS_VERSION

The minimum RubyGems version that can install content-addressable gems. Built into required_rubygems_version so older clients reject skinny gems through both the local and remote install paths.

Attributes

Checksums for the contents of the package

Permission for other files

Permission for directories

The files in this package. This is not the contents of the gem, just the files in the top-level container.

Reference to the gem being packaged.

Permission for program files

The security policy used for verifying the contents of this package.

Sets the Gem::Specification to use to build this package.

Public Class Methods

Builds the gem described by spec and returns the built file name.

When content_addressable is true, builds a gem named by the SHA-256 digest of its contents. The spec must declare a non-Ruby platform and a required_ruby_version identifying a single Ruby ABI. Its required_rubygems_version is constrained to at least MINIMUM_RUBYGEMS_VERSION. Content-addressable builds are incompatible with file_name.

# File lib/rubygems/package.rb, line 161
def self.build(spec, skip_validation = false, strict_validation = false, file_name = nil, content_addressable: false)
  if content_addressable && file_name
    raise ArgumentError, "Cannot specify an output file name for a content-addressable gem as these gems must use the generated file name."
  end
  if content_addressable
    require "digest"
    require "stringio"

    io = StringIO.new
    io.set_encoding(Encoding::BINARY)

    package = new io
    package.spec = spec.dup
    gem_file = package.build_content_addressable_file skip_validation, strict_validation

    spec.required_rubygems_version = package.spec.required_rubygems_version
  else
    gem_file = file_name || spec.file_name

    package = new gem_file
    package.spec = spec
    package.build skip_validation, strict_validation
  end
  gem_file
end

Creates a new Gem::Package for the file at gem. gem can also be provided as an IO object.

If gem is an existing file in the old format a Gem::Package::Old will be returned.

Calls superclass method BasicObject::new
# File lib/rubygems/package.rb, line 194
def self.new(gem, security_policy = nil)
  gem = if gem.is_a?(Gem::Package::Source)
    gem
  elsif gem.respond_to? :read
    Gem::Package::IOSource.new gem
  else
    Gem::Package::FileSource.new gem
  end

  return super unless self == Gem::Package
  return super unless gem.present?

  return super unless gem.start
  return super unless gem.start.include? "MD5SUM ="

  Gem::Package::Old.new gem, security_policy
end

Extracts the Gem::Specification and raw metadata from the .gem file at path.

# File lib/rubygems/package.rb, line 217
def self.raw_spec(path, security_policy = nil)
  format = new(path, security_policy)
  spec = format.spec

  metadata = nil

  File.open path, Gem.binary_mode do |io|
    tar = Gem::Package::TarReader.new io
    tar.each_entry do |entry|
      case entry.full_name
      when "metadata" then
        metadata = entry.read
      when "metadata.gz" then
        metadata = Gem::Util.gunzip entry.read
      end
    end
  end

  [spec, metadata]
end

Public Instance Methods

Adds a checksum for each entry in the gem to checksums.yaml.gz.

# File lib/rubygems/package.rb, line 282
def add_checksums(tar)
  Gem.load_yaml

  checksums_by_algorithm = Hash.new {|h, algorithm| h[algorithm] = {} }

  @checksums.each do |name, digests|
    digests.each do |algorithm, digest|
      checksums_by_algorithm[algorithm][name] = digest.hexdigest
    end
  end

  tar.add_file_signed "checksums.yaml.gz", 0o444, @signer do |io|
    gzip_to io do |gz_io|
      if Gem.use_psych?
        Psych.dump checksums_by_algorithm, gz_io
      else
        gz_io.write Gem::YAMLSerializer.dump(checksums_by_algorithm)
      end
    end
  end
end

Builds this package based on the specification set by spec=

# File lib/rubygems/package.rb, line 361
  def build(skip_validation = false, strict_validation = false)
    raise ArgumentError, "skip_validation = true and strict_validation = true are incompatible" if skip_validation && strict_validation

    Gem.load_yaml

    @spec.validate true, strict_validation unless skip_validation

    setup_signer(
      signer_options: {
        expiration_length_days: Gem.configuration.cert_expiration_length_days,
      }
    )

    @gem.with_write_io do |gem_io|
      Gem::Package::TarWriter.new gem_io do |gem|
        add_metadata gem
        add_contents gem
        add_checksums gem
      end
    end

    message = <<-EOM
  Successfully built RubyGem
  Name: #{@spec.name}
  Version: #{@spec.version}
EOM

    message += "  File: #{File.basename(@gem.path)}\n" if @gem.path
    say message
  ensure
    @signer = nil
  end

Builds this package and writes it to a content-addressable file name derived from the SHA-256 digest of the gem contents, for example “example-1.0-01234567.gem”. Returns the written file name.

The spec is validated for content addressing. Its required_ruby_version determines the Ruby ABI, while its required_rubygems_version is constrained before building.

# File lib/rubygems/package.rb, line 403
def build_content_addressable_file(skip_validation = false, strict_validation = false)
  Gem::ContentAddress.eligible!(@spec)
  ruby_abi = Gem::ContentAddress.ruby_abi_for(@spec.required_ruby_version)
  @spec.required_rubygems_version = normalized_required_rubygems_version(ruby_abi)

  build skip_validation, strict_validation

  bytes = @gem.with_read_io(&:read)
  gem_file = "#{@spec.name}-#{@spec.version}-#{Gem::ContentAddress.address_for(bytes)}.gem"
  File.binwrite(gem_file, bytes)

  say "  File: #{gem_file}"
  say "  Platform: #{@spec.platform}"
  say "  Ruby ABI: #{ruby_abi}"

  gem_file
end

Derives the content address from the gem’s file name and verifies it against the SHA256 digest of the file contents.

# File lib/rubygems/package.rb, line 268
def content_address
  path = @gem&.path
  return unless path

  address = Gem::ContentAddress.verified_file_name_claim(path, spec)
  return nil unless address
  return nil unless Gem::ContentAddress.eligible?(spec)

  address
end

A list of file names contained in this gem

# File lib/rubygems/package.rb, line 424
def contents
  return @contents if @contents

  verify unless @spec

  @contents = []

  @gem.with_read_io do |io|
    gem_tar = Gem::Package::TarReader.new io

    gem_tar.each do |entry|
      next unless entry.full_name == "data.tar.gz"

      open_tar_gz entry do |pkg_tar|
        pkg_tar.each do |contents_entry|
          @contents << contents_entry.full_name
        end
      end

      return @contents
    end
  end
rescue Zlib::GzipFile::Error, EOFError, Gem::Package::TarInvalidError => e
  raise Gem::Package::FormatError.new e.message, @gem
end

Copies this package to path (if possible)

# File lib/rubygems/package.rb, line 260
def copy_to(path)
  FileUtils.cp @gem.path, path unless File.exist? path
end

Extracts the files in this package into destination_dir

If pattern is specified, only entries matching that glob will be extracted.

# File lib/rubygems/package.rb, line 483
def extract_files(destination_dir, pattern = "*")
  verify unless @spec

  FileUtils.mkdir_p destination_dir, mode: dir_mode && 0o755

  @gem.with_read_io do |io|
    reader = Gem::Package::TarReader.new io

    reader.each do |entry|
      next unless entry.full_name == "data.tar.gz"

      extract_tar_gz entry, destination_dir, pattern

      break # ignore further entries
    end
  end
rescue Zlib::GzipFile::Error, EOFError, Gem::Package::TarInvalidError => e
  raise Gem::Package::FormatError.new e.message, @gem
end

Gzips content written to gz_io to io.

# File lib/rubygems/package.rb, line 599
def gzip_to(io) # :yields: gz_io
  gz_io = Zlib::GzipWriter.new io, Zlib::BEST_COMPRESSION
  gz_io.mtime = @build_time

  yield gz_io
ensure
  gz_io.close
end

Reads and loads checksums.yaml.gz from the tar file gem

# File lib/rubygems/package.rb, line 666
def read_checksums(gem)
  Gem.load_yaml

  @checksums = gem.seek "checksums.yaml.gz" do |entry|
    Zlib::GzipReader.wrap entry do |gz_io|
      Gem::SafeYAML.safe_load limit_read(gz_io, "checksums.yaml.gz", 10 * 1024 * 1024)
    end
  end
end

Prepares the gem for signing and checksum generation. If a signing certificate and key are not present only checksum generation is set up.

# File lib/rubygems/package.rb, line 680
def setup_signer(signer_options: {})
  passphrase = ENV["GEM_PRIVATE_KEY_PASSPHRASE"]
  if @spec.signing_key
    @signer =
      Gem::Security::Signer.new(
        @spec.signing_key,
        @spec.cert_chain,
        passphrase,
        signer_options
      )

    @spec.signing_key = nil
    @spec.cert_chain = @signer.cert_chain.map(&:to_s)
  else
    @signer = Gem::Security::Signer.new nil, nil, passphrase
    @spec.cert_chain = @signer.cert_chain.map(&:to_pem) if
      @signer.cert_chain
  end
end

The spec for this gem.

If this is a package for a built gem the spec is loaded from the gem and returned. If this is a package for a gem being built the provided spec is returned.

# File lib/rubygems/package.rb, line 707
def spec
  verify unless @spec

  @spec
end

Verifies that this gem:

  • Contains a valid gem specification

  • Contains a contents archive

  • The contents archive is not corrupt

After verification the gem specification from the gem is available from spec

# File lib/rubygems/package.rb, line 723
def verify
  @files     = []
  @spec      = nil

  @gem.with_read_io do |io|
    Gem::Package::TarReader.new io do |reader|
      read_checksums reader

      verify_files reader
    end
  end

  verify_checksums @digests, @checksums

  @security_policy&.verify_signatures @spec, @digests, @signatures

  true
rescue Gem::Security::Exception
  @spec = nil
  @files = []
  raise
rescue Errno::ENOENT => e
  raise Gem::Package::FormatError.new e.message
rescue Zlib::GzipFile::Error, EOFError, Gem::Package::TarInvalidError => e
  raise Gem::Package::FormatError.new e.message, @gem
end

Protected Instance Methods

Creates a new package that will read or write to the file gem.

# File lib/rubygems/package.rb, line 241
def initialize(gem, security_policy) # :notnew:
  require "zlib"

  @gem = gem

  @build_time      = Gem.source_date_epoch
  @checksums       = {}
  @contents        = nil
  @digests         = Hash.new {|h, algorithm| h[algorithm] = {} }
  @files           = nil
  @security_policy = security_policy
  @signatures      = {}
  @signer          = nil
  @spec            = nil
end