Skip to content

Commit a5d8f3b

Browse files
committed
fix(build): 修复 CI 编译原生模块失败(旧版 node-gyp 找不到 VS 2022)
v0.30.0 的 CI 中 native:build 在 8 秒内失败:electron-rebuild 用的是 client 下的旧版 node-gyp,其 VS 探测逻辑仍在找 VS 2013/2015,在 windows-latest 报 'Could not find any Visual Studio installation'。因该步骤 continue-on-error, 发布流水线全绿但产物静默缺少原生模块(自动降级为端点环回)。 不只改 CI,而是让本地与 CI 共用同一构建入口,避免两套隐式前提: - 新增 native/process-audio/build.mjs:固定使用 addon 目录自带的新版 node-gyp(支持 VS 2022),从 client 实际安装解析 Electron 版本, 显式传 --target/--dist-url 保证按 Electron ABI 编译 - native:build 指向该脚本;CI 与本地命令一致 本地验证:识别到 Electron 35.7.5,编译通过。 沉淀知识卡片:这是第三次'本地能跑 CI 挂'(前两次为 Git LFS 指针、 .env.production 被 gitignore),共同规律是本地存在而 CI 不具备的隐式前提。
1 parent 4006a92 commit a5d8f3b

5 files changed

Lines changed: 158 additions & 3 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ jobs:
5959
cache: 'npm'
6060
cache-dependency-path: client/package-lock.json
6161
- run: cd client && npm ci
62-
# 进程环回原生模块(ADR-001):windows-latest 自带 MSVC,针对 Electron ABI 编译。
62+
# 进程环回原生模块(ADR-001):windows-latest 自带 VS 2022。
63+
# 必须走 native:build 脚本(本地与 CI 同一入口):直接用 client 下的
64+
# electron-rebuild 会命中旧版 node-gyp,在 CI 报
65+
# "Could not find any Visual Studio installation"(它仍在找 VS 2013/2015)。
6366
# continue-on-error:它是可选增强(运行时未加载到则降级为端点环回),
6467
# 不应因其编译失败而阻断整条发布流水线
6568
- name: Build native process-audio module
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// 进程环回原生模块构建脚本(本地与 CI 共用)
2+
//
3+
// @ai-context: 必须本地与 CI 用同一入口——曾因 CI 使用 client 下的旧版
4+
// node-gyp(仍在查找 VS 2013/2015)而报 "Could not find any Visual Studio
5+
// installation",本地却用得好,属典型"本地能跑 CI 挂"。故此处固定使用本
6+
// 目录自带的新版 node-gyp(支持 VS 2022),并显式传入 Electron 的 ABI 目标。
7+
// @ai-context: 必须针对 Electron 的 Node ABI 编译(--target + --dist-url),
8+
// 否则主进程 require 时报 NODE_MODULE_VERSION 不匹配而加载失败。
9+
10+
import { spawnSync } from 'node:child_process';
11+
import { createRequire } from 'node:module';
12+
import path from 'node:path';
13+
import { fileURLToPath } from 'node:url';
14+
15+
const here = path.dirname(fileURLToPath(import.meta.url));
16+
17+
/** 从 client 的实际安装解析 Electron 版本(比读 package.json 的 ^范围更准) */
18+
function resolveElectronVersion() {
19+
const clientRequire = createRequire(path.join(here, '..', '..', 'package.json'));
20+
try {
21+
return clientRequire('electron/package.json').version;
22+
} catch {
23+
return null;
24+
}
25+
}
26+
27+
const electronVersion = resolveElectronVersion();
28+
if (!electronVersion) {
29+
console.error('[native-build] 未能解析 Electron 版本,请先在 client 目录执行 npm ci');
30+
process.exit(1);
31+
}
32+
33+
const args = [
34+
'rebuild',
35+
`--target=${electronVersion}`,
36+
'--arch=x64',
37+
'--dist-url=https://electronjs.org/headers',
38+
];
39+
40+
console.log(`[native-build] 针对 Electron ${electronVersion} 编译 process_audio.node`);
41+
42+
const result = spawnSync('npx', ['node-gyp', ...args], {
43+
cwd: here,
44+
stdio: 'inherit',
45+
shell: true,
46+
});
47+
48+
if (result.status !== 0) {
49+
console.error(`[native-build] 编译失败(exit=${result.status})`);
50+
process.exit(result.status ?? 1);
51+
}
52+
console.log('[native-build] 编译完成');

‎client/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"preview": "vite preview",
1515
"electron:dev": "concurrently -k -n vite,electron \"cross-env ELECTRON_BUILD=1 vite --mode test\" \"wait-on http://localhost:5173 && tsc -p electron/tsconfig.json && cross-env ELECTRON_BUILD=1 NODE_ENV=development VITE_AI_GATEWAY_URL=http://101.37.70.235:8000 electron .\"",
1616
"electron:build": "cross-env ELECTRON_BUILD=1 vite build && tsc -p electron/tsconfig.json && electron-builder",
17-
"native:build": "electron-rebuild --module-dir native/process-audio --only @entropydecrease/process-audio",
17+
"native:build": "node native/process-audio/build.mjs",
1818
"native:install": "npm --prefix native/process-audio install --no-audit --no-fund",
1919
"test": "vitest run",
2020
"test:watch": "vitest",
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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)

‎docs/knowledge/index.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
| 日期 | 标题 | 标签 |
88
|------|------|------|
9+
| 2026-07-31 | [CI 编译原生模块报「Could not find any Visual Studio installation」——旧版 node-gyp 找不到 VS 2022](./bugs/2026-07-ci-node-gyp-vs2022-not-found.md) | #CI #原生模块 #node-gyp #本地能跑CI挂 |
910
| 2026-07-31 | [课堂助手精细采集三症状:视觉抓页面元数据、ASR 静音幻觉、截断 JSON 泄漏 UI](./bugs/2026-07-classroom-capture-asr-hallucination-json-leak.md) | #课堂助手 #多模态 #ASR幻觉 #prompt工程 |
11+
| 2026-07-31 | [番茄钟"跳过"退化为"取消":一个 onClose 回调承载两种意图,空目标番茄无法启动](./bugs/2026-07-pomodoro-goal-skip-acts-as-cancel.md) | #React #番茄钟 #弹窗交互 #回调语义 #意图区分 |
1012
| 2026-07-31 | [登录失败后持续要求登录:AuthGuard 与“跳过登录”的模式降级缺口 + session-expired 事件风暴](./bugs/2026-07-login-loop-authguard-mode-gap.md) | #认证 #AuthGuard #路由守卫 #模式管理 #事件去重 #死循环 |
1113
| 2026-07-31 | [Tailwind v3 对 `var()` 令牌色的 `/透明度` 修饰符静默失效,明亮主题弹窗背景全透明](./bugs/2026-07-tailwind-var-alpha-modifier-silent-drop.md) | #Tailwind #CSS #主题 #DesignTokens #color-mix |
1214
| 2026-07-31 | [番茄钟计数异常:无重置路径的周期计数 + 跨模式状态残留 + store/hook 副作用双重执行](./bugs/2026-07-pomodoro-count-reset-and-duplicate-side-effects.md) | #Zustand #状态管理 #副作用 #番茄钟 #数据统计 |
@@ -31,4 +33,4 @@ _(暂无)_
3133

3234
- **技术**:#CSS #a11y #React #Vite #Tailwind #Zustand #CI #GitLFS #electron-builder #GitHubActions #CDN #阿里云 #环境变量 #Supabase #DesignTokens #color-mix #认证 #AuthGuard
3335
- **类型**:#bug #方案 #学习 #复盘
34-
- **模块**:#启动仪式 #reduced-motion #animation #发布 #安装包 #性能诊断 #测量方法 #番茄钟 #主题 #状态管理 #副作用 #数据统计 #路由守卫 #模式管理 #事件去重
36+
- **模块**:#启动仪式 #reduced-motion #animation #发布 #安装包 #性能诊断 #测量方法 #番茄钟 #主题 #状态管理 #副作用 #数据统计 #路由守卫 #模式管理 #事件去重 #弹窗交互 #回调语义 #意图区分

0 commit comments

Comments
 (0)