Skip to content

Commit

Permalink
Set SOURCE_DATE_EPOCH env var if not provided.
Browse files Browse the repository at this point in the history
Fixes rubygems#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.
  • Loading branch information
duckinator committed Aug 17, 2019
1 parent 714a5ff commit 5be84d1
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 6 deletions.
17 changes: 17 additions & 0 deletions lib/rubygems.rb
Expand Up @@ -1242,6 +1242,23 @@ class << self

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"].to_s.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.
Expand Down
2 changes: 1 addition & 1 deletion lib/rubygems/package.rb
Expand Up @@ -188,7 +188,7 @@ def self.raw_spec(path, security_policy = nil)
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] = {} }
Expand Down
8 changes: 4 additions & 4 deletions lib/rubygems/package/tar_writer.rb
Expand Up @@ -123,7 +123,7 @@ def add_file(name, mode) # :yields: io

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
Expand Down Expand Up @@ -217,7 +217,7 @@ def add_file_simple(name, mode, size) # :yields: io

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
Expand Down Expand Up @@ -245,7 +245,7 @@ def add_symlink(name, target, mode)
: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

Expand Down Expand Up @@ -298,7 +298,7 @@ def mkdir(name, mode)
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

Expand Down
2 changes: 1 addition & 1 deletion lib/rubygems/specification.rb
Expand Up @@ -1667,7 +1667,7 @@ def has_conflicts?
# 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 ||= ENV["SOURCE_DATE_EPOCH"] ? Time.utc(*Gem.source_date_epoch.utc.to_a[3..5].reverse) : TODAY
end

DateLike = Object.new # :nodoc:
Expand Down
69 changes: 69 additions & 0 deletions test/rubygems/test_gem_package.rb
Expand Up @@ -2,6 +2,7 @@
# frozen_string_literal: true

require 'rubygems/package/tar_test_case'
require 'digest'

class TestGemPackage < Gem::Package::TarTestCase

Expand Down Expand Up @@ -123,6 +124,74 @@ def test_build_time_source_date_epoch
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_build_time_source_date_epoch_reproducible
epoch = ENV["SOURCE_DATE_EPOCH"]
ENV["SOURCE_DATE_EPOCH"] = nil

spec = Gem::Specification.new 'build', '1'
spec.summary = 'build'
spec.authors = 'build'
spec.files = ['lib/code.rb']
spec.rubygems_version = Gem::Version.new '0'

package1 = Gem::Package.new spec.file_name

assert package1.build_time.is_a?(Time)

ENV["SOURCE_DATE_EPOCH"] = package1.build_time.to_i.to_s

# Guarantee the time has changed.
sleep 1 if Time.now.to_i == package1.build_time.to_i

package2 = Gem::Package.new spec.file_name

# Verify the build times are the same.
assert_equal package1.build_time, package2.build_time

s1 = StringIO.new
package1.gzip_to s1 do |io|
io.write spec.to_yaml
end
package1_digest = Digest::SHA512.hexdigest s1.string

s2 = StringIO.new
package2.gzip_to s2 do |io|
io.write spec.to_yaml
end
package2_digest = Digest::SHA512.hexdigest s2.string

# Verify the contents are the same.
assert_equal package1_digest, package2_digest
ensure
ENV["SOURCE_DATE_EPOCH"] = epoch
end

def test_add_files
spec = Gem::Specification.new
spec.files = %w[lib/code.rb lib/empty]
Expand Down

0 comments on commit 5be84d1

Please sign in to comment.