Rbenv — How it works

Dealing with rbenv errors without Google or Stackoverflow

Today I installed a new gem. When I tried to run the command provided by the gem, I got an error zsh: command not found: wheneverize.

I realized I forgot to run rbenv rehash. So I ran it and this is what I got rbenv: cannot rehash: /Users/shot/.rbenv/shims/.rbenv-shim exists. I searched about it and found a Github issues link that had a good discussion on why this happens.

The file .rbenv-shim gets created during the rehash process to indicate a rehash is in progress. If a previous rehashing was interrupted in the middle or couldn’t finish for some reason, the file will still be there. It then needs to be deleted manually, so that you can rehash again.

I deleted the file, did a rehash and everything worked fine. I have run across issues with rbenv before and every-time I would just search on the internet about it and apply the solution I find on a stackoverflow link. This time I thought I will try to understand how rbenv works. So, next time something goes wrong, I will be well equipped to handle it myself. And also I get a kick out of knowing how my tools work underneath.

Rbenv

rbenv is really simple at its core. You install different ruby versions with rbenv install version and pick a version for a specific project with rbenv local version and set a global ruby version with rbenv global version.

Rbenv puts different Ruby versions in ~/.rbenv/versions.

Shims

If you go into ~/.rbenv, you will find another directory shims. Shims? What is it? Here is its wikipedia description.

In computer programming, a shim is a small library that transparently intercepts API calls and changes the arguments passed, handles the operation itself, or redirects the operation elsewhere.

This is exactly what rbenv shims do. A rbenv shim intercepts a ruby related command and calls rbenv exec which takes care of running the right executable.

How does rbenv intercept ruby commands? It does so by putting all possible commands as separate executable files in ~/.rbenv/shims and adding this directory before all the other directories in $PATH env variable. So when you run ruby -v, the system will find the shim script ruby in~/.rbenv/shims directory and execute it.

Lets see what’s inside a file in shims directory.

#!/usr/bin/env bash

set -e

[ -n "$RBENV_DEBUG" ] && set -x



program="${0##*/}"

if [ "$program" = "ruby" ]; then

for arg; do

case "$arg" in

-e* | -- ) break ;;

*/* )

if [ -f "$arg" ]; then

export RBENV_DIR="${arg%/*}"

break

fi

;;

esac

done

fi



export RBENV_ROOT="/Users/shot/.rbenv"

exec "/usr/local/Cellar/rbenv/1.0.0/libexec/rbenv" exec "$program" "$@"

Something you will observe when you open different files in the shims folder is that all the files have exactly the same content. Be it .rbenv/shims/rails or .rbenv/shims/bundler they are all the same.

Here is a translation of that shell script into pseudo-code for those who aren’t familiar with shell scripting.

set ‘e’ so that the script exits when it encounters an error.

if RBENV_DEBUG is set, set ‘x’ to print every command executed in this session. (Just trying running `RBENV_DEBUG=1 ruby -v`) store the command name into $program. (If the command was rails, $program == “rails”) if $program == “ruby”

for ‘arg’ in (each command line arguments)

if `arg` startswith ‘-e’ or equals ‘ — ‘

break out of the loop

if `arg` matches the pattern ‘*/*’ and `arg` == a regular file

export RBENV_DIR=(first part of the path i.e directory name)

break out of the loop export RBENV_ROOT=”/Users/shot/.rbenv”

exec “/usr/local/Cellar/rbenv/1.0.0/libexec/rbenv” exec “$program” “$@”

So a shim file on its own is not doing much. It sets up the environment variables RBENV_DIR and RBENV_ROOT. It then executes the command /usr/local/Cellar/rbenv/1.0.0/libexec/rbenv exec original-command original-args.

eg. rails s is interpreted by ~/.rbenv/shims/rails which then runs rbenv exec rails s.

rbenv exec

Rbenv exec script takes over from the shim script.

From the documentation:

Runs an executable by first preparing PATH so that the selected Ruby version’s ‘bin’ directory is at the front.

Lets examine how it works by going through the steps involved. We will use rbenv exec rails s here as an example.

Rbenv exec starts off by finding the right Ruby version to apply. To find the right version it runs rbenv version-name. rbenv version-name first looks at the current directory for a local version file named .ruby-version. If it exists it reads the version from there. Else it reads the version from the global version file at ~/.rbenv/version.

RBENV_VERSION=2.2.3

2. It then finds the command which is the first argument to exec script.

RBENV_COMMAND=rails

3. It then runs rbenv which cmd to find the path of the right executable by using the above found values.

-> rbenv which rails /Users/shot/.rbenv/versions/2.2.3/bin/rails RBENV_COMMAND_PATH=/Users/shot/.rbenv/versions/2.2.3/bin/rails

4. It then trims the last part of RBENV_COMMAND_PATH to the find the value of RBENV_BIN_PATH. This path is then prepended to the $PATH environment variable.

RBENV_BIN_PATH=/Users/shot/.rbenv/versions/2.2.3/bin export PATH=”${RBENV_BIN_PATH}:${PATH}”

5. Finally the original command is run. Now the system will find the right binary instead of the shim.

rails s

So to summarize, when you run rbenv exec rails s, it is roughly turned into PATH=”~/.rbenv/versions/2.2.3/bin:$PATH” rails s by rbenv exec.

rbenv rehash

This commands jobs is to create shim scripts. After you install a gem, its shim won’t be there. Hence the need to run rbenv rehash every-time after you install a gem.

Here is step by step breakdown on how shims are created.

Rehash sees if the directory ~/.rbenv/shims exists. If it doesn’t, it is created. It then checks if the shim prototype file .rbenv/shims/.rbenv-shim exists. If it exists, it means there is already an instance of rehash running. So the current rehash exits. If it doesn’t, rehash creates prototype file and also acquires a lock on it. It then puts the contents of shim script(the script we saw earlier) into the prototype file. Next, it iterates through all the files in bin directories of all versions of Ruby installed. For each file, it creates a shim script in shims directory and copies the contents of the prototype file into the shim script. (Now you know why all the shim scripts are the same.) Finally, rehash deletes the prototype file.

So that wraps up this post on rbenv internals. I hope it gave you a good idea on how rbenv works. I will update this post if I happen to read more on other topics like rbenv hooks and plugins.

Happy hacking!