brew -- 创建自己的 Cask

摘要

  • brewformula 从源码(或 bottle)安装软件,用 cask 安装已经打好的成品。成品常见是 .app / .pkg,也可以只是一个命令行二进制,不一定非要是 GUI。

  • 本文介绍如何在 macos 下创建自己的 Cask,和 brew -- 创建自己的 Formula 是一对:那篇讲 Formula,这篇讲 Cask。

  • 本文基于 MacOS 15.x,brew 版本为 Homebrew 7.0.4。

  • 关于 brew 的安装及使用可以参考 MacOS软件包管理器--brew

Formula 和 Cask 的区别

  • 选择哪种包装,看的是怎么拿到软件,不是「命令行必须 Formula、GUI 必须 Cask」:

    • 能从源码构建(开源命令行工具、库)→ Formula
    • 上游已经提供预编译包 → Cask(可以是 .app / .pkg,也可以是单个命令文件)
对比项 Formula Cask
典型对象 从源码构建的命令行工具、库 预编译成品:GUI 或命令行二进制都可以
下载物 源码 tarball,本地编译或 bottle .dmg / .zip / .tar.gz / .pkg 等容器
安装产物 Cellar 里的文件,再软链到 $(brew --prefix)/bin app/Applicationspkg → 系统 installer;binary$(brew --prefix)/bin
DSL class Xxx < Formula cask "token" do ... end
文件位置 Formula/xxx.rb Casks/xxx.rb
安装命令 brew install xxx brew install --cask xxx
卸载 brew uninstall xxx brew uninstall --cask xxx,可用 --zap 清残留
  • .dmg / .zip / .tar.gz 只是下载容器,真正装进系统的是 artifact:apppkgbinary 等。Reasonix 就是 Cask 只装一个命令文件的例子,见文末。

  • 自己写的、需要从源码安装的命令行脚本仍应走 Formula。已经有各平台预编译二进制时,用 Cask 的 binary 更合适。

从一个简单示例开始

  • 这里用一个最小的 macOS 应用 HelloCask.app 做演示:用 AppleScript 生成 .app,打成 zip,上传到 GitHub Release,再写 Cask 让别人 brew install --cask 安装。

  • 和 Formula 的关键差别:Formula 通常下载源码包archive/refs/tags/v1.0.0.tar.gz);Cask 下载的是已经打好的应用包

制作 HelloCask.app 并发布

  • 创建一个 Github 仓库,用于存放发布产物,仓库名称:hanqunfeng/hello_cask

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 用 AppleScript 生成一个最简单的 .app,双击会弹出对话框
osacompile -o HelloCask.app -e 'display dialog "Hello Cask!" buttons {"OK"} default button "OK"'

# 查看生成结果
ls HelloCask.app/Contents
# Info.plist MacOS Resources

# 打包,--keepParent 会保留 HelloCask.app 这一层目录,Cask 里才能写 app "HelloCask.app"
ditto -c -k --keepParent HelloCask.app HelloCask-1.0.0.zip

# 计算 sha256,后面写 Cask 时要用
shasum -a 256 HelloCask-1.0.0.zip
## 输出示例(以你自己算出来的为准)
# 745717e0025690a54a05ea2401987944de3d83fa3cf3f838d229313c434986a7 HelloCask-1.0.0.zip
  • 把仓库推到 GitHub,然后在 GitHub 上创建一个 Release:

    • Tag:v1.0.0
    • 附件上传:HelloCask-1.0.0.zip
  • 下载地址会是:

1
https://github.com/hanqunfeng/hello_cask/releases/download/v1.0.0/HelloCask-1.0.0.zip

不要用 archive/refs/tags/v1.0.0.tar.gz 这种源码归档当地址。Cask 不会帮你编译 .app,它只负责下载、校验、把里面的 .app 拷到 /Applications

创建 tap 仓库

  • Cask 和 Formula 可以放在同一个 tap 里(Formula/Casks/ 并列),也可以单独建一个。这里单独建,结构和上一篇保持一致。

1
2
3
4
5
6
7
8
9
10
11
brew tap-new hanqunfeng/hello_cask
## 输出
Warning: tap-new is a developer command, so Homebrew's
developer mode has been automatically turned on.
To turn developer mode off, run:
brew developer off

Initialized empty Git repository in /usr/local/Homebrew/Library/Taps/hanqunfeng/homebrew-hello_cask/.git/
[main (root-commit) ...] Create hanqunfeng/hello_cask tap
==> Created hanqunfeng/hello_cask
/usr/local/Homebrew/Library/Taps/hanqunfeng/homebrew-hello_cask
  • brew tap-new 默认只创建 Formula/ 目录,Cask 需要自己补:

1
mkdir -p "$(brew --repo hanqunfeng/hello_cask)/Casks"

仓库在 GitHub 上的名字必须是 homebrew-hello_caskbrew tap hanqunfeng/hello_cask 会自动补上 homebrew- 前缀。

手写一个 Cask 文件

  • 真正有约束的是 token 三处一致

    • 文件名:Casks/hello-cask.rb
    • header:cask "hello-cask" do
    • 安装命令:brew install --cask hello-cask
  • name "HelloCask" 是给人看的显示名,可以有大小写和空格,不必等于 token。

  • brew generate-cask-token 不是安装所必需的步骤。它只是按官方规则给一个建议名,方便向 homebrew/cask 投稿、避免重名。个人 tap 里你自己定 hello-cask 完全可以,不必先跑这个命令。

  • 如果要用它,注意它只做「小写 + 空格/下划线变 -」,不会拆驼峰:

1
2
3
4
5
6
7
8
9
10
11
brew generate-cask-token "HelloCask"
## 实际输出(驼峰不会被拆开)
Proposed token: hellocask
Proposed file name: hellocask.rb
Cask Header Line: cask "hellocask" do

brew generate-cask-token "Hello Cask"
## 实际输出(有空格才会变成 hello-cask)
Proposed token: hello-cask
Proposed file name: hello-cask.rb
Cask Header Line: cask "hello-cask" do
  • 后面示例采用 hello-cask。选定之后,文件名和 header 不要混用 hellocask

1
2
cd "$(brew --repo hanqunfeng/hello_cask)/Casks"
touch hello-cask.rb

写入内容(把 sha256 换成你自己算出来的值):

1
2
3
4
5
6
7
8
9
10
11
12
13
cask "hello-cask" do
version "1.0.0"
sha256 "745717e0025690a54a05ea2401987944de3d83fa3cf3f838d229313c434986a7"

url "https://github.com/hanqunfeng/hello_cask/releases/download/v#{version}/HelloCask-#{version}.zip"
name "HelloCask"
desc "Tiny demo app for learning Homebrew Casks"
homepage "https://github.com/hanqunfeng/hello_cask"

depends_on :macos

app "HelloCask.app"
end
  • desc 不要以 A/An/The 开头,也不要写 macOS,否则 brew style 会报错。

  • 只有 .app 的 Cask 必须写 depends_on :macos

  • 提交前在本地跑一次,能修的会自动改:

1
brew style --fix "$(brew --repo hanqunfeng/hello_cask)/Casks/hello-cask.rb"

【推荐】也可以用 brew create --cask URL --tap hanqunfeng/hello_cask --set-name hello-cask 生成模板。它会下载 zip、自动填 sha256,再打开编辑器,根据需要修改生成的模板文件。

提交 Cask 文件到 Github 仓库

  • 创建一个 Github 仓库:hanqunfeng/homebrew-hello_cask

1
2
3
4
5
cd "$(brew --repo hanqunfeng/hello_cask)"
git add .
git commit -m "Add hello-cask 1.0.0"
git remote add origin https://github.com/hanqunfeng/homebrew-hello_cask.git
git push -u origin main
  • 如果推送 .github/workflows/*.yml 失败,提示缺少 workflow 权限,给 PAT 加上 repoworkflow 后重推。具体处理和 brew -- 创建自己的 Formula 里一样:

1
2
git remote set-url origin https://ghp_xxxxx@github.com/hanqunfeng/homebrew-hello_cask.git
git push -u origin main

GitHub Actions 报错(brew test-bot

  • brew tap-new 会生成 .github/workflows/tests.yml。推到 main 后会在 Ubuntu 和 macOS 上跑 brew test-bot --only-tap-syntax,核心是 brew style 检查脚本格式。

  • matrix 默认 fail-fast:Ubuntu 一失败,后面的 macos-26macos-15-intel 会显示 canceled,那不是 macOS 自己坏了。

  • 常见 brew style 错误(HelloCask 实测会踩):

规则 问题 改法
Cask/Desc descA/An/The 开头 删掉冠词
Cask/Desc desc 里写了 macOS 不要提平台名
Homebrew/OSDependsOn 只有 .app 却没声明系统 depends_on :macos
Layout/EmptyLines 多空了一行 按 stanza 分组,组与组之间只空一行
Layout/TrailingEmptyLines 文件末尾缺少换行 文件必须以换行符结束,--fix 会自动补上
Cask/ArrayAlphabetization zap trash: 里只有一个元素还用了 [] 改成字符串,或没有残留就删掉 zap
  • 本地改完再推:

1
2
3
4
5
cd "$(brew --repo hanqunfeng/hello_cask)"
brew style --fix Casks/hello-cask.rb
git add Casks/hello-cask.rb
git commit -m "Fix brew style for hello-cask"
git push

安装 Cask

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 添加 tap
brew tap hanqunfeng/hello_cask

# Homebrew 7 起,非官方 tap 默认不信任,安装前需要先声明信任
# 只信任这一个 cask
brew trust --cask hanqunfeng/hello_cask/hello-cask
# 或者信任整个 tap 里的所有 formula / cask
# brew trust --tap hanqunfeng/hello_cask

# 搜索
brew search hello-cask
## 输出
==> Casks
hanqunfeng/hello_cask/hello-cask

# 安装,--cask 可以省略(名字不冲突时),写上更明确
brew install --cask hello-cask
# 完整包名写法,避免和官方 cask 重名
# brew install --cask hanqunfeng/hello_cask/hello-cask
## 输出
==> Fetching downloads for: hello-cask
==> Installing Cask hello-cask
==> Moving App 'HelloCask.app' to '/Applications/HelloCask.app'
🍺 hello-cask was successfully installed!
  • 安装完成后:

    • .app/Applications/HelloCask.app
    • 下载缓存和版本目录在 $(brew --caskroom)/hello-cask/1.0.0,Intel Mac 一般是 /usr/local/Caskroom/hello-cask/1.0.0

测试、卸载

1
2
3
4
5
6
7
8
9
10
11
# 查看信息
brew info --cask hello-cask

# 打开应用(或到 /Applications 里双击)
open -a HelloCask

# 普通卸载:只删掉 /Applications/HelloCask.app
brew uninstall --cask hello-cask

# --zap 会额外清理 zap 里列出的用户数据。本示例没有 zap,加了也没什么可删
# brew uninstall --cask --zap hello-cask

Cask 文件模板

  • Cask 不是 Formula 那种继承类,而是一个声明式 DSL:把“去哪下载、叫什么、装什么、怎么卸”写清楚,Homebrew 自己决定执行顺序。

1
2
3
cask "hello-cask" do
# 所有 stanza 都写在这个块里
end
  • 文件名、token、header 三者必须一致:Casks/hello-cask.rbcask "hello-cask" do

必填字段

1
2
3
4
5
6
7
8
version "1.0.0"
# 下载文件的 sha256;算不出来或 url 每次都变时可以用 :no_check,但能算就不要省
sha256 "..."

url "https://github.com/hanqunfeng/hello_cask/releases/download/v#{version}/HelloCask-#{version}.zip"
name "HelloCask" # 软件的正式名称,可含大小写、空格
desc "一句话描述,brew info 会显示"
homepage "https://github.com/hanqunfeng/hello_cask"
  • url 里用 #{version} 插值,以后升版本通常只改 versionsha256

  • version :latest 必须搭配 sha256 :no_check,个人 tap 能打出版本号就不要用 :latest

  • 另外至少要有一个 artifact(真正安装的东西)。最常见的是 app(GUI),也可以是 binary(命令文件),不必非有 .app

app:安装 .app

1
2
3
4
5
6
7
8
9
# zip/dmg 解压后,根目录就是 HelloCask.app
app "HelloCask.app"
# 默认安装到 /Applications/HelloCask.app

# 如果解压后还套了一层目录
# app "HelloCask/HelloCask.app"

# 极少情况下才用 target: 改目标名称,例如避免冲突
# app "HelloCask.app", target: "Hello Cask Demo.app"

pkg:安装 .pkg

  • .pkg 走系统 installer,Cask 必须同时写 uninstall,否则用户卸不干净。

1
2
3
4
5
pkg "HelloCask.pkg"

uninstall pkgutil: "com.example.HelloCask"
# pkgutil: 的值是安装后的 package id,可在已安装机器上查:
# pkgutil --pkgs | grep -i hello
  • uninstall 常用键:

作用
pkgutil: 按 package id 卸载,.pkg 首选
quit: 按 bundle id 发送退出事件(相当于 Cmd+Q)
launchctl: 卸载 launchd 服务
delete: / trash: 按路径删除,trash: 进废纸篓,更安全
script: 跑官方卸载脚本
1
2
uninstall quit:    "com.example.HelloCask",
pkgutil: "com.example.HelloCask"

binary:把可执行文件链到 prefix/bin

  • Cask 的安装产物不必须.app / .dmg / .pkgbinary 可以把压缩包里的命令文件软链到 $(brew --prefix)/bin,装完就能在终端直接跑。Reasonix 整份脚本只有这一个 artifact,没有 .app

  • 两种常见用法:

1
2
3
4
5
6
7
# 1. 压缩包里就是一个命令文件(Reasonix 这种)
binary "reasonix"
# 默认链到 $(brew --prefix)/bin/reasonix

# 2. GUI 应用附带命令行入口
app "HelloCask.app"
binary "#{appdir}/HelloCask.app/Contents/MacOS/HelloCask", target: "hello-cask"

zap:彻底清理

  • 普通 brew uninstall --cask 只移除 artifact(例如 /Applications 里的 .app)。用户数据、偏好设置、缓存要写在 zap 里,只有加 --zap 才会执行。

  • 不要凭 bundle id 猜路径。HelloCask 这种 osacompile 对话框应用,打开后通常也不会生成 ~/Library/Saved Application State/...。路径不存在时,zap 里写了也只是空操作。

  • 真实 GUI 应用常见残留大致在这些位置,以本机实际存在的为准:

1
2
3
4
5
6
zap trash: [
"~/Library/Application Support/HelloCask",
"~/Library/Preferences/com.example.HelloCask.plist",
"~/Library/Caches/com.example.HelloCask",
"~/Library/Saved Application State/com.example.HelloCask.savedState",
]
  • 正确做法是:先安装并打开一次应用,再自动扫描:

1
2
3
brew generate-zap hello-cask
# 或还没有 cask 文件时
brew generate-zap --name HelloCask
  • 扫不到额外文件时,官方习惯写一行注释:# No zap stanza required。HelloCask 演示就属于这种情况。

depends_on:依赖和系统约束

1
2
3
4
depends_on macos: :sonoma          # 最低 macOS
depends_on arch: :arm64 # 仅 Apple Silicon
depends_on formula: "ffmpeg" # 依赖某个 formula
depends_on cask: "macfuse" # 依赖另一个 cask

按架构提供不同下载地址

  • 很多应用 Intel / Apple Silicon 各一个包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cask "hello-cask" do
arch arm: "arm64", intel: "x64"

version "1.0.0"
sha256 arm: "aaa...",
intel: "bbb..."

url "https://example.com/HelloCask-#{version}-#{arch}.dmg"
name "HelloCask"
desc "Demo"
homepage "https://example.com"

app "HelloCask.app"
end

token 命名要点

  • 个人 tap:文件名去掉 .rb 必须等于 cask "..." 里的字符串,用户安装时也敲这个名字。这是硬约束。brew generate-cask-token 只是帮你起名,可以不用。

  • 若向官方 homebrew/cask 投稿,才建议用它生成 token,规则是:取 .app 名字 → 去掉 .app → 小写 → 空格变 -。它不会拆驼峰:HelloCaskhellocaskHello Caskhello-cask

  • 和已有 formula / cask 重名时,加厂商前缀或 -app 后缀,例如 formula 叫 unison,cask 叫 unison-app

  • 固定大版本用 corretto@11;测试通道用 google-chrome@beta@latest 表示上游的发布通道,和 version :latest 不是一回事。

第三方 tap 的本地修改会生效

  • Homebrew 4.0 之后,官方 homebrew/cask 默认走 JSON API,改本地 .rb 不会参与安装,需要 HOMEBREW_NO_INSTALL_FROM_API=1

  • 自己的 tap 不受这个限制:安装时本来就会 clone 到 Taps 目录,改完 Casks/hello-cask.rbbrew reinstall --cask hello-cask 就会用新脚本。

1
2
brew edit --cask hanqunfeng/hello_cask/hello-cask
brew reinstall --cask hanqunfeng/hello_cask/hello-cask

以 Reasonix 为例,说明脚本配置项

  • 这是一个只有命令文件、没有 .app 的 Cask:按系统/架构下载对应 tar.gz,用 binary "reasonix" 链到 bin。说明 Cask 完全可以用来分发预编译 CLI,本机只会走其中一条 on_* 分支。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# token:文件名、header、安装名必须一致,用户执行 brew install --cask reasonix
cask "reasonix" do

# 安装完成、artifact 链好之后执行;官方 tap 要求用结构化 steps,不能写任意 Ruby
postflight_steps do
on_macos do # 仅 macOS 执行;Linux 没有 quarantine 属性
# run 只启动一个程序,不会走 shell 字符串
# xattr -d 删除属性,-r 递归
# com.apple.quarantine:从网上下载的文件会被打上隔离标记,Gatekeeper 可能拦执行
# {{staged_path}} 是安装时才展开的路径(不是 Ruby 的 #{...}),即本次解压目录
run "/usr/bin/xattr",
args: ["-dr", "com.apple.quarantine", "{{staged_path}}/reasonix"]
end
end

version "1.38.11" # 软件版本;下面 url 里的 #{version} 都用它拼接

# 按 操作系统 × CPU 选择下载包
on_macos do
on_arm do # Apple Silicon
sha256 "7c70ba2f363bfa01a709975062b9fd6bd0ac82b8a0209ddb988277dbfbef7d07" # 该文件校验值,对不上就中止
url "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v#{version}/reasonix-darwin-arm64.tar.gz"
end
on_intel do # Intel Mac
sha256 "ebf5da3adcf8cce2e3c559718e5c853b2eb6a6793fc42a88ce0b1ca8dc1b09e7"
url "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v#{version}/reasonix-darwin-amd64.tar.gz"
end
end
on_linux do
on_arm do # Linux ARM
sha256 "0408b76a32f75f72387d07cc55b27dafd666f3bfe3be0424c3f55c11f73b8267"
url "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v#{version}/reasonix-linux-arm64.tar.gz"
end
on_intel do # Linux x86_64
sha256 "a8ae1baf4c81eaa5d8a455d1336e9c08328f3fcd176b25129902d787a19f821a"
url "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v#{version}/reasonix-linux-amd64.tar.gz"
end
end

name "reasonix" # 软件正式名称,给搜索、brew info 用,可与 token 不同
desc "Cache-first DeepSeek coding agent for the terminal." # 一句话介绍
homepage "https://github.com/esengine/DeepSeek-Reasonix" # 项目主页,brew home reasonix 会打开

livecheck do
# 跳过 brew livecheck 自动查新版本。本 cask 由发版流程生成,版本写死在脚本里
skip "Auto-generated on release."
end

# 真正安装的产物:解压目录里名为 reasonix 的可执行文件
# 会软链到 $(brew --prefix)/bin/reasonix,装完可直接在终端运行
binary "reasonix"
end

后记