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_versionso 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
Source
# File lib/rubygems/package.rb, line 147 def self.build(spec, skip_validation = false, strict_validation = false, file_name = nil, ruby_abi = nil) if ruby_abi && file_name raise ArgumentError, "Cannot specify both a Ruby ABI and an output file name because content addressable gems must use the generated file name." end if ruby_abi 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 ruby_abi, skip_validation, strict_validation spec.required_ruby_version = package.spec.required_ruby_version 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
Builds the gem described by spec and returns the built file name; passing ruby_abi (“X.Y”) builds a content-addressable gem named by the SHA-256 of its contents, updates spec.required_ruby_version to “~> X.Y.0”, and constrains spec.required_rubygems_version to at least MINIMUM_RUBYGEMS_VERSION (incompatible with file_name).
# File lib/rubygems/package.rb, line 181 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
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.
BasicObject::new
# File lib/rubygems/package.rb, line 204 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
Extracts the Gem::Specification and raw metadata from the .gem file at path.
Public Instance Methods
Source
# File lib/rubygems/package.rb, line 269 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
Adds a checksum for each entry in the gem to checksums.yaml.gz.
# File lib/rubygems/package.rb, line 344 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 based on the specification set by spec=
Source
# File lib/rubygems/package.rb, line 388 def build_content_addressable_file(ruby_abi, skip_validation = false, strict_validation = false) validate_ruby_abi ruby_abi @spec.required_rubygems_version = normalized_required_rubygems_version(ruby_abi) @spec.required_ruby_version = Gem::ContentAddress.ruby_abi_requirement(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
Builds this package scoped to ruby_abi (“X.Y”), then writes it to a content-addressable file name derived from the SHA-256 digest of the gem contents, e.g. “example-1.0-01234567.gem”. Returns the file name of the written gem.
The spec is validated for an ABI-scoped build and its required_ruby_version and required_rubygems_version are constrained before building, so every gem this method produces is eligible for content addressing.
Source
# File lib/rubygems/package.rb, line 255 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
Derives the content address from the gem’s file name and verifies it against the SHA256 digest of the file contents.
Source
# File lib/rubygems/package.rb, line 409 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
A list of file names contained in this gem
Source
# File lib/rubygems/package.rb, line 247 def copy_to(path) FileUtils.cp @gem.path, path unless File.exist? path end
Copies this package to path (if possible)
# File lib/rubygems/package.rb, line 468 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
Extracts the files in this package into destination_dir
If pattern is specified, only entries matching that glob will be extracted.
Source
# File lib/rubygems/package.rb, line 579 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
Gzips content written to gz_io to io.
Source
# File lib/rubygems/package.rb, line 636 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
Reads and loads checksums.yaml.gz from the tar file gem
# File lib/rubygems/package.rb, line 650 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
Prepares the gem for signing and checksum generation. If a signing certificate and key are not present only checksum generation is set up.
Source
# File lib/rubygems/package.rb, line 677 def spec verify unless @spec @spec 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.
Source
# File lib/rubygems/package.rb, line 693 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 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
Protected Instance Methods
# File lib/rubygems/package.rb, line 228 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
Creates a new package that will read or write to the file gem.
Private Instance Methods
# File lib/rubygems/package.rb, line 923 def create_symlink(old_name, new_name) File.symlink(old_name, new_name) rescue Errno::EACCES, TypeError from = File.expand_path(old_name, File.dirname(new_name)) FileUtils.cp_r(from, new_name) end
Create a symlink and fallback to copy the file or directory on Windows, where symlink creation needs special privileges in form of the Developer Mode. JRuby on Windows raises TypeError from the wincode path-conversion helper when it cannot create the symlink, so fall back to copy in that case too.
# File lib/rubygems/package.rb, line 762 def excludes_rubygems_floor?(requirement, floor) capped_below_floor = requirement.requirements.any? do |op, version| case op when "<" then version <= floor when "<=", "=" then version < floor when "~>" then version.bump <= floor.release else false end end return true if capped_below_floor !requirement.satisfied_by?(floor) && requirement.requirements.any? do |op, version| ["<=", "="].include?(op) && version == floor end end
Whether requirement excludes every RubyGems version satisfying the floor, so that no RubyGems could install the built gem.
Source
# File lib/rubygems/package.rb, line 912 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 728 def normalized_required_rubygems_version(ruby_abi) minimum = Gem::Requirement.new(MINIMUM_RUBYGEMS_VERSION) existing = @spec.required_rubygems_version return minimum if existing.nil? || existing == Gem::Requirement.default floor = minimum.requirements.first.last if excludes_rubygems_floor?(existing, floor) raise ArgumentError, "Cannot build gem for Ruby ABI #{ruby_abi} because required_rubygems_version is set to #{existing}, " \ "which excludes RubyGems #{MINIMUM_RUBYGEMS_VERSION} required to install content addressable gems. " \ "Please remove or loosen the conflicting constraint." end return existing if satisfies_rubygems_floor?(existing, floor) preserved = existing.requirements.filter_map do |op, version| "#{op} #{version}" if ["~>", "<", "<=", "!="].include?(op) end normalized = Gem::Requirement.new([MINIMUM_RUBYGEMS_VERSION, *preserved]) alert_warning \ "required_rubygems_version was changed from \"#{existing}\" to \"#{normalized}\" for this build " \ "because content addressable gems can only be installed by RubyGems #{MINIMUM_RUBYGEMS_VERSION}." normalized end
The required_rubygems_version for a content-addressable build: the spec’s requirement raised to at least MINIMUM_RUBYGEMS_VERSION, warning if it had to be changed. Raises if the requirement excludes every version satisfying that floor, since no RubyGems could install the built gem.
# File lib/rubygems/package.rb, line 783 def satisfies_rubygems_floor?(requirement, floor) requirement.requirements.any? do |op, version| case op when ">=", "~>", "=", ">" then version >= floor else false end end end
Whether one of the lower bounds of requirement already guarantees the floor.
Source
# File lib/rubygems/package.rb, line 798 def validate_ruby_abi(ruby_abi) if !Gem::ContentAddress.valid_ruby_abi?(ruby_abi) raise ArgumentError, "Ruby ABI must be in X.Y format" elsif !Gem::ContentAddress.platform_eligible?(@spec.platform) raise ArgumentError, "Cannot build a gem scoped to a single Ruby ABI as no platform or a Ruby platform has been set" elsif !Gem::ContentAddress.ruby_abi_compatible?(@spec, ruby_abi) raise ArgumentError, "Cannot build gem for Ruby ABI #{ruby_abi} because required_ruby_version is set to #{@spec.required_ruby_version}. Please set required_ruby_version to \"~> #{ruby_abi}.0\"." end end
Validates that the spec can be built as a content-addressable gem scoped to ruby_abi (“X.Y”): the ABI must be well-formed, the spec must declare a non-Ruby platform, and any existing required_ruby_version must match the ABI.
Source
# File lib/rubygems/package.rb, line 862 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 load_spec_from_metadata entry rescue StandardError warn "Exception while verifying #{@gem.path}" raise end
Verifies entry in a .gem file.
Source
# File lib/rubygems/package.rb, line 883 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
Verifies the files of the gem