2017-11-13

rbenvコードリーディング

rbenv/rbenvのコードリーディングの備忘録。バージョンは1.1.0-2-g4f8925a です。

bin/rbenvはrcファイルで明示的にパスを通したメインの実行ファイルです。rcスクリプト内で  eval "$(rbenv init -)" を呼び出すところが起点になります。

まず~/.rbenv/libexecにパスを通します。libexecディレクトリ内に各サブコマンドの実体が入っています。

bin_path="$(abs_dirname "$0")"
for plugin_bin in "${RBENV_ROOT}/plugins/"*/bin; do
  PATH="${plugin_bin}:${PATH}"
done
export PATH="${bin_path}:${PATH}"

環境変数など諸々の設定が終わった後、$command_pathが実行されます。initの場合はlibexec/rbenv-initが実行されます。

command_path="$(command -v "rbenv-$command" || true)"
...
exec "$command_path" "$@"

rbenv init -を実行すると、以下のようなスクリプトが文字列として返されるので、rcスクリプト内でevalします。これによって.rbenv/shimsにパスを通したりcompletionsのスクリプトを実行して補完が効くようにするなどの初期化が行われます。また、bin/rbenvコマンドがラップされたrbenv関数が定義されます。

export PATH="/Users/mtajitsu/.rbenv/shims:${PATH}"
export RBENV_SHELL=zsh
source '/Users/mtajitsu/.rbenv/libexec/../completions/rbenv.zsh'
command rbenv rehash 2>/dev/null
rbenv() {
  local command
  command="$1"
  if [ "$#" -gt 0 ]; then
    shift
  fi

  case "$command" in
  rehash|shell)
    eval "$(rbenv "sh-$command" "$@")";;
  *)
    command rbenv "$command" "$@";;
  esac
}

rbenv init - で実行されるrehashの処理を追っていきます。実体はlibexec/rbenv-rehashです。中身は以下のようになっており、shimsディレクトリ内のshimファイルを作っています。shimファイルとは各バージョン差異を埋めるためのファイルであり、rbenvの場合は、ruby・irb・gemなどのファイル名で作成されるシェルスクリプトになります。これらのシェルスクリプトが各バージョンの実行ファイルを実行するような作りとなっています。

...
create_prototype_shim
remove_outdated_shims
make_shims $(list_executable_names | sort -u)
...
# Allow plugins to register shims.
OLDIFS="$IFS"
IFS=$'\n' scripts=(`rbenv-hooks rehash`)
IFS="$OLDIFS"

for script in "${scripts[@]}"; do
  source "$script"
done

install_registered_shims
remove_stale_shims

create_prototype_shim関数を見るとわかるとおり、全てのshimファイルは同じスクリプトであり、呼び出された名前によって呼び出す実行ファイルを切り替えています。

create_prototype_shim() {
  cat > "$PROTOTYPE_SHIM_PATH" <<SH
#!/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="$RBENV_ROOT"
exec "$(command -v rbenv)" exec "\$program" "\$@"
SH
  chmod +x "$PROTOTYPE_SHIM_PATH"
}

このファイルをプロトタイプとして作成し、install_registerd_shimsによってコピーを行い、rubyやgemのファイル名でシェルスクリプトを作成します。

install_registered_shims() {
  local shim file
  for shim in $registered_shims; do
    file="${SHIM_PATH}/${shim}"
    [ -e "$file" ] || cp "$PROTOTYPE_SHIM_PATH" "$file"
  done
}

このプロトタイプが呼び出すrbenv execの実体はlibexec/rbenv-execです。バージョンから適切な実行ファイルのパスを取得し、execコマンドで実行しています。

RBENV_VERSION="$(rbenv-version-name)"
RBENV_COMMAND="$1"
...
RBENV_COMMAND_PATH="$(rbenv-which "$RBENV_COMMAND")"
RBENV_BIN_PATH="${RBENV_COMMAND_PATH%/*}"
...
if [ "$RBENV_VERSION" != "system" ]; then
  export PATH="${RBENV_BIN_PATH}:${PATH}"
fi
exec -a "$RBENV_COMMAND" "$RBENV_COMMAND_PATH" "$@"

rbenv-version-nameはRBENV_VERSIONの環境変数を取得するか、無ければrbenv-version-file、rbenv-version-file-readによってバージョンを取得します。

if [ -z "$RBENV_VERSION" ]; then
  RBENV_VERSION_FILE="$(rbenv-version-file)"
  RBENV_VERSION="$(rbenv-version-file-read "$RBENV_VERSION_FILE" || true)"
fi

rbenv-version-file関数は.ruby-versionファイルを指定のディレクトリから階層を上にたどって検索してパスを返します。なければ~/.rbenv/version(globalで指定したバージョン)を返します。

find_local_version_file() {
  local root="$1"
  while ! [[ "$root" =~ ^//[^/]*$ ]]; do
    if [ -e "${root}/.ruby-version" ]; then
      echo "${root}/.ruby-version"
      return 0
    fi
    [ -n "$root" ] || break
    root="${root%/*}"
  done
  return 1
}

if [ -n "$target_dir" ]; then
  find_local_version_file "$target_dir"
else
  find_local_version_file "$RBENV_DIR" || {
    [ "$RBENV_DIR" != "$PWD" ] && find_local_version_file "$PWD"
  } || echo "${RBENV_ROOT}/version"
fi

rbenv-version-file-readはrbenv-version-fileで取得したファイルパスからバージョンを取得します。

#!/usr/bin/env bash
# Usage: rbenv version-file-read <file>
set -e
[ -n "$RBENV_DEBUG" ] && set -x

VERSION_FILE="$1"

if [ -e "$VERSION_FILE" ]; then
  # Read the first non-whitespace word from the specified version file.
  # Be careful not to load it whole in case there's something crazy in it.
  IFS="${IFS}"$'\r'
  words=( $(cut -b 1-1024 "$VERSION_FILE") )
  version="${words[0]}"

  if [ -n "$version" ]; then
    echo "$version"
    exit
  fi
fi

exit 1

rbenv localを使うと.ruby-versionファイルが書き込まれたり、現在のバージョンを表示できます。

RBENV_VERSION="$1"

if [ "$RBENV_VERSION" = "--unset" ]; then
  rm -f .ruby-version
elif [ -n "$RBENV_VERSION" ]; then
  rbenv-version-file-write .ruby-version "$RBENV_VERSION"
else
  if version_file="$(rbenv-version-file "$PWD")"; then
    rbenv-version-file-read "$version_file"
  else
    echo "rbenv: no local version configured for this directory" >&2
    exit 1
  fi
fi

ちなみにrbenv自体のバージョンはgit describeの結果(タグ+タグからのコミット数+コミット番号)を表示しています。

version="1.1.0"
git_revision=""

if cd "${BASH_SOURCE%/*}" 2>/dev/null && git remote -v 2>/dev/null | grep -q rbenv; then
  git_revision="$(git describe --tags HEAD 2>/dev/null || true)"
  git_revision="${git_revision#v}"
fi

echo "rbenv ${git_revision:-$version}"
このエントリーをはてなブックマークに追加