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`); 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]) { 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] 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;当前构建未进行商业代码签名。", "", `版本:${version}`, `源码提交:${revision}`, `构建时间:${builtAt}` ].join("\r\n"), "utf8"); console.log("[package] zip"); const zip = new AdmZip(); zip.addLocalFolder(productFolder, productFolderName); zip.writeZip(zipPath); const sha256 = createHash("sha256").update(fs.readFileSync(zipPath)).digest("hex"); fs.writeFileSync(path.join(releaseRoot, "SHA256SUMS.txt"), `${sha256} ${path.basename(zipPath)}\r\n`, "utf8"); console.log(JSON.stringify({ productFolder, zipPath, sha256 }, null, 2));