Module: GemChecksums

Defined in:
lib/gem_checksums.rb,
lib/gem_checksums/version.rb

Overview

Semantic version for the GemChecksums namespace

Defined Under Namespace

Modules: Version Classes: Error

Constant Summary collapse

VERSION_REGEX =

Final clause of Regex ‘(?=.gem)` is a positive lookahead assertion See: 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.

/((\d+\.\d+\.\d+)([-.][0-9A-Za-z-]+)*)(?=\.gem)/.freeze
RUNNING_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") == 0
CHECKSUMS_DIR =
ENV.fetch("GEM_CHECKSUMS_CHECKSUMS_DIR", "checksums")
PACKAGE_DIR =
ENV.fetch("GEM_CHECKSUMS_PACKAGE_DIR", "pkg")
BUILD_TIME_WARNING =
"WARNING: Build time not provided via environment variable SOURCE_DATE_EPOCH.\nWhen using Bundler < 2.7.0, you must set SOURCE_DATE_EPOCH *before* building\nthe gem to ensure consistent SHA-256 & SHA-512 checksums.\n\nPREFERRED: Upgrade to Bundler >= 2.7.0, which uses a constant timestamp for gem builds,\n         making SOURCE_DATE_EPOCH unnecessary for reproducible checksums.\n\nIMPORTANT: If you choose to set the build time via SOURCE_DATE_EPOCH,\n         you must re-build the gem, i.e. `bundle exec rake build` or `gem build`.\n\nHow to set the build time (only needed for Bundler < 2.7.0):\n\nIn zsh shell:\n- export SOURCE_DATE_EPOCH=$EPOCHSECONDS && echo $SOURCE_DATE_EPOCH\n- If the echo above has no output, then it didn't work.\n- Note that you'll need the `zsh/datetime` module enabled.\n\nIn fish shell:\n- set -x SOURCE_DATE_EPOCH (date +%s)\n- echo $SOURCE_DATE_EPOCH\n\nIn bash shell:\n- export SOURCE_DATE_EPOCH=$(date +%s) && echo $SOURCE_DATE_EPOCH\n\n"

Class Method Summary collapse

Class Method Details

.generate(git_dry_run: false) ⇒ void

This method returns an undefined value.

Script, stolen from myself, from 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.



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/gem_checksums.rb', line 89

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 = "Detected Bundler \#{bundler_ver || \"(unknown)\"} which is older than 2.7.0.\nFor reproducible builds without SOURCE_DATE_EPOCH, please update Bundler to >= 2.7.0.\nIf you still want to proceed with this older Bundler, you must set SOURCE_DATE_EPOCH and re-run.\nTip: set GEM_CHECKSUMS_ASSUME_YES=true to proceed non-interactively (still requires SOURCE_DATE_EPOCH).\n      PROMPT\n      warn(prompt_msg)\n      # Continue to enforce SOURCE_DATE_EPOCH below; if not set, this will raise.\n    end\n\n    build_time = ENV.fetch(\"SOURCE_DATE_EPOCH\", \"\")\n    build_time_missing = !(build_time =~ /\\d{10,}/)\n\n    if build_time_missing\n      warn(BUILD_TIME_WARNING)\n      raise Error, BUILD_TIME_ERROR_MESSAGE\n    end\n  end\n\n  gem_path_parts =\n    case RUNNING_AS\n    when \"rake\", \"gem_checksums\"\n      first_arg = ARGV.first\n      first_arg.respond_to?(:split) ? first_arg.split(\"/\") : []\n    else # e.g. \"rspec\"\n      []\n    end\n\n  if gem_path_parts.any?\n    gem_name = gem_path_parts.last\n    gem_pkg = File.join(gem_path_parts)\n    puts \"Looking for: \#{gem_pkg.inspect}\"\n    gems = Dir[gem_pkg]\n    raise Error, \"Unable to find gem \#{gem_pkg}\" if gems.empty?\n\n    puts \"Found: \#{gems.inspect}\"\n  else\n    gem_pkgs = File.join(PACKAGE_DIR, \"*.gem\")\n    puts \"Looking for: \#{gem_pkgs.inspect}\"\n    gems = Dir[gem_pkgs]\n    raise Error, \"Unable to find gems \#{gem_pkgs}\" if gems.empty?\n\n    # Sort by newest last\n    # [ \"my_gem-2.3.9.gem\", \"my_gem-2.3.11.pre.alpha.4.gem\", \"my_gem-2.3.15.gem\", ... ]\n    gems.sort_by! { |gem| Gem::Version.new(gem[VERSION_REGEX]) }\n    gem_pkg = gems.last\n    gem_path_parts = gem_pkg.split(\"/\")\n    gem_name = gem_path_parts.last\n    puts \"Found: \#{gems.length} gems; latest is \#{gem_name}\"\n  end\n\n  pkg_bits = File.read(gem_pkg)\n\n  # SHA-512 digest is 8 64-bit words\n  digest512_64bit = Digest::SHA512.new.hexdigest(pkg_bits)\n  digest512_64bit_path = \"\#{CHECKSUMS_DIR}/\#{gem_name}.sha512\"\n  Dir.mkdir(CHECKSUMS_DIR) unless Dir.exist?(CHECKSUMS_DIR)\n  File.write(digest512_64bit_path, digest512_64bit)\n\n  # SHA-256 digest is 8 32-bit words\n  digest256_32bit = Digest::SHA256.new.hexdigest(pkg_bits)\n  digest256_32bit_path = \"\#{CHECKSUMS_DIR}/\#{gem_name}.sha256\"\n  File.write(digest256_32bit_path, digest256_32bit)\n\n  version = gem_name[VERSION_REGEX]\n\n  git_cmd = <<-GIT_MSG.rstrip\ngit add \#{CHECKSUMS_DIR}/* && \\\ngit commit \#{git_dry_run_flag} -m \"\u{1F512}\uFE0F Checksums for v\#{version}\"\n  GIT_MSG\n\n  if git_dry_run_flag\n    git_cmd += <<-CLEANUP_MSG\n&& \\\necho \"Cleaning up in dry run mode\" && \\\ngit reset \#{digest512_64bit_path} && \\\ngit reset \#{digest256_32bit_path} && \\\nrm -f \#{digest512_64bit_path} && \\\nrm -f \#{digest256_32bit_path}\n    CLEANUP_MSG\n  end\n\n  puts <<-RESULTS\n[ GEM: \#{gem_name} ]\n[ VERSION: \#{version} ]\n[ GEM PKG LOCATION: \#{gem_pkg} ]\n[ CHECKSUM SHA-256: \#{digest256_32bit} ]\n[ CHECKSUM SHA-512: \#{digest512_64bit} ]\n[ CHECKSUM SHA-256 PATH: \#{digest256_32bit_path} ]\n[ CHECKSUM SHA-512 PATH: \#{digest512_64bit_path} ]\n\n... Running ...\n\n\#{git_cmd}\n  RESULTS\n\n  if git_dry_run_flag\n    %x{\#{git_cmd}}\n  else\n    # `exec` will replace the current process with the git process, and exit.\n    # Within the generate method, Ruby code placed after the `exec` *will not be run*:\n    #   See: https://www.akshaykhot.com/call-shell-commands-in-ruby\n    # But we can't exit the process when testing from RSpec,\n    #   since that would exit the parent RSpec process\n    exec(git_cmd)\n  end\nend\n"

.install_tasksvoid

This method returns an undefined value.

Make this gem’s rake tasks available in your Rakefile:

require "gem_checksums"


60
61
62
# File 'lib/gem_checksums.rb', line 60

def install_tasks
  load("gem_checksums/tasks.rb")
end