Files
Genarrative/apps/ai-game-creator-shell/scripts/build-release.mjs
T
kdletters b913aa9ed3
Project CI / Repository checks (pull_request) Failing after 14s
Project CI / Frontend tests (pull_request) Successful in 3m40s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / Native shell tests (pull_request) Failing after 16m26s
修复Windows命令脚本启动
使用shell模式执行npm.cmd以兼容Windows子进程
2026-09-01 11:54:53 +08:00

258 lines
8.3 KiB
JavaScript
Raw 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.
import { spawnSync } 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 defaultReleaseTarget = 'x86_64-pc-windows-msvc';
const releaseTarget =
process.env.AGC_BUILD_TARGET?.trim() || defaultReleaseTarget;
const bundleRoot = path.join(
appRoot,
'src-tauri',
'target',
releaseTarget,
'release',
'bundle',
);
const packageJsonPath = path.join(appRoot, 'package.json');
const rootPackageLockPath = path.resolve(appRoot, '../..', 'package-lock.json');
const tauriConfigPath = path.join(appRoot, 'src-tauri', 'tauri.conf.json');
const cargoManifestPath = path.join(appRoot, 'src-tauri', 'Cargo.toml');
const cargoLockPath = path.join(appRoot, 'src-tauri', 'Cargo.lock');
const defaultOssBaseUrl =
'https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc';
const updateManifestUrl =
process.env.VITE_AGC_UPDATE_MANIFEST_URL?.trim() ||
`${process.env.AGC_UPDATE_OSS_BASE_URL?.trim() || defaultOssBaseUrl}/latest.json`;
function readPackageJson() {
return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
}
export function compareVersions(left, right) {
const leftParts = left.split('.').map(Number);
const rightParts = right.split('.').map(Number);
for (let index = 0; index < 3; index += 1) {
if (leftParts[index] !== rightParts[index]) {
return leftParts[index] > rightParts[index] ? 1 : -1;
}
}
return 0;
}
function parseVersion(value, label) {
if (typeof value !== 'string' || !/^\d+\.\d+\.\d+$/u.test(value)) {
throw new Error(`${label} 不是有效的三段版本号:${String(value)}`);
}
return value;
}
export function nextPatchVersion(localVersion, remoteVersion) {
const local = parseVersion(localVersion, '本地版本');
const remote =
remoteVersion == null ? null : parseVersion(remoteVersion, 'OSS版本');
const base = remote && compareVersions(remote, local) > 0 ? remote : local;
const [major, minor, patch] = base.split('.').map(Number);
if (patch === Number.MAX_SAFE_INTEGER) {
throw new Error(`版本号 patch 已达到上限:${base}`);
}
return `${major}.${minor}.${patch + 1}`;
}
async function readRemoteVersion() {
let response;
try {
response = await fetch(updateManifestUrl, {
headers: { Accept: 'application/json' },
});
} catch (error) {
throw new Error(`读取 OSS 版本清单失败:${error.message}`);
}
if (response.status === 404) return null;
if (!response.ok) {
throw new Error(`读取 OSS 版本清单失败:HTTP ${response.status}`);
}
let manifest;
try {
manifest = await response.json();
} catch (error) {
throw new Error(`OSS 版本清单不是有效 JSON${error.message}`);
}
return parseVersion(manifest?.version, 'OSS版本清单 version');
}
function replaceVersionLine(source, version, pattern, label) {
if (!pattern.test(source)) throw new Error(`未找到${label}版本字段`);
return source.replace(pattern, `$1${version}$3`);
}
export async function prepareReleaseVersion() {
const localVersion = parseVersion(readPackageJson().version, '本地版本');
const remoteVersion = await readRemoteVersion();
const nextVersion = nextPatchVersion(localVersion, remoteVersion);
const packageSource = fs.readFileSync(packageJsonPath, 'utf8');
fs.writeFileSync(
packageJsonPath,
replaceVersionLine(
packageSource,
nextVersion,
/("version"\s*:\s*")([^"]+)(")/u,
'package.json',
),
);
const rootPackageLockSource = fs.readFileSync(rootPackageLockPath, 'utf8');
fs.writeFileSync(
rootPackageLockPath,
replaceVersionLine(
rootPackageLockSource,
nextVersion,
/("apps\/ai-game-creator-shell"\s*:\s*\{\s*\n\s*"name"\s*:\s*"@genarrative\/ai-game-creator-shell"\s*,\s*\n\s*"version"\s*:\s*")([^"]+)(")/u,
'root package-lock.json',
),
);
const tauriSource = fs.readFileSync(tauriConfigPath, 'utf8');
fs.writeFileSync(
tauriConfigPath,
replaceVersionLine(
tauriSource,
nextVersion,
/("version"\s*:\s*")([^"]+)(")/u,
'tauri.conf.json',
),
);
const cargoSource = fs.readFileSync(cargoManifestPath, 'utf8');
fs.writeFileSync(
cargoManifestPath,
replaceVersionLine(
cargoSource,
nextVersion,
/(^\[package\][\s\S]*?^version\s*=\s*")([^"]+)(")/mu,
'Cargo.toml',
),
);
const cargoLockSource = fs.readFileSync(cargoLockPath, 'utf8');
fs.writeFileSync(
cargoLockPath,
replaceVersionLine(
cargoLockSource,
nextVersion,
/(^name\s*=\s*"genarrative-ai-game-creator-shell"\s*\nversion\s*=\s*")([^"]+)(")/mu,
'Cargo.lock',
),
);
console.log(
`[ai-game-creator-shell] 版本 ${localVersion} / OSS ${remoteVersion ?? '不存在'} -> ${nextVersion}`,
);
return nextVersion;
}
export function runTauriBuild(args = []) {
const noBundle = args.includes('--no-bundle');
const hasTarget = args.includes('--target');
const targetArgs = noBundle || hasTarget ? [] : ['--target', releaseTarget];
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const result = spawnSync(
npmCommand,
[
'--prefix',
'../..',
'exec',
'tauri',
'--',
'build',
...targetArgs,
...args,
],
{ cwd: appRoot, stdio: 'inherit', shell: process.platform === 'win32' },
);
if (result.error) throw result.error;
if (result.status !== 0) process.exit(result.status ?? 1);
}
function listFiles(root) {
if (!fs.existsSync(root)) return [];
return fs.readdirSync(root, { withFileTypes: true }).flatMap((entry) => {
const fullPath = path.join(root, entry.name);
return entry.isDirectory() ? listFiles(fullPath) : [fullPath];
});
}
function artifactPriority(filePath) {
const name = path.basename(filePath).toLowerCase();
if (releaseTarget.includes('windows')) return name.endsWith('.exe') ? 0 : 99;
if (process.platform === 'darwin') return name.endsWith('.dmg') ? 0 : 99;
if (name.endsWith('.appimage')) return 0;
if (name.endsWith('.deb')) return 1;
if (name.endsWith('.rpm')) return 2;
return 99;
}
export function selectReleaseArtifact(files) {
const explicit = process.env.AGC_UPDATE_ARTIFACT?.trim();
if (explicit) {
const resolved = path.resolve(explicit);
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) {
throw new Error(`AGC_UPDATE_ARTIFACT 不是有效文件:${resolved}`);
}
return resolved;
}
return (
[...files]
.filter((filePath) => artifactPriority(filePath) < 99)
.sort((left, right) => {
const priority = artifactPriority(left) - artifactPriority(right);
return priority || left.localeCompare(right);
})[0] ?? null
);
}
export function createUpdateManifest(artifactPath) {
const bytes = fs.readFileSync(artifactPath);
const version = readPackageJson().version;
const fileName = path.basename(artifactPath);
const baseUrl = (
process.env.AGC_UPDATE_OSS_BASE_URL?.trim() || defaultOssBaseUrl
).replace(/\/+$/u, '');
const encodedFileName = encodeURIComponent(fileName).replace(/%2F/giu, '/');
return {
version,
downloadUrl: `${baseUrl}/${encodeURIComponent(version)}/${encodedFileName}`,
sha256: createHash('sha256').update(bytes).digest('hex'),
size: bytes.length,
...(process.env.AGC_UPDATE_RELEASE_NOTES?.trim()
? { releaseNotes: process.env.AGC_UPDATE_RELEASE_NOTES.trim() }
: {}),
};
}
export function generateUpdateManifest() {
const artifact = selectReleaseArtifact(listFiles(bundleRoot));
if (!artifact) {
throw new Error(`未找到可发布的 AGC 安装包:${bundleRoot}`);
}
const manifest = createUpdateManifest(artifact);
const manifestPath = path.join(bundleRoot, 'latest.json');
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
console.log(`[ai-game-creator-shell] 已生成 ${manifestPath}`);
console.log(`[ai-game-creator-shell] 安装包:${artifact}`);
return { artifact, manifestPath, manifest };
}
if (
process.argv[1] &&
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
) {
const args = process.argv.slice(2);
if (!args.includes('--no-bundle')) await prepareReleaseVersion();
runTauriBuild(args);
if (!args.includes('--no-bundle')) generateUpdateManifest();
}