Content addressable gems POC - #169
Draft
jenshenny wants to merge 5 commits into
Draft
Conversation
Content-addressable ("skinny") binaries put a 10-char hex prefix of the gem's
sha256 in the platform slot of the version token (name-version-<sha>). Plain
Gem::Platform normalizes any unknown string to os="unknown", which would destroy
the hash used in filenames, lockfile entries, and download URLs.
Keep such a token verbatim in its own version_suffix field so it never
masquerades as an operating system. It round-trips through to_a/to_s, and
==/hash/=== treat it as an exact-match token that never fuzzy-matches a real
platform (and vice versa). Ordinary platforms are unaffected.
Assisted-By: devx/4ab7951d-76be-4b93-8ffb-c3581711ac1f
A "skinny" binary is a platformed gem pinned to a single Ruby ABI (e.g. required_ruby_version "~> 3.3.0", or rake-compiler's ">= 3.3, < 3.4.dev"). Today every per-Ruby build of the same name-version-platform collides on one identity, so a registry can host only one. Content-address them by name-version-<sha>, where <sha> = sha256(.gem)[0, 10], so multiple per-Ruby builds of the same version+platform can coexist. - specification.rb: content_addressable? / content_addressable_ruby_abi detect skinny binaries (single ABI), and to_ruby writes the version suffix as an optional 5th stub-line field. - package.rb / package_task.rb: Gem::Package.build renames a skinny gem to name-version-<sha>.gem and the package task moves the produced file. - basic_specification.rb / stub_specification.rb: a version_suffix accessor and full_name reconstruction (name-version-<sha>); the stub line carries the sha so full_name works offline, and to_spec copies it onto the loaded full spec. - installer.rb: assign_version_suffix derives the sha from the gem's bytes before any path is computed, so the install dir and stub are content-addressed. The sha is computed from content, so the builder, the installer, and a registry all derive the same value without coordination. Fat binaries and source gems are not content-addressable and are untouched. Assisted-By: devx/4ab7951d-76be-4b93-8ffb-c3581711ac1f
Encode content-addressable gems in the v1 compact index (no v2 endpoint): the per-gem info line carries the content address in the version token's platform slot and the real platform in a platform: requirement token, gated by a rubygems:>= requirement so old clients ignore the rows: 1.0.0-<sha> |checksum:<sha256>,ruby:<req>,rubygems:>= 4.1.0.dev,platform:= <real> - endpoint_specification.rb: parse the platform: token into platform_requirement; content_addressable?; installable_on_platform? matches on the real platform (since @platform holds the sha); version_suffix reconstructs the download name. - match_platform.rb: when a skinny binary compatible with the running Ruby exists, prefer it exclusively; otherwise fall back to the fat/ruby variant so a usable binary is never stranded. Per-minor ~> ranges are disjoint, so at most one skinny qualifies (no skinny-vs-skinny tie-break needed). - lazy_specification.rb: carry platform_requirement/version_suffix; reconstruct full_name as name-version-<sha>; on a --local exact-match miss, retry by name+version so the portable lockfile entry (name-version-platform) resolves to the on-disk name-version-<sha> gem. - stub_specification.rb: delegate full_name/version_suffix to the RubyGems stub. Old clients drop the skinny rows because their RubyGems version doesn't satisfy the rubygems:>= gate (and the <sha> token doesn't match a real platform anyway), so they fall back to the fat binary. Assisted-By: devx/4ab7951d-76be-4b93-8ffb-c3581711ac1f
Self-contained, native end-to-end tests for content-addressable ("skinny")
binaries served from a v1 compact index (no v2 endpoint, no rubygems.org, no
containers):
- demo/: a tiny native gem whose required_ruby_version pins the building Ruby's
minor, so it builds as a skinny binary.
- fake_compact_index.rb: a dependency-free threaded HTTP server that serves a
directory as a v1 compact index (/versions, /info/<gem>, /gems/*).
- prove_resolution.rb / test_resolution.sh: using the real parser, the real
EndpointSpecification, and the real MatchPlatform selection, prove a new client
picks the skinny variant over source+fat, and an old client drops it via the
rubygems:>= gate.
- test_local.sh: build, gem install (gems/name-version-<sha>/ + sha in the stub),
bundle install --local (lockfile round-trip), require, idempotent re-install.
- test_remote_v1.sh: bundle install against the fake v1 server; the skinny gem
is selected and downloaded as /gems/name-version-<sha>.gem.
- test_remote_v1_oldclient.sh: a stock RubyGems/Bundler older than the gate
installs the fat binary and never requests the skinny gem, while the patched
client picks the skinny one, against the same index.
README.md documents what each proves and how this differs from PR #168.
Assisted-By: devx/4ab7951d-76be-4b93-8ffb-c3581711ac1f
jenshenny
force-pushed
the
content-addressable-gems-v1
branch
from
June 17, 2026 03:23
7ee1a23 to
c05d308
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds support for content-addressable ("skinny") precompiled binaries to
RubyGems and Bundler, encoded entirely in the v1 compact index — no
/v2/endpoint required.
A "skinny" binary is a platformed gem pinned to a single Ruby ABI (e.g.
required_ruby_version = "~> 3.3.0", or rake-compiler's>= 3.3, < 3.4.dev).Today every per-Ruby build of the same
name–version–platformcollides onone identity, so a registry can host only one. This PR makes such gems built,
named, installed, and resolved by content address —
name-version-<sha>,where
<sha> = sha256(.gem)[0, 10]— so multiple per-Ruby builds of the sameversion+platform can coexist, and the correct one is selected at resolve time
based on the running Ruby.
Old clients keep working: skinny rows in the index carry a
rubygems:>=requirement, so a client older than the cutoff transparently ignores them and
installs the ordinary fat binary instead.
Why v1 (and not a v2 endpoint)
This is the alternative to a separate
/v2/compact-index namespace. Instead ofhiding content-addressable entries behind a new endpoint, we keep everything in
the existing v1 index and hide skinny rows from old clients with a
rubygems:>=gate. The per-gem info line carries the content address in the version token's
platform slot and the real platform in a
platform:requirement token:<sha>slot becomes the gem'sfull_name(and download path/gems/name-version-<sha>.gem).platform:token carries the real platform, used for compatibilitymatching.
rubygems:>= 4.1.0.devgates the row: clients older than the version thatships this support don't satisfy it and drop the row during resolution. (The
exact cutoff is a placeholder to be set to the release version; consuming
support should ship before any registry starts publishing skinny gems.)
Design
version_suffix = sha256(.gem)[0, 10], computed from bytes, sothe builder, the installer, and a registry all derive the same value without
coordination.
pinned to a single Ruby minor. Fat binaries (spanning many ABIs) and pure-Ruby
gems are untouched.
Gem::Package.buildbuilds a skinny gem in memory, computes thecontent address from the bytes, and writes
name-version-<sha>.gemin a singlepass — no write-then-read-then-rename. (A streaming digest can't be used here
because
TarWriterseeks backward to backfill headers, so the build IO must beseekable.)
gems/name-version-<sha>/, with the 10-char sharecorded in the stub line so
full_namereconstructs the content-addressedname offline (no
.gembytes required).name (version-platform);Bundler bridges that back to the on-disk
name-version-<sha>at install time.Ruby exists, Bundler prefers it exclusively; otherwise it falls back to the
fat/ruby variant so a usable binary is never stranded. Per-minor
~>rangesare disjoint, so at most one skinny qualifies (no skinny-vs-skinny tie-break).
Changes
RubyGems core
lib/rubygems/platform.rb— preserve a 10-hex-charversion_suffixtokenverbatim instead of normalizing it to the bogus platform
"unknown";round-trips through
to_a/to_s, and==/hash/===treat it as anexact-match token.
lib/rubygems/specification.rb—content_addressable?/content_addressable_ruby_abi(skinny detection, both~> X.Y.Zandrake-compiler's
>= X.Y, < X.(Y+1).dev);to_rubywrites the version suffixas an optional 5th stub-line field.
lib/rubygems/package.rb—Gem::Package.buildbuilds skinny gems inmemory and writes
name-version-<sha>.gemonce; the build success messagetolerates a
nilpath (soIOSourcein-memory builds don't crash).lib/rubygems/package_task.rb— moves the (renamed) built file to thepackage dir.
lib/rubygems/basic_specification.rb—version_suffixaccessor;full_namereturnsname-version-<sha>when present.lib/rubygems/stub_specification.rb— reads the optional 5th stub-linefield;
full_namereconstructs the content-addressed name;to_speccarriesthe suffix onto the loaded full spec.
lib/rubygems/installer.rb—assign_version_suffixderives the sha fromthe gem's bytes before any path is computed.
Bundler
endpoint_specification.rb— parses the compact-indexplatform:tokeninto
platform_requirement;content_addressable?;installable_on_platform?uses the real platform (since
@platformholds the sha).match_platform.rb—usable_skinny?and the "prefer a Ruby-compatibleskinny exclusively, else fall back to fat" selection.
lazy_specification.rb— carriesplatform_requirement/version_suffix;content-addressed
full_name; on a--localexact-match miss, retries byname+versionso the portable lockfile entry resolves to the on-diskname-version-<sha>gem.stub_specification.rb— delegatesfull_name/version_suffixto theunderlying RubyGems stub.
Tests / tooling
dev/content-addressable-v1-demo/— self-contained, dependency-free,native end-to-end tests (no containers, no rubygems.org): a tiny native demo
gem, a fake v1 compact-index server, and scripts documented in a README.
Testing
Verified natively on macOS (
arm64-darwin); each script ends withALL GOOD:test_resolution.sh— using the real parser +EndpointSpecification+MatchPlatform, a new client picks the skinny variant over source+fat, and anold client drops it via the
rubygems:>=gate.test_local.sh—rake native gembuilds the content-addressed gem;gem installlands it undergems/name-version-<sha>/with the sha in thestub;
bundle install --localresolves it (lockfile round-trip),require+bundle listwork, and re-install is idempotent.test_remote_v1.sh— against a fake v1 server, Bundler fetches/versions+/info/<gem>(no/v2/), selects the skinny variant, downloads/gems/name-version-<sha>.gem, installs, andrequireworks.test_remote_v1_oldclient.sh— a stock RubyGems/Bundler older than thegate installs the fat binary and never requests the skinny gem, while the
patched client picks the skinny one, against the same index.
cd dev/content-addressable-v1-demo ./test_resolution.sh ./test_local.sh PORT=8920 ./test_remote_v1.sh PORT=8920 ./test_remote_v1_oldclient.shRelationship to #168
This is the v1-index alternative to #168 (which negotiates a
/v2/compact-indexnamespace). The build/install/resolution machinery is shared in spirit; the key
differences are no v2 endpoint (old clients are handled by the
rubygems:>=gate instead) and the naming (
version_suffix/platform_requirementhere vscontent_address/real_platformin #168).Known nuance
On the remote path the gem currently installs into
gems/name-version-<real-platform>/, whereas the--localpath installsinto
gems/name-version-<sha>/. Both are internally consistent and work for asingle active Ruby, but they disagree on the on-disk directory name.