2026-10-27 06:16:03 -04:00
|
|
|
|
import { spawn, spawnSync } from "node:child_process";
|
2026-10-26 23:35:13 -04:00
|
|
|
|
import { createHash } from "node:crypto";
|
|
|
|
|
|
import fs from "node:fs";
|
2026-10-27 06:16:03 -04:00
|
|
|
|
import os from "node:os";
|
2026-10-26 23:35:13 -04:00
|
|
|
|
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;
|
2026-10-27 06:16:03 -04:00
|
|
|
|
const releaseRoot = path.resolve(root, "release", "dist");
|
2026-10-26 23:35:13 -04:00
|
|
|
|
const productFolderName = `RPA-Studio-${version}-win-x64`;
|
|
|
|
|
|
const productFolder = path.join(releaseRoot, productFolderName);
|
|
|
|
|
|
const zipPath = path.join(releaseRoot, `${productFolderName}.zip`);
|
2026-10-27 04:43:28 -04:00
|
|
|
|
const setupPath = path.join(releaseRoot, `RPA-Studio-Setup-${version}-win-x64.exe`);
|
2026-10-27 06:16:03 -04:00
|
|
|
|
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";
|
2026-10-26 23:35:13 -04:00
|
|
|
|
|
|
|
|
|
|
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}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-27 06:16:03 -04:00
|
|
|
|
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`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-26 23:35:13 -04:00
|
|
|
|
fs.mkdirSync(releaseRoot, { recursive: true });
|
2026-10-27 06:16:03 -04:00
|
|
|
|
for (const target of [productFolder, zipPath, setupPath, installerPayloadArchive, installerBootstrap]) {
|
2026-10-26 23:35:13 -04:00
|
|
|
|
assertInsideRelease(target);
|
|
|
|
|
|
fs.rmSync(target, { recursive: true, force: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log("[package] typecheck");
|
|
|
|
|
|
run("npm", ["run", "typecheck"]);
|
|
|
|
|
|
console.log("[package] tests");
|
|
|
|
|
|
run("npm", ["test"]);
|
2026-10-27 04:44:33 -04:00
|
|
|
|
console.log("[package] browser e2e");
|
|
|
|
|
|
run("npm", ["run", "test:e2e"]);
|
2026-10-26 23:35:13 -04:00
|
|
|
|
console.log("[package] build");
|
|
|
|
|
|
run("npm", ["run", "build"]);
|
|
|
|
|
|
|
2026-10-27 06:16:03 -04:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-26 23:35:13 -04:00
|
|
|
|
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,
|
2026-10-27 06:16:03 -04:00
|
|
|
|
electronZipDir,
|
2026-10-26 23:35:13 -04:00
|
|
|
|
appVersion: version,
|
|
|
|
|
|
win32metadata: {
|
|
|
|
|
|
CompanyName: "RPA Studio",
|
|
|
|
|
|
FileDescription: "RPA Studio Desktop Automation",
|
|
|
|
|
|
ProductName: "RPA Studio",
|
|
|
|
|
|
InternalName: "RPA Studio"
|
|
|
|
|
|
},
|
|
|
|
|
|
ignore: [
|
|
|
|
|
|
/^[/\\]\.git($|[/\\])/,
|
|
|
|
|
|
/^[/\\]release($|[/\\])/,
|
|
|
|
|
|
/^[/\\]_analysis($|[/\\])/,
|
|
|
|
|
|
/^[/\\]docs($|[/\\])/,
|
|
|
|
|
|
/^[/\\]scripts($|[/\\])/,
|
|
|
|
|
|
/^[/\\]src($|[/\\])/,
|
2026-10-27 06:16:03 -04:00
|
|
|
|
/^[/\\]node_modules[/\\]\.cache($|[/\\])/,
|
2026-10-26 23:35:13 -04:00
|
|
|
|
/\.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;当前构建未进行商业代码签名。",
|
2026-10-27 04:43:28 -04:00
|
|
|
|
"6. 如果双击没有窗口,请运行 启动诊断.cmd,并把自动打开的 startup.log 发给开发人员。",
|
2026-10-26 23:35:13 -04:00
|
|
|
|
"",
|
|
|
|
|
|
`版本:${version}`,
|
|
|
|
|
|
`源码提交:${revision}`,
|
|
|
|
|
|
`构建时间:${builtAt}`
|
|
|
|
|
|
].join("\r\n"), "utf8");
|
2026-10-27 04:43:28 -04:00
|
|
|
|
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");
|
2026-10-27 06:16:03 -04:00
|
|
|
|
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);
|
2026-10-26 23:35:13 -04:00
|
|
|
|
|
|
|
|
|
|
console.log("[package] zip");
|
|
|
|
|
|
const zip = new AdmZip();
|
|
|
|
|
|
zip.addLocalFolder(productFolder, productFolderName);
|
|
|
|
|
|
zip.writeZip(zipPath);
|
2026-10-27 04:43:28 -04:00
|
|
|
|
const zipSha256 = createHash("sha256").update(fs.readFileSync(zipPath)).digest("hex");
|
|
|
|
|
|
|
|
|
|
|
|
let setupSha256 = "";
|
2026-10-27 06:16:03 -04:00
|
|
|
|
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
|
|
|
|
|
|
]));
|
2026-10-27 04:53:03 -04:00
|
|
|
|
setupSha256 = createHash("sha256").update(fs.readFileSync(setupPath)).digest("hex");
|
2026-10-27 06:16:03 -04:00
|
|
|
|
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);
|
|
|
|
|
|
|
2026-10-27 04:43:28 -04:00
|
|
|
|
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");
|
2026-10-26 23:35:13 -04:00
|
|
|
|
|
2026-10-27 04:43:28 -04:00
|
|
|
|
console.log(JSON.stringify({ productFolder, zipPath, setupPath: setupSha256 ? setupPath : null, zipSha256, setupSha256: setupSha256 || null }, null, 2));
|