我最近一直在玩nix软件包管理器。目前,我主要是想看看我是否可以用它来为不同的项目配置独立的开发环境,而这些项目对软件包的依赖程度各不相同。这方面的典型案例是两个Rails应用程序,一个使用PostgreSQL,另一个使用MySQL。理想情况下,我希望开发环境能够完全指定所有项目的依赖关系,并且只让这些依赖关系在相关开发环境中可用。
对我来说,需要为不同的项目同时安装多个版本的PostgreSQL和MySQL是很正常的。虽然使用Homebrew可以做到这一点,但我从来没有发现它是非常直接的。在Go Free Range,我们经常使用Vagrant来解决这个问题,最近我使用Docker和docker-compose取得了一些成功,使用的方法与邪恶火星人类似。然而,前者似乎总是矫枉过正,使用了大量的资源,而后者往往引入了额外的协调复杂性,因为它促使你在每个容器中只有一个进程的能力。
熟悉nix
当我在运行MacOS Catalina的笔记本电脑上安装nix时,遇到了一个小麻烦,但当有人在nix论坛上回答我的问题时,这个问题很快就解决了。然后,我花了一些时间观看Burke Libby优秀的Nixology Youtube系列视频,阅读他在Shopify工程博客上发表的《什么是Nix》文章,学习一些Nix表达式语言教程,阅读nix.dev指南的部分内容,同时在我的笔记本电脑上的终端中玩弄nix。
像这样玩耍是非常好的,但我发现只有当我尝试真正使用它们时,我才真正开始学习一些东西。虽然最终我想用nix来为Rails应用程序创建开发环境,但我想我应该从更简单的东西开始--这个网站是一个使用中间人Ruby库生成的静态网站。
使用nix-shell的Ruby开发环境
首先,我使用nix-env --install bundix 安装了bundix。由于我已经有了一个Gemfile.lock ,我使用bundix --init --ruby=ruby_2_6 来生成一个gemset.nix 和(因为有--init 选项)一个骨架shell.nix 。我需要指定Ruby的版本,因为我不想要最新的版本,在写这篇文章的时候是v2.7。
在这一点上,我意识到,与其他操作系统级的软件包管理器一样,在nix中没有一个简单的方法来指定Ruby的补丁版本;相反,你必须使用你当前版本的nixpkgs中的任何补丁版本。所以为了应对这个问题,我把Gemfile &Gemfile.lock 中使用的Ruby版本从v2.6.5升级到v2.6.6,目前是v2.6的最新补丁版本。我读到过,如果你想要一个特定的Ruby补丁版本,可以将nixpkgs的版本钉在一个较早的版本上,但我还没有试过。当我在做这件事的时候,我把bundler从 v2.0.2 升级到 v2.1.4,也就是 Ruby v2.6.6 所包含的版本,并运行bundle install 来更新Gemfile.lock 中针对BUNDLED_WITH 记录的版本,然后再按照上面的方法重新生成gemset.nix 。
然后我运行了nix-shell ,看看开发环境是否被正确设置。运行which ruby ,我发现它仍然(不正确地)指向我的Rbenv。我将ruby 加入到shell.nix 中的buildInputs 的阵列中,从而解决了这个问题。现在which ruby (正确地)指向了nix-store中的Ruby。
运行middleman build 触发了一个Bundler::GemNotFound 异常,信息是。"无法在任何源中找到RedCloth-4.3.2"。我通过从我的主目录中删除.bundle/config 文件来解决这个问题--我通常用这个文件将BUNDLE_PATH 设为.bundle/gems ,将BUNDLE_BIN 设为.bundle/bin ,这样,一个项目的捆绑宝石就保存在每个项目目录的.bundle 目录中,也就是说,不同项目的宝石是相互隔离的。删除这个配置文件似乎起到了作用,网站成功建立了。
事实上,我对构建成功感到有点惊讶,因为我基于Vagrant和Docker的开发环境都明确安装了node.js。通过调查,我意识到node.js是通过execjsgem从nvm中获得的,并强调了一个事实,即我的nix-shell开发环境实际上并不是非常孤立的,因为我在PATH 。
当我从shell中移除nvm配置时,execjs又回到了从Homebrew中提供node.js,而当我从shell中移除Homebrew配置时,它又回到了从MacOS的JavaScriptCore框架中提供node.js!buildInputs 总之,这一切使我相信,我应该在我的nix-shell开发环境中加入对node.js的明确依赖,因此我在nodejs ,并在shell.nix 。
with (import <nixpkgs> {});
let
ruby = ruby_2_6;
env = bundlerEnv {
name = "jamesmead.org-bundler-env";
inherit ruby;
gemdir = ./.;
};
in stdenv.mkDerivation {
name = "jamesmead.org";
buildInputs = [ env ruby nodejs ];
}
结论
在这一点上,我非常确信(虽然不确定)我有一个完全指定的开发环境,用于我的个人网站,而且我在这一路上学到了一些东西。
虽然我知道为什么nix不满足于指定Ruby的补丁版本,但我觉得我在过去的开发环境中需要这样做,我不确定我是否准备好失去这个由rbenv提供的能力。
按照我的理解,gemset.nix 实际上是将Gemfile.lock 翻译成 nix 派生列表,然后由shell.nix 通过bundlerEnv 纳入我的 nix-shell 环境。我认为这样做的效果是,你不需要在nix-shell环境中运行bundle install ,但我想知道这种好处是否值得让gemset.nix 与Gemfile.lock 保持同步。然而,如果你有很多宝石,特别是一些具有本地扩展的宝石,也许好处就会更明显。
我现在在想,对于开发环境来说,一个明智的半途而废的办法是继续以正常的方式使用rbenv、nvm、bundler和npm,但使用nix来提供操作系统级的软件包(如PostgreSQL、MySQL等),作为Homebrew的替代。
接下来的步骤
正如我前面提到的,在这一点上我并不确定我是否有一个完全指定的开发环境。我本可以在Vagrant虚拟机或Docker容器上试用,但我决定在GitHub Action工作流中利用它,该工作流自动发布这个网站,我以前写过这个网站。我将在另一篇文章中介绍我的经验。
在解决了一个相对琐碎的Ruby应用之后,我想在一个简单的Rails应用中尝试同样的方法。最明显的候选人是Go Free Range网站,这是一个没有数据库的Rails应用。我会让你知道我的进展情况。