Yehuda Katz is a member of the Ember.js, Ruby on Rails and jQuery Core Teams; he spends his daytime hours at the startup he founded, Tilde Inc.. Yehuda is co-author of best-selling jQuery in Action and Rails 3 in Action. He spends most of his time hacking on open source—his main projects, like Thor, Handlebars and Janus—or traveling the world doing evangelism work. He can be found on Twitter as @wycats and on Github.

Using >= Considered Harmful (or, What’s Wrong With >=)

TL;DR Use ~> instead.

Having spent far, far too much time with Rubygems dependencies, and the problems that arise with unusual combinations, I am ready to come right out and say it: you basically never, ever want to use a >= dependency in your gems.

When you specify a dependency for your gem, it should mean that you are fairly sure that the unmodified code in the released gem will continue to work with any future version of the dependency that matches the version you specified. So for instance, let’s take a look at the dependencies listed in the actionpack gem:

activemodel (= 3.0.0.rc, runtime)
activesupport (= 3.0.0.rc, runtime)
builder (~> 2.1.2, runtime)
erubis (~> 2.6.6, runtime)
i18n (~> 0.4.1, runtime)
rack (~> 1.2.1, runtime)
rack-mount (~> 0.6.9, runtime)
rack-test (~> 0.5.4, runtime)
tzinfo (~> 0.3.22, runtime)

Since we release the Rails gems as a unit, we declare hard dependencies on activemodel and activesupport. We declare soft dependencies on builder, erubis, i18n, rack, rack-mount, rack-test, and tzinfo.

You might not know what exactly the ~> version specifier means. Essentially, it decomposes into two specifiers. So ~> 2.1.2 means >= 2.1.2, < 2.2.0. In other words, it means “2.1.x, but not less than 2.1.2″. Specifying ~> 1.0, like many people do for Rack, means “any 1.x”.

You should make your dependencies as soft as the versioning scheme and release practices of your dependencies will allow. If you’re monkey-patching a library outside of its public API (not a very good practice for libraries), you should probably stick with an = dependency.

One thing for certain though: you cannot be sure that your gem works with every future version of your dependencies. Sanely versioned gems take the opportunity of a major release to break things, and until you have actually tested against the new versions, it’s madness to claim compatibility. One example: a number of gems have dependencies on activesupport >= 2.3. In a large number of cases, these gems do not work correctly with ActiveSuport 3.0, since we changed how components of ActiveSupport get loaded to make it easier to cherry-pick.

Now, instead of receiving a version conflict, users of these gems will get cryptic runtime error messages. Even worse, everything might appear to work, until some weird edge-case is exercised in production, and which your tests would have caught.

But What Happens When a New Version is Released?

One reason that people use the activesupport >= 2.3 is that, assuming Rails maintains backward-compatibility, their gem will continue to work in newer Rails environments without any difficulty. If everything happens to work, it saves you the time of running their unit tests against newer versions of dependencies and cutting a new release.

As I said before, this is a deadly practice. By specifying appropriate dependencies (based on your confidence in the underlying library’s versioning scheme), you will have a natural opportunity to run your test suite against the new versions, and release a new gem that you know actually works.

This does mean that you will likely want to release patch releases of old versions of your gem. For instance, if I have AuthMagic 1.0, which worked against Rails 2.3, and I release AuthMagic 2.0 once Rails 3.0 comes out, it makes sense to continue patching AuthMagic 1.0 for a little while, so your Rails 2.3 users aren’t left out in the cold.

Applications and Gemfiles

I should be clear that this versioning advice doesn’t necessarily apply to an application using Bundler. That’s because the Gemfile.lock, which you should check into version control, essentially converts all >= dependencies into hard dependencies. However, because you may want to run bundle update at some point in the future, which will update everything to the latest possible versions, you might want to use version specifiers in your Gemfile that seem likely to work into the future.

18 Responses to “Using >= Considered Harmful (or, What’s Wrong With >=)”

Yehuda — your year of screwing over the Rails community with Bundler isn’t going to be fixed by hard-coding every dependency of every gem. Bundler is considered harmful — period. The only thing worse than a gem that doesn’t work because of an updated dependency is a gem that doesn’t work because of a hardcoded gemspec.

See Rails 2.3 and Rack 1.1.0…
http://boblet.tumblr.com/post/493502322/rack
https://rails.lighthouseapp.com/projects/8994/tickets/4031-having-rails-23pre-or-rack-11-installed-breaks-rails-2x

If Rails had specified Rack >= 1.0 in the first place, there would have been no multi-month-long hassle.

Ryan Daivs mentioned this 2 years ago:

http://blog.zenspider.com/2008/10/rubygems-howto-preventing-cata.html

But perhaps not that eloquently as you put here now.

Still today, seems that both users and gem developers do not pay attention to their gem and application dependencies.

Even with tools like Bundler and RubyTracker, chances are that a new gem release can crash the world of so many users :-(

Perhaps one day everybody will learn to know their dependencies better ;-)

This problem has bitten me quite a few times, and more often than not it’s been ActiveSupport. It’s too bad the Gemcutter API isn’t a bit more robust so one could search through all the Gemspecs and work on getting patches to everyone who uses >= (especially with ActiveSupport)

The worst part is that the error is never obviously a dependency problem, and it’s really only after half an hour of wasted time do you realize what is rong.

Even better, use =.

I want to be as sure as I can that my code will work across environments, specifically from development to production. Using = seems to be the strictest way and it’s no big deal to upgrade the dependencies in a specific commit, after ensuring all my tests pass.

Oops, posted too fast! In my previous comment I was specifically talking about non library/rubygem code, where I believe having tighter dependencies makes more sense.

Agreed. You’ll still need something like bundler for dependencies of dependencies though, unless you manually list every single gem and all the dependencies (and then manually update the list when you want to make a change).

@Mark = dependencies doesn’t get you bug fixes in patch releases. ~> is what I tend to go for.

How does the ~> work with beta gems? Like activemodel ~> 3.0.0.beta?

Versioning in Ruby in general is insanely screwed up. Version bumps that look patch level have public API changes. Patch level releases get api changing version bumps. I’ve locked all of my dependencies in my libraries to that exact version that I know works.

@wycats I agree back :)

@Carl Sure, it won’t automatically get you the patches, but of course you can just change the dependency version manually. My point is that I don’t want my code using different versions of gems across environments.

Its the dependency hell that every dependency library has (Ivy with .net or java and Maven with Java face similar issues).

Just the example where the “weird edge-case is exercised in production, and which your tests would have caught.”, if your tests did not get the failures in your test and continuous integration environment, it means you did not test that corner case. So even if you change by hand the gem dependency you would not find the failure in your test enviroment.

Of course, the minimum quality requirement here is that you use the same gems in production that you use in dev/test. But there is a huge number of people who does not, too bad. In those cases, its surely better to play safer and use ~>.

It turns out that the question is, as with the other engines, do you want your tests to fail early or fail late? Do you want (in Ruby/Rails case) builder to complain or your tests to fail? It seems to me that it is a choice, and with every choice there is a trade off behind it, it might be dangerous to say “always do” and “never do” because we are leaving the negative aspect of our decision aside.

Introducing the ~> operator makes no sense to me. Why not just allow Bundler to accept version strings in a format like “2.1.x”?

Brian,

I agree. When I publish gems, I follow the rational versioning policy prescribed by rubygems: http://docs.rubygems.org/read/chapter/7 If everybody did this, then we would be able to manage our gem dependencies better.

I think locking your gems to the exact version that you know works is a great way to prevent problems and the only way to be sure that your configuration will always work.

Good stuff. I agree that people need to be more careful about declaring gem version dependencies, and better about following semantic versioning.

I’ve run into a situation where I’m not totally sure of the best practice: my VCR gem has an either/or dependency. It works with either FakeWeb or WebMock but not both. I’m not declaring either as a runtime dependency since that would cause a tool like bundler to install them. Instead, I’m doing a runtime check and raising an error if the FakeWeb/WebMock version is too old. After reading this, I’m thinking I’ll probably update the code to have it print a warning if the version is too new (based on the ~> operator).

Is there a better, more standardized approach to declaring the version requirement for an either/or gem dependency?

Code, for anyone interested in the details, is here:
http://github.com/myronmarston/vcr/blob/v1.1.0/lib/vcr/http_stubbing_adapters/fakeweb.rb#L10-14
http://github.com/myronmarston/vcr/blob/v1.1.0/lib/vcr/http_stubbing_adapters/webmock.rb#L10-14
http://github.com/myronmarston/vcr/blob/v1.1.0/lib/vcr/http_stubbing_adapters/base.rb#L20-31

Myron

It’s sad to see that Ruby is sliding more and more towards the tangled mess of Java when it comes to versioning and API Stability. Things like bundler and the suggestions in this post are pragmatic tips to adapt to the currently existing Ruby world. They are not solutions, and over time will just lead to enormous headaches in other areas – a few years from now, who will want to figure out how to distribute a patch for a critical security flaw in authlogic ?

Versioning and API stability have long been addressed in the C world and in Linux package managers, and it would be great if the Ruby community learned about those headaches (e.g. from the Fedora packaging guidelines) and adopted as much of them as possible.

The native tools for versioning in C are way more primitive than those in rubygems. But libtool versioning has made it possible for people to version in a predictable way, and made everybody’s habits fairly uniform.

When I look at a C library, I don’t need to look for their versioning policy, the guarantees they make around API stability etc. If the library uses libtool’s versioning mechanisms (and most do), I know what the policy is.

It would be great if rubygems made assigning version numbers more uniform in a similar manner:
* always use versions like X.Y.Z
* if X changes, assume API incompatibility
* if Y changes, API stays stable, but might have been expanded
* if Z changes, API stays identical, but implementation has changed
The goal for any library has to be that X will never change

With such a scheme widely adopted, gem could know that ‘foo >= 1.2.3′ implies ‘foo < 2.0.0′

Using ~>, you’re hoping that the gem developers are only pushing bug fixes that won’t break anything you have in your gem.

As Yehuda stated, “One thing for certain though: you cannot be sure that your gem works with every future version of your dependencies”.

I prefer hard versioning and deal with updating gem dependencies after testing to make sure all is good.

Robert: hard versioning is fine for an application. In a gem in can cause problems. Consider this:

- gem X depends on gem Y = 1.2.3
- gem Z depends on gem Y ~> 1.2.4

If a user wants to use both gem X and gem Z in their application, they’re out of luck because of gem X’s hard version dependency. If gem X declared the dependency as ~> 1.2.3, there wouldn’t be a problem.

“Consider this:

- gem X depends on gem Y = 1.2.3
- gem Z depends on gem Y ~> 1.2.4

If a user wants to use both gem X and gem Z in their application, they’re out of luck because of gem X’s hard version dependency. If gem X declared the dependency as ~> 1.2.3, there wouldn’t be a problem.”

If Y 1.2.4 breaks gem x there is a problem. In Ruby-land this is a real danger.

The issue with ruby gems is the rank amateurism going on. Not only are 99% of the gems junk, they are undocumented and hardly anyone in the Ruby community cares if a patch release breaks the API it is presenting to the user.

I use as few gems as possible and roll my own because it is faster, the gem will do exactly what I need it to, and I don’t have to worry about the stupidity of a patch version breaking compatibility. Yes,rolling my own is faster(because of crappy code in gems and lack of docs) and less error-prone.

Bundler was a good idea, sort of executed well, but the problem can’t be fixed until we boot the amateurs back to PHP and VB where they belong and then clear out github of all the crap floating around.

Leave a Reply

Archives

Categories

Meta