A Bundler plugin that repeats bundle install, bundle update and bundle add against every other gemfile in your project, so each lockfile stays in step with the main Gemfile.
Projects that test against several dependency sets keep extra gemfiles (ci.gemfile, gemfiles/rails7.gemfile, engine Gemfiles, and so on). Without help, every one of them needs its own BUNDLE_GEMFILE=… bundle install. With bundle_matrix, running the command on the main Gemfile does that for you.
Install the plugin once for your user:
bundle plugin install bundle_matrixOr declare it in the project's Gemfile:
plugin "bundle_matrix"Run the command on the main Gemfile as usual:
bundle install
bundle update rails --conservative
bundle update --all
bundle add rack --group developmentAfter the main run succeeds, each discovered gemfile is run with the same command and flags. Output is headed by the gemfile it applies to:
== bundle_matrix: gemfiles/rails7.gemfile -> bundle update rails --conservative
== bundle_matrix: engines/admin/Gemfile -> bundle install
Only install, update and add are fanned out. bundle check, bundle exec and similar commands are not, because Bundler does not run plugin hooks for them.
Bundler's short forms work too: bundle i, bundle ins, bundle a, bundle u, and any other unambiguous prefix of a command name.
A plain bundle, or bundle with only flags such as bundle --jobs=4, runs as install and fans out as install, but only while default_cli_command is unset or install. With it set to cli_help, those commands print help and nothing fans out. Bundler's documentation already names cli_help as the default, while its code still runs install and prints a notice that this will change. To keep fanning out whatever Bundler does, set the setting explicitly, or run bundle install:
bundle config set default_cli_command installThe main gemfile is Gemfile in the project root, or the file named by matrix.default_gemfile. It is never treated as a secondary gemfile. Secondary gemfiles are found in:
*.gemfilein the project root, e.g.ci.gemfilewithci.gemfile.lock*.gemfileingemfiles/- any directories listed in
matrix.paths, each contributing itsgems.rb, or itsGemfilewhen it has nogems.rb, the order Bundler uses
Settings use bundle config or BUNDLE_MATRIX__* environment variables:
| Setting | Default | Meaning |
|---|---|---|
matrix.default_gemfile |
Gemfile |
File name of the main gemfile in the project root. Fan-out starts only when the command runs on this file, e.g. gems.rb |
matrix.paths |
none | Extra directories or globs to search for a gems.rb or Gemfile, separated by commas, e.g. engines/*,apps/admin |
matrix.disabled |
false |
Turn fan-out off |
matrix.fail_fast |
false |
Stop at the first failing gemfile |
matrix.bundler_warnings |
true |
Warn when a gemfile runs a different Bundler from the main Gemfile. Set it to false for projects that pin different Bundlers on purpose |
bundle config set matrix.paths "engines/*,apps/admin"For a project whose main file is gems.rb, set the main gemfile name once, and run the command on gems.rb:
bundle config set matrix.default_gemfile gems.rb
bundle installTo skip the matrix for a single run, set BUNDLE_MATRIX__DISABLED on that command:
BUNDLE_MATRIX__DISABLED=1 bundle installbundle config set matrix.disabled true is the persistent version, and stays in effect until you change it back.
Flags are forwarded as typed, and Bundler's own option table decides what each flag means. The exceptions below are where a flag can't be applied to another gemfile as-is.
install:--lockfileand--standaloneonly apply to the main Gemfile, so they are not forwarded.update <gems>: only the named gems that appear in that gemfile's lockfile are updated. A gemfile with none of them is skipped.--groupis limited to groups it declares. If it has no lockfile yet,installis run to create one.update --all,--conservative,--patch,--minor,--major,--pre,--strictand the rest: forwarded unchanged.add <gems>: gems the gemfile already declares, including througheval_gemfile, are not added again. If every gem is already declared,installis run to re-lock the gemfile. A relative--pathis rewritten so it resolves from that gemfile's directory.--gemfileis never forwarded. The target is set throughBUNDLE_GEMFILEinstead.
Each gemfile is run the way bundle would run it on its own, with the project's settings:
- The project's
.bundle/configand theBUNDLE_*environment variables you set are passed on, sofrozen,without,jobsand the rest apply to every gemfile. A relativepath,binorcache_pathstill resolves from the project root, so every gemfile shares the samevendor/bundle. A.bundle/confignext to a secondary gemfile takes priority for that gemfile. gemfileandlockfileare not passed on, because they name the main Gemfile's own files.- Each gemfile runs with the Bundler version in its own
BUNDLED WITHline, andinstallinstalls that version first if it is missing. Agem "bundler"line in a gemfile that has no lockfile yet is not installed for you, so install that version yourself first. Bundler'sversionsetting controls this:bundle config set version systemkeeps every gemfile on the running version, andbundle config set version 4.0.23pins one.BUNDLER_VERSIONis not passed on, because it would hold every gemfile to one version. - When a gemfile runs with a different Bundler from the main Gemfile, a one-line warning names both versions, and the run ends by suggesting either
bundle update --bundler, which updates Bundler in every lockfile, orbundle config set matrix.bundler_warnings false, which turns these warnings off when each gemfile should keep its own Bundler.
bundle_matrix needs Bundler 2.6.9 or newer, below 4.2 and its prereleases, and is tested against 2.6.9, 2.7.2, 4.0.24 and 4.1.0.beta3. The upper bound moves up once a new Bundler minor has been tested. Under any other Bundler the plugin still runs, but prints a one-line warning that it is untested there, with a link to the issue tracker.
Fan-out starts from Bundler's documented plugin hook, after-install-all, and each gemfile runs in its own bundle process. Everything else uses Bundler classes that work today but are not a plugin API, so a new Bundler release could change them:
- Thor's command and option tables (
Bundler::CLI), which decide what each flag means and which command was run (ArgvParser,Invocation). Command names such asinsare resolved by calling Thor's protectednormalize_command_namewithsend Bundler::DslandBundler::LockfileParser, which read each secondary gemfile and lockfile (Planner).Bundler.definitionandBundler.locked_gemsonly describe the main Gemfile, so they can't be used for the others.- The settings' storage levels, through
Bundler.settings(ProjectSettings)
bundle checkis not supported. Bundler only calls plugin hooks around installation.bundle add --skip-installnever runs the hook, because no installation happens.- Flags that Bundler does not know about are forwarded as-is, and any value after them is treated as a gem name.
Run bin/setup, then rake. The default task runs the unit specs, the end-to-end specs, RuboCop, rake rbs, which runs rbs validate on the signatures in sig/ with the Bundler stubs in sig_stubs/, and rake steep, which type checks lib/ against those signatures. Run rake e2e on its own for the end-to-end specs, which run real bundle commands and take longer. Run COVERAGE=true bundle exec rake spec to also write a coverage report to coverage/index.html. That run fails below 90% line coverage or 80% branch coverage, and CI runs it this way.
Run bundle exec guard to re-run the unit specs and RuboCop on every change. It also regenerates the docs when the files they are built from change.
docs/ is generated by Boondoc, so edit README.md and the code comments rather than docs/. To regenerate it by hand, run bundle exec boondoc
To try a change against a real project, install from a local checkout with bundle plugin install bundle_matrix --path /path/to/bundle_matrix (the directory must contain plugins.rb).
Bug reports and pull requests are welcome. Send issues and pull requests to https://github.com/joe-sharp/bundle_matrix.
Run bundle exec rake before you open a pull request. The Development section above explains what it runs.
A bug report should include:
- the Bundler version, from
bundle -v - the command you ran, e.g.
bundle update rails --conservative - your gemfile layout: which gemfiles exist, where they are, and any
matrix.*settings
The gem is available as open source under the terms of the MIT License.
