185 lines
9.1 KiB
JavaScript
185 lines
9.1 KiB
JavaScript
import { spawnSync } from "node:child_process";
|
||
import { createHash } from "node:crypto";
|
||
import fs from "node:fs";
|
||
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");
|
||
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 installerPayload = path.join(releaseRoot, ".installer-payload");
|
||
const installerSed = path.join(releaseRoot, ".installer.sed");
|
||
|
||
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}`);
|
||
}
|
||
|
||
fs.mkdirSync(releaseRoot, { recursive: true });
|
||
for (const target of [productFolder, zipPath, setupPath, installerPayload, installerSed]) {
|
||
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"]);
|
||
|
||
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,
|
||
appVersion: version,
|
||
win32metadata: {
|
||
CompanyName: "RPA Studio",
|
||
FileDescription: "RPA Studio Desktop Automation",
|
||
ProductName: "RPA Studio",
|
||
InternalName: "RPA Studio"
|
||
},
|
||
ignore: [
|
||
/^[/\\]\.git($|[/\\])/,
|
||
/^[/\\]release($|[/\\])/,
|
||
/^[/\\]_analysis($|[/\\])/,
|
||
/^[/\\]docs($|[/\\])/,
|
||
/^[/\\]scripts($|[/\\])/,
|
||
/^[/\\]src($|[/\\])/,
|
||
/\.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");
|
||
|
||
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 = "";
|
||
const iexpress = path.join(process.env.SystemRoot || "C:/Windows", "System32", "iexpress.exe");
|
||
if (!fs.existsSync(iexpress)) throw new Error(`找不到 Windows IExpress:${iexpress}`);
|
||
console.log("[package] setup iexpress");
|
||
fs.mkdirSync(installerPayload, { recursive: true });
|
||
fs.copyFileSync(zipPath, path.join(installerPayload, "payload.zip"));
|
||
fs.writeFileSync(path.join(installerPayload, "install.cmd"), [
|
||
"@echo off",
|
||
"chcp 65001 >nul",
|
||
"set \"TARGET=%LOCALAPPDATA%\\Programs\\RPA Studio\"",
|
||
"set \"STAGE=%TEMP%\\RPA-Studio-Install-%RANDOM%\"",
|
||
"powershell -NoProfile -ExecutionPolicy Bypass -Command \"Expand-Archive -LiteralPath '%~dp0payload.zip' -DestinationPath '%STAGE%' -Force\"",
|
||
`robocopy "%STAGE%\\${productFolderName}" "%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\"",
|
||
"rmdir /s /q \"%STAGE%\"",
|
||
"exit /b 0"
|
||
].join("\r\n"), "utf8");
|
||
const sourceDir = `${installerPayload.replace(/\//g, "\\")}\\`;
|
||
fs.writeFileSync(installerSed, [
|
||
"[Version]", "Class=IEXPRESS", "SEDVersion=3", "[Options]", "PackagePurpose=InstallApp", "ShowInstallProgramWindow=1",
|
||
"HideExtractAnimation=0", "UseLongFileName=1", "InsideCompressed=0", "CAB_FixedSize=0", "CAB_ResvCodeSigning=0",
|
||
"RebootMode=N", "InstallPrompt=%InstallPrompt%", "DisplayLicense=", "FinishMessage=%FinishMessage%",
|
||
`TargetName=${setupPath}`, `FriendlyName=RPA Studio ${version}`, "AppLaunched=install.cmd", "PostInstallCmd=<None>",
|
||
"AdminQuietInstCmd=install.cmd", "UserQuietInstCmd=install.cmd", "SourceFiles=SourceFiles", "[Strings]",
|
||
`InstallPrompt=Install RPA Studio ${version} for the current Windows user?`, "FinishMessage=RPA Studio installation completed.",
|
||
"FILE0=payload.zip", "FILE1=install.cmd", "[SourceFiles]", `SourceFiles0=${sourceDir}`, "[SourceFiles0]", "%FILE0%=", "%FILE1%=", ""
|
||
].join("\r\n"), "utf8");
|
||
const builtSetup = spawnSync(iexpress, ["/N", installerSed], { cwd: releaseRoot, stdio: "inherit", windowsHide: true });
|
||
if (builtSetup.status !== 0 || !fs.existsSync(setupPath)) throw new Error(`IExpress 安装包生成失败,退出码 ${builtSetup.status}`);
|
||
setupSha256 = createHash("sha256").update(fs.readFileSync(setupPath)).digest("hex");
|
||
fs.rmSync(installerPayload, { recursive: true, force: true });
|
||
fs.rmSync(installerSed, { force: true });
|
||
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));
|