A Day in the Life of Ruby Dependencies
Typical day as a Ruby developer:
- Notice that guard gem processes are using 80%+ CPU (each) at idle. The version we are using is over a year old, and there have been many new versions since, so I decide to try upgrading the gem to see if that fixes it. I don’t want to upgrade to the latest version, because it is a different major version and there may be breaking changes. I decide to upgrade to the latest 1.x version.
- Bundler doesn’t have a way to specify a version when updating a gem, so I modify the Gemfile to specify the version I want (1.8.3).
- Run `bundle update guard`. Bundler downloads the source index from rubygems.org, which it does every time, and takes about half a minute. Then it attempts to resolve dependencies and install/update gems, which takes a few more minutes (depending on how many gems you’re using).
- Bundler “resolves” dependencies by telling you that it can’t resolve dependencies. It turns out that many gems depend on specific, mutually-exclusive versions of the same gem, and the dependent version changes every time the parent gem is updated.
- In this case the dependent gem is “listen”. Run `bundle update listen`. Bundler downloads the source index from rubygems.org and attempts to resolve dependencies.
- Failure: Bundler can’t update the listen gem because the guard gem has been locked to the old version. If this sounds like a circular problem, congratulations — welcome to Bundler hell!
- Manually edit the Gemfile.lck file so that the guard gem uses the new version.
- Run `bundle update listen`. Bundler downloads the source index from rubygems.org and attempts to resolve dependencies. This takes a couple minutes.
- Run `bundle update guard`. Bundler downloads the source index from rubygems.org and attempts to resolve dependencies. This takes a couple minutes.
- Failure: guard requires v1.3 of the listen gem. We are using v0.7.3.
- Bundler says that the compass and sass gems both require old versions of the listen gem, despite the fact that the rubygems.org pages for compass and sass don’t mention listen.
- Update the compass version in the Gemfile and run `bundle update compass`. Bundler downloads the source index from rubygems.org and attempts to resolve dependencies. This takes a couple minutes.
- Failure: Newest version of compass requires a newer version of rb-inotify.
- Run `bundle update rb-inotify`. Bundler downloads the source index from rubygems.org and attempts to resolve dependencies. This takes a couple minutes.
- Failure: Cannot update compass, because it is locked to an older version. (Never mind that we specifically told Bundler to update rb-inotify — it’s going to try and update everything.)
- Revert Gemfile change to compass.
- Run `bundle update rb-inotify` again. Bundler downloads the source index from rubygems.org and attempts to resolve dependencies. This takes a couple minutes.
- Update the Gemfile again and run `bundle update compass` again. Bundler downloads the source index from rubygems.org and attempts to resolve dependencies. This takes a couple minutes.
- Failure: Newest version of compass requires a newer version of rb-fsevent.
- Run `bundle update rb-fsevent`. Bundler downloads the source index from rubygems.org and attempts to resolve dependencies. This takes a couple minutes.
- Failure: Cannot update compass, because it is locked to an older version. (Never mind that we specifically told Bundler to update rb-fsevent — it’s going to try and update everything.)
- Revert Gemfile change to compass.
- Run `bundle update rb-fsevent` again. Bundler downloads the source index from rubygems.org and attempts to resolve dependencies. This takes a couple minutes.
- Update the Gemfile again and run `bundle update compass` again. Bundler downloads the source index from rubygems.org and attempts to resolve dependencies. This takes a couple minutes.
- Run `bundle update guard` again. Bundler downloads the source index from rubygems.org and attempts to resolve dependencies. This takes a couple minutes.
- Whoops — Bundler says Compass requires v1.1.x of the listen gem. (Again, I can’t find where that is specified anywhere.) But guard 1.8.3 requires listen v1.3.x! Looks like another typical no-win situation.
- Looking through the guard version log, I see that 1.8.2 only requires v1.0 or higher of the listen gem.
- Set guard version in the Gemfile and Gemfile.lck to 1.8.2.
- Run `bundle update guard` again. Bundler downloads the source index from rubygems.org and attempts to resolve dependencies. This takes a couple minutes.
- Failure: guard requires a newer version of the formatador gem.
- Run `bundle update formatador`. Bundler downloads the source index from rubygems.org and attempts to resolve dependencies. This takes a couple minutes.
- Run `bundle update guard` again. Bundler downloads the source index from rubygems.org and attempts to resolve dependencies. This takes a couple minutes.
There you go. Updating a single gem — something that should take less than five minutes — stretched out into over an hour. (BTW, upgrading guard did fix the CPU problem.)
If you’re like me, you’ll have a lot of time to wonder about this process:
- Why can’t Bundler automatically update dependencies?
- Why does Bundler stop as soon as it hits an incompatible gem (instead of showing all incompatible gems)?
- Why is rubygems.org so slow?
- Why can’t Bundler cache the source indexes?
- Why does Bundler constantly recommend running `bundle update`, when it’s common knowledge among Ruby developers that this is something you should never do?
- Why are gem authors setting dependencies to specific minor versions, sometimes at random?
- Why are gem authors introducing backward incompatibilities in minor versions?
|
gem |
next time try `bundle update guard listen …` in one command
Eric |
Okay, but how do I know that listen is a dependency without researching that or rerunning the command? I expect a dependency manager to manage my dependencies for me.
Michael Franzl |
I hear you. Bundler is supposed to make life easier, but it makes it harder, and there are no alternatives. It tells me “rake not found in any of the sources”, even though I followed the documentation of checking in my gems into vendor/cache with “bundle package”. I’m not a newbie to the Ruby world, and bundler keeps consuming my time. Bundler is useful in a very, very narrow field of application.