Baulk - 开发一个简单的包管理工具历程

on under toolset
4 minute read

前言

相对于其他操作系统,我更习惯使用 Windows,但一直以来 Windows 缺乏官方的包管理器,而第三方无论是 Scoop 还是 Chocolatey 都无法满足我独特的需求,我的要求很简单,所有的软件包都应该使用绿色解压模式,这包括了 MSI 安装包,另外安装软件时不应该修改系统和用户环境变量。包管理工具应该足够快,还要支持创建启动器等等。

从毕业工作以来,我开发了 clangbuilder,一个简化在 Windows 上使用 Visual Studio 构建 LLVM/Clang 的工具(含 GUI 工具),为了解决安装构建 LLVM 时所需工具依赖的问题,后来多次开坑编写各种软件,多年的失败经验与认知不断积累,于是在今年这个环球同此凉热的年份,我开始了新坑 ➡ baulk,baulk 花费了我半年的下班时间,现在基本可用,ReadMe 也写好了,应该可以给大家分享一下了。

安装和使用 Baulk

这个步骤只有四五步,下载 baulk 二进制包,解压 baulk 二进制包,点击 baulkterminal,更新元数据,安装你需要的软件,🆗,你可以使用 baulk 安装的软件了。

baulk update
# i 是 install 的别名
# baulk i cmake curl 7z ...
baulk install cmake curl 7z
cmake --version

baulk 升级命令可以使用如下命令:

baulk update
baulk upgrade

也可以使用别名:

# u 是特殊的别名,包含 update 和 upgrade 两个步骤
# baulk update and upgrade
baulk u

升级 baulk 自身可以使用帮助程序 baulk-update

baulk-update

卸载软件也很容易:

# r 是 uninstall 的别名
baulk uninstall 7z

在开发 baulk 时,我就决定只支持 Windows 10 1903 或更新的版本,好处显而易见,可以使用 Windows Terminal,利用 ANSI 转义输出颜色,体验非常不错:

baulk 提供了 baulk-exec 在命令行中执行 baulk-exec 可以初始化 baulk 环境,还提供了 ssh-askpass-baulk,ssh-askpass-baulk 用于 TunnelSSH 某些 ssh 无法打开标准输入时请求输入密码,截图如下:

在 baulk 中我引入了 VirtualEnv 机制,这种机制能够使得用户并行安装多个软件版本,在 baulk-exec,baulkterminal 中通过指定参数支持启动任意的 VENV,baulk 还提供了 baulk-dock 能够选择按照特定的 VENV 启动环境:

在 Baulk 元数据存储库 bucket,收录了 OpenJDK8(Java),OpenJDK9(Java),GraalVM8(Java),Zulu14(Java),Go(golang),DMD(dlang) 等支持 VENV 的包,如果有人想把 Ruby 收录一下,在 Windows 上实现类似 rbenv 功能也不是什么难事。

Baulk 的内幕

入门说完,可以稍稍讲一下 Baulk 的实现细节,baulk 的包管理类似于 Scoop,即将包的元数据存储在 Github 上,官方源为 baulk/bucket,baulk 还可以通过编写 $BAULK_ROOT/config/baulk.json 添加新的源,或者删除某个源,如果不同的源中存在相同名字的包,baulk 还能根据 bucket 的权重去选择使用哪个源,但目前为止,只有 baulk/bucket 一个源。

{
    "bucket": [
        {
            "description": "Baulk default bucket",
            "name": "Baulk",
            "url": "https://github.com/baulk/bucket",
            "weights": 100
        }
    ]
}

baulk update 命令更新 bucket,这里我们使用了 Github Atom RSS 机制,以官方源为例,通过 HTTP 请求项目的 commits/master.atom 获得当前的最新的 commitID,如果本地不存在或者与其不同,则说明 bucket 有更新,于是 baulk 下载对应的 bucket 的压缩包,解压完成元数据的更新。然后检测本地已安装的包是否存在更新版本,存在就输出提示。

GET https://github.com/baulk/bucket/commits/master.atom

当人们运行 baulk upgrade 时,就会真正的升级已安装的包,更新元数据和升级分开这种机制类似于 apt-get,为了简化操作,baulk 提供了 baulk u 别名将 update/upgrade 合并在一起简化升级。

在 Baulk 中,安装软件大的步骤只有解压和生成启动器(创建符号链接),不存在什么执行初始化脚本,修改注册表,关联文件打开方式等等等等等。我的想法是最好做到隔离互不影响,因此,在开发 baulk 的过程中,我一直时朝这个方向去设计。在 baulk 中的 package 中,存在 extension 的关键字,extension 用于描述 package 压缩包(安装包)如何被 baulk 解压缩, extension 支持 zip, msi, 7z, exetar,baulk 按照 extension 的类型执行相应的解压缩程序。扩展的解压程序如下:

扩展 解压程序 限制
exe - -
zip 内置,基于 minizip 支持 deflate/bzip2/zstd,不支持加密和 deflate64(deflate64 可以使用 7z
msi 内置,基于 MSI API 此方式仅作解压,和在资源管理器点击安装不同
7z 优先级:</br>baulk7z - Baulk 发行版</br>7z - 使用 baulk install 安装的</br>7z - 环境变量中的 tar.* 之类格式解压不能一次完成,因此建议使用 tar 解压 tar.* 压缩包
tar 优先级:</br>baulktar - BaulkTar bsdtar 的现代重构</br>bsdtar - Baulk 构建版</br>MSYS2 tar - Git for Windows 携带的</br>Windows tar Windows 内置的 tar 不支持 xz(基于 libarchive bsdtar),但 baulk 构建的 bsdtar 支持,解压 zip 时均不不支持 deflate64

baulk 的哲学是不要修改系统和用户环境变量,环境变量的生效应该是和终端关联或者启动器关联,因此,在 baulk 中,我编写了 baulkterminal 和 baulk-exec 以及 baulk-dock 程序,baulkterminal 主要用于用户通过创建桌面快捷方式或者将 baulkterminal 添加到桌面、文件夹右键菜单,通过用户点击快速打开初始化 Baulk 环境的 Windows Terminal(如果没有安装 Windows Terminal 则打开 Windows 控制台),而 baulk-exec 则是一个启动器,在运行 baulk-exec 时,根据输入的命令行参数 baulk-exec 初始化环境变量,然后启动相关的子命令,比如 Windows 操作系统中没有安装 cmake,而 baulk 安装了 cmake,以下命令就能够正常运行:

# 打印 cmake 版本信息
baulk-exec cmake --version

为了避免环境变量中 PATH 条目过多,降低 SearchPath 搜索相关命令的命中率,baulk 使用了 links 机制,对于一些不依赖自身目录下的 dll 的命令,我们使用创建符号链接的方式将其软连接到 baulk 根目录的 bin\links 目录,如果自身依赖发行携带的 dll,或者需要处理 GetModuleFileName 且没有正确处理符号链接行为的命令,我们使用 launchers 机制,根据命令的类型调用 MSVC 生成特定的启动器,启动器大小 5K 左右,体积能够接受。如果用户没有安装 Visual Studio,则使用 baulk-lnk 实现相关逻辑(baulk-lnk 需要解析 baulk.linkmeta.json,效率有一点点损失)。baulk 在环境变量和启动器这块做了很多事情,需要解析 PE 可执行文件的信息,还需要获得 PE 文件的版本信息,在之前使用 PowerShell 编写的 devi 中,同样是这样做的,但 PowerShell 脚本执行不太快,baulk 相比是一个巨大的效率提升。

baulk 使用 WinHTTP 实现 HTTP 下载功能,能够很好的处理代理的情况,另外,baulk 还会解析 HTTPS_PROXY 环境变量,还支持 --https-proxy 设置代理,但是,我们建议应该优先使用支持设置 Windows 系统代理的工具。

baulk 基于 minizip 内置了 zip 提取能力,支持使用 ZSTD 压缩算法的 ZIP 文件,这比市面上很多压缩软件要快一步。baulk 还内置了 bela::hash 支持 SHA2(SHA224, SHA256, SHA384, SHA512) SHA3(SHA3-224, SHA3-256, SHA3-384, SHA3-512),以及 BLAKE3。由于安全问题不支持 MD5 和 SHA1,因此在 bucket 存储包哈希时应该选择使用这里列出的哈希算法,哈希字符串使用前缀匹配,默认即无前缀时为 SHA256。

  constexpr HashPrefix hnmaps[] = {
      {L"BLAKE3", hash_t::BLAKE3},     // BLAKE3
      {L"SHA224", hash_t::SHA224},     // SHA224
      {L"SHA256", hash_t::SHA256},     // SHA256
      {L"SHA384", hash_t::SHA384},     // SHA384
      {L"SHA512", hash_t::SHA512},     // SHA512
      {L"SHA3", hash_t::SHA3},         // SHA3 alias for SHA3-256
      {L"SHA3-224", hash_t::SHA3_224}, // SHA3-224
      {L"SHA3-256", hash_t::SHA3_256}, // SHA3-256
      {L"SHA3-384", hash_t::SHA3_384}, // SHA3-384
      {L"SHA3-512", hash_t::SHA3_512}, // SHA3-512
  };

Baulk VirtualEnv 介绍

这里需要重点说的时 Baulk VirtualEnv,很多时候,开发者不得不并行安装一个软件的多个版本以适配不同的开发需求,但这些软件在处理环境变量的时候并没有做的那么好,因此有了 VirtualEnv 这样的工具,比如 rbenv 。baulk 目前能够很好的大多数编程语言开发工具的 VirtualEnv,以下截图是加载 Zulu14(Java JDK) 和 Go 的截图:

目前 baulkterminal 和 baulk-exec 能够加载任意的 venv,baulk-dock 仅支持一个。

最后

baulk 花费了我很多的时间,我自己用还是很好用的,并且 baulk 沉淀了我这些年来在 Windows 系统上的技术积累,所以写一篇文章记录一下还是有一些必要的,如果有人对 baulk 里面的技术细节感兴趣,可以与我本人联系。