PRA-only-demo/scripts/package.mjs

236 lines
10 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 { 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] browser e2e");
run("npm", ["run", "test:e2e"]);
console.log("[package] build");
run("npm", ["run", "build"]);
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}`);
}
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");
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 未随包发布");
}
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 安装包冒烟失败:未生成主程序");
}
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");
console.log(JSON.stringify({ productFolder, zipPath, setupPath, zipSha256, setupSha256 }, null, 2));