summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEllen Marie Dash <the@smallest.dog>2019-08-17 04:45:09 +0000
committerHiroshi SHIBATA <hsbt@ruby-lang.org>2019-09-26 17:48:00 +0900
commit508afe2c26737e0be60a72faa9d6740a06b0914c (patch)
treeb912da8888b6d1b476b8fdc67562cbf9b3b9cdef
parent8436b2717c458a554dd81456a8e6e030e2c3e038 (diff)
[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. https://github.com/rubygems/rubygems/commit/d830d53f59
-rw-r--r--lib/rubygems.rb17
-rw-r--r--lib/rubygems/package.rb2
-rw-r--r--lib/rubygems/package/tar_writer.rb8
-rw-r--r--lib/rubygems/specification.rb2
-rw-r--r--test/rubygems/test_gem_commands_build_command.rb32
-rw-r--r--test/rubygems/test_gem_package.rb27
6 files changed, 82 insertions, 6 deletions
diff --git a/lib/rubygems.rb b/lib/rubygems.rb
index 4d06d98fdf..2676fbd3db 100644
--- a/lib/rubygems.rb
+++ b/lib/rubygems.rb
@@ -1242,6 +1242,23 @@ An Array (#{env.inspect}) was passed in from #{caller[3]}
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
# methods, and then we switch over to `class << self` here. Pick one or the
# other.
diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb
index 16cda5affe..bef37aedc1 100644
--- a/lib/rubygems/package.rb
+++ b/lib/rubygems/package.rb
@@ -193,7 +193,7 @@ class Gem::Package
def initialize(gem, security_policy) # :notnew:
@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 = {}
@contents = nil
@digests = Hash.new { |h, algorithm| h[algorithm] = {} }
diff --git a/lib/rubygems/package/tar_writer.rb b/lib/rubygems/package/tar_writer.rb
index 87ee39a944..96d8184e8e 100644
--- a/lib/rubygems/package/tar_writer.rb
+++ b/lib/rubygems/package/tar_writer.rb
@@ -123,7 +123,7 @@ class Gem::Package::TarWriter
header = Gem::Package::TarHeader.new :name => name, :mode => mode,
: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.pos = final_pos
@@ -217,7 +217,7 @@ class Gem::Package::TarWriter
header = Gem::Package::TarHeader.new(:name => name, :mode => mode,
: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
os = BoundedStream.new @io, size
@@ -245,7 +245,7 @@ class Gem::Package::TarWriter
:size => 0, :typeflag => "2",
:linkname => target,
: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
@@ -298,7 +298,7 @@ class Gem::Package::TarWriter
header = Gem::Package::TarHeader.new :name => name, :mode => mode,
:typeflag => "5", :size => 0,
: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
diff --git a/lib/rubygems/specification.rb b/lib/rubygems/specification.rb
index c023e4f4aa..b3db311cbf 100644
--- a/lib/rubygems/specification.rb
+++ b/lib/rubygems/specification.rb
@@ -1667,7 +1667,7 @@ class Gem::Specification < Gem::BasicSpecification
# https://reproducible-builds.org/specs/source-date-epoch/
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
DateLike = Object.new # :nodoc:
diff --git a/test/rubygems/test_gem_commands_build_command.rb b/test/rubygems/test_gem_commands_build_command.rb
index 0c511b2f08..50c447e2eb 100644
--- a/test/rubygems/test_gem_commands_build_command.rb
+++ b/test/rubygems/test_gem_commands_build_command.rb
@@ -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)
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
diff --git a/test/rubygems/test_gem_package.rb b/test/rubygems/test_gem_package.rb
index f2f712ebe0..a282e0cc5f 100644
--- a/test/rubygems/test_gem_package.rb
+++ b/test/rubygems/test_gem_package.rb
@@ -2,6 +2,7 @@
# frozen_string_literal: true
require 'rubygems/package/tar_test_case'
+require 'digest'
class TestGemPackage < Gem::Package::TarTestCase
@@ -123,6 +124,32 @@ class TestGemPackage < Gem::Package::TarTestCase
ENV["SOURCE_DATE_EPOCH"] = epoch
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
spec = Gem::Specification.new
spec.files = %w[lib/code.rb lib/empty]