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: 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. /((\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 =
<<-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
Class Method Summary collapse
- .current_project_spec ⇒ Object
-
.generate(git_dry_run: false) ⇒ void
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.
-
.install_tasks ⇒ void
Make this gem's rake tasks available in your Rakefile:.
- .preferred_project_package(gems) ⇒ Object
- .validate_package_against_project?(gem_pkg, project_spec) ⇒ Boolean
- .validate_project_package!(gem_pkg) ⇒ Object
Class Method Details
.current_project_spec ⇒ Object
240 241 242 243 244 245 |
# File 'lib/gem_checksums.rb', line 240 def current_project_spec gemspecs = Dir["*.gemspec"] return unless gemspecs.length == 1 Gem::Specification.load(gemspecs.first) end |
.generate(git_dry_run: false) ⇒ void
This method returns an undefined value.
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, the planned
dry-run commit is printed and temporary files are cleaned up without touching the
Git index.
88 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 218 219 |
# File 'lib/gem_checksums.rb', line 88 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 # 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 = preferred_project_package(gems) || gems.last gem_path_parts = gem_pkg.split("/") gem_name = gem_path_parts.last puts "Found: #{gems.length} gems; selected #{gem_name}" end validate_project_package!(gem_pkg) 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_commit_flag = git_dry_run_flag ? "--dry-run" : nil git_add_flag = git_dry_run_flag ? "--dry-run" : nil git_cmd = <<-GIT_MSG.rstrip git add #{git_add_flag} #{CHECKSUMS_DIR}/* && \ git commit #{git_commit_flag} -m "🔒️ Checksums for v#{version}" GIT_MSG if git_dry_run_flag git_cmd += <<-CLEANUP_MSG && \ echo "Cleaning up in dry run mode" && \ 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 warn("Cleaning up in dry run mode") FileUtils.rm_f([digest512_64bit_path, digest256_32bit_path]) 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 |
.install_tasks ⇒ void
This method returns an undefined value.
Make this gem's rake tasks available in your Rakefile:
require "gem_checksums"
58 59 60 |
# File 'lib/gem_checksums.rb', line 58 def install_tasks load("gem_checksums/tasks.rb") end |
.preferred_project_package(gems) ⇒ Object
248 249 250 251 252 253 254 |
# File 'lib/gem_checksums.rb', line 248 def preferred_project_package(gems) project_spec = current_project_spec return unless project_spec expected_name = "#{project_spec.name}-#{project_spec.version}.gem" gems.find { |gem| File.basename(gem) == expected_name } end |
.validate_package_against_project?(gem_pkg, project_spec) ⇒ Boolean
257 258 259 |
# File 'lib/gem_checksums.rb', line 257 def validate_package_against_project?(gem_pkg, project_spec) File.basename(File.(PACKAGE_DIR)) == "pkg" || File.basename(gem_pkg).start_with?("#{project_spec.name}-") end |
.validate_project_package!(gem_pkg) ⇒ Object
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 |
# File 'lib/gem_checksums.rb', line 222 def validate_project_package!(gem_pkg) project_spec = current_project_spec return unless project_spec return unless validate_package_against_project?(gem_pkg, project_spec) package_spec = Gem::Package.new(gem_pkg).spec return if package_spec.name == project_spec.name && package_spec.version == project_spec.version raise Error, [ "Built gem version mismatch for #{gem_pkg}.", "Current gemspec resolves to #{project_spec.name} #{project_spec.version}, but selected package is #{package_spec.name} #{package_spec.version}.", "Remove stale packages or pass the intended .gem path explicitly before generating checksums." ].join("\n") rescue Gem::Package::Error => error raise Error, "Unable to inspect built gem #{gem_pkg}: #{error.}" end |