Files
MailUI4Agents/client/electron/scripts/build-info.mjs
JianFeeeee 317f3265e3 fix(判据): 探针三值 + 发布候选标签 —— 顺带查出探针从写下那天起一次都没跑成过
pi 的两条"真实的洞",都落了,而且第一条当场抓到实证。

1. **探针三值**(可用 / 不可用 / 拿不准→红):`RESULT static=5 probe=ok|unknown`
   把"欠账余额"和"探针是否健康"拆成两个数字。
   **换完第一次运行就报 probe=unknown** —— 一查:探针调的是 `execFileSync`,
   而这个文件 import 的是 `spawnSync`,**名字根本没定义**。也就是说
   **探针从写下的那天起一次都没跑成过**,旧的两值设计把 `ReferenceError`
   和"没有设备"一起吞掉、统一报成"设备不可用":机制在、闸门从没开过,
   而它看起来完全健康。这正是 pi 描述的"恒不开闸",只是比预想更彻底。
   现在:命令在但跑不成 → unknown → 红;所有候选都不存在(本机没装 hdc)→ 可判的
   "没有设备工具" → false,避免没装 SDK 的机器天天假红。
   附 `--probe-selftest`(只跑分类器,不跑套件)+ 变异验证(把 unknown 当"不成立"→ 红)。

2. **releaseCandidate = !gitDirty**(从展示升成标签):BUILD_INFO 现在自报
   `releaseCandidate`,发布脚本在脏树时会打印"这个包不是发布候选"。
   判据 `build-stamp` 断言"标签与 gitDirty 必须一致"。
   **实证**:本轮我打的包正是这种情况 —— `gitDirty: true`(含着 gui-lab 未提交的
   NarrowStack/index.css),`releaseCandidate: false`,日志里明确说了"不是发布候选"。

3. 附带:`criteria-hygiene` 加一条"用到 `code/prose/bytes` 就必须真的 import"。
   理由是同一形状我这轮在三个文件里各犯过一次(最后一次是 `execFileSync`/`spawnSync`),
   而它表现为"判据红了"(ReferenceError 抛在判据自己身上),看起来像判据失败、
   不像判据写错。这条至少把最常写错的那几个名字变成明确的红。
2026-09-14 16:35:40 +08:00

123 lines
5.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 构建产物的**自证**`dist/BUILD_INFO.json`。
*
* # 为什么不是时间戳
*
* 原来那条判据是"`dist` 比 `src` 新" —— pi 2026-09-14 指出它有两层代理:
* ① 看不见"构建是否成功"(实测:构建失败但碰过 dist 时它照样绿,见 CRITERIA.md §6.7.1
* ② **`dist` 比 `src` 新也不等于 `dist` 是从这份 `src` 构建的**`checkout`/`cp`/时钟都骗得过 mtime
* 换成内容指纹之后,②也消掉了:判据说的是"产物记的源状态 == 当前源状态"
* 而不是"谁的时间更晚"。任何"包不是从这份源码来的"都变成一条可读出的不等。
*
* # 这个文件同时是"套件自报来源"的构建侧同款
*
* pi 的建议:**一条红/一个包都应自带"它对应哪个源码状态"**`git status --porcelain` 那一族)。
* 消费方:
* - `test/build-stamp.test.mjs` 精确比对(不是比新旧);
* - `scripts/release-linux.sh` 打完包把同一份信息打进日志(出问题时先看它)。
*
* 指纹范围 = **会影响产物的一切**`src/` + 入口 html + 构建配置 + 生成脚本),
* 排除生成物 `background-takeover.generated.css`(它由 `gen:bg` 现生成,
* 内容已由生成脚本决定 —— 计入会让"先 build 后 gen"这种无害顺序变化产生假红)。
*/
import { createHash } from 'node:crypto';
import { execFileSync } from 'node:child_process';
import { readFileSync, readdirSync, statSync, writeFileSync, mkdirSync } from 'node:fs';
import { dirname, join, relative, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
export const ELECTRON_DIR = join(dirname(fileURLToPath(import.meta.url)), '..');
/** 参与指纹的文件/目录(相对 client/electron */
const TRACKED = [
'src',
'index.html',
'vite.config.ts',
'tsconfig.json',
'package.json',
'scripts/gen-background-takeover.mjs'
];
/** 生成物:由上面某个文件决定内容,计入会产生假红 */
const EXCLUDE = new Set(['src/background-takeover.generated.css']);
function walk(abs, rel, out) {
const st = statSync(abs);
if (st.isDirectory()) {
for (const name of readdirSync(abs).sort()) {
walk(join(abs, name), `${rel}/${name}`, out);
}
return;
}
const key = rel.split(sep).join('/');
if (EXCLUDE.has(key)) return;
out.push(key);
}
/**
* 当前源码状态。
*
* 返回 `{ gitRev, gitDirty, srcHash }`
* - `gitRev`HEAD 短哈希(读不到 git 时给 `''`,此时只看指纹);
* - `gitDirty`:工作树是否有未提交改动(**只作信息**:共享工作区常年是脏的,
* 判据不拿它当红/绿依据,拿它当"复核时先看这一行"
* - `srcHash`:影响产物的那批文件的**内容**指纹。
*/
export function srcState(root = ELECTRON_DIR) {
const files = [];
for (const t of TRACKED) {
const abs = join(root, t);
try { walk(abs, t, files); } catch { /* 缺文件就不计(例如还没有 dist 时) */ }
}
files.sort();
const h = createHash('sha256');
for (const f of files) {
h.update(f).update('\0').update(readFileSync(join(root, f))).update('\0');
}
let gitRev = '';
let gitDirty = false;
const repoRoot = join(root, '..', '..');
try {
gitRev = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' }).trim();
gitDirty = execFileSync('git', ['status', '--porcelain'], { cwd: repoRoot, encoding: 'utf8' }).trim().length > 0;
} catch { /* 不在 git 里(例如解包后的目录):指纹仍然有效 */ }
return { gitRev, gitDirty, srcHash: h.digest('hex'), files: files.length };
}
/** 写 `dist/BUILD_INFO.json`(构建路径的最后一步) */
export function writeBuildInfo(root = ELECTRON_DIR, buildCmd = process.env.npm_lifecycle_event || '') {
const st = srcState(root);
const info = {
gitRev: st.gitRev,
gitDirty: st.gitDirty,
srcHash: st.srcHash,
srcFiles: st.files,
buildCmd,
builtAt: new Date().toISOString(),
// 说明这份文件是什么、谁在比它(读到此文件的人不用去翻代码)
/*
* **标签**不是展示pi 2026-09-14 §1
*
* 共享树上永远有人在写别的文件,所以"脏树"是常态 —— 拿它当红会让门禁天天假红。
* 但"从脏树打出来的包"和"从干净树打出来的包"**在门禁上必须可区分**
* 前者含别人的半成品,只能自用,不能给人装。所以给一个是/否:
*
* releaseCandidate = !gitDirty
*
* 部署路径可以据此拒绝安装(判据见 build-stamp 的"脏树产物不得自称发布候选")。
*/
releaseCandidate: !st.gitDirty,
note: '构建自证test/build-stamp.test.mjs 用 srcHash/gitRev 精确比对,不是比时间戳'
};
const out = join(root, 'dist', 'BUILD_INFO.json');
mkdirSync(dirname(out), { recursive: true });
writeFileSync(out, `${JSON.stringify(info, null, 2)}\n`);
return info;
}
// 直接执行时 = 构建的最后一步
if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
const info = writeBuildInfo();
console.log(`[build-info] dist/BUILD_INFO.json 已写rev=${info.gitRev || '(无 git)'} srcHash=${info.srcHash.slice(0, 12)} dirty=${info.gitDirty}`);
}