Skip to content

Allow gem push to select content-addressable gems by platform and Ruby ABI - #174

Open
girachawda wants to merge 7 commits into
ho/ca-changes-gem-buildfrom
gc/ca-gem-push
Open

Allow gem push to select content-addressable gems by platform and Ruby ABI#174
girachawda wants to merge 7 commits into
ho/ca-changes-gem-buildfrom
gc/ca-gem-push

Conversation

@girachawda

@girachawda girachawda commented Aug 4, 2026

Copy link
Copy Markdown

What

This PR updates gem push so maintainers can select a local gem file by its internal metadata instead of only by filename. This is for the case in which a maintainer may build multiple content-addressed skinny binaries for the same gem version/platform, but gem push still pushes one gem at a time. This lets them select the one target they want to push by platform/Ruby ABI instead of manually mapping hashes back to gem metadata.

It adds two selector options: --platform PLATFORM --ruby-abi RUBY_ABI

When either selector is provided, gem push can accept multiple candidate .gem files, inspect each file’s internal gemspec, and push the single file that matches the requested platform and/or Ruby ABI.

This PR is stacked on #171, which adds content-addressable gem build support.

Why

Content-addressable skinny gems use hash-based filenames, for example:

demo-0.1.0-662574271f.gem
demo-0.1.0-7b9a11c162.gem

From the filename alone, a maintainer cannot tell which file is for Ruby ABI 3.3 vs 3.4.

A maintainer may build multiple skinny gems for the same gem version and platform:

demo / 0.1.0 / arm64-darwin / ruby_abi 3.3
demo / 0.1.0 / arm64-darwin / ruby_abi 3.4

With this PR, they can run:

gem push demo-*.gem --platform arm64-darwin --ruby-abi 3.4

and RubyGems will select the matching .gem file by reading its internal spec.

If no file matches, or if multiple files match, gem push raises an error instead of guessing.

For ambiguous matches, this PR keeps gem push scoped to one final file and makes the error actionable. For example, if --platform arm64-darwin matches both fat and skinny gems, the error suggests adding --ruby-abi to select a skinny gem, or passing the exact filename to push the fat gem.

Manual tophat

  1. Run this from the RubyGems checkout:
See Code
cd /path/to/rubygems
RUBYGEMS="$PWD"
WORKDIR="$PWD/tmp/ca-gem-push-option-d-tophat"
rm -rf "$WORKDIR"
mkdir -p "$WORKDIR/lib"
cd "$WORKDIR"
printf '# hello\n' > lib/demo.rb

ruby --disable-gems -I"$RUBYGEMS/lib" - <<'RUBY'
require "rubygems"
require "rubygems/package"
require "rubygems/commands/push_command"


def build_ca(name, platform:, ruby_abi:)
  spec = Gem::Specification.new(name, "0.1.0") do |s|
    s.summary = name
    s.authors = ["Tester"]
    s.files = ["lib/demo.rb"]
    s.platform = platform
    s.required_ruby_version = "~> #{ruby_abi}.0"
  end
  Gem::Package.build(spec, false, false, nil, ruby_abi)
end

def build_fat(name, platform:)
  spec = Gem::Specification.new(name, "0.1.0") do |s|
    s.summary = name
    s.authors = ["Tester"]
    s.files = ["lib/demo.rb"]
    s.platform = platform
    s.required_ruby_version = ">= 3.1"
  end
  Gem::Package.build(spec)
end

puts "== Build candidate gems =="
fat = build_fat("demo", platform: "arm64-darwin")
skinny_33 = build_ca("demo", platform: "arm64-darwin", ruby_abi: "3.3")
skinny_34_arm = build_ca("demo", platform: "arm64-darwin", ruby_abi: "3.4")
skinny_34_linux = build_ca("demo", platform: "x86_64-linux", ruby_abi: "3.4")

puts "\n== Candidate metadata =="
Dir["*.gem"].sort.each do |file|
  spec = Gem::Package.new(file).spec
  ruby_abi = spec.ruby_abi || "none"
  kind = ruby_abi == "none" ? "fat/no ruby_abi" : "skinny"
  puts "#{file}"
  puts "  kind: #{kind}"
  puts "  platform: #{spec.platform}"
  puts "  ruby_abi: #{ruby_abi}"
end

cases = [
  ["success: skinny arm64 Ruby ABI 3.4", [skinny_33, skinny_34_arm, skinny_34_linux], { platform: "arm64-darwin", ruby_abi: "3.4" }],
  ["success: fat exact filename", [fat], {}],
  ["ambiguous: platform only with fat + skinny", [fat, skinny_34_arm], { platform: "arm64-darwin" }],
  ["ambiguous: platform only with multiple skinny ABIs", [skinny_33, skinny_34_arm], { platform: "arm64-darwin" }],
  ["ambiguous: Ruby ABI only with multiple platforms", [skinny_34_arm, skinny_34_linux], { ruby_abi: "3.4" }],
  ["no match: wrong platform + Ruby ABI", [skinny_33, skinny_34_arm], { platform: "x86_64-linux", ruby_abi: "3.4" }],
]

puts "\n== gem push selector tophat =="
cases.each do |label, files, options|
  cmd = Gem::Commands::PushCommand.new
  cmd.options[:args] = files
  cmd.options[:platform] = options[:platform] if options[:platform]
  cmd.options[:ruby_abi] = options[:ruby_abi] if options[:ruby_abi]

  selector = []
  selector << "--platform #{options[:platform]}" if options[:platform]
  selector << "--ruby-abi #{options[:ruby_abi]}" if options[:ruby_abi]

  puts "\n#{label}"
  puts "gem push #{files.join(" ")} #{selector.join(" ")}".rstrip

  begin
    selected = cmd.send(:resolve_gem_name, files)
    puts "=> selected: #{selected}"
  rescue Gem::CommandLineError => e
    puts "=> error: #{e.message}"
  end
end
RUBY
  1. Expected behavior:
  • --platform arm64-darwin --ruby-abi 3.4 selects the matching skinny gem.
  • Passing the exact fat filename selects the fat gem.
  • --platform arm64-darwin with fat + skinny candidates errors and suggests --ruby-abi or exact filename.
  • --platform arm64-darwin with multiple skinny ABIs errors and suggests available --ruby-abi values.
  • --ruby-abi 3.4 with multiple platforms errors and suggests available --platform values.
  • Non-matching selectors still return a no-match error.

@girachawda
girachawda marked this pull request as draft August 4, 2026 23:03
@girachawda
girachawda marked this pull request as ready for review August 5, 2026 21:00
@jenshenny
jenshenny force-pushed the ho/ca-changes-gem-build branch 4 times, most recently from 7d030f2 to 8efebaf Compare August 6, 2026 21:16
OughtPuts and others added 5 commits August 6, 2026 21:11
… the --ruby_abi flag to gem build

Assisted-By: devx/9a9728d9-8128-4a99-9993-fe35c4eefb45
Validate the requested Ruby ABI without mutating the spec, then build
using a dup of the spec with the derived required_ruby_version applied.
The original spec is only updated once the build has succeeded, so a
failed build no longer leaves the passed-in spec permanently modified.
@jenshenny
jenshenny force-pushed the ho/ca-changes-gem-build branch from 8efebaf to 36d68df Compare August 7, 2026 01:11
Assisted-By: devx/6563d395-3a93-455c-8523-fe2052782e76
Assisted-By: devx/6563d395-3a93-455c-8523-fe2052782e76
@jenshenny
jenshenny force-pushed the ho/ca-changes-gem-build branch from 36d68df to 8e4d2d1 Compare August 7, 2026 17:36

@jenshenny jenshenny left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! I put a few comments to make sure we have some edge cases covered.


def resolve_gem_name(names)
candidates = names.map do |name|
[name, Gem::Package.new(name).spec]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gem::Package.new(name).spec could raise a Gem::Package::FormatError and output a confusing error

   ERROR:  While executing gem ... (Gem::Package::FormatError)                                                                                                                                                 
       package metadata is missing in demo-0.1.0-662574271f.gem 

could we rescue these errors and skip them? eg. alert_warning "Skipping #{name}: #{e.message}".

suggestions << "Specify --ruby-abi with one of: #{ruby_abis.join(", ")}" unless ruby_abis.empty?
suggestions << "To push a gem without a Ruby ABI, pass the exact filename." if matches.any? {|_, spec| spec.ruby_abi.nil? }
suggestions.join("\n") unless suggestions.empty?
elsif options[:ruby_abi] && !options[:platform]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ruby ABI isn't being validated, could we use validate_ruby_abi to verify the Ruby ABI before filtering?

Comment on lines +124 to +131
case matches.length
when 1
matches.first.first
when 0
raise Gem::CommandLineError, "No gem matched #{gem_name_selector_description}"
else
raise Gem::CommandLineError, multiple_matches_message(matches)
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: stylistic preference to make the error cases more readable

Suggested change
case matches.length
when 1
matches.first.first
when 0
raise Gem::CommandLineError, "No gem matched #{gem_name_selector_description}"
else
raise Gem::CommandLineError, multiple_matches_message(matches)
end
raise Gem::CommandLineError, "No gem matched #{gem_name_selector_description}" if matches.empty?
raise Gem::CommandLineError, multiple_matches_message(matches) if matches.length > 1
gem_name, _spec = matches.first

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants