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
Checksums for the contents of the package
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.
The security policy used for verifying the contents of this package.
Sets the Gem::Specification
to use to build this package.
Permission for directories
Permission for program files
Permission for other files
# File tmp/rubies/ruby-3.3.0/lib/rubygems/package.rb, line 132
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
# File tmp/rubies/ruby-3.3.0/lib/rubygems/package.rb, line 149
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
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.
# File tmp/rubies/ruby-3.3.0/lib/rubygems/package.rb, line 172
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
.
# File tmp/rubies/ruby-3.3.0/lib/rubygems/package.rb, line 222
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
Adds a checksum for each entry in the gem to checksums.yaml.gz.
# File tmp/rubies/ruby-3.3.0/lib/rubygems/package.rb, line 293
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.mark_version
@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
Builds this package based on the specification set by spec=
# File tmp/rubies/ruby-3.3.0/lib/rubygems/package.rb, line 328
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
# File tmp/rubies/ruby-3.3.0/lib/rubygems/package.rb, line 215
def copy_to(path)
FileUtils.cp @gem.path, path unless File.exist? path
end
Copies this package to path
(if possible)
# File tmp/rubies/ruby-3.3.0/lib/rubygems/package.rb, line 387
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.
# File tmp/rubies/ruby-3.3.0/lib/rubygems/package.rb, line 492
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
.
# File tmp/rubies/ruby-3.3.0/lib/rubygems/package.rb, line 196
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
.
# File tmp/rubies/ruby-3.3.0/lib/rubygems/package.rb, line 519
def normalize_path(pathname)
if Gem.win_platform?
pathname.downcase
else
pathname
end
end
# File tmp/rubies/ruby-3.3.0/lib/rubygems/package.rb, line 555
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 gz_io.read
end
end
end
Reads and loads checksums.yaml.gz from the tar file gem
# File tmp/rubies/ruby-3.3.0/lib/rubygems/package.rb, line 569
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.
# File tmp/rubies/ruby-3.3.0/lib/rubygems/package.rb, line 596
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.
# File tmp/rubies/ruby-3.3.0/lib/rubygems/package.rb, line 612
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
# File tmp/rubies/ruby-3.3.0/lib/rubygems/package.rb, line 661
def verify_entry(entry)
file_name = entry.full_name
@files << file_name
case file_name
when /\.sig$/ then
@signatures[$`] = entry.read 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 entry
in a .gem file.
# File tmp/rubies/ruby-3.3.0/lib/rubygems/package.rb, line 687
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