brew -- 创建自己的 Cask
摘要
-
brew 用
formula从源码(或 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 → /Applications;pkg → 系统 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:app、pkg、binary等。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 | # 用 AppleScript 生成一个最简单的 .app,双击会弹出对话框 |
-
把仓库推到 GitHub,然后在 GitHub 上创建一个 Release:
- Tag:
v1.0.0 - 附件上传:
HelloCask-1.0.0.zip
- Tag:
-
下载地址会是:
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 | brew tap-new hanqunfeng/hello_cask |
-
brew tap-new默认只创建Formula/目录,Cask 需要自己补:
1 | mkdir -p "$(brew --repo hanqunfeng/hello_cask)/Casks" |
仓库在 GitHub 上的名字必须是
homebrew-hello_cask。brew 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 | brew generate-cask-token "HelloCask" |
-
后面示例采用
hello-cask。选定之后,文件名和 header 不要混用hellocask。
1 | cd "$(brew --repo hanqunfeng/hello_cask)/Casks" |
写入内容(把 sha256 换成你自己算出来的值):
1 | cask "hello-cask" do |
-
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 | cd "$(brew --repo hanqunfeng/hello_cask)" |
-
如果推送
.github/workflows/*.yml失败,提示缺少workflow权限,给 PAT 加上repo和workflow后重推。具体处理和 brew -- 创建自己的 Formula 里一样:
1 | git remote set-url origin https://ghp_xxxxx@github.com/hanqunfeng/homebrew-hello_cask.git |
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-26、macos-15-intel会显示 canceled,那不是 macOS 自己坏了。 -
常见
brew style错误(HelloCask 实测会踩):
| 规则 | 问题 | 改法 |
|---|---|---|
Cask/Desc |
desc 以 A/An/The 开头 |
删掉冠词 |
Cask/Desc |
desc 里写了 macOS |
不要提平台名 |
Homebrew/OSDependsOn |
只有 .app 却没声明系统 |
加 depends_on :macos |
Layout/EmptyLines |
多空了一行 | 按 stanza 分组,组与组之间只空一行 |
Layout/TrailingEmptyLines |
文件末尾缺少换行 | 文件必须以换行符结束,--fix 会自动补上 |
Cask/ArrayAlphabetization |
zap trash: 里只有一个元素还用了 [] |
改成字符串,或没有残留就删掉 zap |
-
本地改完再推:
1 | cd "$(brew --repo hanqunfeng/hello_cask)" |
安装 Cask
1 | # 添加 tap |
-
安装完成后:
.app在/Applications/HelloCask.app- 下载缓存和版本目录在
$(brew --caskroom)/hello-cask/1.0.0,Intel Mac 一般是/usr/local/Caskroom/hello-cask/1.0.0
测试、卸载
1 | # 查看信息 |
发布新版本
-
应用打了新 tag、上传了新的 zip 之后,要改 tap 里的 Cask,再提交到 GitHub。别人
brew update之后才会装到新版本。brew livecheck只报告有没有新版本,不会改文件。brew create只用于第一次生成,文件已经存在时不能拿来升级。 -
可以手工改
Casks/hello-cask.rb。url里已经有#{version},把version和sha256换成新包的值即可。sha256用shasum -a 256计算。 -
也可以用命令改。
--write-only会下载新包、算出sha256、写回本地.rb,不会 fork,也不会开拉取请求。不加--write-only时,这条命令会按官方homebrew/cask的流程尝试开拉取请求。 -
两种改法最后都要在 tap 仓库里提交并推送。
1 | # 命令改文件 |
Cask 文件模板
-
Cask 不是 Formula 那种继承类,而是一个声明式 DSL:把“去哪下载、叫什么、装什么、怎么卸”写清楚,Homebrew 自己决定执行顺序。
-
Homebrew 把
cask do ... end里的每一条配置声明叫做 stanza,这个词原意是诗的一节。version、sha256、url、name、app、binary、zap各算一条,彼此独立。前面brew style要求「按 stanza 分组、组与组之间只空一行」,指的就是这些配置项。
1 | cask "hello-cask" do |
-
文件名、token、header 三者必须一致:
Casks/hello-cask.rb↔cask "hello-cask" do
必填字段
1 | version "1.0.0" |
-
url里用#{version}插值,以后升版本改version和sha256即可,不必重写整段地址。怎么改见前面示例里的「发布新版本」。 -
version :latest必须搭配sha256 :no_check,个人 tap 能打出版本号就不要用:latest。 -
另外至少要有一个 artifact(真正安装的东西)。最常见的是
app(GUI),也可以是binary(命令文件),不必非有.app。
app:安装 .app
1 | # zip/dmg 解压后,根目录就是 HelloCask.app |
pkg:安装 .pkg
-
.pkg走系统 installer,Cask 必须同时写uninstall,否则用户卸不干净。
1 | pkg "HelloCask.pkg" |
-
uninstall常用键:
| 键 | 作用 |
|---|---|
pkgutil: |
按 package id 卸载,.pkg 首选 |
quit: |
按 bundle id 发送退出事件(相当于 Cmd+Q) |
launchctl: |
卸载 launchd 服务 |
delete: / trash: |
按路径删除,trash: 进废纸篓,更安全 |
script: |
跑官方卸载脚本 |
1 | uninstall quit: "com.example.HelloCask", |
binary:把可执行文件链到 prefix/bin
-
Cask 的安装产物不必须是
.app/.dmg/.pkg。binary可以把压缩包里的命令文件软链到$(brew --prefix)/bin,装完就能在终端直接跑。Reasonix 整份脚本只有这一个 artifact,没有.app。 -
两种常见用法:
1 | # 1. 压缩包里就是一个命令文件(Reasonix 这种) |
zap:彻底清理
-
普通
brew uninstall --cask只移除 artifact(例如/Applications里的.app)。用户数据、偏好设置、缓存要写在zap里,只有加--zap才会执行。 -
不要凭 bundle id 猜路径。HelloCask 这种
osacompile对话框应用,打开后通常也不会生成~/Library/Saved Application State/...。路径不存在时,zap里写了也只是空操作。 -
真实 GUI 应用常见残留大致在这些位置,以本机实际存在的为准:
1 | zap trash: [ |
-
正确做法是:先安装并打开一次应用,再自动扫描:
1 | brew generate-zap hello-cask |
-
扫不到额外文件时,官方习惯写一行注释:
# No zap stanza required。HelloCask 演示就属于这种情况。
depends_on:依赖和系统约束
1 | depends_on macos: :sonoma # 最低 macOS |
livecheck:检查上游新版本
-
brew livecheck --cask <token>只把查到的版本号和脚本里的version比较,不下载、不安装。原理是:请求url的页面,用regex扫描全文,圆括号里捕获的那段才是版本号;匹配到多个时取得最大的一个。 -
这个命令不走 JSON API,只读本机 tap 里的
.rb。官方homebrew/cask默认不在本地,直接查claude-code会报These casks are not in any locally installed taps!。要先brew tap homebrew/cask,并且脚本里写了livecheck,命令才能按这段配置查出新版本。没写livecheck时,它会改去试 cask 的url和homepage;地址如果只是某一个 zip,页面上没有版本列表,就查不出更新。Redis 的 cask 就是这样。skip则明确不请求,文末 Reasonix 用的就是这种。 -
调试:
brew livecheck --debug --cask <token>。Formula 的写法见 brew -- 创建自己的 Formula。
1 | # claude-code:这个地址的正文就是一行版本号,不是文件列表 |
安装时操作文件
-
Cask 没有 Formula 的
def install。brew -- 创建自己的 Formula 里那些Dir、File、Pathname、FileUtils不能写在cask do顶层:顶层只是在登记 stanza,调用chmod、cp会报undefined method。 -
解压之后要改文件,写在
preflight_steps(artifact 安装之前)或postflight_steps(artifact 安装之后)。卸载前后对应uninstall_preflight_steps、uninstall_postflight_steps。 -
这两个块在读取 cask 文件时只把步骤记下来,安装时才执行。块里只能调用下面这些步骤方法,不能写
Dir[...]、File.chmod、FileUtils.cp。 -
相对路径默认相对本次解压目录。步骤块里没有
staged_path这个方法,要写{{staged_path}}、{{appdir}},安装时才替换。#{staged_path}在这里会报错;它属于读文件时就算完的 Ruby 插值,不会留到安装时。
1 | # 相对路径都相对 staged_path,它等于 $(brew --caskroom)/<token>/<version>,例如 .../Caskroom/hello-cask/1.0.0 |
-
和 Formula 里那几类写法的对应关系:
Formula 的 install |
Cask 的 *_steps |
|---|---|
Dir[path/"lib/*.sh"] |
步骤参数里直接写 "lib/*.sh",安装时展开 |
(path/"dir").mkpath |
mkdir_p "dir" |
(path/"file").write "..." |
write_file "file", "..." |
chmod 0755, file |
set_permissions "file", "0755", recursive: false |
cp src, dst |
copy "src", "dst" |
rm_rf "cache" |
remove "cache", recursive: true |
File.exist?(path) |
if_path_exists "path" do ... end |
-
copy、move要匹配恰好一个文件时,加source_glob: true。remove、set_permissions、set_ownership的路径列表会自动展开通配符。 -
旧的
preflight do、postflight do、uninstall_preflight do、uninstall_postflight do从 Homebrew 6.0.16(2026-08-10)起,官方 tap 跑brew style会被Cask/InstallSteps拒绝,6.0.15 还不会。官方 tap 的路径在Taps/homebrew/homebrew-*,当时的报错是Casks in official Homebrew taps must use postflight_steps instead of postflight(preflight_steps等同理)。brew install不跑brew style,安装本身不会因此失败。同一版起,加载这些旧块会打废弃提示,代码仍会执行。 -
7.0.0(2026-09-13)起,个人 tap 执行
brew style也会报Casks must use postflight_steps instead of postflight。这只在你运行brew style时出现,不阻止安装,所以第三方仓库可以继续发布旧写法。 -
Redis 官方 tap
redis/redis的 cask(当前 8.10.2)就是这样:仍使用postflight do和uninstall_postflight do,里面用FileUtils、Dir、File复制redis.conf、链接bin和模块。*_steps不能写这种任意 Ruby。对本机Casks/redis.rb执行brew style能看到Cask/InstallSteps,brew install --cask redis仍会执行这些块。 -
官方仍推荐
*_steps,就是为了安装时不执行 Cask 文件里的任意 Ruby。Homebrew 4.0 起,官方homebrew/cask默认不在本机执行那个.rb,安装用的是 formulae.brew.sh 上预先生成并签名的 JSON。mkdir_p、copy、set_permissions、run是固定动作,参数是字面量,能原样放进 JSON,安装时在沙箱里按清单执行。FileUtils、Dir、if、循环写不进这份清单,不执行就看不出这段 Ruby 会做什么,所以官方包不能把可执行代码放进安装流程,brew style直接拒绝。 -
redis/redis不在官方 tap 里,安装时仍会加载本地Casks/redis.rb并执行postflight do,所以还能用旧写法。这种写法进不了homebrew/cask。 -
能用步骤表达的逻辑写
*_steps。必须写任意 Ruby 时,个人 tap 仍可使用旧块,staged_path在安装时是真实的Pathname。提交到官方homebrew/cask会被brew style拒绝。
1 | # 个人 tap 里仍可安装;brew style 会报 Cask/InstallSteps |
按系统和架构提供不同下载地址
-
很多应用按操作系统和 CPU 各提供一个包。
arch、os、sha256的键都是 Homebrew 规定的,不能改名。安装时只看当前这一台机器:用选中的字符串拼url,再用对应的那一个sha256比对,另外三组不参与这次安装。
1 | cask "hello-cask" do |
-
同样的区分也可以写成
on_macos、on_linux、on_arm、on_intel:安装时只执行匹配当前机器的那一支,里面直接写该平台的url和sha256。文末 Reasonix 的下载地址就是这样写的。 -
on_*是条件块,分支里可以放任意 stanza(上面说的那些配置声明),因此还能表达更复杂的差异:各架构version不同、安装产物不同、按 macOS 版本(如on_sonoma、on_tahoe)分叉,或某段安装后逻辑只在一个系统上执行。只是文件名和校验值不同时,用上面的arch/os更短。
token 命名要点
-
个人 tap:文件名去掉
.rb必须等于cask "..."里的字符串,用户安装时也敲这个名字。这是硬约束。brew generate-cask-token只是帮你起名,可以不用。 -
若向官方
homebrew/cask投稿,才建议用它生成 token,规则是:取.app名字 → 去掉.app→ 小写 → 空格变-。它不会拆驼峰:HelloCask→hellocask,Hello Cask→hello-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.rb再brew reinstall --cask hello-cask就会用新脚本。
1 | brew edit --cask hanqunfeng/hello_cask/hello-cask |
以 Reasonix 为例,说明脚本配置项
-
这是一个只有命令文件、没有
.app的 Cask:按系统/架构下载对应tar.gz,用binary "reasonix"链到bin。说明 Cask 完全可以用来分发预编译 CLI,本机只会走其中一条on_*分支。
1 | # token:文件名、header、安装名必须一致,用户执行 brew install --cask reasonix |
以 Redis 为例,说明旧的 postflight
-
个人和第三方 tap 安装时仍会执行
.rb,所以旧的postflight do、uninstall_postflight do还能用。下面是 redis/homebrew-redis 8.10.2 的脚本。brew style会报Cask/InstallSteps,brew install仍会执行这些块。官方homebrew/cask不能这样写。
1 | # token、文件名、安装名都是 redis |
后记
-
更完整的 Cask DSL 见官方文档:Cask Cookbook、Acceptable Casks
-
学习现有写法:
brew edit --cask firefox(官方 cask 会先 clonehomebrew/cask) -
创建 Formula 见 brew -- 创建自己的 Formula