Ruby版本管理器

Download Ruby 官方下载页面介绍中可以知晓,除了Linux/UNIX发行版提供的Ruby安装包,另一种常用的Ruby安装方式是采用 Ruby版本管理器 安装:

  • Ruby版本管理器就类似于 npm 之于 JavaScript Atlas ,可以在系统中安装不同版本的Ruby并提供切换能力,这样不同的应用可以采用不同的Ruby版本运行来满足特定需求

  • 由于Ruby是不断发展的语言,所以编写的程序可能需要特定的Ruby版本运行,所以Ruby版本管理器非常适合这种场景

  • 在多个Ruby应用程序的运行环境中,同一台服务器上可能需要不同的Ruby版本来支持不同的Ruby应用

目前常用的Ruby版本管理器有两种:

  • RVM

  • rbenv

这两个Ruby版本管理器工作原理不同,但是结果是相同的。

在Linux平台, RVM 被广泛接受为标准,很大程度上是因为它提供了广泛的toolkit(工具包);但是, rbenv 凭借其轻量级的方法成为强有力的竞争者。

此外,在 macOS 平台,由于系统内置了旧版本Ruby,会导致RVM存在一些问题,所以更推荐通过 Homebrew 安装 rbenv 来管理Ruby版本。

Under the Hood(工作原理)

  • RVM 通过覆盖 cd Shell Atlas 命令来加载当前的Ruby环境变量。这种直接粗暴覆盖的风险是可能会导致意外行为,并且还意味着切换目录时会加载 ruby 以及 gemset

  • rbenw 则相对轻柔很多:

    • PATH 的前面部分插入一个 ~/.rbenv/shims (目录垫片, a directory of shims)

    • 在这个目录下保存了每个Ruby命令的 shim

    • 操作系统搜索与命令名字匹配的 shim ,然后将其传递给 rbenv 来确定要执行的Ruby版本

    • 提供了一个 RBENV_VERSION 变量来快速指定Ruby默认版本(排在第一位)

RVM

  • 典型的 rvm 目录存储了不同的Ruby版本,类似:

典型的 rvm 目录结构
$ tree /usr/local/rvm        # the following is partial output
/usr/local/rvm               # RVM path directory
├── gems
│   ├── ruby-2.2.2
│   │   ├── bin
│   │   │   ├── bundle
│   │   │   └── rubocop
│   │   └── gems
│   │       ├── bundler-1.12.5
│   │       ├── freewill-1.0.0
│   │       │   └── lib
│   │       │       └── freewill.rb
│   │       ├── pry-0.10.4
│   │       └── rubocop-0.43.1
│   └── ruby-2.3.1
│       ├── bin
│       │   ├── bundle
│       │   └── rubocop
│       └── gems
│           ├── bundler-1.12.5
│           ├── freewill-1.0.0
│           │   └── lib
│           │       └── freewill.rb
│           ├── pry-0.10.4
│           └── rubocop-0.45.0
└── rubies
    ├── ruby-2.2.2
    │   └── bin
    │       ├── gem
    │       ├── irb
    │       └── ruby
    └── ruby-2.3.1
        └── bin
            ├── gem
            ├── irb
            └── ruby
  • RVM的核心是一组目录,RVM在目录中储存了所有Ruby版本以及相关工具(gem和irb),每个目录都针对特定的Ruby版本

  • RVM定义了一个名为 rvmShell Atlas 函数,这样shell会优先使用这个函数而不是执行磁盘中的 rvm 命令(原因是函数可以修改环境,但是基于磁盘的命令则不能)

  • 当运行 rvm use VERSION 来修改Ruby版本时,实际上调用了 rvm 函数来修改环境,以便各种 ruby 命令调用正确的版本: 例如 rvm use 2.2.2 会修改 PATH 变量,以便使用 ruby 命令是使用安装在 ruby-2.2.2 目录中的 ruby (还有一些其他修改,但最主要是修改 PATH )

安装

使用 rvm install 安装ruby
# 安装GPG keys
gpg2 --keyserver keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB

# 安装development版本(master分支)
curl -sSL https://get.rvm.io | bash

# 安装development版本同时也安装rails
curl -sSL https://get.rvm.io | bash -s master --rails

# ==== 以下安装适合生产稳定环境 ==== #

# 安装stable版本
curl -sSL https://get.rvm.io | bash -s stable --ruby

# 安装stable版本同时也安装rails
curl -sSL https://get.rvm.io | bash -s stable --rails

安装完成后(我这里的案例是安装 master 分支,并且同时安装 Ruby on Rails )输出类似:

使用 rvm install 安装ruby的输出信息
...
Done installing documentation for zeitwerk, thor, webrick, rack, rackup, concurrent-ruby, tzinfo, i18n, connection_pool, activesupport, mini_portile2, nokogiri, crass, loofah, rails-html-sanitizer, rails-dom-testing, rack-test, rack-session, erubi, builder, actionview, actionpack, railties, marcel, activemodel, activerecord, globalid, activejob, activestorage, actiontext, mini_mime, mail, actionmailer, actionmailbox, websocket-extensions, websocket-driver, nio4r, actioncable, rails after 43 seconds
39 gems installed

  * To start using RVM you need to run `source /home/admin/.rvm/scripts/rvm`
    in all your open shell windows, in rare cases you need to reopen all shell windows.

  * To start using rails you need to run `rails new <project_dir>`.

备注

Ubuntu Linux 发行版内置提供了 RVM

警告

get.rvm.io 实际上是 raw.githubusercontent.com 重定向,所以在墙内是无法直接执行上述安装脚本的。解决的方法是使用 curl代理 结合 SSH隧道 (socks代理) 或者 Squid父级socks代理 ,总之,需要 “梯子”

使用

安装多版本ruby

  • 检查系统已经安装的ruby版本:

使用 rvm list 列出系统中已经安装的ruby版本
rvm list

输出信息:

使用 rvm list 列出系统中已经安装的ruby版本,输出案例
=* ruby-3.3.0 [ x86_64 ]

# => - current
# =* - current && default
#  * - default
  • 安装特定版本(举例 3.1.4 这里选择 3.1.4 是因为当前Gentoo官方提供的安装包就是选择3.1.4,我想既然我安装了最新版本的3.3.0,那么也有必要安装一下主流stable版本):

使用 rvm install 安装指定版本 Ruby
rvm install 3.1.4
  • 再次检查系统已经安装的ruby版本( rvm list ),就会发现当前有2个版本已经完成安装:

使用 rvm list 列出系统中已经安装的ruby版本,可以看到刚安装的 3.1.4 被设置为当前版本,默认版本则是 3.3.0
=> ruby-3.1.4 [ x86_64 ]
 * ruby-3.3.0 [ x86_64 ]

# => - current
# =* - current && default
#  * - default

设置当前ruby版本

  • rvm use XXXX 可以指定当前会话的ruby版本,以下为将当前版本3.1.4切换到3.3.0:

rvm use 用于切换当前会话的ruby版本,也可以指定默认版本 ( --default )
$ ruby --version
ruby 3.1.4p223 (2023-03-30 revision 957bb7cb81) [x86_64-linux]

# 切换到3.3.0版本
$ rvm use 3.3.0
Using /home/admin/.rvm/gems/ruby-3.3.0

# 再次检查当前会话就会看到版本已经从 3.1.4 切换到 3.3.0
$ ruby --version
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [x86_64-linux]

# 设置3.3.0为默认版本
$ rvm use 3.3.0 --default
Using /home/admin/.rvm/gems/ruby-3.3.0

此时检查 rvm list 可以看到输出已经是采用 3.3.0 版本:

rvm use 切换版本到3.3.0后检查 rvm list 输出
   ruby-3.1.4 [ x86_64  ]
   =* ruby-3.3.0 [ x86_64  ]

# => - current
# =* - current && default
#  * - default

备注

设置完成后检查 env 输出信息就可以看到 RVM 修改了用户的环境变量 PATH

PATH=/home/admin/.rvm/gems/ruby-3.3.0/bin:/home/admin/.rvm/gems/ruby-3.3.0@global/bin:/home/admin/.rvm/rubies/ruby-3.3.0/bin:/home/admin/.rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/bin

设置项目使用的ruby版本

在Ruby程序的项目目录下执行 rvm --ruby-version use XXX 指定版本后, RVM 会在当前的项目目录下创建一个 .ruby-version 文件,这个文件内容就是项目期望的ruby版本。这样就不需要每次为项目通过 rvm use 来切换Ruby了:

为ruby项目指定ruby版本
# 案例设置一个 ~/src/magic 目录模拟Ruby项目目录
mkdir -p ~/src/magic
cd ~/src/magic

# 指定该目录使用的ruby版本
rvm --ruby-version use 3.1.4

此时检查项目当前目录,会看到一个 .ruby-version 文件,内容哥就是指定的ruby版本:

为ruby项目指定ruby版本,项目当前目录下增加一个 .ruby_version 文件包含版本信息
$ ls -a
.  ..  .ruby-version

$ cat .ruby-version
ruby-3.1.4

现在来演练一下,先离开项目目录,执行 ruby --version 就会看到默认的ruby版本3.3.0,当重新回到该项目目录,则 ruby --version 就立即显示(切换)成该目录下 .ruby-version 指定的版本:

随着进入和离开ruby项目目录,默认的ruby版本会跟随切换
$ pwd
/home/admin/src/magic

$ ruby --version
ruby 3.1.4p223 (2023-03-30 revision 957bb7cb81) [x86_64-linux]

# 返回用户HOME目录,ruby版本切换回之前选择的默认版本3.3.0
$ cd
admin@gentoo-base-plus ~ $ ruby --version
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [x86_64-linux]

# 再次回到项目目录,ruby版本自动切换到 .ruby-version 文件中指定的项目版本
$ cd src/magic/
$ ruby --version
ruby 3.1.4p223 (2023-03-30 revision 957bb7cb81) [x86_64-linux] 

备注

RVM 通过在目录下创建一个 .ruby-version 文件(内容是ruby版本,类似于Python的 requirements.txt)。则每次用户进入该目录,则 ruby 版本会自动切换到 .ruby-version 文件指定的版本。这样就避免用户反复手工设置调整 RVM 指定ruby版本(也很容易忘记)。

实现的原理是 RVM 将shell中的 cd 命令替换成 shell 函数,该函数调用真正的 cd 命令,然后检查 .ruby-version 文件是否存在,如果存在,则从该文件检索版本号,并以 rvm 函数相同的方式修改环境变量以正确运行Ruby版本。

  • 通过 rvm info rvm 命令可以获取 RVM 详细的信息:

执行 rvm info rvm 可以获得RVM详细信息
rvm info rvm

在不同目录下,注意 ruby 版本不同(项目目录下有 .ruby-version 配置文件):

执行 rvm info rvm 可以获得RVM详细信息
~/src/magic $ rvm info rvm

ruby-3.1.4:

  rvm:
    version:      "1.29.12-next (master)"
    updated:      "3 hours 54 minutes 24 seconds ago"
    path:         "/home/admin/.rvm"
    autolibs:     "[4] Allow RVM to use package manager if found, install missing dependencies, install package manager (only OS X)."

~/src/magic $ cd
~ $ rvm info rvm

ruby-3.3.0:

  rvm:
    version:      "1.29.12-next (master)"
    updated:      "3 hours 57 minutes 31 seconds ago"
    path:         "/home/admin/.rvm"
    autolibs:     "[4] Allow RVM to use package manager if found, install missing dependencies, install package manager (only OS X)."
  • 简单的 rvm info 命令可以输出系统的详细信息:

执行 rvm info 可以获得完整的RVM相关(包含ruby)信息
ruby-3.3.0:

  system:
    uname:        "Linux gentoo-base-plus 6.1.67-gentoo-dist #1 SMP PREEMPT_DYNAMIC Sun Dec 17 15:08:29 CST 2023 x86_64 Intel(R) Core(TM) i5-4260U CPU @ 1.40GHz GenuineIntel GNU/Linux"
    name:         "Gentoo"
    version:      "base-2.14"
    architecture: "x86_64"
    bash:         "/bin/bash => GNU bash, version 5.1.16(1)-release (x86_64-pc-linux-gnu)"
    zsh:          " => not installed"
    remote_path:  "gentoo/base-2.14/x86_64"

  rvm:
    version:      "1.29.12-next (master)"
    updated:      "4 hours 5 minutes 47 seconds ago"
    path:         "/home/admin/.rvm"
    autolibs:     "[4] Allow RVM to use package manager if found, install missing dependencies, install package manager (only OS X)."

  ruby:
    interpreter:  "ruby"
    version:      "3.3.0"
    date:         "2023-12-25"
    platform:     "x86_64-linux"
    patchlevel:   "2023-12-25 revision 5124f9ac75"
    full_version: "ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [x86_64-linux]"

  homes:
    gem:          "/home/admin/.rvm/gems/ruby-3.3.0"
    ruby:         "/home/admin/.rvm/rubies/ruby-3.3.0"

  binaries:
    ruby:         "/home/admin/.rvm/rubies/ruby-3.3.0/bin/ruby"
    irb:          "/home/admin/.rvm/rubies/ruby-3.3.0/bin/irb"
    gem:          "/home/admin/.rvm/rubies/ruby-3.3.0/bin/gem"
    rake:         "/home/admin/.rvm/rubies/ruby-3.3.0/bin/rake"

  environment:
    PATH:         "/home/admin/.rvm/gems/ruby-3.3.0/bin:/home/admin/.rvm/gems/ruby-3.3.0@global/bin:/home/admin/.rvm/rubies/ruby-3.3.0/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/bin:/home/admin/.rvm/bin"
    GEM_HOME:     "/home/admin/.rvm/gems/ruby-3.3.0"
    GEM_PATH:     "/home/admin/.rvm/gems/ruby-3.3.0:/home/admin/.rvm/gems/ruby-3.3.0@global"
    MY_RUBY_HOME: "/home/admin/.rvm/rubies/ruby-3.3.0"
    IRBRC:        "/home/admin/.rvm/rubies/ruby-3.3.0/.irbrc"
    RUBYOPT:      ""
    gemset:       ""

RVM的困扰

RVM 实际上比较复杂(shell函数代替了shell命令),有时候会出现异常:

  • 需要 确保目录名称不包含任何空格 ,也包括上级目录: RVM 当前不支持待空格的目录名称

  • type cd | head -1 ; type rvm | head -1 可以看到 cdrvm 都是shell函数 如果输出不是如下内容,则表明RVM的shell设置出现了问题

    cd is a function
    rvm is a function
    
  • echo $PATH 确保 {RVM PATH}/rubies-{VERSION}/bin{RVM PATH}/gem/rubies-{VERSION}/bin 已经设置在 PATH 环境变量中。并且比其他目录(可能包含相同程序名)列在前面

  • 其他检查信息的方法:

rvm 检查信息的方法列表
# 当前激活版本信息
rvm current

# 修复RVM文件权限,特别是sudo安装,修改或更新RVM,遇到 "permission denied" ,可以尝试运行这个命令
rvm fix-permissions

# 修复帮助RVM管理不同rubies的文件
rvm repair all

# 下载安装最新版本RVM
rvm get latest

# 检查RubyGems系统配置信息
gem env

# 列出当前Ruby安装的所有Gems
gem list

Uninstall RVM

RVM 提供了一个自己卸载的方法:

RVM 卸载
rvm implode

输出显示:

RVM 卸载
Are you SURE you wish for rvm to implode?
This will recursively remove /home/admin/.rvm and other rvm traces?
(anything other than 'yes' will cancel) > yes
Removing rvm-shipped binaries (rvm-prompt, rvm, rvm-sudo rvm-shell and rvm-auto-ruby)
Removing rvm wrappers in /home/admin/.rvm/bin
Hai! Removing /home/admin/.rvm
/home/admin/.rvm has been removed.

Note you may need to manually remove /etc/rvmrc and ~/.rvmrc if they exist still.
Please check all .bashrc .bash_profile .profile and .zshrc for RVM source lines and delete or comment out if this was a Per-User installation.
Also make sure to remove `rvm` group if this was a system installation.
Finally it might help to relogin / restart if you want to have fresh environment (like for installing RVM again).

只需要输入 yes 就能自动完成,必要时按照提示手工移除 ~/.rvm 目录

rbenv

  • 典型的 rbenv 目录结构:

rbenv 的典型目录结构
$ tree /usr/local/rbenv # the following is partial output
/usr/local/rbenv # rbenv root directory
├── shims
│   ├── bundle
│   ├── irb
│   ├── rubocop
│   └── ruby
└── versions
    ├── 2.2.2
    │   ├── bin
    │   │   ├── bundle
    │   │   ├── irb
    │   │   ├── rubocop
    │   │   └── ruby
    │   └── lib
    │       └── ruby
    │           └── gems
    │               └── 2.2.0
    │                   └── gems
    │                       ├── bundler-1.12.5
    │                       ├── freewill-1.0.0
    │                       │   └── lib
    │                       │       └── freewill.rb
    │                       ├── pry-0.10.4
    │                       └── rubocop-0.43.1
    └── 2.3.1
        ├── bin
        │   ├── bundle
        │   ├── irb
        │   ├── rubocop
        │   └── ruby
        └── lib
            └── ruby
                └── gems
                    └── 2.2.0
                        └── gems
                            ├── bundler-1.12.5
                            ├── freewill-1.0.0
                            │   └── lib
                            │       └── freewill.rb
                            ├── pry-0.10.4
                            └── rubocop-0.45.0
  • rbenv 的核心是一组与 RVM 核心目录非常相似的目录

  • 然而,在底层, rbenv 管理 Rubies 的方式与 RVM 的管理方式有很大不同:

    • rbenv 不是直接修改用户的 PATH 环境变量,而是在用户 PATH 中插入一组 shims 小脚本,类似于包装

    • 当运行 rubyGems 时,系统会执行 shim 脚本,然后 shim 脚本会依次执行 rbenv exec PROGRAM ,该命令确定应该使用哪个版本的Ruby

  • rbenv 的优点是:

    • 可以在任何类Unix系统的任何shell中工作,唯一依赖是 Bash 解释器

    • rbenv 只完成最低限度工作,其余部分留给插件生态系统

  • rbenv 的缺点是:

    • 由于 rbenv 会拦截对 ruby 的每次调用,因此它可能会给 ruby 执行时间增加约 50 毫秒的开销 ,对于速度至关重要的时候可能会导致问题

    • 由于 rbenv 在项目之间严格版本隔离,可能会导致难以保留”全局”(系统级)Ruby工具调用,并且无法简单从一个Ruby版本的项目直接 shell out 到另外一个Ruby版本的脚本

    • 无法感知安装到用户主目录的 gems : rbenv 无法阿发现使用 --user-install 安装的 gems 可执行文件,即安装在 ~/.gem 或者 ~/.local/share/gem 目录下的可执行文件(解决方法是避免依赖 rbenv 管理项目中的这些 gem 或者手动将相应的 ~/.local/share/gem/<engine>/<version>/bin 添加到路径中)

安装

macOS 中使用 Homebrew 安装 rbenv
# 安装 rbenv
brew install rbenv ruby-build

# 在shell中加载 rbenv
rbenv init

然后关闭当前终端窗口,并再次打开新终端窗口,则此时修改生效

使用

  • rbenv 基本使用:

rbenv 基本使用
# list latest stable versions:
rbenv install -l

# list all local versions:
rbenv install -L

# install a Ruby version:
rbenv install 3.1.2

容器时代

通过简单的尝试,我感觉:

  • 如果在Linux平台为自己的部署开发环境,例如我采用 Gentoo镜像 运行 在Gentoo上运行Gentoo(容器) ,那么使用 RVM 非常方便,也能够用于小型项目生产环境

  • 如果在 macOS 环境使用 Homebrew 管理,那么 rbenv 或许是更方便的方式,不过按照官方文档,对于生产环境大压力需要考虑 rbenv 包装脚本的细微性能损失

现在随着容器技术的不断普及,生产和开发测试环境也大量使用容器,实际上对于Ruby应用,需要通过ruby版本管理器来区分和切换ruby版本的需求大为降低:

  • 容器天然分隔了不同的应用程序运行环境

  • 每个容器都有自己独立的定制版本,界面清晰

  • 轻量级的容器(非 “富容器”)资源占用低,无版本干扰且必定是准确配置ruby版本

所以后续在容器化运行,乃至 Kubernetes Atlas 化运行Ruby应用,可以替代本文的ruby版本管理器。

参考