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

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

# File lib/rubygems/package.rb, line 131
def self.build(spec, skip_validation = false, strict_validation = false, file_name = nil)
  gem_file = file_name || spec.file_name

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

  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 148
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
end

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

# File lib/rubygems/package.rb, line 171
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 221
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|
      Psych.dump checksums_by_algorithm, gz_io
    end
  end
end

Builds this package based on the specification set by spec=

# File lib/rubygems/package.rb, line 292
  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

    say <<-EOM
  Successfully built RubyGem
  Name: #{@spec.name}
  Version: #{@spec.version}
  File: #{File.basename @gem.path}
EOM
  ensure
    @signer = nil
  end

A list of file names contained in this gem

# File lib/rubygems/package.rb, line 326
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 214
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 385
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 490
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
# File lib/rubygems/package.rb, line 727
def limit_read(io, name, limit)
  bytes = io.read(limit + 1)
  raise Gem::Package::FormatError, "#{name} is too big (over #{limit} bytes)" if bytes.size > limit
  bytes
end
# File lib/rubygems/package.rb, line 517
def normalize_path(pathname)
  if Gem.win_platform?
    pathname.downcase
  else
    pathname
  end
end

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

# File lib/rubygems/package.rb, line 554
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 568
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 595
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 611
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

Verifies entry in a .gem file.

# File lib/rubygems/package.rb, line 660
def verify_entry(entry)
  file_name = entry.full_name
  @files << file_name

  case file_name
  when /\.sig$/ then
    @signatures[$`] = limit_read(entry, file_name, 1024 * 1024) if @security_policy
    return
  else
    digest entry
  end

  case file_name
  when "metadata", "metadata.gz" then
    load_spec entry
  when "data.tar.gz" then
    verify_gz entry
  end
rescue StandardError
  warn "Exception while verifying #{@gem.path}"
  raise
end

Verifies the files of the gem

# File lib/rubygems/package.rb, line 686
def verify_files(gem)
  gem.each do |entry|
    verify_entry entry
  end

  unless @spec
    raise Gem::Package::FormatError.new "package metadata is missing", @gem
  end

  unless @files.include? "data.tar.gz"
    raise Gem::Package::FormatError.new \
      "package content (data.tar.gz) is missing", @gem
  end

  if (duplicates = @files.group_by {|f| f }.select {|_k,v| v.size > 1 }.map(&:first)) && duplicates.any?
    raise Gem::Security::Exception, "duplicate files in the package: (#{duplicates.map(&:inspect).join(", ")})"
  end
end

Protected Instance Methods

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

# File lib/rubygems/package.rb, line 195
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