Files
Genarrative/apps/ai-game-creator-shell/scripts/prepare-macos-codex.mjs
T
suzmii 48985d3447 接入Mac通用构建与Jenkins归档管线
补齐macOS universal双架构Codex资源与构建校验
统一发布清单和检查脚本支持universal目标
新增锁定原生依赖完整性校验与隔离构建smoke
新增Mac Jenkins Agent归档构建Job与本机构建接入规范
2026-09-18 19:34:23 +08:00

135 lines
4.8 KiB
JavaScript

import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const appRoot = fileURLToPath(new URL('..', import.meta.url));
const repoRoot = path.resolve(appRoot, '../..');
const platforms = {
arm64: 'aarch64-apple-darwin',
x64: 'x86_64-apple-darwin',
};
export function lockedMacPackage(lock, arch, version) {
assert.ok(Object.hasOwn(platforms, arch), '未知 macOS 架构');
const alias = `@openai/codex-darwin-${arch}`;
const entry = lock.packages?.[`node_modules/${alias}`];
assert.equal(
entry?.version,
`${version}-darwin-${arch}`,
'原生依赖必须与应用锁定版本一致',
);
assert.deepEqual(entry.os, ['darwin']);
assert.deepEqual(entry.cpu, [arch]);
const url = new URL(entry.resolved);
assert.equal(url.protocol, 'https:');
assert.equal(
url.hostname,
'registry.npmjs.org',
'只下载锁定的官方 npm 原生包',
);
assert.equal(url.username + url.password + url.search + url.hash, '');
assert.match(entry.integrity, /^sha512-[A-Za-z0-9+/]+={0,2}$/);
return { alias, target: platforms[arch], ...entry };
}
export function verifyPackageIntegrity(bytes, expected) {
const actual = `sha512-${createHash('sha512').update(bytes).digest('base64')}`;
assert.equal(actual, expected, 'Codex 下载包 lockfile integrity 不匹配');
}
export function validateArchiveListing(listing) {
const files = listing.trim().split(/\r?\n/u);
assert.ok(files.length > 0);
for (const file of files) {
assert.ok(file.startsWith('package/'), '原生包必须只有 package 根目录');
assert.ok(
!file.split('/').includes('..') && !file.includes('\\'),
'压缩包路径不安全',
);
}
}
export async function prepareMacosCodex() {
assert.equal(process.platform, 'darwin', '该入口仅用于 macOS 构建机');
const lock = JSON.parse(
fs.readFileSync(path.join(repoRoot, 'package-lock.json'), 'utf8'),
);
const app = JSON.parse(
fs.readFileSync(path.join(appRoot, 'package.json'), 'utf8'),
);
const version = app.devDependencies['@openai/codex'];
assert.match(version, /^\d+\.\d+\.\d+$/u, 'Codex 必须锁定精确版本');
const cache = path.join(appRoot, 'src-tauri/target/.macos-native-cache');
fs.mkdirSync(cache, { recursive: true });
for (const arch of Object.keys(platforms)) {
const entry = lockedMacPackage(lock, arch, version);
const archive = path.join(cache, `codex-${entry.version}.tgz`);
if (!fs.existsSync(archive)) {
const response = await fetch(entry.resolved, {
signal: AbortSignal.timeout(300_000),
});
assert.ok(response.ok, `原生包下载失败 HTTP ${response.status}`);
const bytes = Buffer.from(await response.arrayBuffer());
verifyPackageIntegrity(bytes, entry.integrity);
const partial = `${archive}.${process.pid}.tmp`;
fs.writeFileSync(partial, bytes);
fs.renameSync(partial, archive);
}
verifyPackageIntegrity(fs.readFileSync(archive), entry.integrity);
validateArchiveListing(
execFileSync('tar', ['-tzf', archive], { encoding: 'utf8' }),
);
// 拒绝链接、设备及其它特殊条目,不能让 tar 在包目录之外写入。
const entries = execFileSync('tar', ['-tvzf', archive], {
encoding: 'utf8',
});
assert.ok(
entries
.trim()
.split(/\r?\n/u)
.every((line) => /^[-d]/u.test(line)),
'原生包禁止链接或特殊文件',
);
const parent = path.join(repoRoot, 'node_modules/@openai');
fs.mkdirSync(parent, { recursive: true });
const stage = fs.mkdtempSync(path.join(parent, '.mac-native-'));
try {
execFileSync(
'tar',
['-xzf', archive, '-C', stage, '--strip-components=1'],
{ stdio: 'pipe' },
);
const metadata = JSON.parse(
fs.readFileSync(
path.join(stage, 'vendor', entry.target, 'codex-package.json'),
'utf8',
),
);
assert.equal(metadata.version, version);
assert.equal(metadata.target, entry.target);
assert.equal(metadata.entrypoint, 'bin/codex');
const destination = path.join(repoRoot, 'node_modules', entry.alias);
assert.ok(
!fs.existsSync(destination) ||
!fs.lstatSync(destination).isSymbolicLink(),
'拒绝覆盖链接依赖',
);
fs.rmSync(destination, { recursive: true, force: true });
fs.renameSync(stage, destination);
} finally {
fs.rmSync(stage, { recursive: true, force: true });
}
console.log(`[macOS Codex] ${entry.version}: lockfile integrity 已验证`);
}
}
if (
process.argv[1] &&
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
) {
await prepareMacosCodex();
}