我不推荐使用Brew 作为开发方向的环境与依赖包管理器
brew 是 macOS 上使用的最多的命令行包管理器工具,几乎所有 macOS 第三方软件都会提供 brew 安装命令。brew 管理软件方法简单,无论是安装、卸载还是更新都只需要一行命令就可以完成,甚至可以认为它就是开源的第三方 AppStore。
brew 如此流行,以至于 macOS 上的开发环境和工具链也都提供了 brew 的快捷安装方式,甚至设置为 macOS 上的推荐方式。但是,问题在于,brew 本身并非一个“开发”工具,它开发的方向,更像是应用商店。
brew 不擅长版本管理
几乎所有开发者都知道,在维护一个项目时,管理和冻结依赖版本的重要性究竟有多高。
软件包总是在更新,版本号总是向前发展。随着需求的变更和技术的改换,软件包总是不可避免地出现与以前使用方法不同甚至完全不兼容的改变,这种不向下兼容的变更被称为破坏性变更。通常一个项目从编写到维护,会历经非常久的时间。这期间,作为项目依赖的软件包会经历无数次更新迭代,很有可能从某个版本开始,依赖的软件包的使用就发生了翻天覆地的变化,导致原本能运行的代码不再能用。如果一直维护与各个依赖包最新版本的兼容性,就会导致很重的开发负担。这时,基于版本号的管理方式就出现了。
开发一般维护环境和依赖的方式
开发世界里最常见的版本号命名法叫做语义式版本号,这个版本号通常由三个数字组成。
X.Y.Z
X.Y.Z-tag
X.Y.Z-tag.N
上面这是语义式版本号的几个简单举例,X是主版本号,Y 是次版本号,Z 是修订版本号,tag 可以是 beta、RC 等标签,并且标签也可以有自己的序号 N。
一般来说,如果软件在上一个版本的基础上产生了小型变更,比如修复了 bug 等,对软件本身功能、兼容性无改动,那么可以增加修订版本号。
如果软件在上一个版本的基础上添加了新的功能,但是原有用法没有任何影响,只是增加了新的内容,那么可以增加次版本号。到这一步,都还是向下兼容的。
而如果软件做出了不能向下兼容的改动,无论是什么,都应该增加主版本号。
而一般来说不应该频繁更新主版本号,这也就告诉开发者,做出破坏性改动之前要仔细思考,并且减少破坏性改动的频率。
而开发者此时就可以简单地通过版本号控制来管理软件包依赖,尽可能更新,但又不破坏兼容性。比如,绝大部分语言的软件包管理器都会有版本号语法,默认情况下就是只更新次版本号和修订版本,不跟随主版本号更新。而其他版本管理方式的思路和这个有共通之处,基本上可以参考着来设置。
而如果项目本身已经固定了需求,没有意外的情况下一般来说还会冻结依赖,将依赖锁定到开发时的精确版本,以便重现。
一般来说,是这样的。
而上升到开发环境上,开发环境和工具本身也有类似的版本号设计,不同版本开发环境通常就会有很大差异。一般如果不是必要的情况,不会随时升级。如果是经常升级变化比较大的环境,还会有多版本管理的工具。
但,使用 brew 不是这样的。
brew的管理方式
对于 brew 来说,第一要务就是保证所有软件都是最新的。
如果尝试使用 brew 安装软件,你会发现,默认行为下,brew 会先检查自身的更新,然后检查软件包的更新,然后所有内容更新好了之后,才会继续安装软件。这样,每次你安装软件,哪怕你只是临时用一下这个软件,你电脑上的其他由 brew 安装的软件也会得到更新。
听起来不错,对于一般用户来说。但是对于开发来说,这是致命的。
一行命令,安装某个软件,预期得到的结果应该只有增加对应的软件,而不应该对其他软件产生影响。未经允许自动更新软件,会导致整个开发环境出现不可预料的破坏。这通常不是开发会预期的结果。
对比其他 Linux 发行版提供的工具,如 yum、apt 等,虽然它们也会检查更新,但是通常只是提示,而不是在安装其他软件时自动安装。
而相比之下,即便 brew 提供了命令手动、单独把某个软件固定到某个版本,也能看出来 brew 并非为此设计的。
brew不擅长解决冲突
刚刚说过了项目维护时间跨度大的时候通常会冻结依赖,而维护多个项目的时候,就会因为不同项目开发的时间不一样而使用不同的版本的依赖和环境。这时候,如果环境版本不兼容,就需要一个工具来管理不同的版本。
这时,还有一个问题,依赖项目本身也是一种软件包,它也可以有依赖,而且它和当前项目的开发时间差距更大,所以依赖项目很有可能版本也有较大差异。而且,不同依赖项目维护周期、活跃度不同,并不是一个依赖更新,所有下游都会跟着更新。因此,如果有一个依赖项目有另一个依赖包版本 1,另一个依赖项目也依赖相同的软件包但是版本 2 ,这时候就会出现冲突。好在,这时候可以使用依赖隔离来解决问题。
往小了说,软件包管理器本身就有类似依赖隔离的能力,比如 nodejs 的 pnpm 就将解决依赖冲突作为很高优先级的特性支持。而往大了说,环境本身的隔离通常也有工具支持,nodejs 有 nvm、fnm,python 有 pyenv、uv,不同语言有自己的解决方案。就算是之前提到的 Linux 发行版也会出现依赖冲突(软件包冲突),但是也基本都提供了一些方法来解决。
但是 brew 并不能做到这一点。因为 brew 就像一个应用商店而不是软件包管理器,对于它来说,软件不需要安装多份,只需要安装一份。而对于它来说,让所有软件都保持最新就是它的主要职责,这和 AppStore 等应用商店逻辑是完全一致的,只不过它在命令行。每一个软件对于 brew 来说都是全局安装,不会因为你在这个项目需要某个版本,另一个项目另一个版本,就能同时安装不同版本。同时,brew 也只支持每个软件的最新几个版本。
brew就不是开发工具
说到底,brew就不是一个开发工具。它看起来像AppStore,用起来像AppStore,因为它本来就是一个AppStore,只不过是第三方维护的。任何对于开发方面的需求,都不应该过度依赖 brew。无论是 nix 还是 macports 都提供了更适合用于开发环境的软件包管理功能,而 brew 就适合做一个AppStore,作为一个面向普通用户进行软件分发的平台。
brew 没做错,虽然它的开发者看起来有自己的“维护哲学”,脾气看起来不太好,但是 brew 没有做错。它的价值不在开发环境,而是一般用户的环境。
而对于开发者来说,要做的就是尽可能别在开发环境上使用 brew 偷懒,该怎么配置就怎么配置,使用一个更合适的工具来配置会让你节省比一行 brew install 所剩下的更多的时间。