How to specify local Ruby gems in your Gemfile
Stop adding :path in your Gemfile and use bundle config instead
Let's say you're building a Ruby app and your team has extracted one or more gems referenced in your Gemfile, such as your custom Trello API client, Tacokit.rb.
# Gemfile
source "https://rubygems.org"
# lots of gems ...
gem "tacokit"
Maybe Trello made some recent changes to their API that your current feature depends
on, so you need to update the Tacokit
gem as part of your work. You have a
local checkout of the tacokit
gem in another directory in your laptop.
You add some code to the gem, but now you want to test the changes in your app. How do you do that?
According to the most popular answer (and accepted) answer to the question, "How can I specify a local gem in my Gemfile?", we should do the following:
gem "tacokit", path: "/path/to/tacokit"
Here's my take: avoid this recommendation
...especially if you work on a team and/or deploy this code to remote servers.
WAT
Technically, it does work. Run $ bundle update
, restart the app, and - boom! - our changes in
the local tacokit
checkout are showing up as expected.
Then the trouble begins.
We push our app changes and deploy to the staging server to test them out in the shared environment and - wait a minute - the app won't even start.
$ bundle
The path `/Users/ross/does/not/exist` does not exist.
Oops! We forgot to remove the :path
reference in the Gemfile
.
Let's fix that... we remove the :path
reference, push, and redeploy. The app
restarts fine. But while testing the feature, we start getting 500 errors. This wasn't happening locally.
"But it worked on my machine!" - every developer ever
The Rails logs reveal we have a bunch of undefined method errors coming from calls to Tacokit
. That's right, we forgot another key step in this workflow: pushing our local Tacokit
changes to the remote!
OK, after we've done that and redeployed the app, we're still getting 500 errors.
D'oh! We were working on a branch of tacokit
but we reference it in our app's Gemfile
.
Taking a step back
Good thing we weren't pushing that app feature to production. We would have been wise to run the tests on our CI server first where we would have seen the same errors (assuming we had the right tests... and a CI server).
Using the :path
often means pointing to a location that only exists on our local machine. Every time we want to develop against the local tacokit
gem, we have to remember to edit the Gemfile
to remove the option so we don't screw up our teammates or break the build. We also can't forget to point to correct branch.
This workflow is no good because we're human and humans tend to forget to do things.
"bundle config local" to the rescue
Buried deep in the Bundler docs is a better solution for working with local git repo: the bundle config local
command. Instead of specifying the :path
option, we can run the following on command line:
$ bundle config local.tacokit /path/to/tacokit
Here we instruct Bundler to look in a local resource by modifying our local Bundler configuration. That's the one that lives in
.bundle/config
outside of version control.
No more editing shared code for local development settings.
We can confirm the link with bundle config
:
$ bundle config
Settings are listed in order of priority. The top value will be used.
local.tacokit
Set for your local app (/Users/rossta/.bundle/config): "/Users/rossta/path/to/tacokit"
We can scope the configuration to a specific folder with the --local
flag:
$ bundle config --local local.tacokit /path/to/tacokit
$ bundle config
Settings are listed in order of priority. The top value will be used.
local.tacokit
Set for your local app (/Users/rossta/path/to/app/.bundle/config): "/Users/rossta/path/to/tacokit"
To take advantage of this local override in the app, we have to specify the remote repo and branch in the Gemfile
:
gem "tacokit", github: "rossta/tacokit", branch: "master"
Bundler will abort if the local gem branch doesn't match the one in the Gemfile
and checks that the sha in Gemfile.lock exists in the local repository.
This way we ensure our Gemfile.lock contains a valid reference to our local gem.
We don't get these assertions when using the :path
option.
It's easy to remove the local config after we don't need it:
bundle config --delete local.YOUR_GEM_NAME
Caveats
As with the :path
option, we still need to remember to push our
local gem changes to the remote repository when using bundle config local
.
I should also mention that a good use case for using :path
instead of bundle
config local
it when the local gem is in a subdirectory relative to your app,
like when using git submodules.
I don't often see this in practice, but there are valid reasons for doing so.
The main point here is that the Gemfile options work for all systems where the
repository is bundled.
In general, I'd encourage using either approach sparingly for gems that your
team doesn't own as it's typically best to stick the official releases for
active repositories. In my experience, it's most common to develop against local gems for
projects that your team does own, so bundle config local
will ensure your
co-workers know where to look to verify code dependencies.
Don't use :path, use bundle config local instead
Though convenient, using the :path
option in our Gemfile
to point to a local
gem elsewhere on our machine sets us up for three potential problems without automated prevention:
- Committing a nonexistent lookup path on other machines
- Failing to point to the correct repository branch
- Failing to point to an existing git reference
Forget the :path
option and you'll never forget ^^this stuff^^ again.
Just use this command:
bundle config local.YOUR_GEM_NAME
And don't believe everything you read on StackOverflow.