Skip to content

Comments

Let gem install use Compact Index API only when present.#9314

Open
simi wants to merge 3 commits intoruby:masterfrom
RubyElders:compact-gem-install
Open

Let gem install use Compact Index API only when present.#9314
simi wants to merge 3 commits intoruby:masterfrom
RubyElders:compact-gem-install

Conversation

@simi
Copy link
Contributor

@simi simi commented Feb 6, 2026

While I was working on publishing some gems in private repositories, I have realized bundler already uses Compact Index API only when present, but gem commands sometimes not.

This is first change in chain to update gem commands (gem install here) to be able to use Compact Index API only when present. The change is simple, Compact Index API /info/:gem endpoint already provides all info needed to provide gemspec stub needed to resolve dependencies.

I have added also (in separate commit) infrastructure for simple stub of Compact Index API (reusing same API as spec_fetcher) and in last commit I have ported some basic gem install tests to use Compact Index API (as new default option).

As a side-effect it is also faster. Running simple command like GEM_HOME=/tmp/gem_test gem install with empty GEM_HOME makes gem install 5s faster locally on my Linux (Fedora) computer.

@simi simi force-pushed the compact-gem-install branch 3 times, most recently from dc193a0 to 4e508a1 Compare February 6, 2026 22:21
@simi simi force-pushed the compact-gem-install branch from 4e508a1 to fc0f6af Compare February 6, 2026 22:33
##
# Minimal CompactIndex implementation for tests.
# This is a simplified version that only implements what's needed for test fixtures.
module CompactIndexBuilder
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried to use compact_index gem, but it is not possible to load external dependency on ruby-core CI. For now I have provided minimal implementation for now.

Copy link
Member

@colby-swandale colby-swandale left a comment

Choose a reason for hiding this comment

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

@simi I paired with Claude/Copilot on this and found a couple of small areas for improvement. I tried pushing to the PR, but I don't think Allow edits from maintainers is currently enabled.

1. Missing platform suffix in compact index info output

CompactIndexBuilder.info doesn't include the platform in the version string. The compact index format requires VERSION-PLATFORM (e.g., 1.0-java) for non-ruby platforms, which is what GemParser#parse expects when it splits by "-". Without this, all platform-specific gems appear as platform: "ruby".

The existing platform tests (test_install_gem_ignore_dependencies_remote_platform_local, test_execute_required_ruby_version_upper_bound) still pass without this fix because Gem::Source::Local picks up the .gem files built by CompactIndexSetup#stub and bypasses the compact index data entirely for platform resolution.

2. Wrong separator for compound requirements

Dependency#requirement.to_s returns comma-separated strings like ">= 1.0, < 2.0", but the compact index format uses & for compound requirements within a single dependency (e.g., dep_name:>= 1.0&< 2.0). The , separator is reserved for separating different dependencies. Same issue applies to ruby/rubygems version metadata.

Reference: GemVersion#join_multiple in the canonical library does exactly split(", ") then join("&").

Patch

diff --git a/lib/rubygems/resolver/api_specification.rb b/lib/rubygems/resolver/api_specification.rb
index 8772c103be..0c23c9e0be 100644
--- a/lib/rubygems/resolver/api_specification.rb
+++ b/lib/rubygems/resolver/api_specification.rb
@@ -84,7 +84,10 @@ def pretty_print(q) # :nodoc:
   end
 
   ##
-  # Fetches a Gem::Specification for this APISpecification.
+  # Returns a minimal Gem::Specification built from compact index data.
+  # Only includes name, version, platform, dependencies, required_ruby_version,
+  # and required_rubygems_version. Other specification fields (e.g., files,
+  # executables, authors, metadata) are not populated.
 
   def spec # :nodoc:
     @spec ||= build_minimal_spec_from_compact_index
diff --git a/test/rubygems/utilities.rb b/test/rubygems/utilities.rb
index 124dbc9579..5150403fb4 100644
--- a/test/rubygems/utilities.rb
+++ b/test/rubygems/utilities.rb
@@ -426,23 +426,26 @@ def write_spec(spec) # :nodoc:
 module CompactIndexBuilder
   # Generates the /info/{gem_name} response body
   # Format: ---\nVERSION DEPS|METADATA\n
-  # Where DEPS is: dep_name:requirement,dep_name:requirement
-  # And METADATA is: checksum:SHA256,ruby:requirement,rubygems:requirement
+  # Where DEPS is: dep_name:req1&req2,dep_name:req1&req2
+  # And METADATA is: checksum:SHA256,ruby:req1&req2,rubygems:req1&req2
   def self.info(versions)
     lines = ["---"]
     versions.each do |version|
       # Add dependencies (if any)
-      deps = version.dependencies.map {|d| "#{d.name}:#{d.requirement}" }
+      # Compact index uses & to separate compound requirements within a single dependency,
+      # since , is used to separate different dependencies.
+      deps = version.dependencies.map {|d| "#{d.name}:#{d.requirement.gsub(", ", "&")}" }
       deps_string = deps.join(",")
 
       # Build metadata
       metadata = []
       metadata << "checksum:#{version.checksum}" if version.checksum
-      metadata << "ruby:#{version.ruby_version}" if version.ruby_version && version.ruby_version != ">= 0"
-      metadata << "rubygems:#{version.rubygems_version}" if version.rubygems_version && version.rubygems_version != ">= 0"
+      metadata << "ruby:#{version.ruby_version.gsub(", ", "&")}" if version.ruby_version && version.ruby_version != ">= 0"
+      metadata << "rubygems:#{version.rubygems_version.gsub(", ", "&")}" if version.rubygems_version && version.rubygems_version != ">= 0"
 
-      # Format: "VERSION DEPS|METADATA" or "VERSION |METADATA" (space before | only when no deps)
-      line = "#{version.version} #{deps_string}|" + metadata.join(",")
+      # Format: "VERSION[-PLATFORM] DEPS|METADATA" or "VERSION[-PLATFORM] |METADATA"
+      version_string = version.platform && version.platform != "ruby" ? "#{version.version}-#{version.platform}" : version.version
+      line = "#{version_string} #{deps_string}|" + metadata.join(",")
       lines << line
     end
     lines.join("\n") << "\n"
diff --git a/test/rubygems/test_gem_resolver_api_set.rb b/test/rubygems/test_gem_resolver_api_set.rb
index b0b4943bea..43d5193e22 100644
--- a/test/rubygems/test_gem_resolver_api_set.rb
+++ b/test/rubygems/test_gem_resolver_api_set.rb
@@ -55,6 +55,22 @@ def test_find_all
     assert_equal expected, set.find_all(a_dep)
   end
 
+  def test_find_all_platform
+    spec_fetcher
+
+    @fetcher.data["#{@dep_uri}a"] = "---\n1 |checksum:abc123\n1-java |checksum:def456"
+
+    set = Gem::Resolver::APISet.new @dep_uri
+
+    a_dep = Gem::Resolver::DependencyRequest.new dep("a"), nil
+
+    specs = set.find_all(a_dep)
+
+    assert_equal 2, specs.length
+    assert_equal Gem::Platform.new("ruby"), specs[0].platform
+    assert_equal Gem::Platform.new("java"), specs[1].platform
+  end
+
   def test_find_all_prereleases
     spec_fetcher

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.

2 participants