PRA-only-demo/scripts/package.mjs

236 lines
10 KiB
JavaScript
Raw Normal View History

import { spawn, spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import process from "node:process";
import AdmZip from "adm-zip";
const root = process.cwd();
const packageJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
const version = packageJson.version;
const releaseRoot = path.resolve(root, "release", "dist");
const builderWork = path.join(os.tmpdir(), "RPA-Studio-electron-builder");
const builderUnpacked = path.join(builderWork, "win-unpacked");
const productFolderName = `RPA-Studio-${version}-win-x64`;
const productFolder = path.join(releaseRoot, productFolderName);
const zipPath = path.join(releaseRoot, `${productFolderName}.zip`);
const setupPath = path.join(releaseRoot, `RPA-Studio-Setup-${version}-win-x64.exe`);
const builderSetupPath = path.join(builderWork, `RPA-Studio-Setup-${version}-win-x64.exe`);
function assertInsideRelease(target) {
const relative = path.relative(releaseRoot, path.resolve(target));
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
throw new Error(`拒绝操作 release 目录外路径:${target}`);
}
}
function run(command, args) {
const result = spawnSync(command, args, { cwd: root, stdio: "inherit", shell: process.platform === "win32" });
if (result.status !== 0) throw new Error(`${command} ${args.join(" ")} 失败,退出码 ${result.status}`);
}
function delay(milliseconds) {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
function killProcessTree(pid) {
if (process.platform === "win32") {
spawnSync("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
}
}
function removeTree(target) {
fs.rmSync(target, { recursive: true, force: true, maxRetries: 10, retryDelay: 500 });
}
async function verifyLaunch(executable, label) {
const userData = path.join(os.tmpdir(), `RPA-Studio-UserData-${Date.now()}-${Math.random().toString(16).slice(2)}`);
const child = spawn(executable, [`--user-data-dir=${userData}`], {
cwd: path.dirname(executable),
stdio: "ignore",
windowsHide: false,
env: {
...process.env,
RPA_SKIP_SINGLE_INSTANCE: "1",
RPA_E2E: "1",
RPA_FIXTURE_PORT: String(18000 + Math.floor(Math.random() * 1000))
}
});
await delay(8000);
const exited = child.exitCode !== null || child.signalCode !== null;
if (!exited) {
killProcessTree(child.pid);
await delay(2000);
}
removeTree(userData);
if (exited) throw new Error(`${label} 启动后提前退出,退出码 ${child.exitCode}`);
console.log(`[package] ${label} launch verified`);
}
fs.mkdirSync(releaseRoot, { recursive: true });
for (const entry of fs.readdirSync(releaseRoot)) {
if (entry.startsWith(".installer")) {
const legacyTarget = path.join(releaseRoot, entry);
assertInsideRelease(legacyTarget);
fs.rmSync(legacyTarget, { recursive: true, force: true });
}
}
for (const target of [productFolder, zipPath, setupPath]) {
assertInsideRelease(target);
fs.rmSync(target, { recursive: true, force: true });
}
removeTree(builderWork);
console.log("[package] typecheck");
run("npm", ["run", "typecheck"]);
console.log("[package] tests");
run("npm", ["test"]);
console.log("[package] build");
run("npm", ["run", "build"]);
console.log("[package] browser e2e");
run("npm", ["run", "test:e2e"]);
const electronDist = path.join(root, "node_modules", "electron", "dist");
if (!fs.existsSync(path.join(electronDist, "electron.exe"))) {
throw new Error(`本机 Electron 运行时不完整:${electronDist}`);
}
console.log("[package] icon");
run("node", ["scripts/generate-icon.mjs"]);
console.log("[package] electron-builder nsis");
run(path.join(root, "node_modules", ".bin", "electron-builder.cmd"), [
"--win",
"--x64",
"--config",
path.join(root, "electron-builder.config.mjs")
]);
if (!fs.existsSync(builderSetupPath)) {
throw new Error(`electron-builder 未生成安装包:${builderSetupPath}`);
}
if (!fs.existsSync(path.join(builderUnpacked, "RPA Studio.exe"))) {
throw new Error(`electron-builder 未生成应用目录:${builderUnpacked}`);
}
2026-10-27 22:06:37 -04:00
const packagedCatalogPath = path.join(builderUnpacked, "resources", "catalog.json");
const packagedFixturePath = path.join(builderUnpacked, "resources", "fixture", "index.html");
const packagedPickerPath = path.join(builderUnpacked, "resources", "desktop-picker.ps1");
2026-10-27 22:06:37 -04:00
if (!fs.existsSync(packagedCatalogPath) || !fs.existsSync(packagedFixturePath)) {
throw new Error("打包产物缺少运行资源:resources/catalog.json 或 resources/fixture/index.html");
}
if (!fs.existsSync(packagedPickerPath)) {
throw new Error("打包产物缺少桌面拾取脚本:resources/desktop-picker.ps1 未随包发布");
}
2026-10-27 22:06:37 -04:00
const packagedCatalog = JSON.parse(fs.readFileSync(packagedCatalogPath, "utf8"));
if (!Array.isArray(packagedCatalog.components) || packagedCatalog.components.length === 0) {
throw new Error("打包产物 resources/catalog.json 无效:组件目录为空");
}
fs.cpSync(builderUnpacked, productFolder, { recursive: true });
fs.copyFileSync(builderSetupPath, setupPath);
removeTree(builderWork);
const git = spawnSync("git", ["rev-parse", "--short", "HEAD"], { cwd: root, encoding: "utf8" });
const revision = git.status === 0 ? git.stdout.trim() : "unknown";
const builtAt = new Date().toISOString();
fs.writeFileSync(path.join(productFolder, "version.json"), JSON.stringify({
product: "RPA Studio",
version,
revision,
builtAt,
platform: "win32-x64",
electron: packageJson.devDependencies.electron,
distribution: "portable"
}, null, 2));
fs.writeFileSync(path.join(productFolder, "使用说明.txt"), [
"RPA Studio Windows x64 免安装版",
"",
"1. 将整个文件夹解压到普通本地目录。",
"2. 双击 RPA Studio.exe 启动。",
"3. 任务、流程和运行记录默认保存在当前 Windows 用户的 AppData\\Roaming\\rpa-studio。",
"4. Word 自动化需要本机安装 Microsoft Word;其他文件级能力无需安装 Office。",
"5. 首次启动如被 Windows SmartScreen 提示,请核对同目录 SHA256SUMS.txt;当前构建未进行商业代码签名。",
"6. 如果双击没有窗口,请运行 启动诊断.cmd,并把自动打开的 startup.log 发给开发人员。",
"",
`版本:${version}`,
`源码提交:${revision}`,
`构建时间:${builtAt}`
].join("\r\n"), "utf8");
fs.writeFileSync(path.join(productFolder, "系统要求.txt"), [
"RPA Studio 系统要求",
"",
"- Windows 10 64 位或 Windows 11 64 位",
"- 建议至少 4 GB 内存和 500 MB 可用磁盘空间",
"- 必须先完整解压,不能直接在 ZIP 压缩包中双击 exe",
"- Word 自动化需要本机安装 Microsoft Word",
"- 当前版本未购买商业代码签名证书,SmartScreen 可能显示未知发布者",
"- 软件默认关闭 GPU 硬件加速,以兼容远程桌面、虚拟机和旧显卡驱动"
].join("\r\n"), "utf8");
fs.writeFileSync(path.join(productFolder, "启动诊断.cmd"), [
"@echo off",
"chcp 65001 >nul",
"set ELECTRON_ENABLE_LOGGING=1",
"set LOG=%APPDATA%\\rpa-studio\\logs\\startup.log",
"start \"RPA Studio\" \"%~dp0RPA Studio.exe\" --enable-logging --v=1",
"timeout /t 8 /nobreak >nul",
"if exist \"%LOG%\" (start notepad.exe \"%LOG%\") else (echo 未生成启动日志,请检查安全软件拦截记录。 & pause)"
].join("\r\n"), "utf8");
fs.writeFileSync(path.join(productFolder, "安装到本机.cmd"), [
"@echo off",
"chcp 65001 >nul",
"set \"TARGET=%LOCALAPPDATA%\\Programs\\RPA Studio\"",
"if not exist \"%TARGET%\" mkdir \"%TARGET%\"",
"robocopy \"%~dp0\" \"%TARGET%\" /E /R:2 /W:1 /NFL /NDL /NJH /NJS /NP >nul",
"powershell -NoProfile -ExecutionPolicy Bypass -Command \"$w=New-Object -ComObject WScript.Shell;$s=$w.CreateShortcut([IO.Path]::Combine([Environment]::GetFolderPath('Desktop'),'RPA Studio.lnk'));$s.TargetPath=[IO.Path]::Combine($env:LOCALAPPDATA,'Programs','RPA Studio','RPA Studio.exe');$s.WorkingDirectory=[IO.Path]::Combine($env:LOCALAPPDATA,'Programs','RPA Studio');$s.Save()\"",
"start \"RPA Studio\" \"%TARGET%\\RPA Studio.exe\"",
"exit /b 0"
].join("\r\n"), "utf8");
console.log("[package] portable smoke");
const portableSmokeRoot = fs.mkdtempSync(path.join(os.tmpdir(), "RPA-Studio-Portable-Smoke-"));
const portableSmokeFolder = path.join(portableSmokeRoot, productFolderName);
fs.cpSync(productFolder, portableSmokeFolder, { recursive: true });
await verifyLaunch(path.join(portableSmokeFolder, "RPA Studio.exe"), "portable");
removeTree(portableSmokeRoot);
console.log("[package] zip");
const zip = new AdmZip();
zip.addLocalFolder(productFolder, productFolderName);
zip.writeZip(zipPath);
const zipSha256 = createHash("sha256").update(fs.readFileSync(zipPath)).digest("hex");
const setupSha256 = createHash("sha256").update(fs.readFileSync(setupPath)).digest("hex");
console.log("[package] nsis smoke");
const installSmokeRoot = fs.mkdtempSync(path.join(os.tmpdir(), "RPA-Studio-NSIS-Smoke-"));
const installSmokeSetup = path.join(installSmokeRoot, path.basename(setupPath));
const installSmokeTarget = path.join(installSmokeRoot, "RPAStudio");
fs.copyFileSync(setupPath, installSmokeSetup);
const installed = spawnSync(installSmokeSetup, ["/S", `/D=${installSmokeTarget}`], {
cwd: installSmokeRoot,
stdio: "inherit",
windowsHide: true,
timeout: 180000
});
if (installed.status !== 0 || installed.error) {
throw new Error(`NSIS 安装包冒烟失败,退出码 ${installed.status}:${installed.error?.message || "unknown error"}`);
}
if (!fs.existsSync(path.join(installSmokeTarget, "RPA Studio.exe"))) {
throw new Error("NSIS 安装包冒烟失败:未生成主程序");
}
2026-10-27 22:06:37 -04:00
if (!fs.existsSync(path.join(installSmokeTarget, "resources", "catalog.json"))) {
throw new Error("NSIS 安装包冒烟失败:catalog.json 未安装");
}
if (!fs.existsSync(path.join(installSmokeTarget, "resources", "desktop-picker.ps1"))) {
throw new Error("NSIS 安装包冒烟失败:desktop-picker.ps1 未安装");
}
await verifyLaunch(path.join(installSmokeTarget, "RPA Studio.exe"), "installed");
removeTree(installSmokeRoot);
const sums = [
`${zipSha256} ${path.basename(zipPath)}`,
`${setupSha256} ${path.basename(setupPath)}`
];
fs.writeFileSync(path.join(releaseRoot, "SHA256SUMS.txt"), `${sums.join("\r\n")}\r\n`, "utf8");
2026-10-27 22:06:37 -04:00
console.log(JSON.stringify({ productFolder, zipPath, setupPath, zipSha256, setupSha256 }, null, 2));