mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
[rubygems/rubygems] Set SOURCE_DATE_EPOCH env var if not provided.
Fixes #2290.
1. `Gem::Specification.date` returns SOURCE_DATE_EPOCH when defined,
2. this commit makes RubyGems set it _persistently_ when not provided.
This combination means that you can build a gem, check the build time,
and use that value to generate a new build -- and then verify they're
the same.
d830d53f59
This commit is contained in:
parent
8436b2717c
commit
508afe2c26
6 changed files with 82 additions and 6 deletions
|
@ -1242,6 +1242,23 @@ An Array (#{env.inspect}) was passed in from #{caller[3]}
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# The SOURCE_DATE_EPOCH environment variable (or, if that's not set, the current time), converted to Time object.
|
||||||
|
# This is used throughout RubyGems for enabling reproducible builds.
|
||||||
|
#
|
||||||
|
# If it is not set as an environment variable already, this also sets it.
|
||||||
|
#
|
||||||
|
# Details on SOURCE_DATE_EPOCH:
|
||||||
|
# https://reproducible-builds.org/specs/source-date-epoch/
|
||||||
|
|
||||||
|
def self.source_date_epoch
|
||||||
|
if ENV["SOURCE_DATE_EPOCH"].nil? || ENV["SOURCE_DATE_EPOCH"].empty?
|
||||||
|
ENV["SOURCE_DATE_EPOCH"] = Time.now.to_i.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
Time.at(ENV["SOURCE_DATE_EPOCH"].to_i).utc.freeze
|
||||||
|
end
|
||||||
|
|
||||||
# FIX: Almost everywhere else we use the `def self.` way of defining class
|
# FIX: Almost everywhere else we use the `def self.` way of defining class
|
||||||
# methods, and then we switch over to `class << self` here. Pick one or the
|
# methods, and then we switch over to `class << self` here. Pick one or the
|
||||||
# other.
|
# other.
|
||||||
|
|
|
@ -193,7 +193,7 @@ class Gem::Package
|
||||||
def initialize(gem, security_policy) # :notnew:
|
def initialize(gem, security_policy) # :notnew:
|
||||||
@gem = gem
|
@gem = gem
|
||||||
|
|
||||||
@build_time = ENV["SOURCE_DATE_EPOCH"] ? Time.at(ENV["SOURCE_DATE_EPOCH"].to_i).utc : Time.now
|
@build_time = Gem.source_date_epoch
|
||||||
@checksums = {}
|
@checksums = {}
|
||||||
@contents = nil
|
@contents = nil
|
||||||
@digests = Hash.new { |h, algorithm| h[algorithm] = {} }
|
@digests = Hash.new { |h, algorithm| h[algorithm] = {} }
|
||||||
|
|
|
@ -123,7 +123,7 @@ class Gem::Package::TarWriter
|
||||||
|
|
||||||
header = Gem::Package::TarHeader.new :name => name, :mode => mode,
|
header = Gem::Package::TarHeader.new :name => name, :mode => mode,
|
||||||
:size => size, :prefix => prefix,
|
:size => size, :prefix => prefix,
|
||||||
:mtime => ENV["SOURCE_DATE_EPOCH"] ? Time.at(ENV["SOURCE_DATE_EPOCH"].to_i).utc : Time.now
|
:mtime => Gem.source_date_epoch
|
||||||
|
|
||||||
@io.write header
|
@io.write header
|
||||||
@io.pos = final_pos
|
@io.pos = final_pos
|
||||||
|
@ -217,7 +217,7 @@ class Gem::Package::TarWriter
|
||||||
|
|
||||||
header = Gem::Package::TarHeader.new(:name => name, :mode => mode,
|
header = Gem::Package::TarHeader.new(:name => name, :mode => mode,
|
||||||
:size => size, :prefix => prefix,
|
:size => size, :prefix => prefix,
|
||||||
:mtime => ENV["SOURCE_DATE_EPOCH"] ? Time.at(ENV["SOURCE_DATE_EPOCH"].to_i).utc : Time.now).to_s
|
:mtime => Gem.source_date_epoch).to_s
|
||||||
|
|
||||||
@io.write header
|
@io.write header
|
||||||
os = BoundedStream.new @io, size
|
os = BoundedStream.new @io, size
|
||||||
|
@ -245,7 +245,7 @@ class Gem::Package::TarWriter
|
||||||
:size => 0, :typeflag => "2",
|
:size => 0, :typeflag => "2",
|
||||||
:linkname => target,
|
:linkname => target,
|
||||||
:prefix => prefix,
|
:prefix => prefix,
|
||||||
:mtime => ENV["SOURCE_DATE_EPOCH"] ? Time.at(ENV["SOURCE_DATE_EPOCH"].to_i).utc : Time.now).to_s
|
:mtime => Gem.source_date_epoch).to_s
|
||||||
|
|
||||||
@io.write header
|
@io.write header
|
||||||
|
|
||||||
|
@ -298,7 +298,7 @@ class Gem::Package::TarWriter
|
||||||
header = Gem::Package::TarHeader.new :name => name, :mode => mode,
|
header = Gem::Package::TarHeader.new :name => name, :mode => mode,
|
||||||
:typeflag => "5", :size => 0,
|
:typeflag => "5", :size => 0,
|
||||||
:prefix => prefix,
|
:prefix => prefix,
|
||||||
:mtime => ENV["SOURCE_DATE_EPOCH"] ? Time.at(ENV["SOURCE_DATE_EPOCH"].to_i).utc : Time.now
|
:mtime => Gem.source_date_epoch
|
||||||
|
|
||||||
@io.write header
|
@io.write header
|
||||||
|
|
||||||
|
|
|
@ -1667,7 +1667,7 @@ class Gem::Specification < Gem::BasicSpecification
|
||||||
# https://reproducible-builds.org/specs/source-date-epoch/
|
# https://reproducible-builds.org/specs/source-date-epoch/
|
||||||
|
|
||||||
def date
|
def date
|
||||||
@date ||= ENV["SOURCE_DATE_EPOCH"] ? Time.utc(*Time.at(ENV["SOURCE_DATE_EPOCH"].to_i).utc.to_a[3..5].reverse) : TODAY
|
@date ||= Time.utc(*Gem.source_date_epoch.utc.to_a[3..5].reverse)
|
||||||
end
|
end
|
||||||
|
|
||||||
DateLike = Object.new # :nodoc:
|
DateLike = Object.new # :nodoc:
|
||||||
|
|
|
@ -457,4 +457,36 @@ class TestGemCommandsBuildCommand < Gem::TestCase
|
||||||
assert_match(/INFO: Your expired cert will be located at: .+\Wgem-public_cert\.pem\.expired\.[0-9]+/, output.shift)
|
assert_match(/INFO: Your expired cert will be located at: .+\Wgem-public_cert\.pem\.expired\.[0-9]+/, output.shift)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_build_is_reproducible
|
||||||
|
epoch = ENV["SOURCE_DATE_EPOCH"]
|
||||||
|
new_epoch = Time.now.to_i.to_s
|
||||||
|
ENV["SOURCE_DATE_EPOCH"] = new_epoch
|
||||||
|
|
||||||
|
gem_file = File.basename(@gem.cache_file)
|
||||||
|
|
||||||
|
gemspec_file = File.join(@tempdir, @gem.spec_name)
|
||||||
|
File.write(gemspec_file, @gem.to_ruby)
|
||||||
|
@cmd.options[:args] = [gemspec_file]
|
||||||
|
|
||||||
|
util_test_build_gem @gem
|
||||||
|
|
||||||
|
build1_contents = File.read(gem_file)
|
||||||
|
|
||||||
|
# Guarantee the time has changed.
|
||||||
|
sleep 1 if Time.now.to_i == new_epoch
|
||||||
|
|
||||||
|
ENV["SOURCE_DATE_EPOCH"] = new_epoch
|
||||||
|
|
||||||
|
@ui = Gem::MockGemUi.new
|
||||||
|
@cmd.options[:args] = [gemspec_file]
|
||||||
|
|
||||||
|
util_test_build_gem @gem
|
||||||
|
|
||||||
|
build2_contents = File.read(gem_file)
|
||||||
|
|
||||||
|
assert_equal build1_contents, build2_contents
|
||||||
|
ensure
|
||||||
|
ENV["SOURCE_DATE_EPOCH"] = epoch
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'rubygems/package/tar_test_case'
|
require 'rubygems/package/tar_test_case'
|
||||||
|
require 'digest'
|
||||||
|
|
||||||
class TestGemPackage < Gem::Package::TarTestCase
|
class TestGemPackage < Gem::Package::TarTestCase
|
||||||
|
|
||||||
|
@ -123,6 +124,32 @@ class TestGemPackage < Gem::Package::TarTestCase
|
||||||
ENV["SOURCE_DATE_EPOCH"] = epoch
|
ENV["SOURCE_DATE_EPOCH"] = epoch
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_build_time_source_date_epoch_automatically_set
|
||||||
|
epoch = ENV["SOURCE_DATE_EPOCH"]
|
||||||
|
ENV["SOURCE_DATE_EPOCH"] = nil
|
||||||
|
|
||||||
|
start_time = Time.now.utc.to_i
|
||||||
|
|
||||||
|
spec = Gem::Specification.new 'build', '1'
|
||||||
|
spec.summary = 'build'
|
||||||
|
spec.authors = 'build'
|
||||||
|
spec.files = ['lib/code.rb']
|
||||||
|
spec.rubygems_version = Gem::Version.new '0'
|
||||||
|
|
||||||
|
package = Gem::Package.new spec.file_name
|
||||||
|
|
||||||
|
end_time = Time.now.utc.to_i
|
||||||
|
|
||||||
|
assert package.build_time.is_a?(Time)
|
||||||
|
|
||||||
|
build_time = package.build_time.to_i
|
||||||
|
|
||||||
|
assert(start_time <= build_time)
|
||||||
|
assert(build_time <= end_time)
|
||||||
|
ensure
|
||||||
|
ENV["SOURCE_DATE_EPOCH"] = epoch
|
||||||
|
end
|
||||||
|
|
||||||
def test_add_files
|
def test_add_files
|
||||||
spec = Gem::Specification.new
|
spec = Gem::Specification.new
|
||||||
spec.files = %w[lib/code.rb lib/empty]
|
spec.files = %w[lib/code.rb lib/empty]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue