Quần Cam

Bundler Gotcha

A few days ago I encountered a strange behavior of Bundler so this post notes down how my experience with it was.

What’s up?

We know what Bundler provides Bundle.require and Bundle.setup to deal with dependencies grouping/requiring in our project. With Bundle.setup we can explicitly specify which gem groups we want to add to $LOAD_PATH.

Says we have an Gemfile

gem "rack"
gem "sinatra"
gem "puma", group: :production

As the API tells, we can make only “development” gems available for requiring.

# config.ru

require 'bundler'
Bundler.setup(:default, :development)

require 'sinatra/base'
class MyApp < ::Sinatra::Base; end

run MyApp

Then boot up the server with bundle exec

bundle install
bundle exec rackup

# Puma starting in single mode...
# * Version 3.8.2 (ruby 2.2.2-p95), codename: Sassy Salamander
# * Min threads: 0, max threads: 16
# * Environment: development
# * Listening on tcp://localhost:9292
# Use Ctrl-C to stop

KABOOM ! Puma is booted! Why?

What happened?

Rack

By default Rack will boot WEBrick if there’s no server found. But it will detect for a more powerful server like Puma or Thin and if there is one, Rack will prioritize to load that instead.

Bundler

But isn’t Puma un-requireable? Haven’t we grouped that server to only appear on production and Bundler.setup should have their job done well?

The tickle of curiosity drove me to look through the code of bundle exec. It turned out in this line of code, Bundler tries to setup and brings everything we have in Gemfile into $LOAD_PATH. This part of code is executed way in prior to our Bundler.setup and once a Bundler.setup is called, all latter ones are void (see my PR below).

That’s why Rack could see Puma and therefor booted the server up. Additionally, we can require any gem we have in config.ru despite of its group specified in Gemfile.

I actually created an issue on Bundler’s Github and also a pull request to clarify it. Interestingly @segiddins, the repo owner, advised the behavior that strange to me (and some people I assume) is sort of … intentional.

So what?

What’d be the possible solution for this case?

bundle config without

Never run bundle install alone, run it with --with

bundle install --without production

With this option Bundler will not download the gems in production group. Bundler is smart enough to remember our config and use it for future calls.

Build your own bundle exec

# bin/bundle-exec

require "rubygems"
require "bundler"

Bundle.setup(:default, ENV.fetch("RACK_ENV", "development").to_sym)

Use Bundler.reset!

After digging Bundler’s codebase here and there, I realize that we can use Bundle.reset! to reset everything, then use Bundle.setup to set up again.

This doesn’t solve the Puma problem we have above but at least from our Bundle.setup on, there will no gem from other groups can be required.

# config.ru

require "rubygems"
require "bundler"

Bundle.reset!
Bundle.setup(:default, :development)

Anyway, this method is untested, in fact I never try using or testify it. Use it at your own risk, I take no responsible for it.

What’s next?

Personally I think this behavior of Bundler might cause some false positives. What if we accidentally loaded up some gem that’s merely for development or test? What if that gem was … DatabaseCleaner ?

Also there’s no option in bundle exec for us to specify the group either.

So sorry for nagging but once more.

Never ever run bundle install alone, use it with --without!


NGUY HIỂM! KHU VỰC NHIỀU GIÓ!
Khuyến cáo giữ chặt bàn phím và lướt thật nhanh khi đi qua khu vực này.
Chức năng này hỗ trợ markdown và các thứ liên quan.

Bài viết cùng chủ đề

Euruko 2017 Notes

Vừa rồi mình đi Euruko 2017 ở Budapest, một số bài nói cũng khá thú vị nên mình sẽ note lại ở đây.

Five Rails Gotchas

It’s undeniable that Rails is a great framework to speedily build up your application. However, despite of its handiness, like other frameworks, Rails has its own flaws and is never a silver bullet. This post is going to show you some of the gotchas (or pitfalls you name it) I encountered while working with Rails.

You don't need RVM gemset

We can’t deny the contribution RVM gemset gave up to the Ruby community, but do we really need gemsets to isolate our project dependencies these days?