.. _ruby_version_manager:
=====================
Ruby版本管理器
=====================
在 `Download Ruby `_ 官方下载页面介绍中可以知晓,除了Linux/UNIX发行版提供的Ruby安装包,另一种常用的Ruby安装方式是采用 ``Ruby版本管理器`` 安装:
- Ruby版本管理器就类似于 ``npm`` 之于 :ref:`javascript` ,可以在系统中安装不同版本的Ruby并提供切换能力,这样不同的应用可以采用不同的Ruby版本运行来满足特定需求
- 由于Ruby是不断发展的语言,所以编写的程序可能需要特定的Ruby版本运行,所以Ruby版本管理器非常适合这种场景
- 在多个Ruby应用程序的运行环境中,同一台服务器上可能需要不同的Ruby版本来支持不同的Ruby应用
目前常用的Ruby版本管理器有两种:
- RVM
- rbenv
这两个Ruby版本管理器工作原理不同,但是结果是相同的。
在Linux平台, ``RVM`` 被广泛接受为标准,很大程度上是因为它提供了广泛的toolkit(工具包);但是, :ref:`rbenv` 凭借其轻量级的方法成为强有力的竞争者。
此外,在 :ref:`macos` 平台,由于系统内置了旧版本Ruby,会导致RVM存在一些问题,所以更推荐通过 :ref:`homebrew` 安装 :ref:`rbenv` 来管理Ruby版本。
Under the Hood(工作原理)
===========================
- ``RVM`` 通过覆盖 ``cd`` :ref:`shell` 命令来加载当前的Ruby环境变量。这种直接粗暴覆盖的风险是可能会导致意外行为,并且还意味着切换目录时会加载 ``ruby`` 以及 ``gemset``
- ``rbenw`` 则相对轻柔很多:
- 在 ``PATH`` 的前面部分插入一个 ``~/.rbenv/shims`` (目录垫片, a directory of shims)
- 在这个目录下保存了每个Ruby命令的 ``shim``
- 操作系统搜索与命令名字匹配的 ``shim`` ,然后将其传递给 ``rbenv`` 来确定要执行的Ruby版本
- 提供了一个 ``RBENV_VERSION`` 变量来快速指定Ruby默认版本(排在第一位)
.. _rvm:
RVM
=======
- 典型的 ``rvm`` 目录存储了不同的Ruby版本,类似:
.. literalinclude:: ruby_version_manager/rvm_dir
:caption: 典型的 ``rvm`` 目录结构
- RVM的核心是一组目录,RVM在目录中储存了所有Ruby版本以及相关工具(gem和irb),每个目录都针对特定的Ruby版本
- RVM定义了一个名为 ``rvm`` 的 :ref:`shell` 函数,这样shell会优先使用这个函数而不是执行磁盘中的 ``rvm`` 命令(原因是函数可以修改环境,但是基于磁盘的命令则不能)
- 当运行 ``rvm use VERSION`` 来修改Ruby版本时,实际上调用了 ``rvm`` 函数来修改环境,以便各种 ``ruby`` 命令调用正确的版本: 例如 ``rvm use 2.2.2`` 会修改 ``PATH`` 变量,以便使用 ``ruby`` 命令是使用安装在 ``ruby-2.2.2`` 目录中的 ruby (还有一些其他修改,但最主要是修改 ``PATH`` )
安装
------
- 参考官方文档 `Installing RVM `_ 安装RVM:
.. literalinclude:: ruby_version_manager/rvm_install_ruby
:language: bash
:caption: 使用 ``rvm install`` 安装ruby
:emphasize-lines: 4
安装完成后(我这里的案例是安装 ``master`` 分支,并且同时安装 :ref:`rails` )输出类似:
.. literalinclude:: ruby_version_manager/rvm_install_ruby_output
:language: bash
:caption: 使用 ``rvm install`` 安装ruby的输出信息
.. note::
:ref:`ubuntu_linux` 发行版内置提供了 RVM
.. warning::
``get.rvm.io`` 实际上是 ``raw.githubusercontent.com`` 重定向,所以在墙内是无法直接执行上述安装脚本的。解决的方法是使用 :ref:`curl_proxy` 结合 :ref:`ssh_tunneling` (socks代理) 或者 :ref:`squid_socks_peer` ,总之,需要 "梯子"
快速安装
----------
我的实践,通过以下方式可以快速完成最新版本ruby和RoR安装:
.. literalinclude:: ruby_version_manager/rvm_install_ruby_rails
:caption: 安装rvm,并通过rvm安装ruby和RoR
这个安装步骤我整合到 :ref:`debian_tini_image` 中,作为容器运行使用
使用
------
安装多版本ruby
~~~~~~~~~~~~~~~
- 检查系统已经安装的ruby版本:
.. literalinclude:: ruby_version_manager/rvm_list
:caption: 使用 ``rvm list`` 列出系统中已经安装的ruby版本
输出信息:
.. literalinclude:: ruby_version_manager/rvm_list_output
:caption: 使用 ``rvm list`` 列出系统中已经安装的ruby版本,输出案例
- 安装特定版本(举例 3.1.4 这里选择 3.1.4 是因为当前Gentoo官方提供的安装包就是选择3.1.4,我想既然我安装了最新版本的3.3.0,那么也有必要安装一下主流stable版本):
.. literalinclude:: ruby_version_manager/rvm_install_special_version
:caption: 使用 ``rvm install`` 安装指定版本 Ruby
- 再次检查系统已经安装的ruby版本( ``rvm list`` ),就会发现当前有2个版本已经完成安装:
.. literalinclude:: ruby_version_manager/rvm_list_output_multi
:caption: 使用 ``rvm list`` 列出系统中已经安装的ruby版本,可以看到刚安装的 3.1.4 被设置为当前版本,默认版本则是 3.3.0
:emphasize-lines: 1,2
设置当前ruby版本
~~~~~~~~~~~~~~~~~
- ``rvm use XXXX`` 可以指定当前会话的ruby版本,以下为将当前版本3.1.4切换到3.3.0:
.. literalinclude:: ruby_version_manager/rvm_use
:caption: ``rvm use`` 用于切换当前会话的ruby版本,也可以指定默认版本 ( ``--default`` )
此时检查 ``rvm list`` 可以看到输出已经是采用 3.3.0 版本:
.. literalinclude:: ruby_version_manager/rvm_use_output
:caption: ``rvm use`` 切换版本到3.3.0后检查 ``rvm list`` 输出
:emphasize-lines: 2
.. note::
设置完成后检查 ``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了:
.. literalinclude:: ruby_version_manager/rvm_project_use
:caption: 为ruby项目指定ruby版本
:emphasize-lines: 6
此时检查项目当前目录,会看到一个 ``.ruby-version`` 文件,内容哥就是指定的ruby版本:
.. literalinclude:: ruby_version_manager/rvm_project_use_output
:caption: 为ruby项目指定ruby版本,项目当前目录下增加一个 ``.ruby_version`` 文件包含版本信息
现在来演练一下,先离开项目目录,执行 ``ruby --version`` 就会看到默认的ruby版本3.3.0,当重新回到该项目目录,则 ``ruby --version`` 就立即显示(切换)成该目录下 ``.ruby-version`` 指定的版本:
.. literalinclude:: ruby_version_manager/ruby_version_with_project_dir
:caption: 随着进入和离开ruby项目目录,默认的ruby版本会跟随切换
:emphasize-lines: 5,10,15
.. note::
``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`` 详细的信息:
.. literalinclude:: ruby_version_manager/rvm_info_rvm
:caption: 执行 ``rvm info rvm`` 可以获得RVM详细信息
在不同目录下,注意 ruby 版本不同(项目目录下有 ``.ruby-version`` 配置文件):
.. literalinclude:: ruby_version_manager/rvm_info_rvm_output
:caption: 执行 ``rvm info rvm`` 可以获得RVM详细信息
- 简单的 ``rvm info`` 命令可以输出系统的详细信息:
.. literalinclude:: ruby_version_manager/rvm_info_output
:caption: 执行 ``rvm info`` 可以获得完整的RVM相关(包含ruby)信息
RVM的困扰
------------
``RVM`` 实际上比较复杂(shell函数代替了shell命令),有时候会出现异常:
- 需要 **确保目录名称不包含任何空格** ,也包括上级目录: ``RVM`` 当前不支持带空格的目录名称
- ``type cd | head -1 ; type rvm | head -1`` 可以看到 ``cd`` 和 ``rvm`` 都是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`` 环境变量中。并且比其他目录(可能包含相同程序名)列在前面
- 其他检查信息的方法:
.. literalinclude:: ruby_version_manager/rvm_check
:caption: ``rvm`` 检查信息的方法列表
Uninstall RVM
---------------
``RVM`` 提供了一个自己卸载的方法:
.. literalinclude:: ruby_version_manager/rvm_implode
:caption: ``RVM`` 卸载
输出显示:
.. literalinclude:: ruby_version_manager/rvm_implode_output
:caption: ``RVM`` 卸载
:emphasize-lines: 4
只需要输入 ``yes`` 就能自动完成,必要时按照提示手工移除 ``~/.rvm`` 目录
.. _rbenv:
rbenv
==========
- 典型的 ``rbenv`` 目录结构:
.. literalinclude:: ruby_version_manager/rbenv
:caption: ``rbenv`` 的典型目录结构
- ``rbenv`` 的核心是一组与 ``RVM`` 核心目录非常相似的目录
- 然而,在底层, ``rbenv`` 管理 ``Rubies`` 的方式与 ``RVM`` 的管理方式有很大不同:
- ``rbenv`` 不是直接修改用户的 ``PATH`` 环境变量,而是在用户 ``PATH`` 中插入一组 ``shims`` 小脚本,类似于包装
- 当运行 ``ruby`` 或 ``Gems`` 时,系统会执行 ``shim`` 脚本,然后 ``shim`` 脚本会依次执行 ``rbenv exec PROGRAM`` ,该命令确定应该使用哪个版本的Ruby
- ``rbenv`` 的优点是:
- 可以在任何类Unix系统的任何shell中工作,唯一依赖是 :ref:`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///bin`` 添加到路径中)
安装
-------
- 在 :ref:`macos` 中使用 :ref:`homebrew` 安装 ``rbenv`` :
.. literalinclude:: ruby_version_manager/rbenv_install
:caption: 在 :ref:`macos` 中使用 :ref:`homebrew` 安装 ``rbenv``
根据安装执行的 ``rbenv init`` 输出信息(以下是macOS Big Sur环境的zsh SHELL实行输出案例:
.. literalinclude:: ruby_version_manager/rbenv_install_output
:caption: 在 :ref:`macos` zsh环境中中执行 ``rbenv init`` 输出提示
按提示在 ``~/.zshrc`` 中添加:
.. literalinclude:: ruby_version_manager/zshrc
:caption: 添加 :ref:`macos` zsh环境变量( ~/.zshrc )
然后关闭当前终端窗口,并再次打开新终端窗口,则此时修改生效
- 另一种安装方式是使用git从官方源代码检出,然后配置shell环境:
.. literalinclude:: ruby_version_manager/rbenv_git_install
:caption: 通过git方式源代码安装rbenv
使用
-----------
- ``rbenv`` 基本使用:
.. literalinclude:: ruby_version_manager/rbenv_use
:caption: ``rbenv`` 基本使用
容器时代
===========
通过简单的尝试,我感觉:
- 如果在Linux平台为自己的部署开发环境,例如我采用 :ref:`gentoo_image` 运行 :ref:`gentoo_on_gentoo` ,那么使用 :ref:`rvm` 非常方便,也能够用于小型项目生产环境
- 如果在 :ref:`macos` 环境使用 :ref:`homebrew` 管理,那么 :ref:`rbenv` 或许是更方便的方式,不过按照官方文档,对于生产环境大压力需要考虑 ``rbenv`` 包装脚本的细微性能损失
现在随着容器技术的不断普及,生产和开发测试环境也大量使用容器,实际上对于Ruby应用,需要通过ruby版本管理器来区分和切换ruby版本的需求大为降低:
- 容器天然分隔了不同的应用程序运行环境
- 每个容器都有自己独立的定制版本,界面清晰
- 轻量级的容器(非 "富容器")资源占用低,无版本干扰且必定是准确配置ruby版本
所以后续在容器化运行,乃至 :ref:`kubernetes` 化运行Ruby应用,可以替代本文的ruby版本管理器。
参考
======
- `Ruby Version Managers `_
- `GitHub: rbenv/rbenv >> Comparison of version managers `_
- `Choosing a Ruby Version Management Tool: rbenv vs RVM `_