# frozen_string_literal: true# Std librequire"digest/sha2"require"fileutils"# external gemsrequire"version_gem"# this library's versionrequire_relative"gem_checksums/version"# Primary namespace of this librarymoduleGemChecksums# Errors raised by this gem will use this classclassError<StandardError;end# Final clause of Regex `(?=\.gem)` is a positive lookahead assertion# See: https://learnbyexample.github.io/Ruby_Regexp/lookarounds.html#positive-lookarounds# Used to pattern match against a gem package name, which always ends with .gem.# The positive lookahead ensures it is present, and prevents it from being captured.VERSION_REGEX=/((\d+\.\d+\.\d+)([-.][0-9A-Za-z-]+)*)(?=\.gem)/.freezeRUNNING_AS=File.basename($PROGRAM_NAME)BUILD_TIME_ERROR_MESSAGE="Environment variable SOURCE_DATE_EPOCH must be set. You'll need to rebuild the gem. See README.md of stone_checksums"GIT_DRY_RUN_ENV=ENV.fetch("GEM_CHECKSUMS_GIT_DRY_RUN","false").casecmp("true")==0CHECKSUMS_DIR=ENV.fetch("GEM_CHECKSUMS_CHECKSUMS_DIR","checksums")PACKAGE_DIR=ENV.fetch("GEM_CHECKSUMS_PACKAGE_DIR","pkg")BUILD_TIME_WARNING=<<~BUILD_TIME_WARNING
WARNING: Build time not provided via environment variable SOURCE_DATE_EPOCH.
When using Bundler < 2.7.0, you must set SOURCE_DATE_EPOCH *before* building
the gem to ensure consistent SHA-256 & SHA-512 checksums.
PREFERRED: Upgrade to Bundler >= 2.7.0, which uses a constant timestamp for gem builds,
making SOURCE_DATE_EPOCH unnecessary for reproducible checksums.
IMPORTANT: If you choose to set the build time via SOURCE_DATE_EPOCH,
you must re-build the gem, i.e. `bundle exec rake build` or `gem build`.
How to set the build time (only needed for Bundler < 2.7.0):
In zsh shell:
- export SOURCE_DATE_EPOCH=$EPOCHSECONDS && echo $SOURCE_DATE_EPOCH
- If the echo above has no output, then it didn't work.
- Note that you'll need the `zsh/datetime` module enabled.
In fish shell:
- set -x SOURCE_DATE_EPOCH (date +%s)
- echo $SOURCE_DATE_EPOCH
In bash shell:
- export SOURCE_DATE_EPOCH=$(date +%s) && echo $SOURCE_DATE_EPOCH
BUILD_TIME_WARNING# Make this gem's rake tasks available in your Rakefile:## require "gem_checksums"## @return [void]definstall_tasksload("gem_checksums/tasks.rb")endmodule_function:install_tasks# Script, stolen from myself, from https://github.com/rubygems/guides/pull/325# NOTE (Bundler < 2.7.0): SOURCE_DATE_EPOCH must be set in your environment prior to building the gem.# Bundler >= 2.7.0 uses a constant timestamp internally, so SOURCE_DATE_EPOCH is no longer required.# This ensures that the gem build, and the gem checksum will use the same timestamp,# and thus will match the SHA-256 checksum generated for every gem on Rubygems.org.# Generate SHA-256 and SHA-512 checksums for a built .gem and commit them.## Behavior regarding reproducible builds depends on Bundler version:# - Bundler >= 2.7.0: SOURCE_DATE_EPOCH is not required; Bundler uses a constant timestamp.# - Bundler < 2.7.0: you must set SOURCE_DATE_EPOCH, or upgrade Bundler. If# GEM_CHECKSUMS_ASSUME_YES=true is set, the check proceeds non-interactively, but# SOURCE_DATE_EPOCH is still required.## The generated checksum files are written to the directory configured via# GEM_CHECKSUMS_CHECKSUMS_DIR (default: "checksums"). By default, the newest .gem in# GEM_CHECKSUMS_PACKAGE_DIR (default: "pkg") is used, unless a specific .gem path is# passed as the first CLI argument when running under Rake or the gem_checksums CLI.## By default this command will exec a `git add && git commit` to include the checksum# files. When `git_dry_run` is true, or GEM_CHECKSUMS_GIT_DRY_RUN=true, a dry-run commit# is performed, and temporary files are cleaned up.## @param git_dry_run [Boolean] when true, perform a dry-run and do not leave files staged# @return [void]defgenerate(git_dry_run: false)git_dry_run_flag=(git_dry_run||GIT_DRY_RUN_ENV)?"--dry-run":nilwarn("Will run git commit with --dry-run")ifgit_dry_run_flag# Header: identify the gem and version being runbeginputs"[ stone_checksums #{::StoneChecksums::Version::VERSION} ]"rescueStandardError# If for any reason the version constant isn't available, skip header gracefullyend# Bundler version gate for reproducibility requirementsbundler_ver=Gem::Version.new(Bundler::VERSION)requires_epoch=bundler_ver<Gem::Version.new("2.7.0")ifrequires_epoch# For older bundler, ask the user whether to proceed, or quit to update.proceed=ENV.fetch("GEM_CHECKSUMS_ASSUME_YES","").casecmp("true").zero?unlessproceed# Non-interactive prompt: advise and abortprompt_msg=<<~PROMPT
Detected Bundler #{bundler_ver||"(unknown)"} which is older than 2.7.0.
For reproducible builds without SOURCE_DATE_EPOCH, please update Bundler to >= 2.7.0.
If you still want to proceed with this older Bundler, you must set SOURCE_DATE_EPOCH and re-run.
Tip: set GEM_CHECKSUMS_ASSUME_YES=true to proceed non-interactively (still requires SOURCE_DATE_EPOCH).
PROMPTwarn(prompt_msg)# Continue to enforce SOURCE_DATE_EPOCH below; if not set, this will raise.endbuild_time=ENV.fetch("SOURCE_DATE_EPOCH","")build_time_missing=!(build_time=~/\d{10,}/)ifbuild_time_missingwarn(BUILD_TIME_WARNING)raiseError,BUILD_TIME_ERROR_MESSAGEendendgem_path_parts=caseRUNNING_ASwhen"rake","gem_checksums"first_arg=ARGV.firstfirst_arg.respond_to?(:split)?first_arg.split("/"):[]else# e.g. "rspec"[]endifgem_path_parts.any?gem_name=gem_path_parts.lastgem_pkg=File.join(gem_path_parts)puts"Looking for: #{gem_pkg.inspect}"gems=Dir[gem_pkg]raiseError,"Unable to find gem #{gem_pkg}"ifgems.empty?puts"Found: #{gems.inspect}"elsegem_pkgs=File.join(PACKAGE_DIR,"*.gem")puts"Looking for: #{gem_pkgs.inspect}"gems=Dir[gem_pkgs]raiseError,"Unable to find gems #{gem_pkgs}"ifgems.empty?# Sort by newest last# [ "my_gem-2.3.9.gem", "my_gem-2.3.11.pre.alpha.4.gem", "my_gem-2.3.15.gem", ... ]gems.sort_by!{|gem|Gem::Version.new(gem[VERSION_REGEX])}gem_pkg=gems.lastgem_path_parts=gem_pkg.split("/")gem_name=gem_path_parts.lastputs"Found: #{gems.length} gems; latest is #{gem_name}"endpkg_bits=File.read(gem_pkg)# SHA-512 digest is 8 64-bit wordsdigest512_64bit=Digest::SHA512.new.hexdigest(pkg_bits)digest512_64bit_path="#{CHECKSUMS_DIR}/#{gem_name}.sha512"Dir.mkdir(CHECKSUMS_DIR)unlessDir.exist?(CHECKSUMS_DIR)File.write(digest512_64bit_path,digest512_64bit)# SHA-256 digest is 8 32-bit wordsdigest256_32bit=Digest::SHA256.new.hexdigest(pkg_bits)digest256_32bit_path="#{CHECKSUMS_DIR}/#{gem_name}.sha256"File.write(digest256_32bit_path,digest256_32bit)version=gem_name[VERSION_REGEX]git_cmd=<<~GIT_MSG.rstrip
git add #{CHECKSUMS_DIR}/* && \
git commit #{git_dry_run_flag} -m "🔒️ Checksums for v#{version}"
GIT_MSGifgit_dry_run_flaggit_cmd+=<<~CLEANUP_MSG
&& \
echo "Cleaning up in dry run mode" && \
git reset #{digest512_64bit_path} && \
git reset #{digest256_32bit_path} && \
rm -f #{digest512_64bit_path} && \
rm -f #{digest256_32bit_path} CLEANUP_MSGendputs<<~RESULTS
[ GEM: #{gem_name} ]
[ VERSION: #{version} ]
[ GEM PKG LOCATION: #{gem_pkg} ]
[ CHECKSUM SHA-256: #{digest256_32bit} ]
[ CHECKSUM SHA-512: #{digest512_64bit} ]
[ CHECKSUM SHA-256 PATH: #{digest256_32bit_path} ]
[ CHECKSUM SHA-512 PATH: #{digest512_64bit_path} ]
... Running ...
#{git_cmd} RESULTSifgit_dry_run_flag%x{#{git_cmd}}else# `exec` will replace the current process with the git process, and exit.# Within the generate method, Ruby code placed after the `exec` *will not be run*:# See: https://www.akshaykhot.com/call-shell-commands-in-ruby# But we can't exit the process when testing from RSpec,# since that would exit the parent RSpec processexec(git_cmd)endendmodule_function:generateendGemChecksums::Version.class_evaldoextendVersionGem::BasicendGemChecksums.install_tasksifGemChecksums::RUNNING_AS=="rake"