Skip to content

Index gems that come from a git or path source - #1011

Open
paracycle wants to merge 1 commit into
mainfrom
uk-index-git-and-path-gems
Open

Index gems that come from a git or path source#1011
paracycle wants to merge 1 commit into
mainfrom
uk-index-git-and-path-gems

Conversation

@paracycle

Copy link
Copy Markdown
Member

Problem

Graph#add_workspace_dependency_paths finds a dependency's code through Gem::Specification.find_by_name. That method only knows about gems RubyGems installed. A gem the Gemfile takes from a git or path source is never registered with RubyGems, so the lookup raises Gem::MissingSpecError, the rescue swallows it, and the gem is left out of the workspace without any signal.

This is not a rare corner. On a large workspace that pins its framework to a branch, 204 of 882 locked specs were dropped: 154 from path sources, 36 from git sources. Every Rails gem was among them, so nothing in the graph defined ActiveRecord::Base.

The damage is much wider than the missing gems. Ruby resolves a constant by searching the ancestors of the enclosing namespace. A class that inherits from a missing superclass never linearizes its ancestor chain, and then no constant inside that class resolves either, however well defined it is. Constants as ordinary as Array and Hash were reported unresolved even with the RBS core definitions indexed.

Change

When RubyGems cannot find the gem, ask Bundler. Bundler keeps an index per source that knows both where it checked the source out and what the gemspec declares.

Two details matter:

  • The directories come from the gemspec, not from a guess. Assuming lib is wrong often enough to matter. Of the gems resolved this way, grpc alone declares src/ruby/lib, src/ruby/bin and src/ruby/pb. Across the workspace, 77 gems declare something other than ["lib"], including lib/concurrent-ruby, ruby and client.
  • Each source is indexed once. A multi-gem repository such as rails shares one source across the twelve gems it provides, and building that index reads gemspecs off disk.

A source that is not checked out yet still leaves its gems unindexed rather than failing the whole workspace, which is the behaviour today.

Effect

Measured on a workspace of about 110,000 files whose Gemfile takes Rails from a branch.

Before After
Locked specs dropped 204 0
Workspace paths indexed 717 909
Unresolved work items 153,583 34,601
Incomplete ancestor chains 75,028 4,009

Constants such as TestCase, Arel, RecordInvalid and RecordNotFound resolve after the change and did not before.

Notes for the reviewer

  • The silent rescue is kept, because a workspace must still index when one source is unusable. It does mean a workspace can lose a quarter of its dependencies with no diagnostic. Surfacing that needs an error channel this method does not have, so I left it for separate work.
  • Bundler.load.specs would be the tidier API, but it materializes the whole bundle and raises on a workspace whose lockfile and checkout disagree. The current code deliberately avoids requiring a working bundle, and this change keeps that property.
  • The new tests use small doubles for the Bundler objects, so no repository is checked out while testing.

`Gem::Specification.find_by_name` only knows about gems that RubyGems
installed. A gem the Gemfile takes from a `git` or `path` source is
never registered with RubyGems, so the lookup raises, the rescue
swallows it, and the gem is left out of the workspace silently.

That is not a rare corner. On a large workspace that pins its framework
to a branch, 204 of 882 locked specs were dropped this way: 154 from
path sources, 36 from git sources. Every Rails gem was among them, so
nothing defined `ActiveRecord::Base`.

The cost is far larger than the missing gems, because Ruby resolves a
constant by searching the ancestors of the enclosing namespace. A class
that inherits from a missing superclass never linearizes its ancestor
chain, and then no constant inside that class resolves either, however
well defined it is. Measured on that workspace, indexing the git
sources took the unresolved work queue from 153,583 items to 34,601,
and incomplete ancestor chains from 75,028 to 4,009.

Bundler keeps an index per source that knows both where it checked the
source out and what the gemspec declares, so ask the source when
RubyGems cannot help. The index is built once per source: a multi-gem
repository such as rails shares one source across every gem it
provides, and building it reads gemspecs off disk.

Take the directories from the gemspec rather than assuming `lib`. Of
the gems resolved this way, `grpc` alone declares `src/ruby/lib`,
`src/ruby/bin` and `src/ruby/pb`, and none of them would have been
found.

A source that is not checked out yet still leaves its gems unindexed,
rather than failing the whole workspace.
@paracycle
paracycle requested a review from a team as a code owner August 14, 2026 19:02
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.

1 participant