module GemChecksums

def generate(git_dry_run: false)

Returns:
  • (void) -

Parameters:
  • git_dry_run (Boolean) -- when true, perform a dry-run and do not leave files staged
def generate(git_dry_run: false)
  git_dry_run_flag = (git_dry_run || GIT_DRY_RUN_ENV) ? "--dry-run" : nil
  warn("Will run git commit with --dry-run") if git_dry_run_flag
  # Header: identify the gem and version being run
  begin
    puts "[ stone_checksums #{::StoneChecksums::Version::VERSION} ]"
  rescue StandardError
    # If for any reason the version constant isn't available, skip header gracefully
  end
  # Bundler version gate for reproducibility requirements
  bundler_ver = Gem::Version.new(Bundler::VERSION)
  requires_epoch = bundler_ver < Gem::Version.new("2.7.0")
  if requires_epoch
    # For older bundler, ask the user whether to proceed, or quit to update.
    proceed = ENV.fetch("GEM_CHECKSUMS_ASSUME_YES", "").casecmp("true").zero?
    unless proceed
      # Non-interactive prompt: advise and abort
      prompt_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).
      PROMPT
      warn(prompt_msg)
      # Continue to enforce SOURCE_DATE_EPOCH below; if not set, this will raise.
    end
    build_time = ENV.fetch("SOURCE_DATE_EPOCH", "")
    build_time_missing = !(build_time =~ /\d{10,}/)
    if build_time_missing
      warn(BUILD_TIME_WARNING)
      raise Error, BUILD_TIME_ERROR_MESSAGE
    end
  end
  gem_path_parts =
    case RUNNING_AS
    when "rake", "gem_checksums"
      first_arg = ARGV.first
      first_arg.respond_to?(:split) ? first_arg.split("/") : []
    else # e.g. "rspec"
      []
    end
  if gem_path_parts.any?
    gem_name = gem_path_parts.last
    gem_pkg = File.join(gem_path_parts)
    puts "Looking for: #{gem_pkg.inspect}"
    gems = Dir[gem_pkg]
    raise Error, "Unable to find gem #{gem_pkg}" if gems.empty?
    puts "Found: #{gems.inspect}"
  else
    gem_pkgs = File.join(PACKAGE_DIR, "*.gem")
    puts "Looking for: #{gem_pkgs.inspect}"
    gems = Dir[gem_pkgs]
    raise Error, "Unable to find gems #{gem_pkgs}" if gems.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.last
    gem_path_parts = gem_pkg.split("/")
    gem_name = gem_path_parts.last
    puts "Found: #{gems.length} gems; latest is #{gem_name}"
  end
  pkg_bits = File.read(gem_pkg)
  # SHA-512 digest is 8 64-bit words
  digest512_64bit = Digest::SHA512.new.hexdigest(pkg_bits)
  digest512_64bit_path = "#{CHECKSUMS_DIR}/#{gem_name}.sha512"
  Dir.mkdir(CHECKSUMS_DIR) unless Dir.exist?(CHECKSUMS_DIR)
  File.write(digest512_64bit_path, digest512_64bit)
  # SHA-256 digest is 8 32-bit words
  digest256_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_MSG
  if git_dry_run_flag
    git_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_MSG
  end
  puts <<~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}
  RESULTS
  if git_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 process
    exec(git_cmd)
  end
end