坦白说,很多 Rust CLI 教程只讲到 cargo run 就结束了。但真正让工具被别人用起来,卡点往往不在写代码,而在后半段:如何打包二进制、如何处理版本、如何让 macOS 用户通过 Homebrew 一行命令安装。
我见过不少 CLI 项目代码质量不错,却停在 README 里一句“请自行 clone 后 cargo install”。这对开发者还行,对普通用户几乎等于没发布。
这篇文章我会按真实发布链路讲完整:用 Rust 开发一个 CLI 工具,配置命令行参数、日志和错误处理,构建 release 二进制,然后发布到 GitHub Release,最后写 Homebrew Formula,让用户执行:
brew install yourname/tap/mycli就能安装。
先说清楚:Rust CLI 发布到 Homebrew 的本质是什么?
Homebrew 并不会神奇地“理解”你的 Rust 项目。它做的事情很朴素:
- 找到你的源码包或二进制包
- 校验 sha256
- 执行构建或安装命令
- 把生成的可执行文件放到 Homebrew 管理的
bin目录
所以 Rust CLI 发布到 Homebrew 通常有两条路:
| 方式 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 源码构建 | Formula 下载源码,执行 cargo install 或 cargo build --release | 符合 Homebrew 习惯,跨架构更自然 | 用户安装时要编译,速度慢 |
| 预编译二进制 | Formula 下载已构建好的 tar.gz | 安装快,可控性强 | 需要为不同平台准备产物 |
根据我的经验,个人工具、团队内部工具可以先走源码构建,简单可靠;如果工具用户量增长,或者依赖较重,再考虑预编译二进制。
这篇先用“源码构建”路线,因为它最适合第一次把 Rust CLI 发布到 Homebrew。
一个最小但像样的 Rust CLI 项目
我们创建一个叫 mycli 的工具,用来接收一个名字并输出问候语。功能不重要,重要的是项目结构和发布方式。
cargo new mycli
cd mycli推荐的目录结构如下:
mycli/
├── Cargo.toml
├── README.md
├── LICENSE
└── src/
└── main.rsCargo.toml 不要随便写。Homebrew、crates.io、GitHub Release 都会间接依赖这里的元信息。
[package]
name = "mycli"
version = "0.1.0"
edition = "2021"
description = "A small Rust CLI example"
license = "MIT"
repository = "https://github.com/yourname/mycli"
readme = "README.md"
[dependencies]
anyhow = "1"
clap = { version = "4", features = ["derive"] }这里用了两个常见依赖:
clap:处理命令行参数,Rust CLI 事实上的主流选择之一anyhow:简化应用层错误处理,适合 CLI 场景
如果你写的是库,我会更谨慎地选择错误类型;但 CLI 应用不一样,用户更关心错误信息是否清楚,而不是错误类型层级是否优雅。
main.rs:别把 CLI 写成脚本风格
很多 Rust CLI 的第一个坑,是把所有逻辑都塞进 main。短期看很快,后面加子命令、配置文件、日志、测试时就会变得难维护。
一个更稳的写法是:main 只负责接住错误,业务逻辑放到 run。
use anyhow::Result;
use clap::Parser;
#[derive(Parser, Debug)]
#[command(name = "mycli")]
#[command(version)]
#[command(about = "A small Rust CLI example", long_about = None)]
struct Cli {
#[arg(short, long, default_value = "world")]
name: String,
}
fn main() {
if let Err(err) = run() {
eprintln!("error: {err:#}");
std::process::exit(1);
}
}
fn run() -> Result<()> {
let cli = Cli::parse();
println!("hello, {}", cli.name);
Ok(())
}试一下:
cargo run -- --name rust
cargo run -- --help
cargo run -- --version这里有个坑要注意:cargo run -- --name rust 中间的 -- 不是多余的。前面的参数给 Cargo,后面的参数才传给你的程序。
发布前别急:先把 release 构建跑通
Homebrew 最终安装的是 release 构建,不是 debug 构建。所以本地必须先跑:
cargo build --release
./target/release/mycli --name homebrew如果你依赖了 OpenSSL、系统动态库、C 编译工具链,这一步尤其重要。很多“我本地能跑,brew 安装失败”的问题,本质都是构建环境差异导致的。
我建议在提交前至少跑这几条:
cargo fmt --check
cargo clippy -- -D warnings
cargo test
cargo build --release如果项目稍微正式一点,可以加 GitHub Actions:
name: CI
on:
push:
pull_request:
jobs:
test:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo fmt --check
- run: cargo clippy -- -D warnings
- run: cargo test
- run: cargo build --release为什么用 macos-latest?因为我们目标是 Homebrew,至少要保证 macOS 环境构建没问题。当然,实际项目里我通常会再加 Linux。
给 Git 打 tag,并创建 GitHub Release
Homebrew Formula 需要一个稳定 URL。最常见的做法是引用 GitHub 的源码归档:
https://github.com/yourname/mycli/archive/refs/tags/v0.1.0.tar.gz发布流程大概是:
git add .
git commit -m 'release v0.1.0'
git tag v0.1.0
git push origin main --tags然后到 GitHub 创建 Release,选择 v0.1.0 这个 tag。
关键在于版本号要统一:
Cargo.toml里的version = "0.1.0"- Git tag 用
v0.1.0 - Homebrew Formula 里的 URL 指向
v0.1.0
这三处不一致,短期也许能跑,但后续维护会非常痛苦。
创建自己的 Homebrew Tap
Homebrew Tap 本质上就是一个 Git 仓库,命名通常是:
homebrew-tap比如你的 GitHub 用户名是 yourname,就创建:
https://github.com/yourname/homebrew-tap用户安装时会这样写:
brew tap yourname/tap
brew install mycli也可以合并成:
brew install yourname/tap/mycli仓库里创建文件:
Formula/mycli.rb注意目录名是 Formula,不是 formula。大小写在某些环境里会让你踩坑。
编写 Homebrew Formula
一个源码构建版 Formula 可以这样写:
class Mycli < Formula
desc "A small Rust CLI example"
homepage "https://github.com/yourname/mycli"
url "https://github.com/yourname/mycli/archive/refs/tags/v0.1.0.tar.gz"
sha256 "REPLACE_WITH_SHA256"
license "MIT"
depends_on "rust" => :build
def install
system "cargo", "install", "--locked", "--root", prefix, "--path", "."
end
test do
assert_match "hello", shell_output("#{bin}/mycli --name brew")
end
end这里几个点很关键。
depends_on "rust" => :build 表示 Rust 只在构建时需要。安装完成后,用户运行二进制不需要 Rust 工具链。
--locked 很重要。它要求使用 Cargo.lock 中锁定的依赖版本,避免 Homebrew 构建时拉到不同依赖导致结果不可控。CLI 项目建议提交 Cargo.lock。
--root prefix 是 Homebrew 常见写法,确保二进制安装到 Formula 对应路径。
计算 sha256:这是最容易被忽略的小细节
Formula 中的 sha256 必须是 URL 下载内容的哈希。你可以这样算:
curl -L -o mycli-v0.1.0.tar.gz https://github.com/yourname/mycli/archive/refs/tags/v0.1.0.tar.gz
shasum -a 256 mycli-v0.1.0.tar.gz把输出的哈希填进 Formula。
这里要注意:如果你重新打了同名 tag,GitHub 归档内容会变,sha256 也会变。更麻烦的是,用户可能已经缓存了旧版本。
最佳实践是:tag 发布后不要改。如果必须修复,就发 v0.1.1。
本地测试 Formula,而不是直接推给用户试错
在 homebrew-tap 仓库里执行:
brew install --build-from-source Formula/mycli.rb安装后验证:
mycli --name local
brew test mycli如果你已经 tap 了自己的仓库,也可以:
brew tap yourname/tap
brew install yourname/tap/mycli
brew test yourname/tap/mycli更新 Formula 后,本地可能有缓存。必要时清理:
brew uninstall mycli
brew cleanup mycli
brew update还有一点,brew audit --strict --online mycli 对个人 tap 不一定每条都必须满足,但它能帮你发现不少格式和元信息问题。
brew audit --strict --online Formula/mycli.rb用户安装路径应该怎么写在 README 里?
README 不要只写一堆构建命令。用户最关心的是“我怎么安装”和“怎么开始用”。
我通常会这样写:
## Installation
### Homebrew
brew install yourname/tap/mycli
### Cargo
cargo install mycli
## Usage
mycli --name rust
如果你的工具还没发布到 crates.io,就不要写 cargo install mycli,避免误导。可以写:
cargo install --git https://github.com/yourname/mycli说实话,README 的安装命令比很多人想象中重要。一个 CLI 工具的转化率,往往就卡在安装步骤是否清楚。
版本升级:Formula 要改哪些地方?
当你从 0.1.0 升级到 0.1.1:
- 修改
Cargo.toml版本 - 更新
Cargo.lock - 提交代码
- 打
v0.1.1tag - 创建 GitHub Release
- 更新 Formula 的
url - 重新计算并更新
sha256
Formula 变成:
url "https://github.com/yourname/mycli/archive/refs/tags/v0.1.1.tar.gz"
sha256 "NEW_SHA256"然后提交到 homebrew-tap:
git add Formula/mycli.rb
git commit -m 'mycli 0.1.1'
git push用户侧执行:
brew update
brew upgrade mycli常见失败原因:大多不是 Rust 的锅
找不到 Cargo.lock
如果 Formula 使用 --locked,但源码包里没有 Cargo.lock,构建会失败。
CLI 应用应该提交 Cargo.lock。库项目可以不提交,但应用项目提交锁文件是更可控的选择。
sha256 不匹配
常见原因有两个:
- 你填错了哈希
- 你改过同名 tag
解决办法也很直接:重新下载 URL 对应文件,重新计算 sha256。不要猜。
Formula 类名不对
Formula/mycli.rb 对应类名应是:
class Mycli < Formula如果工具名是 my-tool,类名通常写成:
class MyTool < FormulaHomebrew 对命名有自己的约定,名字复杂时建议参考官方 Formula。
安装成功但命令不可用
检查二进制是否真的安装到了 bin:
brew list mycli如果用 cargo install --root prefix --path .,正常会生成:
.../Cellar/mycli/0.1.0/bin/mycli如果没有,通常是 package name、binary name 或 workspace 配置出了问题。
要不要发布到 Homebrew core?
很多人一上来就想进 Homebrew core。我的建议是:先维护自己的 tap。
Homebrew core 对项目成熟度、下载稳定性、依赖、维护状态都有要求,而且审核标准比个人 tap 严格得多。对大多数新工具来说,自建 tap 已经足够好:安装简单、升级可控、维护成本低。
等工具有稳定用户、清晰定位、持续维护记录,再考虑提交到 core 更现实。
一张流程图,把全链路串起来
Rust CLI 项目
│
├─ cargo fmt / clippy / test
│
├─ cargo build --release
│
├─ git tag v0.1.0
│
├─ GitHub Release
│
├─ 计算源码包 sha256
│
├─ 编写 Formula/mycli.rb
│
├─ brew install --build-from-source 本地验证
│
└─ 用户 brew install yourname/tap/mycli这个流程看起来步骤多,但每一步都在解决一个具体问题:可构建、可校验、可安装、可升级。
我认为最值得坚持的几个最佳实践
如果只记几条,我建议记这些:
- Rust CLI 项目提交
Cargo.lock main保持薄,把逻辑放进run- 发布 tag 后不要修改同名 tag
- Formula 使用
--locked - 每次升级都重新计算 sha256
- README 里把 Homebrew 安装命令放在显眼位置
- 本地跑通
brew install --build-from-source后再推送
Rust 写 CLI 很舒服,但“写出来”和“让别人稳定安装”是两件事。Homebrew 发布流程的价值就在于,把你的工具从一个开发者项目,变成用户可以信任的一条安装命令。
如果你正在做第一个 Rust CLI,我的建议是:不要等功能全部完美再发布。先走通一次从代码到 Homebrew 的完整链路。后来你会发现,真正提升工程质量的,往往不是多写一个功能,而是把构建、版本、发布和安装这些环节打磨顺。
