/** * macOS 发布产物的身份与版本守卫。 * * 为什么单独抽出来:2026-09-28 线上核对发现 `dev-mac/0.1.142` 清单指向的更新包 * 其实是 **0.1.139 的 release 身份包**(`world.genarrative.ai-game-creator.release`)。 * 根因是 mac 构建目录里会留下上一轮(甚至上一个渠道身份)的 `*.app.tar.gz`,而 * `generateUpdateManifest()` 是按优先级扫描目录挑产物的——于是「清单写 0.1.142、 * 包里是 0.1.139 且是另一个渠道身份」。这类错误在客户端上表现为「更新后版本没变 * 或渠道身份被换掉」,只能靠构建期失败关闭拦住。 * * 约束:只做纯函数与显式断言,方便单测复现线上那份坏产物;不在这里读文件系统。 */ /** 会被构建期残留影响的 mac 产物后缀:更新包、签名、首装 DMG 与其摘要。 */ const STALE_MACOS_ARTIFACT_SUFFIXES = [ '.app.tar.gz', '.app.tar.gz.sig', '.dmg', '.dmg.sha256', ]; function extractPlistString(plistText, key) { const pattern = new RegExp( `${key}\\s*([^<]*)`, 'u', ); const match = plistText.match(pattern); return match ? match[1] : null; } /** 从 `Info.plist` 文本里读出发布相关的身份字段;缺字段返回 null,由断言决定是否致命。 */ export function readMacosAppInfoIdentity(plistText) { if (typeof plistText !== 'string' || plistText.trim().length === 0) { throw new Error('Info.plist 内容为空,无法核对产物身份'); } return { version: extractPlistString(plistText, 'CFBundleShortVersionString'), identifier: extractPlistString(plistText, 'CFBundleIdentifier'), name: extractPlistString(plistText, 'CFBundleName'), displayName: extractPlistString(plistText, 'CFBundleDisplayName'), }; } /** * 断言本轮构建出来的 `.app` 就是本渠道本轮该发的产物。 * * 三个字段都要对上:版本必须等于即将写进清单的版本;identifier 与产品名必须来自 * 渠道安装身份(`channel-identity.mjs`),否则同一台机器上的 dev / release 会互相顶掉。 */ export function assertMacosAppMatchesChannelIdentity({ identity, expectedVersion, expectedProductName, expectedIdentifier, }) { const problems = []; if (identity.version !== expectedVersion) { problems.push( `版本不一致:产物 ${identity.version ?? '(缺失)'},本轮清单 ${expectedVersion}`, ); } if (identity.identifier !== expectedIdentifier) { problems.push( `bundle identifier 不一致:产物 ${identity.identifier ?? '(缺失)'},本渠道 ${expectedIdentifier}`, ); } const names = [identity.name, identity.displayName].filter(Boolean); if ( names.length === 0 || names.some((value) => value !== expectedProductName) ) { problems.push( `产品名不一致:产物 ${names.join(' / ') || '(缺失)'},本渠道 ${expectedProductName}`, ); } if (problems.length > 0) { throw new Error( `macOS 产物身份核对失败:${problems.join(';')}。` + '这通常意味着构建目录里残留了上一轮/其它渠道的产物,或渠道身份没有注入 Tauri 构建。', ); } } /** * 列出构建前必须清掉的残留产物:构建目录里的更新包/签名/首装包只属于本轮, * 留着就会让按目录扫描的清单生成挑到旧文件。 */ export function listStaleMacosArtifacts(filePaths) { return filePaths.filter((filePath) => STALE_MACOS_ARTIFACT_SUFFIXES.some((suffix) => filePath.endsWith(suffix)), ); } /** 断言清单最终选中的更新包就是本轮写出的那一个,避免「签名对但选错包」。 */ export function assertManifestArtifactMatchesExpected({ artifactPath, expectedPath, }) { if (!artifactPath || !expectedPath) { throw new Error('清单产物路径缺失,无法核对本轮更新包'); } if (artifactPath !== expectedPath) { throw new Error( `清单选中的更新包不是本轮产物:选中 ${artifactPath},本轮应为 ${expectedPath}`, ); } }