138 lines
5.3 KiB
JavaScript
138 lines
5.3 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* APS 桌面端离线升级清单生成器(packaging 内新增文件,不占用 scripts/ 写范围)。
|
||
*
|
||
* 用法:
|
||
* node packaging/build-upgrade-manifest.mjs \
|
||
* --installer release/aps-agent-desktop-0.2.0.exe \
|
||
* --version 0.2.0 \
|
||
* --out packaging/upgrade-manifest.json \
|
||
* [--product aps-agent-desktop] [--platform win32] [--arch x64] \
|
||
* [--releasedAt 2026-08-02T00:00:00Z] [--minSupportedVersion 0.1.0]
|
||
*
|
||
* # 校验已有清单与安装包是否一致(CI / 上传前自检)
|
||
* node packaging/build-upgrade-manifest.mjs --verify packaging/upgrade-manifest.json
|
||
*
|
||
* 设计说明:
|
||
* - 签名/SBOM 上链需要外部证书与信任根,本轮只生成「校验和 + 大小 + 版本」的完整性清单;
|
||
* signature / provenance 字段预留为可选占位,由外部流程填充后发布。
|
||
* - 输出文件与 electron-builder 产物同目录使用时,fileName 建议与 artifactName 模板一致:
|
||
* aps-agent-desktop-${version}.${ext}
|
||
*/
|
||
import { createHash } from 'node:crypto';
|
||
import { createReadStream } from 'node:fs';
|
||
import { readFile, stat, writeFile } from 'node:fs/promises';
|
||
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||
|
||
function usage() {
|
||
process.stderr.write(
|
||
`usage: node ${path.basename(process.argv[1] || 'build-upgrade-manifest.mjs')} ` +
|
||
`--installer <path> --version <v> --out <path> [--product p] [--platform p] [--arch a] [--releasedAt ISO] [--minSupportedVersion v]\n` +
|
||
` node ${path.basename(process.argv[1] || 'build-upgrade-manifest.mjs')} --verify <manifest.json>\n`,
|
||
);
|
||
process.exit(2);
|
||
}
|
||
|
||
function parseArgs(argv) {
|
||
const options = {};
|
||
for (let i = 0; i < argv.length; i += 1) {
|
||
const arg = argv[i];
|
||
const next = () => argv[++i];
|
||
if (arg === '--installer') options.installer = next();
|
||
else if (arg === '--version') options.version = next();
|
||
else if (arg === '--out') options.out = next();
|
||
else if (arg === '--product') options.product = next();
|
||
else if (arg === '--platform') options.platform = next();
|
||
else if (arg === '--arch') options.arch = next();
|
||
else if (arg === '--releasedAt') options.releasedAt = next();
|
||
else if (arg === '--minSupportedVersion') options.minSupportedVersion = next();
|
||
else if (arg === '--verify') options.verify = next();
|
||
else if (arg === '--help' || arg === '-h') usage();
|
||
else {
|
||
process.stderr.write(`unknown argument: ${arg}\n`);
|
||
usage();
|
||
}
|
||
}
|
||
return options;
|
||
}
|
||
|
||
async function computeSha256(filePath) {
|
||
const hash = createHash('sha256');
|
||
await new Promise((resolve, reject) => {
|
||
const stream = createReadStream(filePath);
|
||
stream.on('data', (chunk) => hash.update(chunk));
|
||
stream.on('end', resolve);
|
||
stream.on('error', reject);
|
||
});
|
||
return hash.digest('hex');
|
||
}
|
||
|
||
async function verifyManifest(manifestPath) {
|
||
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
||
const required = ['schemaVersion', 'product', 'version', 'package'];
|
||
for (const key of required) {
|
||
if (!manifest[key]) throw new Error(`manifest missing required field: ${key}`);
|
||
}
|
||
const pkg = manifest.package;
|
||
const installerPath = path.resolve(path.dirname(manifestPath), pkg.fileName);
|
||
let info;
|
||
try {
|
||
info = await stat(installerPath);
|
||
} catch (error) {
|
||
throw new Error(`installer not found next to manifest: ${installerPath} (${error.message})`);
|
||
}
|
||
if (info.size !== pkg.size) {
|
||
throw new Error(`size mismatch: manifest=${pkg.size} actual=${info.size}`);
|
||
}
|
||
const actual = await computeSha256(installerPath);
|
||
if (actual !== pkg.sha256) {
|
||
throw new Error(`sha256 mismatch: manifest=${pkg.sha256} actual=${actual}`);
|
||
}
|
||
return { manifest, installerPath };
|
||
}
|
||
|
||
async function main() {
|
||
const options = parseArgs(process.argv.slice(2));
|
||
if (options.verify) {
|
||
const { manifest, installerPath } = await verifyManifest(options.verify);
|
||
process.stdout.write(
|
||
`OK ${manifest.version} ${installerPath} size=${manifest.package.size} sha256=${manifest.package.sha256}\n`,
|
||
);
|
||
return;
|
||
}
|
||
if (!options.installer || !options.version || !options.out) usage();
|
||
|
||
const installerPath = path.resolve(options.installer);
|
||
const info = await stat(installerPath);
|
||
if (!info.isFile()) throw new Error(`not a file: ${installerPath}`);
|
||
const sha256 = await computeSha256(installerPath);
|
||
|
||
const manifest = {
|
||
schemaVersion: 1,
|
||
product: options.product || 'aps-agent-desktop',
|
||
version: options.version,
|
||
...(options.platform ? { platform: options.platform } : {}),
|
||
...(options.arch ? { arch: options.arch } : {}),
|
||
package: {
|
||
fileName: path.basename(installerPath),
|
||
sha256,
|
||
size: info.size,
|
||
},
|
||
releasedAt: options.releasedAt || new Date().toISOString(),
|
||
...(options.minSupportedVersion ? { minSupportedVersion: options.minSupportedVersion } : {}),
|
||
};
|
||
|
||
const outPath = path.resolve(options.out);
|
||
await writeFile(outPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
||
process.stdout.write(
|
||
`wrote ${path.relative(process.cwd(), outPath)}: ${manifest.version} size=${info.size} sha256=${sha256}\n`,
|
||
);
|
||
}
|
||
|
||
main().catch((error) => {
|
||
process.stderr.write(`ERROR: ${error.message}\n`);
|
||
process.exit(1);
|
||
}); |