Skip to content

【Zig 日报】Homebrew's whole ecosystem, none of its weight - a single Zig binary with native post_install and a themeable TUI & CLI. #359

Description

@jiacai2050

拥有 Homebrew 的整个生态系统,却没有它的沉重负担。一个约 4 MB 的 Zig 二进制文件,能够复用每一个 bottle 和 formula,并且原生运行 post_install(从而确保软件包真正可用),所有这一切都通过一个支持主题切换的 CLI 和 TUI 来实现。
安装到其专属的 /opt/malt 前缀目录;约 3 毫秒冷启动。由人类设计,由 AI 实现。

为什么选择这个,以及有何不同

malt 是 Homebrew 注册表的客户端,而不是分叉(fork)。它复用生态系统中的每一个 formula、bottle、cask、tap 和 Brewfile,安装到其专属的 /opt/malt 前缀目录,绝不碰触 Homebrew 的文件,并且在安装时将它未实现的功能委托给 brew(如果已安装)。它的独特之处在于:

  • 它真的能完成安装。 大多数替代性的 Homebrew 客户端在 post_install 阶段默默放弃,导致软件包处于半损坏状态。malt 原生运行它——内置了一个用于这些代码块所用 Ruby 子集的 Zig 解释器,加上 Homebrew v6 的声明式 post_install_steps——因此当安装返回时,nodeopensslfontconfig 都已完全配置好。 → 原生 post_install
  • 重复的工作零成本。 Bottle 通过 SHA256 进行索引,keg 是 APFS 的 clonefile() 副本,因此同一个 bottle 绝不会被下载或解压两次。第二个软件包与第一个共享库;升级会保留其余的依赖闭包;重新安装和回滚不消耗网络流量且不占用字节——在一个现有存储上安装 ffmpeg 只需数十毫秒即可完成。 → 基准测试
  • 兼顾安全与启动速度。 包管理器以您的用户身份运行,写入具有一定特权的前缀,从互联网获取代码,并补丁 Mach-O 头——因此它应当具备任何根目录附近工具应有的姿态:流式 SHA256、原子性 9 步安装(在验证新版本之前保持旧版本不动)、针对并发修改的 30 秒建议锁(advisory lock)、沙盒化子进程。该二进制文件约为 4 MB,启动时间约为 3 毫秒——所有这些安全性都不是以牺牲启动时间为代价换来的。 → 安全与安全性
  • 全局统一的主题。 单个 MALT_THEME 调色板可同时为 CLI 和 mt tui 仪表盘着色——无需单独的配置。 → 主题
  • 驱动真实 CLI 的仪表盘。 mt tui 是一个内置的、可感知终端大小的终端仪表盘——通过一个屏幕即可完成搜索、安装、升级、服务管理和系统检测(doctor),并将所有操作委托回 mt <subcommand>。没有守护进程,没有配套的二进制文件。 → 交互式仪表盘
  • 支持任意主流代码托管平台(Forge)。 第三方 tap 通过 Forge API 解析,无需克隆整个仓库,支持 GitHub、GitLab(包含自托管)、Codeberg/Forgejo/Gitea 以及 Gogs——针对私有 tap 提供按 Forge 的 Token 认证。 → 支持的托管平台
  • 已签名、可验证的版本。 每一个版本都通过 GitHub OIDC 进行无密钥的 cosign 签名;install.shmt version update 在信任 SHA256 校验和之前会验证签名。泄露的 GitHub Token 不足以发布恶意的二进制文件。 → 安全与安全性

除此之外:短暂运行的 mt run <pkg>(无需永久安装)、完整的运维功能(服务、包、系统检测、清理、备份/恢复、迁移、反向依赖查询),以及在所有合理地方支持的 --json / --output-format=ndjson 脚本化输出。请参阅命令参考

注意:兼容性说明。
“即插即用(Drop-in)”涵盖了典型 Brewfile 使用的指令——tapbrewcaskmasvscode(后两项通过解析器进行往返处理,但尚未由 malt 安装)——外加 hash 选项和 Ruby 符号。它不涵盖 Ruby 的 do … end 块或诸如 if OS.mac? 的条件判断。两者都会引发明确的错误。仅限 macOS——Linux 和 Windows 超出了范围。

注意:人类指导下的 AI 构建。
设计和架构由人类完成;每一个合并的更改都由人类审查;每一行 Zig 提交都由 Claude Code 编写,并通过一系列技能和准则框架(ruflo、superpowers、andrej-karpathy-skills、improve 以及特定于项目的技能)驱动,这些技能编码了人类原本需要亲手强制执行的纪律。malt 已经被端到端地重构了不止一次——安装协议、post_install 解释器、Mach-O 补丁程序——每一轮都由 ADR(架构决策记录)和安全审查引导。仓库、测试套件和运行中的工具就是证据。


安装

三种安装路径——选择与您的设置相匹配的一种。

单行脚本

该脚本会:

  1. 下载最新版本,
  2. 验证 SHA256 校验和以及针对生成它的 GitHub Actions 工作流的 cosign 无密钥签名,
  3. 将二进制文件安装到 /usr/local/bin/
  4. 并创建具有适当所有权的 /opt/malt
curl -fsSL https://raw.githubusercontent.com/indaco/malt/main/scripts/install.sh | bash

该脚本需要在您的 PATH 中安装 cosign。若要绕过验证(不推荐),请设置 MALT_ALLOW_UNVERIFIED=1。如果没有任何版本与您的平台匹配,脚本将回退到从源码构建。

若要对 install.sh 本身进行带外(out-of-band)验证,请固定到某个发布标签(release tag)——下文的最新版本,或您信任的任何版本——并将其 SHA256 与该版本的说明进行比较:

curl -fsSL "https://raw.githubusercontent.com/indaco/malt/v0.22.0/scripts/install.sh" -o install.sh
shasum -a 256 install.sh
bash install.sh

(此处的 v0.20.0 仅为示例——请将其替换为您打算安装的版本,参见顶部的版本徽章)。

通过 Homebrew

malt 作为 Homebrew cask 发布:

brew install --cask indaco/tap/malt

符合规范的 <tap>/<cask> 简写会自动进行 tap 操作。使用 brew upgrade --cask malt 进行升级。mt version update 会检测到通过 Homebrew 管理的安装,并提示您改用 brew upgrade --cask malt

从源码构建

克隆仓库并运行安装脚本。它会检测本地检出并自动从源码构建:

git clone https://github.com/indaco/malt.git
cd malt
./scripts/install.sh

构建需要 Zig 0.16.x,并在 zig-out/bin/ 中生成 malt,旁边带有作为 malt 符号链接的 mt。有关开发构建(debug、测试、通用二进制文件),请参阅开发构建


首次命令

在启动前,使 malt 的二进制文件在新的 shell 中可被发现。mt shellenveval "$(brew shellenv)" 的即插即用替代品:

echo 'eval "$(mt shellenv)"' >> ~/.zshrc          # 或 ~/.bashrc
mt shellenv fish | source                          # fish: 使用 set -gx,而不是 export

随后的第一次会话将如下所示:

mt install jq wget ripgrep        # 并行下载,单锁机制
mt list --versions                # 查看已安装的内容
mt info ripgrep                   # 版本、tap、cellar 路径、置顶状态
mt outdated                       # 有哪些可用的更新
mt upgrade ripgrep                # 原子操作;失败时恢复旧版本

mtmalt 是同一个二进制文件——mtmalt 的符号链接,并随所有安装方法附带。其他别名:remove 用于卸载,ls 用于列表。任何接受 --formula--cask 的标志也接受 --formulae--casks——选择读起来更自然的即可。

如果您输入了 malt 未实现的内容,malt 会检查 brew 并静默委托。如果未安装 brew

malt: '<cmd>' is not a malt command and brew was not found.
Install Homebrew: https://brew.sh

主题

MALT_THEME 为所有 malt 输出选择调色板——包括 CLI 和 mt tui——因此 MALT_THEME=dracula mt outdatedMALT_THEME=dracula mt tui 会以相同的颜色渲染。内置了一个感知背景的默认主题以及十个命名调色板,按其针对的终端背景分组:

背景 主题
自适应 (Adaptive) default - 跟随终端背景(auto / light / dark 可选择它)
深色 (Dark) draculacatppuccin-mocharose-pinenordtokyo-nightgruvbox-darkeverforest
浅色 (Light) catppuccin-latterose-pine-dawngruvbox-light

默认调色板和所有命名主题 - CLI + mt tui 并排展示

  • default(深色终端) / default(浅色终端)
  • MALT_THEME=dracula / MALT_THEME=catppuccin-mocha
  • MALT_THEME=rose-pine / MALT_THEME=nord
  • MALT_THEME=tokyo-night / MALT_THEME=gruvbox-dark
  • MALT_THEME=catppuccin-latte / MALT_THEME=rose-pine-dawn
  • MALT_THEME=gruvbox-light / MALT_THEME=everforest

light / dark / auto 保持感知背景的默认调色板(auto 通过 OSC 11 自动检测)。命名主题需要真彩色(truecolor)终端,如果在基本终端上,或者在背景与主题相矛盾的终端上(例如在浅色终端上使用深色主题),则会降级为默认调色板。

自定义主题

MALT_THEMES_FILE(否则为 {prefix}/etc/malt/themes.json)的 JSON 文件中定义您自己的调色板。它在启动时读取一次,并通过与内置主题相同的接口解析,因此自定义主题可以为 CLI 和 mt tui 同时着色。使用 MALT_THEME=<name> 选择一个,或者将文件中的某个主题标记为 default 以在未设置 MALT_THEME 时应用。内置名称总是优先,因此自定义主题不能遮蔽(shadow)dracula

{
  "version": 1,
  "default": "my-dark",
  "themes": {
    "my-dark": {
      "polarity": "dark",
      "accent": "#bd93f9",
      "secondary": [139, 233, 253],
      "success": "#50fa7b",
      "warning": "#ffb86c",
      "danger": "#ff5555",
      "muted": 240
    }
  }
}

每个主题都需要一个极性(polaritydark/light)和所有六个角色。颜色可以是十六进制字符串 ("#rgb"/"#rrggbb")、[r, g, b] 数组(0–255),或单个 0–255 整数(256 色索引)。

文件采用全有或全无(all-or-nothing)的验证方式:任何畸形的值都会拒绝整个文件,并且 malt 会保留内置主题(仅有一行提示,绝不会崩溃)。主题像内置主题一样受到门控限制——它仅在其极性与检测到的背景匹配时应用,且仅当终端能够渲染其最深的颜色时应用:十六进制 / [r,g,b] 需要真彩色(COLORTERM=truecolor / 24bit),256 色索引至少需要一个 256 色终端(COLORTERM,或名称中包含 256colorTERM)。终端无法渲染的主题将整体降级为默认调色板。


交互式仪表盘

mt tui 打开一个持久的、可感知调整大小的仪表盘——通过一个屏幕即可进行搜索、安装、升级、服务管理和系统检测,每个操作都委托回 mt <subcommand>。没有守护进程,无需预先安装任何东西。

mt tui                                   # 启动仪表盘
MALT_THEME=dracula mt tui                # 使用命名主题启动

注意
mt tui 需要一个真实的终端:如果是在管道中、在 CI 中,或者设置了 NO_COLOR,它会拒绝启动并退出状态码 2,而不是将转义序列流式传输到非 TTY 中。

五个标签页,每一个都是对 mt … --json 的实时视图:

标签页 显示内容 操作方式
Search (搜索) mt search 命中项,跨查询的 basket 选择 mt install basket 中的内容
Installed (已安装) 每个带有详细信息面板的 keg + cask mt uninstall
Outdated (过时) 可升级的软件包,多选(置顶项置灰) mt upgrade
Services (服务) launchd 服务 + 运行时状态 mt services start/stop/restart
Doctor (检测) 结构化的 mt doctor 结果,错误优先 mt doctor --fix <class>

使用鼠标或键盘驱动:点击标签页切换,点击任意行进行选中,使用滚轮滚动活动列表。每个标签页都在页脚列出了自己的操作键。

它支持跨搜索的批量安装。Search 标签页带有跨查询的 basket(购物车):空格键添加高亮的命中项,并且在您运行新查询时所选内容依然保留——因此您可以搜索 bat,然后搜索 redis,并通过单个 i 安装这两者。l 打开 basket 进行审查(空格键/d 移除一个选择,n 清空);页脚会跟踪运行计数,显示为 i: install N selected

它通过 --json 读取并通过委托执行。每次修改操作都会退出屏幕交替模式,内联运行真实的 mt <subcommand>——因此输出和提示会原封不动地落入您的滚动历史中——然后重新进入并刷新当前标签页(其他标签页懒加载刷新)。它从不重新实现安装、升级或修复。

它支持实时调整大小。布局是终端大小的纯函数:拖动窗口时列会自动重新流式传输,视口重新对齐,长行在无需按键的情况下自动截断。低于可用最小值时,它会显示“终端太小(terminal too small)”提示,而不是损坏的画面。


命令参考

按您的操作进行分组的命令。每个命令都可以与 maltmt 一起工作,接受 --help 以获取完整的标志列表,并支持 --quiet--dry-run(在发生修改时)和 --json(在适用的情况下)。有关跨切面设置,请参见全局标志

概览 - malt -h

malt - Homebrew's whole ecosystem, none of its weight.
Reuses every formula, bottle, and Brewfile; runs post_install natively.
Themeable TUI and CLI.

Usage: malt <command> [options] [arguments]
       mt <command> [options] [arguments]    (alias)

常用命令列表

  • install:安装 formula、cask 或 tap 中的 formula
  • reinstall:擦除并重新实体化已安装的软件包
  • uninstall:移除已安装的软件包
  • upgrade:升级已安装的软件包
  • update:刷新元数据缓存
  • outdated:列出已安装版本与 tap 不同步的软件包
  • list:列出已安装的软件包
  • info:显示详细的软件包信息
  • search:搜索 formula 和 cask
  • uses:显示依赖于某个 formula 的已安装软件包
  • deps:显示某个 formula 依赖的内容(uses 的正向操作)
  • which:将前缀二进制文件(或路径)解析为其 keg
  • doctor:系统健康检查
  • tap / untap:管理 tap
  • migrate:导入现有的 Homebrew 安装
  • rollback:将软件包恢复到其前一个版本
  • link:为已安装的 keg 创建符号链接
  • unlink:移除符号链接(keg 保持安装状态)
  • pin:保护已安装的 formula 或 cask 免受 upgrade 影响
  • unpin:解除置顶,以便 upgrade 恢复对其更新
  • run:无需安装即可运行软件包二进制文件
  • completions:生成 shell 补全脚本(bashzshfish
  • shellenv:打印用于 shell 初始化的 PATH/MANPATH/HOMEBREW_PREFIX 导出
  • backup:将已安装的软件包转储到可恢复的文本文件
  • restore:重新安装备份文件中列出的每一个软件包
  • purge:日常维护或彻底擦除(--store-orphans--unused-deps--cache--downloads--stale-casks--old-versions--housekeeping--wipe
  • cleanuppurge --housekeeping 的简写
  • services:管理长期运行的 launchd 服务(start / stop / status / logs
  • tui:交互式仪表盘(searchinstalledoutdatedservicesdoctor
  • bundle:安装或导出 Brewfile / Maltfile.json 软件包集合
  • version:显示版本(使用 version update 自我更新)

获取和移除软件

mt install 安装 formula、cask 和 tap formula,并对常见情况进行自动检测:

mt install wget                          # 自动检测 formula 或 cask
mt install --cask firefox                # 显式 cask
mt install user/tap/formula              # 内联 tap,无需单独的 tap 步骤
mt install openssl@3                     # 带版本的 formula
mt install jq wget ripgrep               # 并行下载,单锁机制
mt install --only-dependencies wget      # 仅安装传递依赖
mt install --local ./hello.rb            # 本地 Ruby formula(见“本地 formula”)
mt install --dry-run jq                  # 预览而不安装

其他标志:--force(覆盖现有内容)、--use-system-ruby[=<name>,…](将 post_install 委托给系统 Ruby,沙盒化,按 formula 配置)、--quiet / -q--json

mt reinstall <pkg>mt install --force 的可发现对等项:如果软件包未安装则拒绝执行,擦除并重新实体化现有的 keg 或 cask。传递依赖不会被重新安装。全局标志(--json--quiet--dry-run)会向下传递。

mt run <pkg> -- <args...> 无需永久安装即可运行二进制文件。对一次性调用非常有用:

mt run jq -- --version
mt run --keep ripgrep -- --help          # 为下次运行缓存 bottle

--keep{cache}/run/<sha256>/ 下解压,以便后续调用跳过下载。缓存可通过 mt purge --cache 擦除。

mt uninstall 移除一个软件包,如果存在依赖项,或者对于 cask 而言如果应用程序正在运行,则会拒绝移除。--force (-f) 会绕过这两项检查。--cask 强制进行 cask 卸载。存储条目会被保留以供 mt purge --store-orphans 使用。

mt migrate 导入现有的 Homebrew 安装:它扫描 Cellar 并通过 malt 重新安装每个软件包,而不碰触 Homebrew 安装本身。

  • 带有 post_install 的软件包通过 malt 的原生解释器运行;不支持的脚本会回退到 --use-system-ruby,或者带有报告被跳过。
  • --dry-run 预览迁移。
  • --parallel 并发执行每个 keg 的工作(默认 4 个工作线程;可通过 MALT_MIGRATE_PARALLEL_WORKERS=N 调整)。
  • 进度记录在 {prefix}/cache/migrate.progress.json 中,因此在崩溃或 ^C 后重新运行会从停止的地方恢复。

保持最新

三个命令构成了升级循环:

mt update                                # 刷新 API 缓存 + 删除过时的快照
mt update --check                        # 仅写入新鲜的过时快照

mt outdated                              # 已安装版本与 tap 不同步
mt outdated --pinned-only                # 对保留版本进行 CVE 监视
mt outdated --refresh                    # 绕过缓存的快照

mt upgrade <name>                        # 升级特定的 formula 或 cask
mt upgrade --formula                     # 所有过时的 formula
mt upgrade --cask                        # 所有过时的 cask
mt upgrade --pinned --dry-run            # 审计置顶漂移而不进行修改
mt upgrade --force <name>                # 针对某次升级绕过置顶

mt outdated 读取缓存的快照(5 分钟 TTL;MALT_OUTDATED_MAX_AGE=<minutes> 可覆盖,0 表示总是重新计算),一旦过期就会实时刷新,因此它与 mt upgrade 保持同步。条目通过实时数据库进行过滤,因此被移除或手动升级的 keg 永远不会出现。添加 --json 以获取机器可读输出。

mt upgrade 安装新版本,对其进行验证,原子化地切换符号链接,并且仅在成功后才移除旧版本。如果失败,则恢复旧版本。

mt pin <name> / mt unpin <name> 将软件包保持在其当前版本。置顶的软件包会被 mt upgrade 跳过,并显示“pinned, skipped”行;mt list --pinned 用于检查。

mt rollback <package> 将 formula 恢复到其前一个版本。存储区保留了每一个先前安装的 bottle,因此回滚是 unlinkre-clone → 更新数据库,而无需重新下载。--list 显示保留的版本;--to <version> 恢复到特定版本而不是最新的先前版本;--dry-run 进行预览。


检查已安装内容

mt list                                  # 所有已安装的软件包
mt list --versions --formula             # 缩小范围 + 显示版本
mt list --pinned                         # 仅保留的版本
mt list --json

mt info wget                             # 版本、tap、cellar 路径、置顶状态
mt info --cask firefox

mt search ripgrep                        # brew 兼容性:查询 Homebrew API
mt search ripgrep --formula              # 缩小范围
mt search ripgrep --installed            # 仅本地数据库;无网络
mt search ripgrep --all                  # 本地 + API,合并

mt uses openssl@3 显示直接依赖项:

  • mt uses --recursive openssl@3:完整的传递闭包
  • mt deps ffmpeg:直接依赖项(uses 的正向操作)
  • mt deps --recursive ffmpeg:完整的前向闭包
  • mt deps --installed -r node@20:限制为本地解析的 keg
  • mt which jq:反向查找:bin -> keg-path

mt which 接受纯名称(通过 {prefix}/bin/<name> 解析)或指向 malt 管理的符号链接的绝对路径。输出格式为 <name> <version> <keg-path>(或在带 --json 时为 {"name", "version", "keg"})。它是只读且离线的;当二进制文件不属于 malt 所有时,会以非零状态码退出并显示清晰的错误信息。

mt search 默认匹配 brew search,并在 formula 和 cask 中对子串匹配进行排名。

标志 作用域
(默认) / --api Homebrew API - 跨 formula 和 cask 进行子串匹配
--installed 本地数据库扫描(kegs.namecasks.token)- 无网络
--all 两次扫描合并并去重
--offline (或 MALT_OFFLINE=1) 将每个作用域折叠为 --installed

--json--formula--cask 可以与上述所有作用域组合使用。

mt depsmt uses 的正向对称操作:“X 依赖于什么?”而不是“谁依赖于 X?”。已安装的 keg 从本地数据库读取;未安装的 formula 遍历上游 API。--offline 是离线安全的。--json 每个访问的节点发出一个条目,在递归遍历时保留图的形状。


维护 malt

mt doctor 运行一系列健康检查(见下表)。它退出 0(正常)、1(警告)、2(错误)。

mt doctor
mt doctor --fix                          # 修复安全类别的警告
mt doctor --fix --dry-run                # 预览修复计划
mt doctor --post-install-status          # 检查已安装 formula 的 DSL 支持情况
检查项 通过 失败
SQLite 完整性 PRAGMA integrity_check 返回 ok 错误:数据库损坏
目录结构 前缀下的所有必需目录都存在 警告:缺少目录
僵尸锁 无锁文件,或锁的 PID 正在运行 警告:建议移除
APFS 卷 /opt/malt 位于 APFS 上 警告:clonefile 不可用
API 可达性 formulae.brew.sh 的 HEAD 请求返回 2xx 警告:离线
孤儿存储 keg 引用的所有存储条目 警告:建议 mt purge --store-orphans
缺失的 keg 磁盘上存在所有数据库 keg 路径 错误:建议重新安装
损坏的符号链接 bin/lib/ 等中的所有符号链接均可解析 警告:建议 mt cleanup
磁盘空间 前缀卷上有 > 1 GB 的可用空间 警告:磁盘空间不足
Post-install DSL 所有已安装的 post_install formula 均可解析 警告:不支持的结构

--fix 仅修复安全类别——可逆且绝不触及用户数据的操作:过期的建议锁(记录的 PID 已死)、bin/lib/include/share/sbin/ 下损坏的符号链接,以及孤儿存储条目。危险类别(损坏的数据库、缺失的 keg、缺少前缀目录、弱权限、未打补丁的 Mach-O 占位符)保留其内联的手动修复提示。

mt purge 是日常维护和彻底擦除的入口点。必须提供作用域标志。

# 安全作用域
mt purge --store-orphans
mt purge --unused-deps
mt purge --cache=30
mt purge --stale-casks
mt purge --housekeeping

# 具破坏性的
mt purge --downloads
mt purge --old-versions
mt purge --wipe
mt purge --wipe --backup ~/snapshot.txt --remove-binary --yes
作用域 移除内容 确认门控
--store-orphans {prefix}/store 中引用计数为 0 的 blob
--unused-deps 没有其他软件包需要的间接安装 keg
--cache[=DAYS] 超过 DAYS 天的缓存文件(默认 30)
--downloads 整个 {cache}/downloads 目录 输入 downloads
--stale-casks 未安装 cask 的 cask 缓存 + Caskroom 条目
--old-versions {prefix}/Cellar 中非最新版本的目录 输入 old-versions
--housekeeping = --store-orphans --unused-deps --cache --stale-casks
--wipe 磁盘上的每一个 malt 制品(互斥) 输入 purge

共享标志:--dry-run / -n(预览)、--yes / -y(跳过键入确认)、--quiet / -q--backup / -b <path>(在删除前写入与 mt restore 兼容的清单)。仅限 --wipe 的标志:--keep-cache(保留已下载的 bottle)、--remove-binary(同时取消 /usr/local/bin/{mt,malt} 的符号链接)。

用于脚本的结构化输出:mt --json purge --<scope>... 在标准输出上发出单个摘要对象(versiondry_runscopestotalstime_ms);mt --output-format=ndjson purge ... 流式传输 scope_started / scope_completed / purge_complete 事件,每行一个。无论在哪种模式下,标准错误流(stderr)始终保持人类可读界面。

--wipe 不能与其他任何作用域组合。所有其他作用域都可以在获取一个锁的情况下同时运行。mt purge 尊重 MALT_PREFIXMALT_CACHE,因此将它们指向一个丢弃路径是端到端测试该命令的安全方法。对于单个软件包的移除,请使用 mt uninstall

mt cleanupmt purge --housekeeping 的 Homebrew 风格别名——即安全的日常驱动作用域。尾随标志保持不变(mt cleanup --dry-runmt cleanup --cache=7 --yes),因此每周的肌肉记忆依然有效,同时完整的作用域菜单保留在 mt purge 下。

mt link <formula>mt unlink <formula> 管理前缀符号链接。link 报告冲突并中止,除非传递了 --overwrite / --force / -funlinkbin/lib/ 等目录以及 opt/ 符号链接中移除符号链接,但保持 keg 安装在磁盘上。


后台服务

mt servicesbrew services 的即插即用替代品:

mt services list                         # 已注册的服务 + 运行时状态
mt services start postgresql@16          # 通过 launchctl bootstrap 到 gui/<uid>
mt services stop postgresql@16
mt services restart postgresql@16
mt services status postgresql@16
mt services logs postgresql@16 --tail 50
mt services logs postgresql@16 --stderr
mt services logs postgresql@16 -f        # 跟踪日志直到收到 SIGINT

当已安装的 formula 带有服务块(例如 postgresql@16redis)时,服务会自动注册。Plist + 日志文件位于 {prefix}/var/malt/services/<name>/;运行时状态位于 SQLite 的 services 表中。仅限 macOS——Linux/Windows 会返回 OsNotSupported


可复现的设置

mt bundlebrew bundle 的即插即用替代品,无需转换 Brewfile:

mt bundle install                        # ./Brewfile 或 ./Maltfile.json
mt bundle install path/to/Brewfile
mt bundle install --dry-run
mt bundle cleanup                        # 移除 Brewfile 中不存在的直接软件包
mt bundle cleanup --yes                  # 跳过键入确认
mt bundle create                         # 将已安装状态快照到 ./Brewfile
mt bundle create --format json my.json
mt bundle export                         # 打印到标准输出
mt bundle list                           # 已注册的 bundle
mt bundle remove devtools                # 取消注册;--purge 同时卸载
mt bundle import path/to/Brewfile        # 注册但不安装

查找顺序(未给定路径时):./Brewfile./Maltfile.json~/.config/malt/Brewfile~/.config/malt/Maltfile.json。Brewfile 解析涵盖 tapbrewcaskmasvscode,以及 hash 选项(version:restart_service:link:)和 Ruby 符号(restart_service: :changed)。条件判断(if OS.mac?)和 do … end 块会被拒绝,并给出指向 Maltfile.json 的清晰错误信息。

mt backupmt restore 涵盖更简单的场景——直接安装的软件包的纯文本清单,易于手动编辑或纳入 dotfiles 管理:

mt backup                                # 在当前工作目录写入 malt-backup-<timestamp>.txt
mt backup -o my-setup.txt                # 自定义路径;"-o -" 写入标准输出
mt backup --versions                     # 将每个条目固定为其安装版本

mt restore my-setup.txt
mt restore my-setup.txt --dry-run
mt restore my-setup.txt --force

仅记录直接安装的软件包;传递依赖在恢复时解析。文件格式为每行一个条目(formula <name> / cask <token>),带 # 注释。恢复操作会将其分批合并为两个 mt install 调用,因此依赖解析、并行下载和原子安装全都适用。带有 @<version> 后缀的行将安装该特定版本。


自定义源

mt tap user/repo                                  # 注册一个 tap
mt tap                                            # 列出已注册的 tap
mt tap user/repo --repo owner/exact-repo          # 无前缀的 GitHub 仓库
mt tap user/repo --repo owner/exact-repo --force  # 重新绑定到新仓库
mt untap user/repo                                # 移除一个 tap

Tap 在安装过程中会自动解析(mt install user/repo/formula),因此除非您想要显式的 Homebrew 风格工作流,否则这是可选的。

支持的托管平台(Forge)

一个 tap 可以托管在四个平台的任意一个上。GitHub 是默认平台;其他平台需要显式传入 --host(这总是需要显式的 --repo,因为 homebrew-<repo> 约定仅适用于 GitHub)。

  • --url https://<host>/<owner>/<repo>--host + --repo 的自包含替代方案:一个标志同时承载主机和精确仓库,适用于包括 GitHub 在内的所有平台。
  • --url 不能与 --host--repo 组合使用(因为它已经编码了这两者)。
  • 当主机无法自动分类时(自托管的 GitLab/Gitea,或任何 Gogs 主机),--url 仍需要 --forge
托管平台 主机 Token 环境变量
GitHub github.com MALT_GITHUB_TOKEN
GitLab (含自托管) gitlab.comgitlab.gnome.org、自定义 MALT_GITLAB_TOKEN
Codeberg / Forgejo / Gitea codeberg.org、自托管的 Forgejo/Gitea MALT_GITEA_TOKEN
Gogs 自托管的 Gogs MALT_GITEA_TOKEN

只有 gitlab.*codeberg.org 可以从主机自动分类;其他所有主机都需要显式指定 --forgegitlabgiteagogs),如下所示。Gogs 总是需要显式指定——它共享 Gitea API 和 MALT_GITEA_TOKEN,但其 pin 端点不同,因此无法自动分类。有关每个 Token 发送时的名称,请参阅环境变量表。

# 每个 forge 接受 --host + --repo 对或等效的 --url。

# GitHub (默认)
mt tap user/repo --repo owner/exact-repo
mt tap user/repo --url https://github.com/owner/exact-repo

# GitLab (gitlab.* 从主机自动分类)
mt tap grp/tap --host gitlab.com --repo grp/homebrew-tap
mt tap grp/tap --url https://gitlab.com/grp/homebrew-tap

# 自托管 GitLab (主机无法分类 - 命名 forge)
mt tap acme/tap --host code.acme.com --forge gitlab --repo acme/tap
mt tap acme/tap --url https://code.acme.com/acme/tap --forge gitlab

# Codeberg / Forgejo / Gitea (codeberg.org 自动分类)
mt tap org/tap --host codeberg.org --repo org/homebrew-tap
mt tap org/tap --url https://codeberg.org/org/homebrew-tap

# 自托管 Forgejo/Gitea (主机无法分类 - 命名 forge)
mt tap org/tap --host git.acme.com --forge gitea --repo org/tap
mt tap org/tap --url https://git.acme.com/org/tap --forge gitea

# Gogs (从不自动分类 - 始终命名 forge)
mt tap org/tap --host git.acme.com --forge gogs --repo org/tap
mt tap org/tap --url https://git.acme.com/org/tap --forge gogs

非 GitHub tap 注册时为未置顶状态;mt tap --refresh <slug> 会将其固定到当前的 HEAD。mt doctor 列出每个注册的 tap 及其解析的目标 Forge 主机,以便您确认 --host 注册落在了您预期的位置。

置顶注意事项:优先选择发布资产(Release assets)而不是生成的归档文件
GitLab /-/archive/ 和 Gitea/Gogs /archive/ 的 tarball 是在服务器端重新生成的——它们的 gzip 帧结构可能会在 Forge 升级过程中发生变化,即使文件内容没有变,因此针对其中一个计算出的 sha256 稍后可能会不匹配。当您进行置顶时,请优先选择发布资产 URL(即上传的 tarball,其字节是不可变的),而不是 /-/archive//archive/ URL。不匹配通常表现为常见的 Sha256Mismatch;对于生成的归档 URL,这通常意味着 Forge 重新打包了 tarball,而不是下载损坏——此时请针对发布资产重新置顶。


管理 malt

mt version                               # 显示当前版本
mt version update                        # 交互式自我更新
mt version update --check                # 仅检查,不下载
mt version update --yes                  # 非交互式 (CI / 脚本)
mt version update --cleanup              # 移除过时的 .old + 孤儿 staging 文件

eval "$(mt shellenv)"                    # PATH/MANPATH;从 $SHELL 自动检测
mt shellenv fish | source                # fish 需要 `set -gx`

eval "$(malt completions zsh)"           # 在 `compinit` 之后运行
malt completions bash > /usr/local/etc/bash_completion.d/malt
malt completions fish > ~/.config/fish/completions/malt.fish

mt version update 查询 GitHub releases API,使用 cosign 和 SHA256 验证版本(与 install.sh 相同的信任锚点),并原子化地替换正在运行的二进制文件。先前的二进制文件保留为 <target>.old;累积的 .old 文件(以及来自被杀死的更新的任何孤儿 .malt-update-<pid> staging 文件)可以通过 --cleanup 清理,该操作不进行网络调用。Homebrew 安装的 malt 会检测到 brew 收据并提示您改用 brew upgrade --cask malt 而不是覆盖它。

若要绕过 cosign(强烈不建议),install.sh 接受 MALT_ALLOW_UNVERIFIED=1mt version update 同时需要环境变量和 --no-verify,因为 update 是一个重复运行的命令:

MALT_ALLOW_UNVERIFIED=1 mt version update --no-verify

mt shellenv 导出 HOMEBREW_PREFIXHOMEBREW_CELLARHOMEBREW_REPOSITORY(以便 brew 感知的脚本继续探测)并将 malt 的 binsbinshare/manshare/info 前置到 PATHMANPATHINFOPATH 中。不带参数时会从 $SHELL 检测 shell;无法识别的 $SHELL 将安全失败(fails closed)。

mt completions 向标准输出打印用于 bashzshfish 的补全脚本,涵盖子命令(针对 malt 和 mt 两者)、每个命令的标志、全局标志,以及补全本身的位置 shell 名称。未知的 shell 会以非零状态码退出。


全局标志

标志 描述
--verbose-v 详细输出
--debug 显示每一个 DSL 诊断信息(隐含 verbose);配合 issue 报告使用
--quiet-q 抑制非错误输出
--json JSON 输出(读取命令;同时发出每个软件包的 post_install 状态行)
--output-format=ndjson 每个状态转换流式传输一个 JSON 事件(stdout);人类可读输出保持在 stderr
--dry-run 预览而不执行
--offline 从快照缓存提供每次获取;在未命中时通过 OfflineRequired 快速失败(对应 MALT_OFFLINE=1
--help-h 显示帮助
--version 显示版本

环境变量

变量 描述 默认值
MALT_PREFIX 覆盖安装前缀 /opt/malt
MALT_CACHE 覆盖缓存目录 {prefix}/cache
MALT_BREW_PATH 覆盖未知命令回退到的真实 brew 二进制文件(自定义安装前缀) 探测标准安装路径
NO_COLOR 禁用彩色输出。无条件否决:它优于 CLICOLOR_FORCE 未设置
CLICOLOR 设置为 0 以禁用彩色输出 未设置
CLICOLOR_FORCE 设置为除 0 之外的非空值,即使 stderr 不是终端也强制输出彩色(管道传输到 less -R,CI 日志中的 ANSI) 未设置
MALT_NO_EMOJI 禁用输出中的 emoji 未设置
MALT_NO_VERSION_NOTIFIER 设置为 1 以抑制“有更新的 malt 可用”提示 未设置
MALT_VERSION_NOTIFIER_ASSUME_TTY 测试/自动化:设置为 1 以绕过非 TTY 抑制,以便脚本化运行可以在没有 pty 的情况下断言提示(仅绕过 TTY 门控) 未设置
MALT_PROGRESS 安装/升级/迁移的进度报告器:ttyplainnoneCI=trueGITHUB_ACTIONS=true 会将默认值翻转为 plain tty
MALT_THEME 所有输出的颜色主题(CLI 和 mt tui)。参见主题了解调色板列表和回退规则。 auto
MALT_THEMES_FILE 自定义主题的 JSON 文件路径(参见“自定义主题”);在启动时读取一次,否则如果存在则使用 {prefix}/etc/malt/themes.json {prefix}/etc/malt/themes.json
HOMEBREW_GITHUB_API_TOKEN 用于更高 API 速率限制的 GitHub token 未设置
MALT_GITHUB_TOKEN 仅在 tap /commits/HEAD 调用中作为 Authorization: Bearer 发送的 GitHub token 未设置
MALT_GITLAB_TOKEN 在 GitLab 托管的 tap 的 tap commit + raw .rb 调用中作为 PRIVATE-TOKEN 发送的 GitLab token (PAT) 未设置
MALT_GITEA_TOKEN 在 tap commit + raw .rb 调用中作为 Authorization: token 发送的 Codeberg/Forgejo (Gitea) token;涵盖 Codeberg 和自托管的 Forgejo/Gitea 未设置
MALT_HTTP_IDLE_TIMEOUT_SECS HTTP 空闲(无进度)读取超时时间(秒,限制在 [5, 600] 范围内) 30
MALT_API_DOMAIN 覆盖元数据 API 基础 URL;仅 HTTPS;回退到 HOMEBREW_API_DOMAIN https://formulae.brew.sh/api
MALT_BOTTLE_DOMAIN 覆盖 bottle 注册表基础 URL;仅 HTTPS;回退到 HOMEBREW_BOTTLE_DOMAIN https://ghcr.io
MALT_OFFLINE 设置为 1/true 以将每次获取通过快照缓存路由;未命中时触发 OfflineRequired 而不是在连接时卡住(对应 --offline 未设置
MALT_MIGRATE_PARALLEL_WORKERS mt migrate --parallel 的工作线程数(限制在 [1, 32] 范围内) 4
MALT_OUTDATED_MAX_AGE outdated.json 快照的 TTL(分钟) 5
MALT_ALLOW_RAW_POST_INSTALL 禁用 post_install 输出上的终端转义过滤器(无论在原生还是 ruby 路径上),子进程将保持 TTY 未设置
MALT_ALLOW_UNVERIFIED 跳过签名 + 校验和验证——在 install.sh 中以及在 mt version update --no-verify 中(仅在 cosign 不可用时使用) 未设置
MALT_ALLOW_UNVERIFIED_SOURCE 允许在未解析出发布标签时 install.sh 克隆 main 未设置

安全性

malt 的正确性建立在几个承重属性之上:

  • SHA256 验证。 在下载期间计算流式哈希,并在提取前进行验证。没有任何未验证的数据会触及存储。
  • Tar 条目预扫描。 在写入任何字节之前,验证每个条目的名称和符号链接目标。通过每个条目的校验和验证 512 字节的 tar 头。硬链接通过 linkat(..., 0) 应用,它拒绝跟随符号链接——因此恶意的 tarball 无法通过指向 /etc/passwd 的符号链接将硬链接落入 keg 内部。
  • 预检检查。 在任何下载开始之前,解析依赖项、验证磁盘空间、检测链接冲突。
  • 原子安装。 9 步协议在每个阶段都使用 errdefer。中断的安装不会留下部分状态。
  • 并发访问。 30 秒超时的建议文件锁可防止并发修改。只读命令不会获取此锁。
  • 升级回滚。 在触及旧版本之前,新版本已完全安装并经过验证。
  • 存储不可变性。 存储条目在提交后永不修改。补丁操作发生在 Cellar 克隆副本上。
  • Mach-O 解析器加固。 使用溢出检查的算术对片(slice)的节偏移量和字符串表索引进行验证,因此带有精心构造的加载命令的 bottle 无法将整数包装成边界绕过漏洞。
  • DSL 路径沙盒化。 post_install 解释器中的每个修改操作都针对 Cellar/malt 前缀进行验证;包含 .. 以及通过符号链接转义沙盒的路径将被拒绝。
  • DSL 系统仅限 argv。 解释器的 system 内置命令使用 argv 切片孵化并固定可执行文件——绝不使用 /bin/sh -c,绝不通过 PATH 解析。编写了 system "rm", arg 的 formula 无法触及父 shell。

供应链方面:

  • 已签名版本。 每一个版本都通过 GitHub OIDC 进行无密钥的 cosign 签名;install.sh 在信任 SHA256 校验和之前会验证签名。泄露的 GitHub Token 不足以发布一个恶意的 malt 二进制文件。
  • 置顶的第三方源。 homebrew-core 和第三方 tap 都被固定到一个特定的提交 SHA。Formula Ruby 源码会针对该提交中嵌入的清单进行 SHA256 验证。被重写的上游分支无法在安装中途替换 formula 的 bottle URL。使用 mt tap --refresh user/repo 显式推进 tap 置顶。
  • 沙盒化 post_install。 可选的 --use-system-ruby 路径运行在限定于 formula cellar 的 sandbox-exec 配置文件中。敌意的 formula 只能影响其自身的安装前缀,而不能影响其他任何东西。
  • 边界验证。 MALT_PREFIXlaunchd 服务声明、安装脚本校验和以及 HTTP 重定向对畸形或可疑输入采取安全失败(fail-closed)策略——没有静默的 HTTPS→HTTP 降级,服务 argv 中没有 /bin/sh,前缀路径中没有 ..。未声明 sha256 的 cask 会被拒绝而不是被视为选择跳过;只有显式的 sha256 :no_check 才会跳过验证。
  • 姿态可见性。 mt doctor 标记 /opt/malt 下世界可写或组可写的路径以及意外的所有权,以便多用户机器能够一目了然地看到其攻击面。

本地 formula:信任边界

mt install --local ./formula.rb 值得单独用一个段落说明,因为它是 malt 其余部分所不具备的代码执行表面。.rb 文件指定了最终落入您系统中的归档 URL 和 SHA256——安装它就等于信任该文件。请将其用于您自己的 formula、在更改进入 tap 之前试验上游变更,或用于私有的内部软件包。请不要将其用于您未曾阅读过的 .rb 文件。

  • 路径回显。 malt 在每次安装时打印规范的 realpath,因此细心的读者会注意到诸如 /tmp/.... 之类的意外情况。
  • 自动检测。.//~/ 开头的路径,或任何带有 .rb 后缀的嵌入式斜杠都会自动检测为本地路径;无论哪种情况都会触发相同的警告。纯文件名(例如 wget.rb)不会自动检测——请传递 --local 进行区分。
  • 方案白名单。 归档 URL 必须是 https://;明文 HTTP、file://ftp://data: 在下载开始前即被拒绝。
  • 常量时间比较。 SHA256 检查在常量时间内运行。
  • 所有权警告。 如果 .rb 是世界可写的或由其他用户拥有,将触发额外的 行。
  • 拒绝标志冲突。--local--cask--formula--use-system-ruby 组合使用会在前期被拒绝。

对于本地安装,malt 仅读取 bottle 风格的 version + url + sha256 三元组(可选嵌套在 on_macos / on_arm / on_intel 下)。它不评估 depends_onpost_install——如果您需要这两者,请将 formula 发布到 tap 并通过 mt install user/tap/formula 安装。

支持的归档格式为 .tar.gz.tgz.tar.xz.zip。Formula 名称来自文件的基本名称:hello.rb 安装 hello。一个最小兼容的 .rb 示例:

class Hello < Formula
  version "1.2.3"
  on_macos do
    on_arm do
      url "https://example.com/hello-#{version}-arm64.tar.gz"
      sha256 "aaaa…"   # 64 个十六进制字符
    end
    on_intel do
      url "https://example.com/hello-#{version}-x86_64.tar.gz"
      sha256 "bbbb…"
    end
  end
end

顶层的扁平 url / sha256 适用于单架构归档。有关可运行的示例,请参见 scripts/fixtures/local_formulae/hello.rb

--use-system-ruby 按设计是按 formula 级别的:它防止某个软件包失败的 post_install 默默扩大整个批次的信任边界。

  • 单个软件包: 裸标志即可工作(mt install jq --use-system-ruby)。
  • 多软件包: 显式限定范围(mt install jq wget --use-system-ruby=jq)。
  • mt migrate 完全拒绝裸形式。

架构

malt 的行为源于少数几个设计选择——每一个都是追求安全并发和抗中断生存能力的直接结果。

其专属的前缀

malt 安装到 /opt/malt 并且绝不触及 Homebrew。该路径短是有意为之:Mach-O 加载命令补丁需要空间来原地替换原始的 Homebrew 路径,而 /opt/malt 总是装得下。

/opt/malt/
├── store/          # 内容寻址的 bottle 存储(不可变,按 SHA256)
├── Cellar/         # 已安装的 keg(从 store/ 进行 APFS 克隆)
├── Caskroom/       # 已安装的 cask 应用程序
├── opt/            # 带版本的 formula 符号链接
├── bin/            # keg 二进制文件的符号链接
├── lib/            # keg 库的符号链接
├── include/        # keg 头文件的符号链接
├── share/          # keg 共享数据的符号链接
├── tmp/            # 进行中的下载和提取
├── cache/          # 缓存的 API 响应(基于 TTL)
└── db/             # SQLite 数据库 + 建议锁

内容寻址存储

Bottle 按其 SHA256 存储。同一个 bottle 绝不会被下载或提取两次;多个已安装的 keg 引用同一个存储条目。存储条目是不可变的——只有 mt purge --store-orphans 会移除它们。Cellar/ 中的 keg 是通过 APFS clonefile() 实体化的,它以零磁盘成本创建了一个写时复制(CoW)克隆;非 APFS 卷会回退到递归复制。

这就是使 mt rollback 成为瞬间操作的原因:每个先前安装的 bottle 仍然在存储区中,因此回滚就是 unlinkre-clone → 数据库更新,无需重新下载。

流式下载管道

每个 bottle 下载都是一个单通管道:

网络 (来自 GHCR CDN 的 HTTPS)
    ├──► SHA256 哈希器 (流式——随着数据块到达计算)
    └──► gzip/zstd 解压器
            └──► tar 提取器
                    └──► 写入 tmp/ 的文件系统

磁盘上不写入中间归档文件。流完成后立即针对 Homebrew API 清单验证 SHA256;如果不匹配,提取的目录将在发生任何提交之前被删除。

Mach-O 补丁

Homebrew bottle 在 Mach-O 加载命令中包含硬编码的 /opt/homebrew/Cellar/... 路径。malt 通过四个步骤更正它们:

  1. 使用结构感知解析(而不是原始字节扫描)解析头部。
  2. 识别每个相关的加载命令(LC_ID_DYLIBLC_LOAD_DYLIBLC_RPATH 等)。
  3. 原地重写路径,并用空字节填充剩余空间。
  4. 在 arm64 上,通过 codesign --force --sign - 对打过补丁的二进制文件进行临时签名(ad-hoc codesign)。

包含 @@HOMEBREW_PREFIX@@@@HOMEBREW_CELLAR@@ 占位符的文本文件(.pc 配置、shell 脚本)以相同的方式打补丁。补丁总是发生在 Cellar 副本上,绝不发生在存储区原件上——如果失败,Cellar 副本将被删除,存储条目保持 pristine 状态以供重试。

Post_install 解释器

当 formula 定义了 post_install 时,malt 首先尝试其原生解释器。它解析并评估这些代码块实际使用的 Ruby 子集:

  • Pathname 操作、FileUtils、inreplace、Dir.glob
  • 字符串插值、%w[] 数组、布尔运算符
  • 控制流:if/unless.each / .select / .map
  • Formula["name"] 交叉查找、ENV 访问

如果本地未克隆 tap,homebrew-core formula 的源码会按需从 GitHub 获取。

Homebrew v6 正在将 formula 从这些 Ruby 块迁移到声明式的 post_install_steps 数组。malt 同样原生运行这些步骤——贯穿安装、升级和迁移——因此随着上游的转换,软件包将继续自行配置。

每一个修改文件系统的操作——writermchmodsymlink——都要针对 formula 的 Cellar 前缀和 malt 前缀进行验证;包含 .. 或通过符号链接解析到沙盒之外的路径将被立即拒绝。

当解释器遇到不支持的结构时,用户会被引导使用 --use-system-ruby,它会委托给一个限定在 formula cellar 内的沙盒化 Ruby 子进程,具有:

  • 被清洗的环境变量
  • RLIMIT_CPU/AS/FSIZE 上限
  • 从子进程输出中过滤掉的终端转义序列
Formula 有 post_install 吗?
  │
  ├── 是 → 尝试原生 DSL 解释器
  │           │
  │           ├── 成功 → 完成(软件包完全配置)
  │           │
  │           └── 不支持的结构 → 设置了 --use-system-ruby 吗?
  │                                         │
  │                                         ├── 是 → 委托给沙盒化 Ruby 子进程
  │                                         └── 否  → 带着清晰的提示跳过
  │
  └── 否 → 完成 (不需要 post_install)

原子安装协议

每次安装都遵循九个步骤。任何步骤失败都只会触发该步骤的清理——先前状态不会被修改。

  1. 获取锁——获取 db/malt.lock 上的排他建议锁。
  2. 预检——解析依赖项、检查磁盘空间、检测链接冲突。
  3. 下载——通过流式 SHA256 验证从 GHCR CDN 获取 bottle。
  4. 提取——解压并 untar 到 tmp/
  5. 提交到存储——从 tmp/store/ 的原子重命名。
  6. 实体化——从 store/Cellar/ 的 APFS clonefile,打 Mach-O 补丁,codesign。
  7. 链接——在 bin/lib/ 等中创建符号链接,记录在数据库中。
  8. 数据库提交——在单个事务中插入到 kegsdependencieslinks 表中。
  9. 释放锁——清理临时文件。

升级在旧版本被移除之前对新版本遵循相同的协议;失败时,恢复旧的符号链接。只读命令(listinfosearch)不获取锁。


开发 builds

用于在 malt 本身上进行黑客开发——debug 构建、测试套件和通用二进制文件。

# 需要 Zig 0.16.x
zig build                                # debug 构建
zig build -Doptimize=ReleaseSafe         # release 构建 (~4 MB)
zig build test                           # 运行测试
zig build universal                      # 通用二进制文件 (通过 lipo 的 arm64 + x86_64)

有关从本地检出安装 malt(最终用户路径),请参见安装下的从源码构建


基准测试

macOS 14 (Apple Silicon) 上的安装时间。

冷安装 (中位数 ± σ)

软件包 malt 0.22.3 nanobrew v0.1.205 zerobrew v0.3.2 Homebrew
tree (0 依赖) 0.342±0.406s 0.573±0.718s 0.876±0.086s 1.865±0.127s
wget (6 依赖) 2.834±1.820s 4.352±4.431s 5.367±1.424s 2.050±0.233s
ffmpeg (11 依赖) 3.390±0.519s 5.192±0.353s 8.486±1.107s 5.969±0.401s

热安装

软件包 malt nanobrew zerobrew
tree (0 依赖) 0.006s 0.015s 0.236s
wget (6 依赖) 0.018s 0.020s 0.715s
ffmpeg (11 依赖) 0.027s 0.028s 3.409s

二进制大小

工具 大小
malt 4.0 MB
nanobrew 3.1 MB
zerobrew 8.7 MB

Apple Silicon (GitHub Actions macos-14),2026-08-03。通过基准测试工作流每周自动更新。


二进制文件内部结构

malt 的二进制文件之所以小,是因为它仅搭载了五个子系统以及它们之间的胶水代码:

  1. SQLite。 ACID 写入、反向依赖查询、链接器冲突检测、升级失败后的原子回滚。在写入中途遭遇 kill -9 也能存活。
  2. 原生 post_install 解释器。 Zig 中的 Ruby 子集解释器——仅针对没有它就无法配置的 formula(nodeopenssl 等)激活。
  3. 带 arm64 临时签名的 Mach-O 补丁。 重写 /opt/homebrewMALT_PREFIX 并重新签名,以便 dyld 在现代 macOS 上加载结果。
  4. 安装锁。 db/malt.lock 上的 flock 加上符号链接树遍历,由每个修改命令获取,因此两个调用——或者被 Ctrl-C 打断的安装——不会破坏状态。
  5. sandbox-exec 配置文件。 可选的 --use-system-ruby 路径在默认拒绝(deny-default)的沙盒中运行 formula 脚本(具有上述的上限和转义过滤)。

这五个子系统都在每次安装时运行。上面的热安装(Warm)行是它们组合起来的挂钟时间成本。

交互式仪表盘(mt tui)是唯一不在每次安装时运行的部分——它被编译进同一个二进制文件中,而不是作为配套工具发布,并且只耗费大约 300 KB 的空间。


方法论

每个单元格是 5 轮测试的中位数(BENCH_ROUNDS=5scripts/bench.sh 中的默认值)——比平均值更能抵抗单次运行的抖动。可以通过 BENCH_ROUNDS=N 进行覆盖。每次运行还会向 $GITHUB_OUTPUT 发出每个工具的 _min_stddev 键,并将其打印在本地终端摘要中。

此处的冷样本(cold sample)从每个工具被擦除的安装前缀开始,因此第一轮会练习完整的下载 → 提取 → 链接 → 数据库写入路径。一些基准测试脚本将“冷”定义为卸载/重新安装,这会保持下载缓存为热状态;这两种定义可能会在相同的硬件上为相同的工具产生不同的绝对冷启动数字。

BENCH_TRUE_COLD=1 在每个冷样本之前擦除每个工具的安装前缀和 bottle 下载缓存,因此“冷”意味着磁盘上没有任何 bottle。

  • maltnanobrewzerobrew:一个前缀擦除即可覆盖两者(缓存位于前缀内部)。
  • Homebrew:缓存位于前缀外部 (~/Library/Caches/Homebrew/downloads),因此需要通过 brew --cache 针对每个 formula 及其传递依赖进行显式擦除。
  • 如果没有额外的 Homebrew 擦除,本地 brew 数据会比 CI 快 5–25 倍——因为 brew 正在复用早期轮次缓存的 bottle。

每个软件包基准测试以一个丢弃的热身轮次开头:每个工具运行一个安装/卸载对,其计时被丢弃,因此在计时开始前,DNS、TLS 会话缓存、TCP 拥塞窗口和磁盘缓存都已填满。

随后,测量的轮次会轮换工具顺序(第 r 轮从 tools[r mod N] 开始),因此没有任何单个工具能够可靠地吃掉“冷网络”槽位或受益于最热的槽位。

scripts/bench.sh 在每次构建前从 git 获取 nanobrew 和 zerobrew,因此比较始终是“今日 malt”对阵每个同类工具的最新提交,而不是几周前的快照。设置 BENCH_SKIP_UPDATE=1 以固定当前已检出的内容。

每个工具都使用其上游发布的 release 标志构建:malt 使用 ReleaseSafe(匹配 .goreleaser.yaml),nanobrew 使用 ReleaseFast,zerobrew 使用 cargo build --release。二进制文件大小可能与每个工具自身仓库中显示的数字不同——差距几乎总是版本漂移,而不是标志差异。

若要本地复现,./scripts/local-bench.sh 按顺序运行四个 CI 阶段(treewgetffmpeg、压力测试)。添加 --clean 以随后擦除 /tmp 基准测试状态。对于迭代式工作,直接使用 scripts/bench.sh <pkg>SKIP_BUILD=1 复用现有二进制文件;SKIP_OTHERS=1 / SKIP_BREW=1 跳过同类对比。


贡献

欢迎贡献。在提交大改动之前,请先开启一个 Issue 进行讨论。请参阅 CONTRIBUTING


许可证

malt 采用 MIT 许可证授权。第三方组件和上游项目——包括 Homebrew (BSD-2-Clause) 和 homebrew-core (BSD-2-Clause)——均在 LICENSE 文件中予以致谢。

加入我们

Zig 中文社区是一个开放的组织,我们致力于推广 Zig 在中文群体中的使用,有多种方式可以参与进来:

  1. 供稿,分享自己使用 Zig 的心得
  2. 改进 ZigCC 组织下的开源项目
  3. 加入微信群QQ 群QQ 频道Telegram 群组Google Groups 与更多 Zig 爱好者交流

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    日报daily report

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions