diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fe4f33cc..c35443079 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Get upgrade notes from Sprockets 3.x to 4.x at https://github.com/rails/sprocket - Add support for Rack 3.0. Headers set by sprockets will now be lower case. [#758](https://github.com/rails/sprockets/pull/758) - Make `Sprockets::Utils.module_include` thread safe on JRuby. [#759](https://github.com/rails/sprockets/pull/759) +- Improve reproducibility. [#761](https://github.com/rails/sprockets/pull/761) ## 4.1.0 diff --git a/lib/sprockets/manifest.rb b/lib/sprockets/manifest.rb index 8eda60e4e..38bcffd79 100644 --- a/lib/sprockets/manifest.rb +++ b/lib/sprockets/manifest.rb @@ -4,8 +4,6 @@ require 'concurrent' -require 'sprockets/manifest_utils' - module Sprockets # The Manifest logs the contents of assets compiled to a single directory. It # records basic attributes about the asset for fast lookup without having to @@ -17,7 +15,6 @@ module Sprockets # that don't have sprockets loaded. See `#assets` and `#files` for more # information about the structure. class Manifest - include ManifestUtils attr_reader :environment @@ -51,19 +48,14 @@ def initialize(*args) # Default dir to the directory of the filename @directory ||= File.dirname(@filename) if @filename - # If directory is given w/o filename, pick a random manifest location - if @directory && @filename.nil? - @filename = find_directory_manifest(@directory, logger) - end - - unless @directory && @filename + unless @directory raise ArgumentError, "manifest requires output filename" end data = {} begin - if File.exist?(@filename) + if !@filename.nil? && File.exist?(@filename) data = json_decode(File.read(@filename)) end rescue JSON::ParserError => e @@ -172,7 +164,7 @@ def compile(*args) end assets_to_export.each do |asset| - mtime = Time.now.iso8601 + mtime = Time.at(1).utc.to_datetime.iso8601 # for reproducibility files[asset.digest_path] = { 'logical_path' => asset.logical_path, 'mtime' => mtime, @@ -277,6 +269,13 @@ def clobber # Persist manifest back to FS def save data = json_encode(@data) + + # If directory is given w/o filename, use reproducible manifest location + if @directory && @filename.nil? + etag = DigestUtils.pack_hexdigest(DigestUtils.digest(@data)) + @filename = File.join(@directory, ".sprockets-manifest-#{etag}.json") + end + FileUtils.mkdir_p File.dirname(@filename) PathUtils.atomic_write(@filename) do |f| f.write(data) diff --git a/lib/sprockets/manifest_utils.rb b/lib/sprockets/manifest_utils.rb deleted file mode 100644 index 703fa1f6e..000000000 --- a/lib/sprockets/manifest_utils.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true -require 'securerandom' -require 'logger' - -module Sprockets - # Public: Manifest utilities. - module ManifestUtils - extend self - - MANIFEST_RE = /^\.sprockets-manifest-[0-9a-f]{32}.json$/ - - # Public: Generate a new random manifest path. - # - # Manifests are not intended to be accessed publicly, but typically live - # alongside public assets for convenience. To avoid being served, the - # filename is prefixed with a "." which is usually hidden by web servers - # like Apache. To help in other environments that may not control this, - # a random hex string is appended to the filename to prevent people from - # guessing the location. If directory indexes are enabled on the server, - # all bets are off. - # - # Return String path. - def generate_manifest_path - ".sprockets-manifest-#{SecureRandom.hex(16)}.json" - end - - # Public: Find or pick a new manifest filename for target build directory. - # - # dirname - String dirname - # - # Examples - # - # find_directory_manifest("/app/public/assets") - # # => "/app/public/assets/.sprockets-manifest-abc123.json" - # - # Returns String filename. - def find_directory_manifest(dirname, logger = Logger.new($stderr)) - entries = File.directory?(dirname) ? Dir.entries(dirname) : [] - manifest_entries = entries.select { |e| e =~ MANIFEST_RE } - if manifest_entries.length > 1 - manifest_entries.sort! - logger.warn("Found multiple manifests: #{manifest_entries}. Choosing the first alphabetically: #{manifest_entries.first}") - end - entry = manifest_entries.first || generate_manifest_path - File.join(dirname, entry) - end - end -end diff --git a/lib/sprockets/utils/gzip.rb b/lib/sprockets/utils/gzip.rb index 3fd5228a6..0010de1da 100644 --- a/lib/sprockets/utils/gzip.rb +++ b/lib/sprockets/utils/gzip.rb @@ -12,7 +12,7 @@ class Gzip module ZlibArchiver def self.call(file, source, mtime) gz = Zlib::GzipWriter.new(file, Zlib::BEST_COMPRESSION) - gz.mtime = mtime + gz.mtime = 1 # for reproducibility gz.write(source) gz.close diff --git a/test/test_manifest.rb b/test/test_manifest.rb index c8f554292..f9d4a92a6 100644 --- a/test/test_manifest.rb +++ b/test/test_manifest.rb @@ -50,11 +50,10 @@ def teardown assert_equal filename, manifest.path end - test "specify manifest directory yields random .sprockets-manifest-*.json" do + test "specify manifest directory reproducible .sprockets-manifest-*.json" do manifest = Sprockets::Manifest.new(@env, @dir) assert_equal @dir, manifest.directory - assert_match(/^\.sprockets-manifest-[a-f0-9]{32}.json/, File.basename(manifest.filename)) manifest.save assert_match(/^\.sprockets-manifest-[a-f0-9]{32}.json/, File.basename(manifest.filename)) diff --git a/test/test_manifest_utils.rb b/test/test_manifest_utils.rb deleted file mode 100644 index 02a1755bb..000000000 --- a/test/test_manifest_utils.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true -require 'minitest/autorun' -require 'sprockets/manifest_utils' -require 'logger' - -class TestManifestUtils < MiniTest::Test - include Sprockets::ManifestUtils - - def test_generate_manifest_path - assert_match(MANIFEST_RE, generate_manifest_path) - end - - def test_find_directory_manifest - root = File.expand_path("../fixtures/manifest_utils", __FILE__) - - assert_match MANIFEST_RE, File.basename(find_directory_manifest(root)) - - assert_equal "#{root}/default/.sprockets-manifest-f4bf345974645583d284686ddfb7625e.json", - find_directory_manifest("#{root}/default") - - end - - def test_warn_on_two - root = File.expand_path("../fixtures/manifest_utils", __FILE__) - - assert_match MANIFEST_RE, File.basename(find_directory_manifest(root)) - - r, w = IO.pipe - logger = Logger.new(w) - # finds the first one alphabetically - assert_equal "#{root}/with_two_manifests/.sprockets-manifest-00000000000000000000000000000000.json", - find_directory_manifest("#{root}/with_two_manifests", logger) - output = r.gets - - assert_match(/W, \[[^\]]+\] WARN -- : Found multiple manifests: .+ Choosing the first alphabetically: \.sprockets-manifest-00000000000000000000000000000000\.json/, output) - end -end