PRA-only-demo/scripts/package.mjs

273 lines
12 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";
import packager from "@electron/packager";
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 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 installerPayloadArchive = path.join(releaseRoot, ".installer-payload.7z");
const installerBootstrap = path.join(releaseRoot, ".installer-bootstrap.exe");
const installerSource = path.join(root, "scripts", "installer", "Program.cpp");
const payloadMagic = Buffer.from("RPASTUDIOPAYLOAD", "ascii");
const electronZipDir = path.join(root, "node_modules", ".cache", "rpa-packager");
const electronZipPath = path.join(electronZipDir, `electron-v${packageJson.devDependencies.electron}-win32-x64.zip`);
const sevenZipRoot = process.env.RPA_7ZIP_DIR || "C:/Program Files/7-Zip";
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
});
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 target of [productFolder, zipPath, setupPath, installerPayloadArchive, installerBootstrap]) {
assertInsideRelease(target);
fs.rmSync(target, { recursive: true, force: true });
}
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"]);
if (!fs.existsSync(electronZipPath)) {
console.log("[package] local electron runtime cache");
const electronDist = path.join(root, "node_modules", "electron", "dist");
if (!fs.existsSync(path.join(electronDist, "electron.exe"))) throw new Error(`本机 Electron 运行时不完整:${electronDist}`);
fs.mkdirSync(electronZipDir, { recursive: true });
const electronZip = new AdmZip();
electronZip.addLocalFolder(electronDist);
electronZip.writeZip(electronZipPath);
}
console.log("[package] electron packager");
const outputs = await packager({
dir: root,
name: "RPA Studio",
platform: "win32",
arch: "x64",
out: releaseRoot,
overwrite: true,
prune: true,
asar: true,
electronVersion: packageJson.devDependencies.electron,
electronZipDir,
appVersion: version,
win32metadata: {
CompanyName: "RPA Studio",
FileDescription: "RPA Studio Desktop Automation",
ProductName: "RPA Studio",
InternalName: "RPA Studio"
},
ignore: [
/^[/\\]\.git($|[/\\])/,
/^[/\\]release($|[/\\])/,
/^[/\\]_analysis($|[/\\])/,
/^[/\\]docs($|[/\\])/,
/^[/\\]scripts($|[/\\])/,
/^[/\\]src($|[/\\])/,
/^[/\\]node_modules[/\\]\.cache($|[/\\])/,
/\.test\.[cm]?[jt]sx?$/
]
});
const packaged = outputs[0];
if (!packaged || !fs.existsSync(path.join(packaged, "RPA Studio.exe"))) {
throw new Error(`打包产物无效:${packaged || "未返回目录"}`);
}
if (path.resolve(packaged) !== path.resolve(productFolder)) {
assertInsideRelease(packaged);
fs.renameSync(packaged, productFolder);
}
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");
const sevenZipLicense = path.join(sevenZipRoot, "License.txt");
if (fs.existsSync(sevenZipLicense)) fs.copyFileSync(sevenZipLicense, path.join(productFolder, "第三方许可-7-Zip.txt"));
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");
let setupSha256 = "";
console.log("[package] setup bootstrap");
const generatedInstallerSource = path.join(releaseRoot, ".installer.generated.cpp");
const source = fs.readFileSync(installerSource, "utf8").replaceAll("__PRODUCT_VERSION__", version);
fs.writeFileSync(generatedInstallerSource, source, "utf8");
const compiler = process.env.CXX || "g++";
const compiled = spawnSync(compiler, [
"-std=c++17", "-O2", "-static", "-mwindows", "-municode", generatedInstallerSource,
"-o", installerBootstrap, "-lole32", "-lshell32", "-luuid"
], { cwd: root, stdio: "inherit", windowsHide: true });
if (compiled.status !== 0 || compiled.error || !fs.existsSync(installerBootstrap)) {
throw new Error(`原生安装器编译失败,退出码 ${compiled.status}:${compiled.error?.message || "unknown error"}`);
}
const sevenCli = path.join(sevenZipRoot, "7z.exe");
const sevenGui = path.join(sevenZipRoot, "7zG.exe");
const sevenDll = path.join(sevenZipRoot, "7z.dll");
for (const required of [sevenCli, sevenGui, sevenDll]) {
if (!fs.existsSync(required)) throw new Error(`安装器缺少 7-Zip 构建依赖:${required}`);
}
const archived = spawnSync(sevenCli, ["a", "-t7z", installerPayloadArchive, ".", "-mx=9", "-mmt=on"], {
cwd: productFolder,
stdio: "inherit",
windowsHide: true
});
if (archived.status !== 0 || !fs.existsSync(installerPayloadArchive)) throw new Error(`安装载荷压缩失败,退出码 ${archived.status}`);
const setupParts = [installerBootstrap, sevenCli, sevenGui, sevenDll, installerPayloadArchive].map((file) => fs.readFileSync(file));
const lengthBuffers = setupParts.slice(1).map((content) => {
const buffer = Buffer.alloc(8);
buffer.writeBigUInt64LE(BigInt(content.length));
return buffer;
});
fs.writeFileSync(setupPath, Buffer.concat([
...setupParts,
...lengthBuffers,
payloadMagic
]));
setupSha256 = createHash("sha256").update(fs.readFileSync(setupPath)).digest("hex");
for (const target of [installerPayloadArchive, installerBootstrap, generatedInstallerSource]) {
assertInsideRelease(target);
fs.rmSync(target, { recursive: true, force: true });
}
console.log("[package] installer smoke");
const installSmokeRoot = fs.mkdtempSync(path.join(os.tmpdir(), "RPA-Studio-Installer-Smoke-"));
const installSmokeSetup = path.join(installSmokeRoot, path.basename(setupPath));
const installSmokeTarget = path.join(installSmokeRoot, "RPA Studio");
fs.copyFileSync(setupPath, installSmokeSetup);
const installed = spawnSync(installSmokeSetup, ["/S", "/NOLAUNCH", "/NOSHORTCUT", `/TARGET=${installSmokeTarget}`], {
cwd: installSmokeRoot,
stdio: "inherit",
windowsHide: true,
timeout: 180000
});
if (installed.status !== 0 || installed.error) {
throw new Error(`安装包冒烟失败,退出码 ${installed.status}:${installed.error?.message || "unknown error"}`);
}
if (!fs.existsSync(path.join(installSmokeTarget, "RPA Studio.exe"))) throw new Error("安装包冒烟失败:未生成主程序");
await verifyLaunch(path.join(installSmokeTarget, "RPA Studio.exe"), "installed");
removeTree(installSmokeRoot);
const sums = [`${zipSha256} ${path.basename(zipPath)}`];
if (setupSha256) sums.push(`${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: setupSha256 ? setupPath : null, zipSha256, setupSha256: setupSha256 || null }, null, 2));