ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

t3code 工具链实战:Electron 打包、CLI 分发与 Homebrew/winget 跨平台部署

t3code 工具链实战:Electron 打包、CLI 分发与 Homebrew/winget 跨平台部署 1. 从 t3code 这个标题说起一个被低估的开发者工具链第一次看到 t3code 这个词很多人会以为是某个新出的代码编辑器或者在线 IDE。实际上它更像是一个围绕终端、命令行和桌面端开发工具的组合概念——把 Electron 桌面应用、CLI 命令行工具、Homebrew 和 winget 这类包管理器串在一起形成一套完整的开发者工作流。我最初接触这个方向是因为团队里有人抱怨为什么每次换电脑光是装环境就要折腾一整天为什么一个 CLI 工具在 Mac 上装好了到 Windows 上又要重新找安装包t3code 这个标题背后核心要解决的就是跨平台开发工具的获取、安装、运行和打包这一整条链路。它涉及的技术点包括 Electron 桌面应用的构建与打包、CLI 工具的设计与分发、Homebrew 在 macOS 上的包管理、winget 在 Windows 上的包管理以及像 codex cli、zcode cli、openspec cli、minimax cli 这类具体命令行工具的安装与排错。适合谁来参考如果你是那种经常需要在多台机器上切换、或者要给团队做统一开发环境的人这套东西值得花时间理清楚。如果你只是偶尔写几行脚本那可能用不上这么重的方案但了解其中的思路也没坏处。我写这篇东西的出发点很简单网上关于 Electron 打包、Homebrew 安装、winget 使用的资料很多但大多是孤立的。你搜 electron 打包 apk 会得到一堆教程搜 homebrew 安装 又是另一堆但很少有人把这两件事放在同一个工作流里讲。而实际做项目的时候这两件事往往是连在一起的——你用 Electron 写了个桌面工具想通过 Homebrew 分发给 Mac 用户通过 winget 分发给 Windows 用户同时还要提供一个 CLI 版本给喜欢终端的人。这就是 t3code 这个标题下真正要面对的问题。2. 整体设计思路为什么是 Electron CLI 包管理器2.1 桌面端与命令行的双轨策略做开发者工具第一个要做的决策就是到底做 GUI 还是 CLI我的经验是这两者不是二选一而是应该同时存在。Electron 负责提供图形界面降低新用户的上手门槛CLI 负责提供脚本化和自动化的能力满足老用户的需求。t3code 这个方向之所以把 Electron 和 CLI 并列就是因为它们服务的是同一批用户在不同场景下的需求。举个例子你做了一个代码格式化工具。新手可能希望打开一个窗口拖入文件点击按钮就完成格式化。老手则希望在终端里敲一行命令直接处理整个目录。如果你只做 Electron老手会觉得你不够专业如果你只做 CLI新手会觉得你不够友好。所以双轨策略不是贪心而是覆盖不同用户群体的必要手段。但双轨策略带来的问题是维护成本翻倍。你需要维护两套 UI 逻辑、两套构建流程、两套分发渠道。这就引出了下一个决策点——如何用最小的代价同时维护桌面端和命令行端。2.2 包管理器作为分发渠道的考量Homebrew 和 winget 分别对应 macOS 和 Windows 上的包管理器。选择它们作为分发渠道核心原因是降低用户的安装成本。在 macOS 上如果没有 Homebrew用户需要手动下载 dmg 文件、拖拽到 Applications 文件夹、处理安全提示。有了 Homebrew一行brew install就搞定。Windows 上同理winget 让安装变成一行命令。但这里有个坑Homebrew 和 winget 的包定义格式完全不同。Homebrew 用 Ruby 写的 Formulawinget 用 YAML 写的 manifest。你需要为同一个工具维护两套包定义。而且 Homebrew 最近取消了对 10.15 的支持这意味着如果你的用户还在用旧版 macOS你的 Formula 可能装不上。winget 虽然对系统版本要求宽松一些但它的 manifest 校验规则也在不断变化。我个人的做法是把包定义文件放在项目仓库里用 CI 自动生成和提交。Homebrew 的 Formula 可以通过 GitHub Actions 自动更新版本号和下载链接winget 的 manifest 也可以用类似的方式处理。这样虽然还是要维护两套但至少不用手动改。2.3 为什么不是 Docker 或者直接下载二进制有人可能会问为什么不直接用 Docker 分发或者干脆让用户下载二进制文件Docker 的问题在于开发者工具往往需要访问宿主机的文件系统、网络、甚至图形界面Docker 的隔离机制反而成了障碍。直接下载二进制的问题在于用户需要自己处理 PATH 环境变量、权限设置、版本更新体验很差。包管理器的优势在于它帮你处理了这些琐事。Homebrew 会自动把二进制链接到/usr/local/bin或者/opt/homebrew/binwinget 会处理 PATH 和快捷方式。用户只需要记住一个命令剩下的交给包管理器。这就是为什么 t3code 这个方向要把 Homebrew 和 winget 纳入核心工具链。3. 核心细节解析Electron 打包与 CLI 安装的实操要点3.1 Electron 打包的三种目标格式Electron 打包这件事说简单也简单说复杂也复杂。简单在于你用 electron-builder 或者 electron-forge几行配置就能打出安装包。复杂在于不同平台的打包目标格式不一样每种格式的坑也不一样。macOS 上主要是 dmg 和 zip。dmg 是给普通用户用的双击挂载、拖拽安装。zip 是给 Homebrew 用的因为 Homebrew 的 Cask 可以直接从 zip 里提取 app。如果你只打 dmgHomebrew 的 Cask 也能处理但需要额外的挂载步骤容易出错。所以我一般会同时打 dmg 和 zipdmg 给手动下载的用户zip 给 Homebrew。Windows 上主要是 nsis 和 portable。nsis 是标准安装程序winget 默认用这个。portable 是免安装版本适合放在 U 盘里随身带。winget 的 manifest 里可以指定 InstallerType 为 nsis 或 portable但如果你同时提供两种用户可能会困惑。我的建议是只提供 nsis因为 winget 对 nsis 的支持最成熟。Linux 上主要是 AppImage、deb、rpm。但 t3code 这个方向主要关注 macOS 和 WindowsLinux 可以暂时放一放。如果你确实需要支持 LinuxAppImage 是最省事的因为它不依赖具体的发行版。注意Electron 打包时一定要检查asar的配置。默认情况下electron-builder 会把你的源代码打包进 asar 文件。如果你的 CLI 部分需要读取外部文件或者需要执行子进程asar 可能会导致路径问题。我踩过的坑是CLI 脚本里用了__dirname打包后路径变成了 asar 内部的虚拟路径导致找不到文件。解决办法是在 electron-builder 的配置里把 CLI 相关的文件排除在 asar 之外或者用process.resourcesPath来定位。3.2 CLI 工具的安装路径与权限问题CLI 工具安装到系统里最头疼的就是路径和权限。macOS 上Homebrew 安装的二进制默认放在/opt/homebrew/binApple Silicon或/usr/local/binIntel。这两个路径通常已经在 PATH 里所以用户装完就能直接用。但如果你是通过 npm 全局安装的 CLI路径可能是/usr/local/lib/node_modules/.bin这个路径不一定在 PATH 里需要额外配置。Windows 上winget 安装的 CLI 通常会放在%LOCALAPPDATA%\Microsoft\WinGet\Packages下面然后通过 shim 机制链接到 PATH。这个机制大部分时候没问题但如果你同时用 npm 和 winget 安装了同一个工具可能会出现版本冲突。我遇到过的情况是winget 装了一个旧版npm 装了一个新版结果终端里调用的还是旧版。排查方法是where命令Windows或which -a命令macOS/Linux看看实际调用的是哪个路径。权限方面macOS 上如果 CLI 需要访问受保护的目录比如 Documents、Downloads需要在系统设置里授予完全磁盘访问权限。Windows 上如果 CLI 需要写入 Program Files需要以管理员身份运行。这些权限问题在开发阶段往往被忽略因为开发者本机通常已经给了权限但用户装完后才发现用不了。3.3 Homebrew 的基本操作与常见报错Homebrew 的基本操作其实就那么几个brew install、brew uninstall、brew upgrade、brew list、brew search。但真正用起来报错五花八门。我整理了几个最常见的报错信息原因解决办法Error: No available formula包名拼错或源里没有用brew search确认包名Error: Permission denied目录权限不对sudo chown -R $(whoami) /opt/homebrewError: Failed to download网络问题或链接失效检查网络或手动下载后brew install --build-from-sourceError: Checksum mismatch下载的文件和 Formula 里的哈希不一致更新 Formula 或清除缓存brew cleanupError: macOS version too oldHomebrew 取消了对旧版系统的支持升级系统或用旧版 HomebrewHomebrew 取消对 10.15 的支持这件事影响挺大的。很多还在用 Catalina 的机器突然就装不了新版的包了。解决办法有两个一是升级到更新的 macOS二是用homebrew-core的历史版本。但历史版本不会收到安全更新所以只适合临时用。卸载残留也是 Homebrew 的一个老问题。brew uninstall只会删除二进制和链接不会删除配置文件、缓存、日志。时间长了~/Library/Caches/Homebrew和~/Library/Logs/Homebrew会占掉好几个 G。我一般会定期跑brew cleanup -s加上手动删除这两个目录。如果你想把某个包彻底删干净可以用brew uninstall --zap但要注意--zap会删除所有相关文件包括你可能还想保留的配置。3.4 winget 的使用与 manifest 编写winget 在 Windows 上的地位相当于 Homebrew 在 macOS 上的地位。基本操作也类似winget install、winget uninstall、winget upgrade、winget list、winget search。但 winget 的 manifest 编写比 Homebrew 的 Formula 要繁琐一些因为它用的是 YAML而且分成了三个文件版本文件、安装程序文件、区域设置文件。版本文件.yaml定义包的标识符、版本号、发布者。安装程序文件.installer.yaml定义安装包的下载链接、哈希、安装类型、静默安装参数。区域设置文件.locale.zh-CN.yaml定义包的中文描述、标签、许可证。三个文件缺一不可而且字段名必须完全匹配 schema。我踩过的坑是winget 的 manifest 校验非常严格字段名大小写错了、缩进不对、缺少必填字段都会导致提交失败。而且 winget 的社区仓库winget-pkgs对 PR 的审核比较慢有时候要等好几天。如果你只是内部使用可以搭建私有的 winget 源但配置起来也不简单。我的建议是先用wingetcreate工具自动生成 manifest然后手动调整。wingetcreate可以帮你抓取安装包的元数据减少手写出错的可能。4. 实操过程从零搭建一个 t3code 风格的工具链4.1 环境准备与工具选型假设我们要做一个叫t3code的工具它有一个 Electron 桌面端一个 CLI 命令行端需要支持 macOS 和 Windows。第一步是准备环境。macOS 上你需要安装 Node.js、Homebrew、以及 electron-builder。Node.js 可以用 Homebrew 装brew install node。electron-builder 用 npm 装npm install -g electron-builder。Windows 上你需要安装 Node.js、wingetWindows 10 1809 以上自带、以及同样的 electron-builder。工具选型方面Electron 的构建工具我推荐 electron-builder 而不是 electron-forge。原因很简单electron-builder 对多平台打包的支持更成熟配置也更直观。CLI 部分我推荐用 Node.js 写因为可以和 Electron 共享代码而且 npm 的分发机制很成熟。如果你追求性能可以用 Go 或 Rust 写 CLI但那样就需要单独维护一套构建流程。提示Node.js 的版本管理很重要。我建议用nvm或fnm来管理 Node 版本而不是直接用 Homebrew 装的全局 Node。因为不同项目可能依赖不同的 Node 版本全局只有一个版本会很难受。fnm比nvm快很多而且支持.node-version文件切换版本很方便。4.2 Electron 项目的初始化与打包配置初始化 Electron 项目我一般用npm init然后手动加依赖而不是用脚手架。脚手架生成的东西太多很多用不上反而增加理解成本。核心依赖就三个electron、electron-builder、electron-updater如果需要自动更新。package.json里的build字段是 electron-builder 的配置入口。我通常会这样配置{ build: { appId: com.t3code.app, productName: t3code, directories: { output: dist }, mac: { target: [dmg, zip], category: public.app-category.developer-tools }, win: { target: [nsis] }, nsis: { oneClick: false, allowToChangeInstallationDirectory: true }, files: [ src/**/*, !src/cli/**/* ], extraResources: [ { from: src/cli, to: cli } ] } }这里的关键点是files和extraResources的配合。files里排除了src/cliextraResources里把src/cli复制到 resources 目录下。这样 CLI 脚本就不会被打进 asar而是作为独立文件存在。Electron 主进程可以通过process.resourcesPath找到它们。打包命令是electron-builder --mac --win可以同时打两个平台的包。但如果你在 macOS 上打 Windows 包需要安装 wine而且打出来的包可能有些兼容性问题。我的建议是在 CI 里分别用 macOS 和 Windows 的 runner 来打包这样最稳妥。4.3 CLI 入口的设计与参数解析CLI 的入口文件通常是一个带 shebang 的 JavaScript 文件#!/usr/bin/env node const { program } require(commander); program .name(t3code) .description(t3code CLI tool) .version(1.0.0); program .command(format) .description(Format code) .argument(path, Path to format) .option(-r, --recursive, Format recursively) .action((path, options) { // 实现格式化逻辑 }); program.parse();commander是最常用的参数解析库简单够用。如果你需要更复杂的子命令和插件机制可以考虑yargs或oclif。但大多数情况下commander就够了。CLI 的安装方式有两种一是通过 npm 全局安装二是通过 Homebrew/winget 安装。npm 全局安装的问题是用户需要先有 Node.js 环境。Homebrew/winget 安装的好处是可以把 Node.js 运行时一起打包进去用户不需要单独装 Node。但这样会让安装包变大而且更新 Node 版本时需要重新打包。我个人的选择是CLI 用 npm 分发Electron 用 Homebrew/winget 分发。因为 CLI 的用户通常已经有 Node 环境而 Electron 的用户可能没有。这样分工明确各取所需。4.4 Homebrew Formula 的编写与提交Homebrew Formula 是一个 Ruby 文件定义了包的下载地址、依赖、安装步骤。一个最简单的 Formula 长这样class T3code Formula desc A developer tool for code formatting homepage https://github.com/yourname/t3code url https://github.com/yourname/t3code/releases/download/v1.0.0/t3code-1.0.0.zip sha256 abc123... version 1.0.0 def install bin.install t3code end endurl指向你的发布包sha256是包的哈希值。install方法里定义安装逻辑bin.install会把可执行文件链接到 Homebrew 的 bin 目录。提交 Formula 到 Homebrew 的官方仓库homebrew-core需要满足一些条件包必须是开源的、有稳定的发布版本、有足够的用户基础。如果你的包不符合这些条件可以放在自己的 tap 里。Tap 就是一个独立的 Homebrew 仓库用户可以通过brew tap yourname/tap来添加。我踩过的坑是Homebrew 对 URL 的格式有要求必须是直接指向文件的链接不能是重定向的链接。GitHub Releases 的链接默认是重定向的需要用https://github.com/yourname/t3code/releases/download/v1.0.0/t3code-1.0.0.zip这种格式。另外sha256必须和实际文件匹配否则安装会失败。每次发布新版本都要更新url、sha256、version三个字段。4.5 winget manifest 的生成与提交winget manifest 的生成可以用wingetcreate工具wingetcreate new https://github.com/yourname/t3code/releases/download/v1.0.0/t3code-1.0.0-setup.exe这个命令会交互式地询问包的标识符、版本号、发布者等信息然后生成三个 YAML 文件。生成后你可以手动调整描述、标签、许可证等内容。提交到 winget-pkgs 仓库需要 fork 仓库、创建分支、添加文件、提交 PR。PR 的审核由社区志愿者完成通常需要几天时间。审核过程中可能会有志愿者提出修改意见比如描述不够清晰、标签不准确等。你需要及时响应否则 PR 可能会被关闭。我踩过的坑是winget 的 manifest 对安装程序的静默安装参数有要求。如果你的安装程序不支持静默安装winget 会安装失败。NSIS 安装程序默认支持/S参数进行静默安装但如果你自定义了安装界面可能需要额外配置。另外winget 对安装程序的哈希校验也很严格如果下载的文件和 manifest 里的哈希不一致会直接报错。5. 常见问题与排查技巧实录5.1 Electron 相关问题的排查问题一Electron 应用启动后白屏。这是最常见的问题原因通常有三种一是main.js里的loadFile路径不对二是渲染进程的 JavaScript 报错三是 CSP内容安全策略阻止了资源加载。排查方法是打开开发者工具CtrlShiftI或CmdOptionI看 Console 里有没有报错。如果是路径问题检查__dirname和process.resourcesPath的区别。如果是 CSP 问题在index.html的 meta 标签里调整策略。问题二Electron 打包后找不到 CLI 脚本。前面提到过asar 会导致路径问题。解决办法是在electron-builder的配置里把 CLI 脚本排除在 asar 之外然后用process.resourcesPath来定位。具体做法是在files里加!src/cli/**/*在extraResources里加{ from: src/cli, to: cli }。然后在代码里用path.join(process.resourcesPath, cli, index.js)来引用。问题三Electron 应用在 macOS 上提示“已损坏无法打开”。这是因为 macOS 的 Gatekeeper 阻止了未签名的应用。解决办法是对应用进行代码签名和公证notarization。代码签名需要 Apple 开发者账号公证需要把应用上传到 Apple 的服务器进行扫描。这个过程比较繁琐但如果你要正式分发这一步绕不过去。临时解决办法是让用户在终端里运行xattr -cr /Applications/t3code.app但这只是权宜之计。5.2 CLI 工具安装与运行的排查问题一command not found。这说明 CLI 的安装路径不在 PATH 里。macOS 上检查/opt/homebrew/bin和/usr/local/bin是否在 PATH 里。Windows 上检查%LOCALAPPDATA%\Microsoft\WinGet\Packages是否在 PATH 里。如果不在手动添加。另外如果你是用 npm 全局安装的检查npm bin -g的输出路径是否在 PATH 里。问题二CLI 运行时报“权限不足”。macOS 上如果 CLI 需要访问受保护的目录需要在“系统设置 - 隐私与安全性 - 完全磁盘访问权限”里添加终端应用。Windows 上如果 CLI 需要写入系统目录需要以管理员身份运行终端。另外如果 CLI 文件本身没有可执行权限用chmod x添加。问题三CLI 版本和预期不符。这通常是因为系统里有多个版本的 CLI。用which -a t3codemacOS/Linux或where t3codeWindows查看所有路径然后决定保留哪个、删除哪个。如果是 Homebrew 和 npm 冲突建议只保留一个。我一般会优先保留 Homebrew 的版本因为更新更方便。5.3 Homebrew 和 winget 的常见故障Homebrew 安装失败提示“Failed to connect”。这通常是网络问题。Homebrew 的默认源在境外国内访问可能不稳定。解决办法是更换国内镜像源比如中科大或清华的镜像。具体操作是设置HOMEBREW_BREW_GIT_REMOTE和HOMEBREW_CORE_GIT_REMOTE环境变量。但要注意更换镜像源后某些包的哈希校验可能会失败因为镜像同步有延迟。winget 安装失败提示“Installer hash does not match”。这说明下载的安装程序和 manifest 里的哈希不一致。原因可能是发布者更新了安装包但没有更新 manifest或者下载过程中文件损坏。解决办法是重新下载或者联系发布者更新 manifest。如果你是自己发布包每次更新安装包后都要重新计算哈希并更新 manifest。Homebrew 卸载残留清理。brew uninstall不会删除配置文件、缓存、日志。彻底清理的步骤是先brew uninstall --zap 包名然后brew cleanup -s最后手动删除~/Library/Caches/Homebrew和~/Library/Logs/Homebrew。如果你想把 Homebrew 本身也卸载掉官方提供了一个卸载脚本但运行前一定要确认没有其他包依赖它。5.4 常见问题速查表问题现象可能原因排查步骤解决办法Electron 白屏路径错误/CSP/JS 报错打开 DevTools 看 Console修正路径或调整 CSPCLI 找不到PATH 未配置which -a或where添加 PATH 或重装Homebrew 安装慢源在境外检查网络更换国内镜像源winget 哈希不匹配manifest 未更新对比哈希值更新 manifest 或重下打包后文件缺失asar 配置问题检查 resources 目录调整 files/extraResourcesmacOS 提示损坏未签名/未公证检查签名状态签名并公证或临时 xattr提示排查问题时养成看日志的习惯。Electron 的日志在~/Library/Logs/t3codemacOS或%APPDATA%\t3code\logsWindows。Homebrew 的日志在~/Library/Logs/Homebrew。winget 的日志在%LOCALAPPDATA%\Packages\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\LocalState\DiagOutputDir。日志里通常有详细的错误信息比终端里的提示有用得多。6. 一些实操心得与避坑建议做 t3code 这类工具链最深的体会是分发比开发更难。你花三天写了一个功能可能花三周都在处理打包、签名、分发的问题。所以我的建议是从项目第一天起就把 CI/CD 搭好不要等到要发布的时候才临时抱佛脚。具体来说GitHub Actions 是一个很好的选择。你可以配置三个 job一个在 macOS 上打包 Electron 的 dmg 和 zip一个在 Windows 上打包 nsis一个在 Ubuntu 上跑测试和发布 npm 包。每个 job 都可以自动上传 artifact 到 Release然后触发 Homebrew 和 winget 的更新流程。这样虽然前期配置麻烦一点但后期每次发版只需要打一个 tag剩下的全自动完成。另一个心得是不要追求一次支持所有平台。我见过很多项目一开始就宣称支持 macOS、Windows、Linux结果每个平台都有一堆 bug。更好的做法是先做好一个平台比如 macOS把打包、签名、分发、更新的流程跑通然后再复制到 Windows。这样虽然看起来慢但实际上更快因为你在第一个平台上踩过的坑在第二个平台上大概率还会遇到但你已经知道怎么解决了。最后分享一个小技巧如果你在 macOS 上打 Windows 包遇到 wine 相关的报错可以试试用electron-builder --win --x64加上--publish never参数。有时候报错是因为 electron-builder 试图自动发布但配置不对。加上--publish never可以跳过发布步骤先看看打包本身有没有问题。如果打包成功但发布失败再单独排查发布配置。这个方向后续还可以扩展的地方很多比如加入自动更新机制electron-updater、支持插件系统、提供云端配置同步等。但那是另一个话题了先把基础的分发链路跑通比什么都重要。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进