Rust开发CLI工具并发布到Homebrew全流程:从项目骨架到用户一行安装

loong
2026-05-27 / 0 评论 / 13 阅读 / 正在检测是否收录...

坦白说,很多 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 项目。它做的事情很朴素:

  1. 找到你的源码包或二进制包
  2. 校验 sha256
  3. 执行构建或安装命令
  4. 把生成的可执行文件放到 Homebrew 管理的 bin 目录

所以 Rust CLI 发布到 Homebrew 通常有两条路:

方式做法优点缺点
源码构建Formula 下载源码,执行 cargo installcargo 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.rs

Cargo.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:

  1. 修改 Cargo.toml 版本
  2. 更新 Cargo.lock
  3. 提交代码
  4. v0.1.1 tag
  5. 创建 GitHub Release
  6. 更新 Formula 的 url
  7. 重新计算并更新 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 < Formula

Homebrew 对命名有自己的约定,名字复杂时建议参考官方 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 的完整链路。后来你会发现,真正提升工程质量的,往往不是多写一个功能,而是把构建、版本、发布和安装这些环节打磨顺。

赏金: 1.99 缘

⚠ 温馨提示: 完成赞赏后 可能有彩蛋哟~

赞赏后可读区
0