Homebrew
2 / 6
系列目录
MacOS软件包管理器--brew
brew -- 创建自己的 Formula
brew -- 创建自己的 Cask
brew 升级 cmake 失败:clang++ 找不到 iostream
brew install 或 upgrade 下载缓慢时,手动下载安装包
brew -- 用 brew-publish-formula 发布 Formula
摘要
brew 是一个软件包管理器,同时支持MacOS和Linux,可以很方便地安装各种软件,比如git、node、python等。
本文介绍如何在macos下创建自己的 Formula
本文基于 MacOS 15.7.2,brew 版本为 Homebrew 5.0.3。
关于 brew 的安装及使用可以参考 MacOS软件包管理器--brew
从一个简单示例开始
创建一个Github仓库,用于存储 color_echo 命令
1 2 3 4 5 mkdir color_echo_dircd color_echo_dirbin/color_echo
1 2 3 4 5 6 7 8 9 10 11 git init git add . git commit -m "Initial commit" git branch -M main git remote add origin https://github.com/hanqunfeng/color_echo.git git push -u origin main git tag v1.0.0 git push --tags
1 2 3 4 wget https://github.com/hanqunfeng/color_echo/archive/refs/tags/v1.0.0.tar.gz shasum -a 256 v1.0.0.tar.gz 9450952a4b477c83ea2d7e28386d6ae38132bf68c46746aa218c03c21aa75f6d v1.0.0.tar.gz
发布 color_echo
创建 tap 仓库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 brew tap-new hanqunfeng/color_echo 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-color_echo/.git/ [main (root-commit) 35d602b] Create hanqunfeng/color_echo tap 3 files changed, 107 insertions(+) create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/tests.yml create mode 100644 README.md ==> Created hanqunfeng/color_echo /usr/local/Homebrew/Library/Taps/hanqunfeng/homebrew-color_echo When a pull request making changes to a formula (or formulae) becomes green (all checks passed), then you can publish the built bottles. To do so, label your PR as `pr-pull` and the workflow will be triggered.
1 2 cd /usr/local/Homebrew/Library/Taps/hanqunfeng/homebrew-color_echo/Formulatouch color_echo.rb
写入内容(模板):模板格式后面会详细介绍
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class ColorEcho < Formula desc "Print colorful text in terminal" homepage "https://github.com/hanqunfeng/color_echo" url "https://github.com/hanqunfeng/color_echo/archive/refs/tags/v1.0.0.tar.gz" sha256 "9450952a4b477c83ea2d7e28386d6ae38132bf68c46746aa218c03c21aa75f6d" license "MIT" def install bin.install "bin/color_echo" end test do system "#{bin} /color_echo" , "--help" end end
【推荐】可以通过 brew create 命令快速创建模板。它会下载 tag、自动填 sha256,再打开编辑器,根据需要修改生成的模板文件。
1 2 3 brew create https://github.com/hanqunfeng/color_echo/archive/refs/tags/v1.0.0.tar.gz \ --tap hanqunfeng/color_echo \ --set-name color_echo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 cd $(brew --repo hanqunfeng/color_echo)git add . git commit -m "Add color_echo 1.0.0" git remote add origin https://github.com/hanqunfeng/homebrew-color_echo.git git push -u origin main Enumerating objects: 11, done . Counting objects: 100% (11/11), done . Delta compression using up to 12 threads Compressing objects: 100% (9/9), done . Writing objects: 100% (11/11), 2.11 KiB | 2.11 MiB/s, done . Total 11 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) To https://github.com/hanqunfeng/homebrew-color_echo.git ! [remote rejected] main -> main (refusing to allow a Personal Access Token to create or update workflow `.github/workflows/publish.yml` without `workflow` scope) error: failed to push some refs to 'https://github.com/hanqunfeng/homebrew-color_echo.git'
报错是因为这次推送改了 .github/workflows/publish.yml,当前令牌没有 workflow 权限。远程地址保持 https://github.com/hanqunfeng/homebrew-color_echo.git,不要把令牌写进地址。
用的是已经保存的那张经典令牌时,在 GitHub 上给它补上 repo 和 workflow。令牌字符串不变,钥匙串里不用换,直接再推一次。细粒度令牌则给这个仓库 Contents 的 write 和 Workflows 的 write。勾选步骤见 创建 GitHub Personal Access Token 。
另建了一张新令牌时,把凭据助手里 github.com 的旧令牌换成新的,再推送。助手怎么更新见 Git 的 HTTPS 凭据和 SSH 密钥 。
1 2 3 4 5 6 7 8 9 10 11 git push -u origin main Enumerating objects: 11, done . Counting objects: 100% (11/11), done . Delta compression using up to 12 threads Compressing objects: 100% (9/9), done . Writing objects: 100% (11/11), 2.11 KiB | 2.11 MiB/s, done . Total 11 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) To https://github.com/hanqunfeng/homebrew-color_echo.git * [new branch] main -> main branch 'main' set up to track 'origin/main' .
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 brew tap hanqunfeng/color_echo brew search color_echo ==> Formulae hanqunfeng/color_echo/color_echo color-code brew search hanqunfeng/color_echo/color_echo ==> Formulae color_echo brew install color_echo ==> Fetching downloads for : color_echo ✔︎ Formula color_echo (1.0.0) [Verifying 2.1KB/ 2.1KB] ==> Installing color_echo from hanqunfeng/color_echo 🍺 /usr/local/Cellar/color_echo/1.0.0: 4 files, 7.4KB, built in 5 seconds ==> Running `brew cleanup color_echo`... Disable this behaviour by setting `HOMEBREW_NO_INSTALL_CLEANUP=1`. Hide these hints with `HOMEBREW_NO_ENV_HINTS=1` (see `man brew`).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 color_echo --help 用法: color_echo [参数] 内容 参数说明: -c, --color <color> 设置前景色(文字颜色) 可选颜色: black red green yellow blue magenta cyan white -b, --bg <color> 设置背景色 可选颜色: black red green yellow blue magenta cyan white --bold 加粗字体 --underline 下划线 --italic 斜体字体(仅部分终端,Terminal 不支持,iTerm2 支持) -n 不换行输出,兼容 echo 的 -n 参数 -h, --help 显示帮助信息 示例: color_echo --color green "Hello World" color_echo -c red -b yellow --bold "Error Message" color_echo --underline "This is underlined"
发布新版本
color_echo 仓库打了新 tag 之后,要改 tap 里的 Formula,再提交到 GitHub。别人 brew update 之后才会装到新版本。brew livecheck 只报告有没有新版本,不会改文件。brew create 只用于第一次生成,文件已经存在时不能拿来升级。
可以手工改 Formula/color_echo.rb。这里的 url 写死了 v1.0.0,把地址里的版本和 sha256 换成新 tag 的值。sha256 用 shasum -a 256 计算,和第一次发布时一样。如果 url 里写的是 #{version},地址会跟着变,只改 version 和 sha256。
也可以用命令改。--write-only 会下载新 tag、算出 sha256、写回本地 .rb,不会 fork,也不会开拉取请求。不加 --write-only 时,这条命令会按官方仓库的流程尝试开拉取请求。
两种改法最后都要在 tap 仓库里提交并推送。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 brew bump-formula-pr --write-only \ --url=https://github.com/hanqunfeng/color_echo/archive/refs/tags/v1.0.1.tar.gz \ hanqunfeng/color_echo/color_echo cd "$(brew --repo hanqunfeng/color_echo) " git diff git add Formula/color_echo.rb git commit -m "color_echo 1.0.1" git push
1 2 3 4 class ColorEcho < Formula end
1 2 3 4 5 desc "Print colorful text in terminal" homepage "https://github.com/hanqunfeng/color_echo" url "https://github.com/hanqunfeng/color_echo/archive/refs/tags/v1.0.0.tar.gz" sha256 "9450952a4b477c83ea2d7e28386d6ae38132bf68c46746aa218c03c21aa75f6d" license "MIT"
1 2 3 4 5 6 7 8 9 def install bin.install "bin/color_echo" end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def install libexec.install "springboot" libexec.install "lib" libexec.install "completions" libexec.install "assets" (libexec/"deps-cache" ).mkpath chmod 0755 , libexec/"springboot" Dir [libexec/"lib/*.sh" ].each { |f | chmod 0755 , f } end
install 里还可以直接用 Ruby 标准库。Dir、File、Pathname 是类,FileUtils 是模块。Formula 已经 include FileUtils,所以 chmod、cp、mv、rm_rf、mkdir_p 不用写 FileUtils. 前缀,也不用再 require
bin、libexec、prefix、lib、share、bash_completion、zsh_completion 不是类名,是 Formula 的方法,返回值是 Pathname
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 def install libexec.install "lib" Dir [libexec/"lib/*.sh" ] Dir .glob(libexec/"lib/**/*.sh" ) Dir .exist?(libexec/"lib" ) Dir .children(libexec/"lib" ) Dir .mkdir(libexec/"cache" ) (libexec/"lib" ).glob("*.sh" ).each { |f | chmod 0755 , f } (libexec/"deps-cache" ).mkpath (libexec/"VERSION" ).write "1.0.0\n" File .exist?(libexec/"springboot" ) File .chmod(0755 , libexec/"springboot" ) chmod 0755 , libexec/"springboot" chmod 0755 , Dir [libexec/"lib/*.sh" ] cp libexec/"springboot" , bin/"springboot" rm_rf libexec/"cache" mkdir libexec/"cache" do touch "keep" end end
1 2 3 4 5 6 7 8 9 10 11 12 def install (bin/"springboot" ).write <<~EOS #!/usr/bin/env bash exec "#{libexec} /springboot" "$@" EOS chmod 0755 , bin/"springboot" end
1 2 3 4 5 6 def install bash_completion.install libexec/"completions/springboot.bash" => "springboot" zsh_completion.install libexec/"completions/_springboot" end
1 2 3 4 5 6 7 8 9 10 test do system "#{bin} /color_echo" , "--help" end
#{bin}: 当前这个 formula 的「安装目录里的 bin 目录」,不同平台会不同
系统 / 架构
实际路径示例
Intel Mac
/usr/local/Cellar/color_echo/1.0.0/bin
Apple Silicon
/opt/homebrew/Cellar/color_echo/1.0.0/bin
Linuxbrew
/home/linuxbrew/.linuxbrew/Cellar/.../bin
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 depends_on "jq" depends_on "jq" => :build depends_on "jq" => :test depends_on "jq" => [:build , :test ] depends_on macos: :sonoma depends_on arch: :x86_64 depends_on xcode: ["9.3" , :build ] depends_on "hanqunfeng/color_echo/color_echo"
1 2 3 4 5 6 7 def caveats <<~EOS python3 is required but NOT installed by this formula. brew install python@3.13 EOS end
安装完成之后还要在用户机器上再做的事,写 post_install_steps,不要写 def post_install。它和 brew -- 创建自己的 Cask 里的 postflight_steps 是同一类限制:步骤会放进 JSON API,brew postinstall <formula> 可以单独再跑一遍,块里只能调用规定好的步骤,不能写 Dir、File、if、循环。
def install 没有被这套步骤替换,官方 Formula 里也仍然是任意 Ruby。普通 brew install 拿到的是已经编好的 bottle,不会在你机器上再执行一遍 install。post_install 不同,bottle 倒进本机之后还要执行,所以不能带上无法审查的代码。
Homebrew 6.0.16 (2026-08-10)起,官方 tap 的检查拒绝旧的 def post_install。7.0.0 (2026-09-13)起,所有 tap 跑 brew style / brew audit 都会报 Formulae must use post_install_steps instead of post_install。第三方 tap 在迁移期间加载旧方法还能执行,但已经废弃。同一个 Formula 里不能同时写 post_install 和 post_install_steps。
和 Cask 不同,Formula 的步骤路径要显式写 base:,例如 :var、:etc、:prefix。安装时才知道的路径用 {{HOMEBREW_PREFIX}},不要用 #{prefix}:#{...} 在读取 Formula 时就算完了。
1 2 3 4 5 6 7 8 9 post_install_steps do mkdir_p "log/foo" , base: :var unless _path_exists "foo.conf" , base: :etc do write_file "foo.conf" , "prefix = {{HOMEBREW_PREFIX}}\n" , base: :etc end end
查上游有没有新版本用 livecheck,再执行 brew livecheck <formula>。它只比较版本号,不下载、不安装。原理是:请求 url 的页面,用 regex 扫描全文,圆括号里捕获的那段才是版本号 ;匹配到多个时取得最大的一个,和脚本里的 version 比较。
这个命令不走 JSON API,只读本机 tap 里的 .rb。Homebrew 4.0 之后官方 homebrew/core 默认不 clone 到本地,所以直接查 git 会报 These formulae are not in any locally installed taps!。要先 brew tap homebrew/core,并且脚本里写了 livecheck,命令才能按这段配置查出新版本。没写 livecheck 时,它会改去试 url 和 homepage;地址如果只是某一个压缩包,页面上没有版本列表,就查不出更新。skip 则明确不请求。
调试时看它请求了哪个地址、匹配到哪些版本:brew livecheck --debug <formula>。Cask 的写法见 brew -- 创建自己的 Cask 。
1 2 3 4 5 6 7 8 9 10 11 12 livecheck do url "https://mirrors.edge.kernel.org/pub/software/scm/git/" regex(/href=.*?git[._-]v?(\d+(?:\.\d+)+)\.t/i ) end livecheck do skip "Auto-generated on release." end
一个真实案例
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 53 54 55 56 57 58 59 60 61 62 63 class JavaProjectGenerator < Formula desc "Generate Spring Boot projects quickly with shell scripts" homepage "https://github.com/hanqunfeng/java-project-generator" url "https://github.com/hanqunfeng/java-project-generator/archive/refs/tags/v1.0.9.tar.gz" sha256 "d648d8ab75cc2b961c19c1cb9c281afbbfda6b21c767b2c607cfbb52ce24ca70" license "MIT" depends_on "glow" depends_on "pandoc" def install libexec.install "springboot" libexec.install "lib" libexec.install "completions" libexec.install "assets" (libexec/"deps-cache" ).mkpath chmod 0755 , libexec/"springboot" Dir [libexec/"lib/*.sh" ].each { |f | chmod 0755 , f } (bin/"springboot" ).write <<~EOS #!/usr/bin/env bash exec "#{libexec} /springboot" "$@" EOS chmod 0755 , bin/"springboot" bash_completion.install libexec/"completions/springboot.bash" => "springboot" zsh_completion.install libexec/"completions/_springboot" end def caveats <<~EOS python3 is required but NOT installed by this formula. Install before use: brew install python@3.13 Needed for: deps list cache, --deps validation, and other Initializr metadata parsing. Optional tools (not installed by this formula): - jq: brew install jq - libxml2 (xmllint): brew install libxml2 - xmlstarlet: brew install xmlstarlet - mvn: brew install maven xmllint / xmlstarlet / mvn: only for Maven "module add" parent POM parsing (awk fallback still works). Network access to Spring Initializr is required (default: https://start.spring.io). Override with: export INITIALIZR_BASE_URL=<mirror-url> EOS end test do assert_match "springboot <command>" , shell_output("#{bin} /springboot --help" ) assert_match "Dry-run 模式" , shell_output("#{bin} /springboot create --name=brewtest --dry-run" ) end end
后记