With Bundler 4, why does a Gemfile with a second top-level `source` line for a private gem server fail, and how should private gems be declared?
answer
- one global source only
- GemfileEvalError since 4.0
- source block or source: option
- children fall back to the global source
- BUNDLE_GEMS__EXAMPLE__COM for credentials
basics
~20 sBundler 4 allows one global source; a second top-level source raises a GemfileEvalError, because a gem name found on both servers was ambiguous. Scope private gems with a source block or a source: option and keep credentials in bundle config.
solid answer
~40 sA Gemfile gets exactly one global `source`. Older Bundler let you stack several and asked them last to first, printing only a warning when a name existed on more than one, which let a public gem shadow a private one. That has been deprecated since Bundler 1.13, and Bundler 4.0 removed it: a second top-level `source` raises `GemfileEvalError` saying each source after the first needs a block. Declare private gems with `source "https://gems.example.com" do ... end` or `gem "billing-client", source: "https://gems.example.com"`. A scoped gem's own dependencies are looked up first in its source, then in the global one. Keep credentials out of the file: `bundle config set --global gems.example.com user:token` on a laptop, `BUNDLE_GEMS__EXAMPLE__COM=user:token` in CI.
code
ruby · 8 linessource "https://rubygems.org"
source "https://gems.example.com" do
gem "billing-client", "~> 2.1"
end
gem "feature-flags", source: "https://gems.example.com"
gem "sinatra", "~> 4.2"go deeper
Recall that a Gemfile has one top-level source and that private gems go inside a source block or carry a source: option.
Explain Bundler's source priority, including how a scoped gem's dependencies fall back to the global source, and how credentials are passed with bundle config or a BUNDLE_ variable.
Migrate Bundler 2 Gemfiles with stacked sources, explain the dependency-confusion risk they carried, and verify in the lockfile diff that every private gem resolves from its own server.
Set policy for internal gems: naming, which server hosts them, whether the private server mirrors public gems, and how credentials are issued and rotated for CI.
## The rule: one global source The first line of most **Gemfiles** is the **global source**, the gem server Bundler asks for every gem that has no source of its own: ```ruby source "https://rubygems.org" ``` Teams with an internal gem server often used to add a second line: ```ruby source "https://rubygems.org" source "https://gems.example.com" # Bundler 4: error ``` In Bundler 4 that Gemfile does not load. Bundler raises a `GemfileEvalError`: "This Gemfile contains multiple global sources. Each source after the first must include a block to indicate which gems should come from that source." The Bundler 4.0.0 changelog lists it under breaking changes as "Remove support for multiple global sources in Gemfile & lockfile". ## Why it was removed With several global sources, Bundler had no way to know which server was meant for which gem. It searched them **last to first** and, when a name existed on more than one, printed a warning after installing and moved on. That is the precondition for **dependency confusion**: if someone publishes a gem on the public server under the same name as your internal `billing-client`, a version bump or a reordered line can make Bundler install the public one. The feature had been deprecated since Bundler 1.13; Bundler 4 turned the deprecation into an error. ## The two correct declarations Scope private gems to their server explicitly: ```ruby source "https://rubygems.org" source "https://gems.example.com" do gem "billing-client" gem "audit-log" end gem "feature-flags", source: "https://gems.example.com" ``` - A **`source` block** attaches every gem inside it to that server. - The **`source:` option** does the same for one gem. - If the gem does not exist on the named server, it is not installed from anywhere else. Bundler's **source priority**, from the gemfile(5) man page: 1. The source attached to the gem itself (`source:`, `path:` or `git:`). 2. For a dependency of such a gem, the parent's source first. 3. Otherwise the global source. Step 2 means a private gem's own dependencies (say, `faraday`) are looked for on the private server first and, when not found there, on the global source. The man page adds a caveat: a `source` block also makes that server available as a possible source for gems that declare none, so it recommends giving every gem an explicit source once you use blocks. Gemfile.lock then records each gem under the server it came from. ## Credentials without committing them Private servers need authentication. Credentials written into the URL (`https://user:[email protected]`) work, and take precedence over configuration, but they end up in version control. The bundle-config man page describes the alternatives: | Where | How | |---|---| | A developer machine | `bundle config set --global gems.example.com user:token` | | CI or a container build | `BUNDLE_GEMS__EXAMPLE__COM=user:token` | The environment variable name comes from the host: `BUNDLE_` prefix, dots become double underscores, dashes triple underscores, upper-cased. The Gemfile keeps the clean `https://gems.example.com` URL. ## Other removed forms - `source :rubygems` (also `:gemcutter`, `:rubyforge`) is disallowed; Bundler raises a removal error because those symbols meant plain http. Write the https URL. - A path source by itself, `path "vendor/gems"` without a block, is also removed; wrap the gems in a `path` block or use `path:` per gem. ## A mirror is not a second source Some organisations proxy every public gem through an internal server. That does not need a second `source` line either. A **mirror** is machine configuration, not Gemfile content: ```bash bundle config set --global mirror.https://rubygems.org https://gems.example.com ``` The Gemfile keeps `source "https://rubygems.org"`, and Bundler fetches from the mirror on machines that set it. The two tools answer different questions: - **A source block** says *which server owns a gem name*; it is part of the Gemfile and applies everywhere. - **A mirror** says *where to download a source's gems on this machine*; it lives in `bundle config` and changes no declaration. - A `mirror.<url>.fallback_timeout` setting lets Bundler return to the original server when the mirror does not answer in time. ## Migrating a Bundler 2 Gemfile 1. Keep one top-level `source`, normally `https://rubygems.org`. 2. Move every gem that comes from the private server into a `source` block, or add `source:` to its line. 3. Move credentials from URLs into `bundle config` or `BUNDLE_*` variables. 4. Run `bundle install` and review the lockfile diff: each private gem should now sit under its own server's section.
- `billing-client` comes from a source block and depends on `faraday`. Where does Bundler look for `faraday`?First on the private server attached to its parent, then, if `faraday` is not found there, on the global source. That is the second step of Bundler's source priority. If the private server mirrors public gems, the private copy wins, which is worth knowing when auditing where a transitive gem came from.
- How is the environment variable for credentials to `gems.my-corp.example` spelled?`BUNDLE_GEMS__MY___CORP__EXAMPLE`. Bundler prefixes `BUNDLE_`, replaces each dot with two underscores and each dash with three, and upper-cases the result. Setting it as a CI secret gives the job access without writing credentials into the Gemfile or `.bundle/config`.
saying these in an interview costs you the question
- A second top-level source just adds a fallback server in Bundler 4
- Bundler asks the private server first for every gem once it is listed
- user:token inside the Gemfile source URL is the recommended way to authenticate
- source :rubygems is an accepted shorthand for the public server
- A gem in a source block gets all its dependencies only from that server