2020-12-30

ruby-buildとrbenvのプラグイン機構

前回の記事 に続いてrbenvネタ。 今回は、ruby-buildとrbenvの関係について。

ruby-buildとは何か

ruby-buildとは、あらゆるバージョンのrubyを簡単にインストールするためのコマンドラインユーティリティ。本家のREADME には次のように書かれている。

ruby-build is a command-line utility that makes it easy to install virtually any version of Ruby, from source.

ruby-buildはrbenvのプラグインとして使うこともできるし、スタンドアロンなコマンドとして使うこともできる。

多くのrubyユーザは前者のrbenvのプラグインとして使っていることと思う。自分もそうなので、今回はこちらについて少し深堀っていく。

rbenv install コマンドはruby-buildが提供している

rbenv --help コマンドを実行すると次のような結果が出力される。

❯ rbenv --help
Usage: rbenv <command> [<args>]

Some useful rbenv commands are:
   commands    List all available rbenv commands
   local       Set or show the local application-specific Ruby version
   global      Set or show the global Ruby version
   shell       Set or show the shell-specific Ruby version
   install     Install a Ruby version using ruby-build
   uninstall   Uninstall a specific Ruby version
   rehash      Rehash rbenv shims (run this after installing executables)
   version     Show the current Ruby version and its origin
   versions    List installed Ruby versions
   which       Display the full path to an executable
   whence      List all Ruby versions that contain the given executable

See `rbenv help <command>' for information on a specific command.
For full documentation, see: https://github.com/rbenv/rbenv#readme

ここにある install というサブコマンドは、実はrbenv本体には組み込まれておらず、rbenvプラグインとしてのruby-buildが提供しているコマンドになっている。

その証拠に、rbenv coreのコマンドが含まれる libexecディレクトリ以下 を見てもinstall機能を提供する実行ファイルは含まれていない。

実際に自分の環境の rbenv/libexec 以下を見ても該当するファイルが存在しないことを確認した (※ rbenv自体はhomebrewでinstallしている)。

❯ ls -1 /usr/local/Cellar/rbenv/1.1.2/libexec/ | grep install
# => 結果なし

rbenvはどのようにしてプラグインを読み込むか

ではrbenv本体はどのようにしてプラグインとしてのruby-buildを読み込み、installコマンドをはやしているか?

これを理解するには、rbenvのプラグイン機構について知る必要がある。 rbenv公式のWiki を見ると、パスが通っているところに rbenv-COMMAND という形式で配置すれば良い といった旨の記述が見つかる。

つまり、install サブコマンドを生やすruby-buildの場合は rbenv-install という実行ファイルがPATHの通っているどこかに存在するということになる。

実際に自分の環境で調べてみたところ、/usr/local/bin/ 以下に rbenv-install ファイルを見つけることができた。/usr/local/bin にはもちろんパスが通っている。

❯ file /usr/local/bin/rbenv-install
/usr/local/bin/rbenv-install: Bourne-Again shell script text executable, ASCII text

# パスが通っていることを確認
❯ echo $PATH | grep --only-matching '/usr/local/bin'
/usr/local/bin

なお、自分の場合はruby-buildもhomebrewでインストールしているので、実際には /usr/local/Cellar/ruby-build/VERSION/bin/rbenv-install へのシンボリックリンクとなっている。

おまけ:さらに深ぼってみる

ここまで来たので、ソースコードレベルでもう少し深ぼってみる。 rbenv-COMMAND という形式でPATHが通っているところに配置すれば実行してくれるのはわかったが、具体的にどうやって探索しているか?を見てみたい。

以下は、rbenvコマンドの実行ファイルの抜粋

command="$1"
case "$command" in
"" )
  { rbenv---version
    rbenv-help
  } | abort
  ;;
-v | --version )
  exec rbenv---version
  ;;
-h | --help )
  exec rbenv-help
  ;;
* )
  command_path="$(command -v "rbenv-$command" || true)"
  if [ -z "$command_path" ]; then
    if [ "$command" == "shell" ]; then
      abort "shell integration not enabled. Run \`rbenv init' for instructions."
    else
      abort "no such command \`$command'"
    fi
  fi

  shift 1
  if [ "$1" = --help ]; then
    if [[ "$command" == "sh-"* ]]; then
      echo "rbenv help \"$command\""
    else
      exec rbenv-help "$command"
    fi
  else
    exec "$command_path" "$@"
  fi
  ;;
esac

今回のruby-buildを例に考えると rbenv install .. とコマンドを実行したときのことを考える。以下、箇条書きでざっくりまとめる。

  • command="$1" の部分は、rbenvコマンドの次に渡される引数がくるので install という文字列が代入される
  • caseの最初の3つの条件には当てはまらず、* ) の分岐に入る
  • この次が肝。command_path="$(command -v "rbenv-$command" || true)" を実行して、rbenv-install コマンドのパスを取得している。
    • 実際に自分の環境で command -v rbenv-install を実行すると /usr/local/bin/rbenv-install という結果が得られた
  • if [ -z .. ]; then の部分は、引数の文字列長が0のときに真となるので、該当しないのでスキップ
  • shift 1 する。これにより次の $1 に入る値が rbenv install xxx コマンドを例にすると xxx に当たる。
  • $1--help では無いのでelse節へ
  • execrbenv-install が実行される

結論、command コマンドでrbenv-installのパスを取得したあとはexecに引き渡して実行するだけだった。

まとめ

  • ruby-buildとは、あらゆるバージョンのrubyを簡単にインストールするためのコマンドラインユーティリティ
  • rbenvのinstallコマンドは、rbenvプラグインとしてのruby-buildが提供している
  • rbenvは rbenv-COMMAND という形式の実行可能ファイルをPATHが通っているところに配置しておけば読み込んでくれる

参考