ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

BrewUI:给Homebrew配一副眼镜,让macOS包管理更直观

BrewUI:给Homebrew配一副眼镜,让macOS包管理更直观 做了这么多年 macOS 下的包管理我算是从 Homebrew 命令行一路走过来的老用户。但真正让我下定决心折腾 BrewUI 这个东西的是一次倒霉的brew upgrade——那天手一抖升级了某个依赖库整个本地开发环境直接崩掉brew list刷出来几百个包我盯着终端里密密麻麻的列表找了半天才定位到问题根源。那一刻我意识到包管理这件事只靠命令行是真的会出人命的。BrewUI 就是我在这种背景下持续使用并且一直参与折腾的一个 Homebrew 图形化工具。简单说它把 brew 的搜索、安装、升级、卸载、依赖分析、服务管理这些操作全部搬到了 GUI 里让你不用再背命令参数也能把几百个包理得清清楚楚。这篇文章不打算写官方案例我想聊聊这个工具解决了哪些痛点、背后是怎么跟 Homebrew 交互的、实际用下来有哪些坑以及如果你也想做一个类似的工具值得注意的地方是什么。适合正在用 Homebrew 管理开发环境、还没找到顺手图形方案的人也适合对 macOS 开发工具有一定好奇心的朋友。1. 为什么我最终决定给 brew 套一层 UI1.1 命令行 Homebrew 的“能力”与“无能”Homebrew 的强项毋庸置疑一条brew install xxx就能搞定绝大多数开源软件的安装、依赖解析、版本管理。但它的弱点也很明显越用越明显信息过度密集。brew list输出的是纯文本列表几百个包名挤在一起你想知道某个包是什么时候装的、现在是什么版本、被哪些包依赖单靠命令要绕好几圈。依赖关系不直观。brew deps --tree能打印依赖树但在终端里看一棵几十层的树眼睛基本是废的。升级风险难以预判。brew upgrade会一次性更新所有过期包但你根本不知道这次升级会影响多少其他包、会不会破坏当前正在跑的服务。服务管理割裂。brew services start mysql和brew services list是另外一套操作逻辑跟包管理本身是分家的。这些痛点叠加起来让我开始寻找一个能把这些信息“可视化”的解决方案。用了一段时间各种工具后我发现市面上已有的图形客户端要么年久失修、要么只覆盖了搜索和安装对依赖分析和服务管理支持得很弱。所以当 BrewUI 出现时我几乎是带着“终于有人做对了”的心情开始用的。1.2 BrewUI 的定位不是替代 brew而是给 brew 配一副眼镜BrewUI 的核心设计思路不是把命令行藏起来而是让命令行里那些结构化但难以一眼读懂的信息变得直观。它本质上是一个 Homebrew 的 GUI 前端底层调用的依然是 brew 命令本身。这意味着你对 brew 命令行的理解不会白费BrewUI 的操作最终都会映射回对应的 brew 命令Homebrew 更新了什么行为brew 支持了什么新参数BrewUI 只要跟进就能同步命令行能做的事情BrewUI 基本都能做但它把“做之前要知道什么”这件事替你提前想好了。我用了一个比较形象的比喻来理解它Homebrew 是一台性能强劲但仪表盘简陋的跑车BrewUI 是后装的中控屏幕。你不能说中控屏能提升发动机马力但它能让你在驾驶时更清晰地知道转速、油温和胎压从而避免把引擎踩爆。1.3 什么时候用命令行什么时候用界面很多人会问既然 BrewUI 能做的事情命令行都能做那我为什么不直接学命令行我的答案是用界面做“决策”用命令行做“操作”。对于“我现在有几个包需要升级”“升级这个包会影响什么”“哪个服务还在后台运行”这类需要全局视角的问题GUI 的效率远高于命令行。而对于“快给我装个 nginx”这种已知明确的执行操作命令行依然是最高效的路径。两件事分开处理你会同时获得两边的优点。BrewUI 对我来说不是替代品而是补足了 Homebrew 生态里缺失的那块拼图。这也是后面我想详细展开每一个功能模块的原因因为它每一个模块都对应着我实际踩过的坑。2. 安装与首次启动环境检查和目录权限别轻视2.1 安装 BrewUI 的前提条件BrewUI 的运行环境并不复杂但要强调几个前提缺了哪一个都会在后续使用中出问题macOS 版本BrewUI 使用 SwiftUI 构建对系统版本有最低要求。在我使用的版本上macOS 12 Monterey 以上基本都能正常跑。Homebrew 必须已经安装这听起来是废话但确实是新人最容易踩的坑——装好 BrewUI 后打开发现找不到 brew。BrewUI 本身不管安装 Homebrew 这件事它只是一个前端。Xcode Command Line Tools虽然不是 BrewUI 直接需要但 brew 在编译安装某些包时需要调用编译器。如果没装 Command Line Tools你在 BrewUI 里安装某些包时会看到莫名其妙的报错其实根因是缺少编译环境。判断 Homebrew 是否可用终端里执行which brew brew --version如果输出正常说明 brew 本身已经就绪。如果which brew没有输出需要先安装 Homebrew再回来装 BrewUI。2.2 Intel 与 Apple Silicon 的目录差异这是首次启动最容易被忽略、也最容易导致 BrewUI “什么都搜不到”的问题。Intel 芯片的 Mac 上Homebrew 默认安装在/usr/local目录下Apple Silicon 芯片的 Mac 上默认安装在/opt/homebrew目录下。也就是说Intel/usr/local/bin/brewApple Silicon/opt/homebrew/bin/brewBrewUI 首次启动时会尝试通过执行which brew或默认路径来定位 brew 可执行文件。如果你用的是 Apple Silicon且之前为了兼容某些工具手动改过路径BrewUI 可能找不到 brew。遇到这种情况可以在 BrewUI 的设置界面手动指定 brew 的绝对路径/opt/homebrew/bin/brew或者先在终端里执行which brew拿到你自己的实际路径再填进去。这个步骤虽然简单但很多人第一次打开 BrewUI 发现“转圈什么都没加载出来”八成就是栽在这里。2.3 首次启动的初始化流程BrewUI 首次启动时会做几件事了解这个流程以后排查问题会快得多探测 Homebrew 路径拿不到就报错并提示手动配置。加载本地包列表执行brew list --formula和brew list --cask把已安装的包分成“命令行工具”和“图形应用”两大类。检查更新状态执行brew update或读取本地更新时间戳在界面上显示“数据库距今已多久未更新”。读取服务列表执行brew services list判断哪些服务在运行。这个过程如果卡住通常不是 BrewUI 的问题而是 brew 本身在执行命令时比较慢。第一次启动时brew 可能还需要自动更新仓库索引耐心等一到两分钟是正常的。如果超过五分钟还没加载出来基本可以判断是网络问题或 brew 本身卡死了需要回到终端去跑一遍brew update验证。3. 核心功能拆解从搜索到清理的完整包管理3.1 搜索与信息查看把 brew info 变成人类能读的东西命令行里执行brew search nginx输出的是一长串包名你分不清哪些是官方仓库的、哪些是第三方 tap 里的、哪些已经装了。BrewUI 的搜索框则把结果分类展示每一行除了包名还直接显示一句话描述、当前安装状态、所属仓库一眼扫过去就知道该不该点开看详情。点进详情页后BrewUI 显示的信息对应的是brew info的完整输出但排版友好得多版本号依赖项Depends on被哪些包依赖Used by是否已安装、安装位置是否有更新的版本描述、主页地址许可证类型其中“被哪些包依赖”这个信息我觉得是最有价值的。命令行里想查这个需要执行brew uses --installed nginx但在 BrewUI 里它是详情页自带的一个 Tab点一下就知道如果你卸载了这个包会有哪些包跟着遭殃。这种信息在指挥升级和卸载时作用极大。3.2 安装与批量升级从失控到可控在命令行里安装一个包就是一条命令但升级所有包是一条稍微危险的命令brew upgrade危险来自不确定性。你只知道有一批包有新版本但你不知道这一批里面有没有哪些升级会引入不兼容的改动。BrewUI 的做法是把决策权交还给你主界面直接列出所有过期outdated的包并显示当前版本与最新版本每个包旁边标出“本次升级涉及哪些依赖变化”你可以勾选想要升级的包单独升级或者全选后一键升级执行升级时BrewUI 会把实时日志拉到一个面板里不用切到终端也能看到输出。单独选择升级目标这个能力帮我避免过一次事故。当时我有一堆包都过期了其中有一个icu4c的 major 版本升级。如果直接brew upgrade那个包会连带升级而它关联的 PHP 扩展当时还不兼容新版。我在 BrewUI 里看到它列在升级清单里顺手把它的勾选取消掉只升级其他包整个开发环境稳稳当当没有重演之前那次崩溃。3.3 卸载与自动清理磁盘空间的隐形管家卸载在命令行里很简单brew uninstall xxx但很多人不知道卸载某些包之后它曾经拉进来的依赖并不会自动删除日积月累会占用大量磁盘空间。命令行里处理这件事的方式是brew autoremove brew cleanup --pruneallBrewUI 把这三件事合并成了一个界面你选择卸载一个包它会分析“这个包被卸载后有哪些依赖不再被任何包使用”并询问你是否一并清理。主界面上还会显示一个“可清理空间”的估算值点击即可执行清理。我有一次在这上面发现本地缓存占了将近 4GB都是以前下载过的旧版本安装包一键清理后整个人神清气爽。如果你平时已经好几个月没做过 cleanup去 BrewUI 的清理页面看一眼大概率会有惊喜。3.4 命令操作与 BrewUI 操作对照操作命令行方式BrewUI 方式搜索包brew search name搜索框输入关键词分类展示查看包详情brew info name详情页标签式展示查看已安装列表brew list --formula/--cask侧边栏分类列表查看过期包brew outdated主界面自动汇总升级单个包brew upgrade name勾选后执行升级升级所有包brew upgrade全选后一键执行卸载包brew uninstall name卸载并提示清理依赖查看依赖树brew deps --tree name依赖关系可视化视图清理缓存brew cleanup --pruneall一键清理并显示释放空间这张表基本可以当作一个速查手册记住一组就够用了。4. 依赖关系与服务管理两个容易被忽视的高频场景4.1 依赖树可视化当 brew deps 变成一张图命令行里的brew deps --tree长什么样呢我给你描述一下wget ├── ca-certificates ├── libidn2 │ ├── gettext │ └── libunistring ├── libunistring └── openssl3 └── ca-certificates几层还能看一旦依赖嵌套超过 5 层整个终端就会被树状字符填满交互上基本没法用。BrewUI 里则把它画成了可展开的依赖图点击某个依赖节点可以直接跳转到那个包的详情页再做反向追踪。依赖图最有价值的场景是“卸载前风险评估”。比如你想卸载libxml2依赖图上可以直接看到哪些包在依赖它如果这些包都是你已经不需要的东西那卸载后可以连坐清理。如果依赖它的包全是正在用的核心工具那你要么不卸载要么先升级那些工具到不依赖旧版的状态。4.2 brew services 的可视化与日志入口Homebrew 的 services 命令其实是个经常被低估的功能brew services list brew services start mysql brew services stop mysql brew services restart mysql它管理的是以launchd方式注册的后台服务。命令行用起来不难但状态展示比较原始——运行中的会显示started没运行的显示none仅此而已。你没法直观看到服务日志也没法知道服务最近一次启动是什么时候。BrewUI 把服务管理做成了独立面板所有服务列表运行状态用颜色和图标标出点击启动、停止、重启不需要手敲命令每个服务旁边能直接打开日志文件路径如果某个服务启动失败BrewUI 会提示查看错误日志并且提供一个快速跳转按钮。这对日常维护本地开发环境的人来说非常实用。比如你的 MySQL 某个早上突然连不上打开 BrewUI 的服务面板发现状态是红点点一下日志入口直接看到报错是磁盘空间满了整个过程一分钟不到。换成命令行你要先brew services list看状态再手动找到日志文件路径用 tail 去看多出好几步。4.3 一个典型场景维护本地 PHP/MySQL 环境我身边不少人的本地环境都是 Nginx PHP MySQL 的组合管理起来要记下面这些服务名和状态brew services start nginx brew services start php brew services start mysql周末想关掉它们又要分别 stop 三个服务。这个过程容易忘且不容易察觉哪个服务没关掉。用 BrewUI 的话服务面板三个服务排在一起状态一眼看清一次性全部关掉不会漏。有一次我排查一个端口冲突问题发现 8080 被占用直接在 BrewUI 的服务面板看到 php-fpm 还在跑而 nginx 已经停了于是发现问题根源是残留的 php-fpm 进程。这种场景下能同时看到“哪些服务在跑”和“端口占用情况”的图形化界面确实节省了排查时间。5. 从自己实现 BrewUI 的过程中学到的技术细节如果你不是一个纯使用者而是对“BrewUI 这种工具到底怎么做到的”感兴趣这一节是给你准备的。5.1 为什么选择调用 brew 命令而非直接读库一个很自然的想法是Homebrew 的数据都存在本地目录里我能不能绕过命令直接读数据库文件来获取包信息技术上可行但不推荐这么做。原因在于Homebrew 的安装目录结构和数据格式是内部实现没有公开的稳定 APIHomebrew 版本更新时这些结构可能随时变化安装、卸载、升级等写入操作如果绕过 brew 自己去做极易把环境搞坏Homebrew 自己有一套锁定机制并发的多进程操作会互相等待自己写一套很容易踩坑。所以 BrewUI 的正确做法是把 brew 命令行当作一个“不受控但可靠”的底层接口像调用外部服务一样调用它。也就是说brew 是后端BrewUI 是前端两者之间用命令和输出做通信协议。5.2 与 brew 交互的核心代码用 Process 执行命令在 Swift 里调用 brew 命令核心是Process类。下面这段代码是 BrewUI 里执行brew list --formula并获取输出的典型方式import Foundation func runBrewCommand(arguments: [String]) async throws - String { let process Process() let pipe Pipe() process.executableURL URL(fileURLWithPath: /usr/bin/env) process.arguments [brew] arguments process.standardOutput pipe process.standardError pipe try process.run() process.waitUntilExit() let data pipe.fileHandleForReading.readDataToEndOfFile() return String(data: data, encoding: .utf8) ?? }这段代码足够完成最简单的调用。但实际工程里还需要处理几个细节用/usr/bin/env brew而不是硬编码/usr/local/bin/brew这样才能兼容 Intel 和 Apple Silicon 两种路径把 stderr 也收集起来因为部分错误信息走 stderr如果不收集你将无法看到报错不要用阻塞式waitUntilExit()处理长时间任务比如brew upgrade可能持续几分钟应该用异步方式或放在后台队列避免卡死 UI。5.3 关键在于 JSON 输出而非文本解析如果你一开始就用NSString直接解析brew list的文本输出你会很快崩溃——因为文本格式随 Homebrew 版本变化还会跟进度条、警告信息混在一起。BrewUI 走的是另一条路尽量让 brew 输出结构化数据。Homebrew 官方提供了一组支持 JSON 输出的子命令最常用的有brew info --jsonv2 --formula name brew info --jsonv2 --cask name brew list --formula --jsonv2 brew outdated --jsonv2这些命令输出的 JSON 字段足够完整包含版本号、依赖项、冲突项、安装日期等关键信息。BrewUI 把这些 JSON 解码成 Swift 的Codable模型然后直接驱动界面渲染。比如brew list --formula --jsonv2返回的结构里每一个 formula 对象包含name、versions、dependencies、installed等字段BrewUI 把这些映射到详情页上的每一个区块。这种方式的好处是只要 Homebrew 认这些 JSON 输出你的解析就是稳定的不需要为了显示一个字段去正则匹配文本数据结构化以后排序、过滤、搜索都变得非常简单。5.4 状态同步与增量刷新用 GUI 包住命令行工具最容易犯的一个错误是每次点击都重新执行一次完整命令并全量刷新界面。在包数量少的时候没问题但当你机器上有三四百个包时每次brew list --json的耗时是不可忽略的。我的经验是做一个三层状态机制启动时全量加载获取完整的包列表、服务状态、更新状态。操作后局部刷新比如只升级了 nginx那么执行完升级命令后只刷新 nginx 所在列表项的状态不重新加载全部。后台定时增量检查每隔一段时间执行一次brew outdated --json仅在有过时包时刷新右上角角标并发送系统通知。这里涉及到 UI 层与命令行执行层的并发问题。SwiftUI 的视图状态更新必须回到主线程而 brew 命令执行应该在后台队列。如果直接把后台进程的返回结果赋值给 UI 状态SwiftUI 会直接崩溃或给出警告需要把结果封装好再调度到主线程更新。另一个并发坑是brew 本身不支持无锁并发命令多个 brew 进程同时跑会出现 “Another active Homebrew process is already in progress” 的报错。因此 BrewUI 内部需要做一个串行队列保证同时只有一个 brew 命令在执行其余命令排队等待。5.5 本地数据库为什么需要缓存brew 的 JSON 输出虽然方便但每次执行都要启动一个 Ruby 进程耗时大概在几百毫秒到几秒不等。如果用户只是翻看列表每次都实时调用 brew 会显得界面一点都不跟手。BrewUI 会在每次成功获取数据后把关键信息写入本地数据库缓存用的 SQLite。这样用户浏览列表时优先展示缓存数据秒开后台刷新时拿最新结果对比缓存有差异才更新 UI搜索功能可以基于缓存做本地过滤不需要实时调用 brew。这个设计和绝大多数开发工具做离线缓存是一个思路。6. 实测踩坑记录权限、路径、网络与版本冲突6.1 权限问题为什么 BrewUI 装 cask 应用时偶尔会失败brew install --cask google-chrome这种安装图形应用的操作如果目标目录比如/Applications当前用户没有写权限brew 会提示需要输入管理员密码。BrewUI 作为 GUI 程序在执行这类命令时不会弹出终端让你输密码所以处理方式上需要特殊设计。我的项目里采用的方案是检测到 brew 输出里包含Error: Permission denied相关关键字的用AuthorizationServices以管理员权限重新执行命令或者提示用户在系统设置里给对应目录授权。对普通使用者来说遇到 cask 安装失败最快的自检路径就是先去系统设置确认你对 /Applications 有写权限或者确认你的账号是管理员账号。别先去怀疑 BrewUI 出了问题。6.2 PATH 问题GUI App 为什么会找不到 brew命令行里运行的这串东西export PATH/opt/homebrew/bin:$PATH生命周期只限于你的终端会话。也就是说你在.zshrc里配置的 PATH对从 Dock 启动的 GUI App 是无效的。这个特性让我调试了很久。现象是BrewUI 安装好后所有 brew 命令调用都会报 “-bash: brew: command not found但终端里执行 brew 完全正常。后来我在代码里加了日志打印出/usr/bin/env传到子进程里的环境变量发现 GUI 环境下确实找不到/opt/homebrew/bin在 PATH 中。解决办法很直接BrewUI 在调用 brew 的时候不依赖宿主的环境变量而是把 brew 的绝对路径拼接进PATH再传给子进程环境process.environment [ PATH: /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin ]这样就能避免 PATH 缺失导致的command not found。如果你是其他 GUI 工具遇到类似问题排查思路也一样——先确认你启动的那个环境里到底有没有需要的路径。6.3 网络问题与 update 卡死BrewUI 的更新检查功能会执行brew update这个命令会拉取远程仓库的更新。网络不稳定或代理配置异常时命令可能长时间挂住无响应。这个坑的特点在于它不是报错而是无声地卡住。BrewUI 界面上表现为所有提示都在转圈数据始终加载不出来。排查链路是先看 BrewUI 的日志确认卡在哪个命令上发现是brew update卡住后去终端手动执行brew update确认网络连通性如果你的网络连接有特殊配置需要确认命令行能正常访问仓库地址排空问题后给 BrewUI 设置里加一个“跳过自动更新”的开关让用户手动触发更新。后来我在代码里加了超时控制brew update超过 60 秒没有返回就终止进程并提示用户手动去终端执行。这个问题对我来说是一个好提醒任何命令行工具包装成 GUI 时都必须考虑慢操作和卡死操作的兜底策略而不是把进程挂在那里。6.4 大版本升级冲突升级 vs 不升级的抉择Homebrew 里最危险的操作不是卸载而是 upgrade。尤其是 major 版本升级比如openssl3从 3.0 升到 3.1或者 Python 从 3.11 升到 3.12可能导致某些依赖旧版的包连锁报错。在命令行里brew upgrade会一股脑全升上去发现问题再去 fix可能已经晚了。BrewUI 里我习惯的做法是升级前先看这个包有没有依赖它的包存在用 detail 页面的 “Used by” 列表如果依赖它的包是当前项目明确需要的我就先升级依赖它的包确保新版本兼容后再升级底层库如果没把握就只升级次要包把核心包留到周末有时间处理时再升级。有一次我在 BrewUI 里看到icu4c有 major 版本升级但我本地有个 PHP 扩展还依赖旧版于是果断跳过。隔了几天 PHP 那边更新兼容后我才手动把icu4c升级掉全程没有出现任何问题。这个经验总结成一句话就是升级之前一定要先搞清楚“谁在依赖它”而不是只管“它依赖谁”。前者才是真正影响系统稳定性的因素。7. 目前还做不好的事与下一步的改进计划即便 BrewUI 已经解决了大部分痛点我还是得客观说它离“完美”还有距离。有些事情天生就不适合做成 GUI比如复杂的批量脚本操作、条件判断、管道串联。这种场景真的应该交给命令行。当前 BrewUI 做得还比较薄弱的地方主要是三个Brewfile 的编辑与导入导出命令行里brew bundle dump和brew bundle install非常强大但我还没在 BrewUI 里做出一个足够友好的界面来编辑 Brewfile。一个能识别依赖关系、可视化展示 Brewfile 内容的编辑器是我接下来想做的事。多用户环境的支持目前 BrewUI 主要面向单用户本地环境如果有多个 macOS 用户共用一个 Homebrew权限和状态管理会更复杂。自定义脚本触发比如升级某个包之后自动重启对应服务这种联动操作需要用户自定义脚本的能力目前的界面还做不到那么灵活。接下来我的计划是实现一个“升级前预检”功能基于依赖图在升级前自动分析可能引发的冲突并给出风险提示。这其实不涉及特别高深的技术重点在于把 brew 的 JSON 数据里那些依赖关系和版本约束读全、读透再跟本地已安装的包做交叉比对。最后再分享一个这几年用 Homebrew 一直没变过的体会越是用工具管理复杂环境越要把“可视化”和“命令行”结合起来用。BrewUI 帮我避开了很多次升级事故也帮我理解了依赖关系的真正含义但每次遇到特殊问题时我第一个想到的还是打开终端自己执行一条命令看看输出。工具是过来帮忙的不是过来取代你判断的。这大概也是我用了这么多包管理工具后最想对所有还在纠结选哪款的人说的一句话。
返回列表