|
| 1 | +# 知识卡片 · 踩坑记录 |
| 2 | + |
| 3 | +## 基本信息 |
| 4 | + |
| 5 | +| 字段 | 内容 | |
| 6 | +|------|------| |
| 7 | +| 标题 | CI 编译原生模块报「Could not find any Visual Studio installation」——旧版 node-gyp 找不到 VS 2022 | |
| 8 | +| 日期 | 2026-07-31 | |
| 9 | +| 类型 | 踩坑记录 | |
| 10 | +| 标签 | #CI #原生模块 #node-gyp #electron-rebuild #本地能跑CI挂 | |
| 11 | + |
| 12 | +--- |
| 13 | + |
| 14 | +## 症状 |
| 15 | + |
| 16 | +首次在 CI 编译自研原生 addon(`client/native/process-audio`,见 ADR-001)时, |
| 17 | +`windows-latest` 上 `electron-rebuild` 失败: |
| 18 | + |
| 19 | +``` |
| 20 | +Error: Could not find any Visual Studio installation to use |
| 21 | + at VisualStudioFinder.findVisualStudio2013 (client/node_modules/node-gyp/lib/find-visualstudio.js:380) |
| 22 | + at VisualStudioFinder.findVisualStudio2015 (...:364) |
| 23 | +``` |
| 24 | + |
| 25 | +关键陷阱: |
| 26 | + |
| 27 | +- **本地(VS 2022 Community)用同一条命令完全正常**,只有 CI 失败 |
| 28 | +- runner 明确自带 VS 2022,报错却说"找不到任何 VS 安装" |
| 29 | +- 堆栈里仍在尝试 `findVisualStudio2013/2015`——暴露了真实原因 |
| 30 | +- 该步骤配了 `continue-on-error`,所以**发布流水线全绿、版本照常发出**, |
| 31 | + 安装包只是静默缺少原生模块(自动降级为端点环回),极易被忽略 |
| 32 | + |
| 33 | +## 环境 |
| 34 | + |
| 35 | +| 项目 | 版本/信息 | |
| 36 | +|------|----------| |
| 37 | +| CI | GitHub Actions `windows-latest`(自带 VS 2022 + MSVC) | |
| 38 | +| 构建 | `@electron/rebuild` + `client/node_modules` 内的 node-gyp | |
| 39 | +| 本地 | VS 2022 Community、Python 3.14、node-gyp ^11(addon 目录自带) | |
| 40 | + |
| 41 | +## 排查过程 |
| 42 | + |
| 43 | +1. release.yml 三 job 全绿、v0.30.0 正常发出,但功能未生效 → 先怀疑运行时加载路径 |
| 44 | +2. 抓 `Build native process-audio module` 步骤完整日志(而非只看 job 结论), |
| 45 | + 发现 `native:install` 成功、`native:build` 在 **8 秒内**失败——耗时过短说明 |
| 46 | + 根本没进入编译阶段 |
| 47 | +3. 读完整堆栈:路径为 `client\node_modules\node-gyp\...`,且函数名是 |
| 48 | + `findVisualStudio2013/2015` → 用的是**旧版 node-gyp**,其 VS 探测逻辑不支持 VS 2022 |
| 49 | +4. 反问"为什么 better-sqlite3 的 rebuild 在同一 CI 上成功?"→ 因为它有 |
| 50 | + **预编译二进制**,从不真正调用 MSVC。**CI 上从未编译过任何原生代码**, |
| 51 | + 这是首次暴露 |
| 52 | + |
| 53 | +## 根因 |
| 54 | + |
| 55 | +`electron-rebuild` 使用的是**调用它的项目(client)内的 node-gyp**,版本较旧, |
| 56 | +VS 探测逻辑只覆盖到 VS 2015/2017;而 addon 目录自带的新版 node-gyp(^11, |
| 57 | +支持 VS 2022)根本没被用上。本地之所以正常,是因为本地曾直接在 addon 目录 |
| 58 | +执行 `npx node-gyp rebuild`(用的是新版)。 |
| 59 | + |
| 60 | +## 解决方案 |
| 61 | + |
| 62 | +不只改 CI(否则仍是两套路径),而是让**本地与 CI 共用同一构建入口**: |
| 63 | + |
| 64 | +新增 `client/native/process-audio/build.mjs`: |
| 65 | + |
| 66 | +- 固定使用 addon 目录自带的新版 node-gyp |
| 67 | +- 从 client 的实际安装解析 Electron 版本(`require('electron/package.json').version`, |
| 68 | + 比读 `^35.7.5` 这类范围更准) |
| 69 | +- 显式传 `--target=<electron> --dist-url=https://electronjs.org/headers` |
| 70 | + 保证按 Electron 的 Node ABI 编译(否则主进程 require 时报 |
| 71 | + NODE_MODULE_VERSION 不匹配) |
| 72 | + |
| 73 | +`package.json` 的 `native:build` 指向该脚本,CI 与本地都只调这一个命令。 |
| 74 | + |
| 75 | +## 教训 |
| 76 | + |
| 77 | +- **「本地能跑 CI 挂」的第三次同类事故**(前两次:Git LFS 指针未拉取、 |
| 78 | + `.env.production` 被 gitignore)。规律是:**本地存在而 CI 不具备的隐式前提** |
| 79 | + ——已 checkout 的 LFS 文件、gitignore 掉的配置、能被解析到的新版工具链。 |
| 80 | + 新增构建步骤时应主动自问:这一步依赖的东西,CI 上真的存在且版本一致吗? |
| 81 | +- **本地与 CI 必须共用同一构建入口**。两套命令等于两套隐式前提,差异只会在 |
| 82 | + 发版时暴露。 |
| 83 | +- **`continue-on-error` 是双刃剑**:它正确地保护了发布流水线(可选增强失败 |
| 84 | + 不该阻断发版),但也让失败变得静默。必须配套**产物存在性检查 + `::warning::` |
| 85 | + 标注**,否则"绿灯发版但功能没进去"会被忽略数个版本。 |
| 86 | +- **排查工具链失败要看完整堆栈而非结论**:本例中 `findVisualStudio2013` 这个 |
| 87 | + 函数名直接指向了根因;只看 "Could not find any Visual Studio" 会误判为 |
| 88 | + runner 缺少 VS 而白费力气去装编译器。 |
| 89 | +- **反问"为什么相邻的同类步骤是成功的"**:better-sqlite3 能 rebuild 却帮不到 |
| 90 | + 自研模块,是因为它走预编译。这个对比直接缩小了范围。 |
| 91 | + |
| 92 | +## 参考 |
| 93 | + |
| 94 | +- 决策背景:`docs/adr/ADR-001-audio-capture-process-loopback.md` |
| 95 | +- 构建脚本:`client/native/process-audio/build.mjs` |
| 96 | +- CI 步骤:`.github/workflows/release.yml`(Build native process-audio module) |
| 97 | +- 同类卡片:[Git LFS 图标未在 CI 拉取](./2026-07-git-lfs-icon-electron-builder-ci-failure.md)、 |
| 98 | + [`.env.production` 被 gitignore](./2026-07-ci-env-production-gitignore-supabase-placeholder.md) |
0 commit comments