Oasis's Cloud

一个人的首要责任,就是要有雄心。雄心是一种高尚的激情,它可以采取多种合理的形式。
—— 《一个数学家的辩白》

Brew 包管理器的工作流程分析


最近将自己的站点从 Node.js 迁移到了基于 Python 实现的 ark。在迁移过程中主要遇到了 ark 的包变更问题,它从之前的 ivy 包更名为 ark,而且 Python 版本也随之进行了变更。

在机器上安装多个版本的 Python 还是要自己捣鼓捣鼓的。所以就想,有没有类似管理 Node.js 版本的工具呢?于是问了下 ChatGPT,得到如下的回复:

Python 的版本管理工具主要有以下三种: 1. pyenv:pyenv 是一个简单的 Python 版本管理工具,可以在不同的 Python 版本之间切换,并且可以为每个项目自定义 Python 版本。 2. Anaconda:Anaconda 是一个流行的 Python 数据科学平台,它包括一个 Python 版本管理工具,可以创建和管理不同版本的 Python 环境。 3. virtualenv:virtualenv 是另一个流行的 Python 版本管理工具,它可以为每个项目创建独立的 Python 环境,并允许在这些环境之间切换。这些工具都可以帮助你管理 Python 版本并避免版本冲突问题。你可以选择其中任何一种工具来管理你的 Python 环境。

我选择安装 pyenv。由于我用的是 Mac OS,所以可以使用 brew 进行安装。借助 brew 很快就安装完了。

在安装的过程中我突然意识到,我以前一直在用 brew,但是我并没有想过它是基于什么原理实现的。于是我看着 brew 的输出信息开始思考。

https://github.com/oasis-cloud/oss/blob/main/640.png?raw=true

最先想到的是 brew 肯定要具有下载的能力,而后具备安装的能力,至于下载进度什么的都是次要信息。紧接着问题又来了,brew 从哪里知道 pyenv 的下载地址?pyenv 的安装脚本和 awk 的安装脚本一样吗?不一样的话,brew 是怎么知道在哪里找到安装脚本呢?

带着这些疑问,我先去问了 ChatGPT,brew 实现原理是什么?

Brew是一款开源的软件包管理器,它是专门为Mac OS X设计的。它的实现原理主要是基于Ruby编程语言的。Brew的主要功能是下载,编译和安装各种软件包,这些软件包都可以在Mac OS X上运行。Brew使用一个简单的命令行界面,用户可以使用它来搜索和安装他们需要的软件包。Brew的实现原理基于Git和Ruby,它使用Git作为软件包存储库的管理工具,而Ruby则提供了一个强大的编程环境,使得Brew可以轻松地实现各种操作,例如软件包下载,编译,安装和卸载等等。此外,Brew还使用了一些其他的开源组件,例如curl,xz和zlib等等,这些组件可以协同工作,使Brew的功能更加强大和灵活。总的来说,Brew的实现原理是利用开源的组件和技术,通过Ruby编程语言编写的软件包管理器,使得用户可以轻松地安装和管理他们需要的软件包。

在这个回答中,我比较关心 brew 是在哪里下载的,所以我又问 ChatGPT:brew 是怎么知道某个包的下载地址?

Brew是通过自己的中央仓库来获取软件包的下载地址的。当用户使用Brew安装某个软件包时,Brew会根据用户提供的软件包名称,从自己的中央仓库中查找相应的软件包信息,包括软件包的下载地址和其他相关信息。Brew的中央仓库包含了大量的软件包信息,这些软件包都是经过Brew团队认证和测试过的,保证了软件包的安全性和质量。如果Brew的中央仓库中没有用户需要的软件包信息,Brew还可以通过用户提供的自定义仓库地址来获取软件包的下载地址。总的来说,Brew是通过自己的中央仓库和用户提供的自定义仓库地址来获取软件包的下载地址的。

在这个回答中,我比较关心中央仓库的地址,于是我接着问了下地址,得到了如下链接:https://github.com/Homebrew/homebrew-core https://github.com/oasis-cloud/oss/blob/main/6401.png?raw=true

图中是仓库的目录结构,从上往下点开看一下。Aliases 中的文件,指向了 Formula 目录中的文件。例如: https://github.com/oasis-cloud/oss/blob/main/6402.png?raw=true 因为 Aliases 中的文件是指向 Formula 中的文件的,所以可以不用关心 Aliases 目录中的文件了。接着去看 Formula 。 https://github.com/oasis-cloud/oss/blob/main/6403.png?raw=true Formula 目录下文件非常多,找个我比较熟悉的包看看,比如 awk。

class Awk < Formula
  desc "Text processing scripting language"
  homepage "https://www.cs.princeton.edu/~bwk/btl.mirror/"
  url "https://github.com/onetrueawk/awk/archive/20220122.tar.gz"
  sha256 "720a06ff8dcc12686a5176e8a4c74b1295753df816e38468a6cf077562d54042"
  # https://fedoraproject.org/wiki/Licensing:MIT?rd=Licensing/MIT#Standard_ML_of_New_Jersey_Variant
  license "MIT"
  head "https://github.com/onetrueawk/awk.git", branch: "master"

  bottle do
    rebuild 1
    sha256 cellar: :any_skip_relocation, arm64_ventura:  "47f3d16ebd3eb5767c9053091532d69d2c8288a262f9007301b757639833ffdb"
    sha256 cellar: :any_skip_relocation, arm64_monterey: "6bb8fdb607cba55ad0f173881e83f8f83ffbd3f2cbf84ca26a494cf614d6d3f6"
    sha256 cellar: :any_skip_relocation, arm64_big_sur:  "253a8eb03a628051cc748336648bc984e4a2bb04322a06c8d8e2e43798d5f581"
    sha256 cellar: :any_skip_relocation, ventura:        "4c14575b3b5c52aa4b7064bfa737e7a2cce5bf00643883f71be87f509fceb546"
    sha256 cellar: :any_skip_relocation, monterey:       "7ab4bfa6706a1cad2f1990962b8067e359da9f49079803c7628483e97a0e396d"
    sha256 cellar: :any_skip_relocation, big_sur:        "d566709d5824930ffecb61d05d86e724bf54c3956964ed39b3a3fffaac3b60e0"
    sha256 cellar: :any_skip_relocation, x86_64_linux:   "6b9a89dc8999517cda6105d5c5e0e94294aba8f507aa85b21ee9a3a75c08a0a7"
  end

  uses_from_macos "bison" => :build

  on_linux do
    conflicts_with "gawk", because: "both install an `awk` executable"
  end

  def install
    system "make", "CC=#{ENV.cc}", "CFLAGS=#{ENV.cflags}"
    bin.install "a.out" => "awk"
    man1.install "awk.1"
  end

  test do
    assert_match "test", pipe_output("#{bin}/awk '{print $1}'", "test")
  end
end
上面的Ruby的代码,看上去是有个继承自 Formula 的 Awk 类,类中实现了属性和方法,从这段代码可以看出 url 用于下载,install 方法用于执行安装。install 方法中会通过 make 进行 awk 的安装处理。这么看来 install 方法中实际就是在执行一些 shell 脚本。

再换个包看一下 Formula/autorestic.rb,中的 install 是通过 go 进行安装的:

def install
    system "go", "build", *std_go_args, "./main.go"
    generate_completions_from_executable(bin/"autorestic", "completion")
end
看到这里就比较了解 brew 的工作机制了。homebrew-core 仓库的设计很不错,借助 OOP,通过模板方法进行约束,来管理不同包的特殊安装过程。

不过 brew 不能算是约束先行。像 NPM 这样的包管理工具,是约束先行的,首先我认为 NPM 具有发布包的概念,而 brew 没有发布包的概念,brew 只有编写安装脚本的概念(Flrmula 中的 rb 脚本),其次 NPM 由于有了发布的概念,所以在包的设计阶段就要按照规范编写相关的 packge.json,而 brew 的设计中,这个步骤是解耦的。正像 brew 的 homebrew-core 是由一群志愿者维护的,而不是谁写的包谁来负责。

通过 ChatGPT 可以轻松地对信息进行聚合,减少自己去 google 的时间。而且还能锻炼自己提出更好的问题。