Quản lý dependency trong Ruby thường chỉ định version Ruby và gem mà project rely on. Theo kinh nghiệm làm việc với Ruby, debug dependencies là một trong những thách thức lớn nhất. Failures không common vì nhiều thứ "just work"; tuy nhiên, khi things go wrong, chúng thường difficult to debug và fix. Trong bài này, tôi sẽ trình bày các pieces involved in dependency management Ruby. Điều này hỗ trợ debug những strange issues khi chúng xảy ra.

Ruby Code Loading
Mặc định, Ruby language cung cấp hai major methods load code defined elsewhere: load & require.
load 'json.rb'
require 'json.rb'
require_relative 'json.rb'
Cả hai loading methods chấp nhận absolute và relative paths. Tuy nhiên, hai differentiating factors:
- Multiple calls
loadsẽ re-execute file, multiple callsrequiresẽ không re-execute; trả vềfalse. - Calls
loadresolves chỉ absolute và relative paths. Callsrequirecheck$LOAD_PATHkhi path không resolve absolute path.
Variant thứ ba require_relative, dùng relative paths require code relative to current file’s location thay vì Ruby process’ working directory.
Rbenv
Version manager là tool manage và easily switch giữa versions interpreter (Ruby) và specify location tìm gems cho project. Version managers largely language agnostic, các ngôn ngữ có implementations: Nvm, n cho Node.js, pyenv cho Python, Rbenv, rvm, chruby cho Ruby. Hãy thử rbenv.
Cài Đặt Ruby Version
Dùng command rbenv install cài bất kỳ version Ruby:
# Install ruby 2.6.1
$ rbenv install 2.6.1
Downloading openssl-1.1.1i.tar.gz... -> https://dqw8nmjcqpjn7.cloudfront.net/e8be6a35fe41d10603c3cc635e93289ed00bf34b79671a3a4de64fcee00d5242
Installing openssl-1.1.1i...
Installed openssl-1.1.1i to /home/directory/.rbenv/versions/2.6.1
Downloading ruby-2.6.1.tar.bz2... -> https://cache.ruby-lang.org/pub/ruby/2.6/ruby-2.6.1.tar.bz2
Installing ruby-2.6.1...
ruby-build: using readline from homebrew
Installed ruby-2.6.1 to /home/directory/.rbenv/versions/2.6.1
# Check Installation
$ rbenv versions
# Shows all versions installed.
system
2.6.1
# Lookup versions available for installation
$ rbenv install -L
1.8.5-p52
1.8.5-p113
1.8.5-p114
...
2.7.0-rc1
2.7.0-rc2
2.7.0
...
truffleruby+graalvm-20.1.0
truffleruby+graalvm-20.2.0
truffleruby+graalvm-20.3.0
# Full list ~500 versions, scroll nhiều.
# Shortcut dễ dàng tìm version cụ thể với fzf.
$ rbenv install `rbenv install -L | fzf`
Chuyển Đổi Giữa Các Version
Có nhiều cách xác định switch giữa versions Ruby; rbenv làm theo thứ tự:
- Check
RBENV_VERSION. - Tìm file
.ruby-versiontrong directory script và parent cho đến root. - Tìm file
.ruby-versiontrong$PWDvà parent directories cho đến root. - Dùng global file
~/.rbenv/version.
Priority từ trên xuống. ~/.rbenv/version fallback cuối cùng, treated as global version. Xem dưới:
# Inside First Project Root
# Select ruby version for project
$ touch .ruby-version && echo "2.7.1" >> .ruby-version
# Verify selected version
$ ruby --version
ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin20]
# Result
$ rbenv version
2.7.1 (set by /path/to/current/directory/.ruby-version)
# Result
# Change selected version
$ : >> .ruby-version && echo "2.6.1" >> .ruby-version
# Verify selection change
$ ruby --version
ruby 2.6.1p33 (2019-01-30 revision 66950) [x86_64-darwin20]
# Result
$ rbenv version
2.6.1 (set by /path/to/current/directory/.ruby-version)
# Change selection with RBENV_VERSION while .ruby-version present
$ export RBENV_VERSION=2.5.1
# Verify selection change
# .ruby-version is ignored.
$ ruby --version
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin20]
# Result
$ rbenv version
2.5.1 (set by RBENV_VERSION environment variable)
# Result
# Change to version not installed & remove RBENV_VERSION
$ unset RBENV_VERSION & : >> .ruby-version && echo "2.4.1" >> .ruby-version
# Verify selection change
$ ruby --version
rbenv: version `2.4.1' is not installed (set by full/path/to/current/directory/.ruby-version)
# Result
Shims Và Rehashing
Hai concepts này cần hiểu đúng để debug rbenv hiệu quả.
Shims là lightweight bash scripts tồn tại trong PATH intercept commands và route đến appropriate version để execute. High level, mọi command (ví dụ: rspec) translated thành rbenv exec rspec. Chi tiết dưới.
Thứ nhất, rbenv tạo shim cho all commands (rspec, bundle, etc.) across all installed Ruby versions intercept CLI calls regardless version. Shims tại ~/.rbenv/shims. Mỗi shim chứa same bash script:
#!/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="/home/directory/.rbenv"
exec "/usr/local/Cellar/rbenv/1.1.2/libexec/rbenv" exec "$program" "$@"
Script trên roughly translates to:
- Nếu program name
rubyvới argument-e,- Translate to
rbenv exec ruby <args>
- Translate to
- Nếu program name
rubyvới path to script,- Set
RBENV_DIRto script's directory. Cho phéprbenvtìm.ruby-versiontrong script's directory trước$PWD. Nếu.ruby-versionspecified cả hai,rbenvchọn script's directory.
- Set
- Nếu program name không phải Ruby,
- Translate to
rbenv exec <program-name> <args>.
- Translate to
Cuối cùng, rbenv exec <command-name> <args> xác định correct version pass command bằng check RBENV_VERSION environment variable. Nhớ RBENV_VERSION set bởi algorithm trên.
Shims trên PATH phải prepended; đảm bảo chúng first point of contact cho Ruby executables và intercept properly. Cách tốt nhất hiểu PATH setup và biết shims intercept properly:
$ which -a bundle
/path/to/home/.rbenv/shims/bundle
/usr/bin/bundle
which -a bundle: naive look through PATH print out order found, locations bundle có thể tìm thấy. Nếu in ra gì trước ~/.rbenv/shims, shims không setup properly. rbenv which bundle không reveal vì command works in context rbenv không search PATH.
Rehashing là process tạo shims. Khi mới install Ruby gem cung cấp executable, như rspec, cần chạy rbenv rehash tạo shim để subsequent calls rspec intercepted by rbenv và passed on appropriate Ruby version.
RubyGems
Tiếp theo RubyGems. Available từ official Ruby site. RubyGems là Ruby packaging system thiết kế facilitate creation, sharing, installation libraries; cách nào đó giống distribution packaging system như apt-get, nhưng target Ruby software. RubyGems de-facto method sharing gems. Thường cài tại ~/.rbenv/versions/{version-number}/lib/ruby/gems/{minor-version}/, hoặc variant, tùy version manager. Ruby's default required method Kernel.require không provide mechanism load gems từ Gems installation directory. RubyGems monkey-patches Kernel.require để
- First, search gems trong
$LOAD_PATH. - Nếu không tìm, search gems trong
GEMS INSTALLATION DIRECTORY.- Khi tìm thấy, add path vào
$LOAD_PATH.
- Khi tìm thấy, add path vào
Điều này works "natively" vì Ruby đi kèm RubyGems default từ version 1.9; Ruby versions trước cần cài RubyGems manually. Mặc dù works natively, quan trọng biết difference khi debugging.
Gem là bunch related code solve specific problem. Install gem và get info gem environment:
$ gem install gemname
$ gem env
RubyGems Environment:
- RUBYGEMS VERSION: 3.1.2
- RUBY VERSION: 2.7.1 (2020-03-31 patchlevel 83) [x86_64-darwin20]
- INSTALLATION DIRECTORY: /path/to/home/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0
- USER INSTALLATION DIRECTORY: /path/to/home/.gem/ruby/2.7.0
- RUBY EXECUTABLE: /path/to/home/.rbenv/versions/2.7.1/bin/ruby
- GIT EXECUTABLE: /usr/bin/git
- EXECUTABLE DIRECTORY: /path/to/home/.rbenv/versions/2.7.1/bin
- SPEC CACHE DIRECTORY: /path/to/home/.gem/specs
- SYSTEM CONFIGURATION DIRECTORY: /path/to/home/.rbenv/versions/2.7.1/etc
- RUBYGEMS PLATFORMS:
- ruby
- x86_64-darwin-20
- GEM PATHS:
- /path/to/home/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0
- /path/to/home/.gem/ruby/2.7.0
- GEM CONFIGURATION: ...
- REMOTE SOURCES:
- https://rubygems.org/
- SHELL PATH:
- /path/to/home/.rbenv/versions/2.7.1/bin
RubyGems giải quyết problem như thế nào? Nó monkey patches Kernel's require system với own require method. Với in-place, khi require honeybadger called, search gems folder honeybadger.rb và activate gem khi found.
Ví dụ, require 'honeybadger' produces something similar:
spec = Gem::Specification.find_by_path('honeybadger')spec.activate
Activating gem nghĩa là put nó trong $LOAD_PATH. RubyGems cũng giúp download all gem's dependencies trước download gem itself.
Cũng, Rubygems ship với nice feature mở associated gem's directory với gem open <gem-name>; ví dụ,

Cho phép dễ dàng find/trace specific version gem app referencing.
Bundler
Ở layer này, Bundler giúp easily specify all project dependencies và optionally specify version cho mỗi. Sau đó, resolve gems, install nó và dependencies. Build real-world apps pre-bundler có myriad challenges:
- Apps exist với numerous dependencies, dependencies có various other dependencies và versions. Install wrong version one gem dễ break app, fix involved lots of tears.
- Hai dependencies có thể refer same third-level dependency. Tìm compatibility issue, nếu có thì problem.
- Multiple apps trên same machine, various dependencies, app access any gems installed on machine, against principle of least privilege và expose app all gems regardless malicious.
Bundler giải quyết cả ba problems và gives sane way manage app dependencies bằng:
Bundler Resolves Dependencies Và Generates Lockfile:
# Gemfile
gem 'httparty'
Chạy bundle hoặc bundle install, generate lockfile:
GEM
specs:
httparty (0.18.1)
mime-types (~> 3.0)
multi_xml (>= 0.5.2)
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2020.1104)
multi_xml (0.6.0)
PLATFORMS
ruby
DEPENDENCIES
httparty
BUNDLED WITH
2.1.4
Từ trên, bundler generate version httparty install, và dependencies trong Gemfile.lock. File này blueprint app dependencies, nên check into version control. Đảm bảo project dependencies consistent across environments (development, staging, production).
Bundler Resolves Compatibility Among Dependencies
Resolve dependencies httparty bằng cách tìm suitable version cho dependencies và specify chúng. Bundler cũng thử resolve dependencies giữa gems. Ví dụ,
# Gemfile
gem 'httparty'
# That relies on gem 'mime-types', '>= 3.0.1, < 4.0.1'
gem 'rest-client'
# That relies on gem 'mime-types', '>= 2.0.1, < 3.0'
Bundler could not find compatible versions for gem "mime-types":
In Gemfile:
httparty was resolved to 0.18.1, which depends on mime-types ('>= 3.0.1, < 4.0.1')
rest-client was resolved to 2.0.4, which depends on mime-types ('>= 2.0.1, < 3.0')
Vì hai gems có dependencies không compatible và không auto resolve.
Bundler Restricts Access To Gems Installed But Not Specified In Gemfile
Trong sample gemfile như sau,
# Gemfile
gem 'httparty'
# irb
require 'rest-client'
# raises LoadError (cannot load such file -- rest-client)
Đảm bảo chỉ dependencies specified in Gemfile có thể required by project.
Bundle exec
Khi chạy rspec trong project directory, có possibility chạy version khác so với Gemfile. Vì most recent version selected run versus version in Gemfile. bundle exec rspec đảm bảo rspec run in context project (gems specified in Gemfile).
Bundle binstubs
Thường đọc articles chạy commands như ./bin/rails; command này similar bundle exec rails. Binstubs là wrappers around Ruby executables ease usage bundle exec.
Generate binstub chạy bundle binstubs gem-name. Tạo binstub in ./bin folder nhưng configurable with --path directory nếu set.
References
Để learn more, check references:
- How Do Gems Work?
- Rbenv
- RubyGems
- Bundler