electron-pack-win.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. // Windows x64:Vite build + electron-builder + 将 node/python/adb 与 pack-resources 内 static、模板等并入 win-unpacked。入口:package/package-x64.bat 或 npm run electron:build:x64
  2. const { execSync, spawn } = require('child_process')
  3. const path = require('path')
  4. const fs = require('fs')
  5. const crypto = require('crypto')
  6. const projectRoot = path.resolve(__dirname, '..', '..')
  7. const buildDir = path.join(projectRoot, 'package', 'build')
  8. /** 打包资源目录(与本脚本同目录):打进/并入 win-unpacked 的 static、模板 config/run、kill-all-process.ps1,以及 electron zip、nsis、winCodeSign、patches 等 */
  9. const pkgResDir = __dirname
  10. /** Node 目录(源码执行与打包均使用),需先下载 Node 到 nodejs/node */
  11. const NODE_PORTABLE_DIR = path.join(projectRoot, 'nodejs', 'node')
  12. const ELECTRON_MIRROR = process.env.ELECTRON_MIRROR
  13. /** 构建前校验:pack-resources 内离线依赖必须齐全,缺一不可。不通过则直接退出,绝不下载。 */
  14. function checkLocalResourcesOrExit() {
  15. const version = getElectronVersion()
  16. const missing = []
  17. if (!version) {
  18. missing.push('无法读取 Electron 版本(node_modules/electron/package.json)')
  19. } else {
  20. const electronDir = path.join(pkgResDir, `electron-v${version}-win32-x64`)
  21. const electronZip = path.join(pkgResDir, `electron-v${version}-win32-x64.zip`)
  22. if (!fs.existsSync(electronDir) && !fs.existsSync(electronZip)) {
  23. missing.push(`Electron: 需要目录或 zip → ${path.basename(electronDir)} 或 ${path.basename(electronZip)}`)
  24. }
  25. }
  26. const nsisDir = path.join(pkgResDir, 'nsis-3.0.4.1')
  27. if (!fs.existsSync(nsisDir) || !fs.statSync(nsisDir).isDirectory()) {
  28. missing.push('NSIS: 需要目录 → package/pack-resources/nsis-3.0.4.1')
  29. }
  30. const winCodeSignDir = path.join(pkgResDir, 'winCodeSign')
  31. if (!fs.existsSync(winCodeSignDir) || !fs.statSync(winCodeSignDir).isDirectory()) {
  32. missing.push('winCodeSign: 需要目录 → package/pack-resources/winCodeSign')
  33. }
  34. if (process.platform === 'win32') {
  35. const portableExe = path.join(NODE_PORTABLE_DIR, 'node.exe')
  36. if (!fs.existsSync(portableExe)) {
  37. missing.push('Node: 需要 nodejs/node/node.exe,请先下载 Node 到 nodejs/node')
  38. }
  39. }
  40. if (missing.length === 0) return
  41. console.error('\n[错误] 本地资源不完整,禁止下载,已停止打包。请将所需资源放入 package/pack-resources/ 后重试:')
  42. missing.forEach((m) => console.error(' - ' + m))
  43. process.exit(1)
  44. }
  45. /** 确保 package/pack-resources/winCodeSign 存在(app-builder 通过 getBin("winCodeSign") 使用),缺则退出。 */
  46. function ensureWinCodeSignInPackageOrExit() {
  47. const targetDir = path.join(pkgResDir, 'winCodeSign')
  48. if (fs.existsSync(targetDir) && fs.statSync(targetDir).isDirectory()) return
  49. console.error('\n[错误] 缺少 package/pack-resources/winCodeSign,已停止打包(禁止下载)。')
  50. process.exit(1)
  51. }
  52. /** 取当前 Electron 版本 */
  53. function getElectronVersion() {
  54. try {
  55. return require(path.join(projectRoot, 'node_modules', 'electron', 'package.json')).version
  56. } catch (e) {
  57. return null
  58. }
  59. }
  60. /** 若存在已解压目录 package/pack-resources/electron-vX.Y.Z-win32-x64,返回其绝对路径,供 electronDist 直接引用 */
  61. function getUnpackedElectronDir() {
  62. const version = getElectronVersion()
  63. if (!version) return null
  64. const dir = path.join(pkgResDir, `electron-v${version}-win32-x64`)
  65. return fs.existsSync(dir) ? dir : null
  66. }
  67. /** 与 @electron/get 的 Cache.getCacheDirectory 一致:用 URL 目录部分的 sha256 作为缓存子目录 */
  68. function getElectronCacheSubdir(version) {
  69. const base = (ELECTRON_MIRROR || 'https://github.com/electron/electron/releases/download').replace(/\/$/, '')
  70. const strippedUrl = `${base}/v${version}`
  71. return crypto.createHash('sha256').update(strippedUrl).digest('hex')
  72. }
  73. /** 若未使用解压目录,则确保本目录下的 Electron zip 已放入 @electron/get 使用的缓存路径 */
  74. function ensureElectronZipInCache() {
  75. const unpacked = getUnpackedElectronDir()
  76. if (unpacked) return
  77. const version = getElectronVersion()
  78. if (!version) return
  79. const zipName = `electron-v${version}-win32-x64.zip`
  80. const zipInPkg = path.join(pkgResDir, zipName)
  81. const cacheSubdir = getElectronCacheSubdir(version)
  82. const cacheDir = path.join(pkgResDir, cacheSubdir)
  83. const zipInCache = path.join(cacheDir, zipName)
  84. if (!fs.existsSync(zipInPkg)) return
  85. if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true })
  86. if (fs.existsSync(zipInCache)) return
  87. try {
  88. fs.copyFileSync(zipInPkg, zipInCache)
  89. } catch (_) {}
  90. }
  91. function getBuilderEnv() {
  92. ensureElectronZipInCache()
  93. ensureWinCodeSignInPackageOrExit()
  94. const electronCacheRoot = pkgResDir + path.sep
  95. const builderCache = path.resolve(pkgResDir)
  96. process.env.ELECTRON_BUILDER_CACHE = builderCache
  97. return {
  98. ...process.env,
  99. ELECTRON_MIRROR,
  100. ELECTRON_CACHE: electronCacheRoot,
  101. electron_config_cache: electronCacheRoot,
  102. /** ELECTRON_BUILDER_CACHE 指向 pack-resources:nsis、winCodeSign 等均从本目录读,禁止下载 */
  103. ELECTRON_BUILDER_CACHE: builderCache,
  104. }
  105. }
  106. /** 输出到 package/build */
  107. const X64_OUTPUT_DIR = 'package/build'
  108. const X64_UNPACKED_DIR = 'win-unpacked'
  109. /** Windows:结束可能占用输出目录的进程,避免 EBUSY。 */
  110. function killProcessesLockingOutput() {
  111. if (process.platform !== 'win32') return
  112. const exeNames = ['AndroidRemoteController.exe', 'electron-react-vite-app.exe']
  113. for (const name of exeNames) {
  114. try {
  115. execSync(`taskkill /F /IM "${name}" 2>nul`, { stdio: 'ignore', windowsHide: true })
  116. } catch (_) {}
  117. }
  118. }
  119. function sleep(ms) {
  120. return new Promise((r) => setTimeout(r, ms))
  121. }
  122. /** 生成临时配置:仅打 dir、输出到 package/build、用本地 electronDist、不签名不 rcedit;显式包含 files/asarUnpack 确保 nodejs 打进包 */
  123. function getElectronDistConfigPath() {
  124. const unpacked = getUnpackedElectronDir()
  125. const config = {
  126. electronDist: unpacked || undefined,
  127. directories: { output: X64_OUTPUT_DIR },
  128. files: [
  129. 'electron/**',
  130. 'config.js',
  131. 'nodejs/**',
  132. '!nodejs/node/**',
  133. 'python/scripts/**',
  134. 'lib/**'
  135. ],
  136. asarUnpack: ['nodejs/**', 'config.js', 'lib/**', 'python/scripts/**'],
  137. win: {
  138. target: [{ target: 'dir', arch: ['x64'] }],
  139. signAndEditExecutable: false
  140. }
  141. }
  142. if (!config.electronDist) delete config.electronDist
  143. const configPath = path.join(pkgResDir, 'electron-builder-x64-tmp.json')
  144. fs.writeFileSync(configPath, JSON.stringify(config, null, 0), 'utf8')
  145. return configPath
  146. }
  147. function runElectronBuilder(cwd, env) {
  148. const cliPath = path.join(cwd, 'node_modules', 'electron-builder', 'out', 'cli', 'cli.js')
  149. if (!fs.existsSync(cliPath)) {
  150. return Promise.reject(new Error('未找到 electron-builder,请先在项目根目录执行 npm install。'))
  151. }
  152. const args = ['--win', '--x64', '--config', getElectronDistConfigPath()]
  153. const builderCache = path.resolve(pkgResDir)
  154. const envCopy = { ...process.env, ...env }
  155. envCopy.ELECTRON_BUILDER_CACHE = builderCache
  156. if (process.platform === 'win32') {
  157. const quoted = (s) => (s.includes(' ') || s.includes('"') ? `"${String(s).replace(/"/g, '""')}"` : s)
  158. const cmdLine = `set "ELECTRON_BUILDER_CACHE=${builderCache}" && ${quoted(process.execPath)} ${quoted(cliPath)} ${args.map(quoted).join(' ')}`
  159. const opts = { cwd, stdio: ['inherit', 'inherit', 'inherit'], env: envCopy, windowsHide: false, shell: false }
  160. return new Promise((resolve, reject) => {
  161. const child = spawn('cmd', ['/c', cmdLine], opts)
  162. child.on('close', (code) => {
  163. const tmpConfig = path.join(pkgResDir, 'electron-builder-x64-tmp.json')
  164. if (fs.existsSync(tmpConfig)) try { fs.unlinkSync(tmpConfig) } catch (_) {}
  165. if (code !== 0) {
  166. reject(new Error(`electron-builder 退出码: ${code}`))
  167. } else resolve()
  168. })
  169. child.on('error', (err) => reject(err))
  170. })
  171. }
  172. const opts = { cwd, stdio: ['inherit', 'inherit', 'inherit'], env: envCopy, windowsHide: false, shell: false }
  173. return new Promise((resolve, reject) => {
  174. const child = spawn(process.execPath, [cliPath, ...args], opts)
  175. child.on('close', (code) => {
  176. const tmpConfig = path.join(pkgResDir, 'electron-builder-x64-tmp.json')
  177. if (fs.existsSync(tmpConfig)) try { fs.unlinkSync(tmpConfig) } catch (_) {}
  178. if (code !== 0) reject(new Error(`electron-builder 退出码: ${code}`))
  179. else resolve()
  180. })
  181. child.on('error', (err) => reject(err))
  182. })
  183. }
  184. function cleanDist(outputDir, keepDir) {
  185. const dir = path.join(projectRoot, outputDir)
  186. if (!fs.existsSync(dir)) return
  187. for (const name of fs.readdirSync(dir)) {
  188. if (name === keepDir) continue
  189. const full = path.join(dir, name)
  190. try {
  191. if (fs.statSync(full).isDirectory()) fs.rmSync(full, { recursive: true })
  192. else fs.unlinkSync(full)
  193. } catch (e) {
  194. console.warn('clean:', e.message)
  195. }
  196. }
  197. }
  198. async function main() {
  199. killProcessesLockingOutput()
  200. await sleep(400)
  201. checkLocalResourcesOrExit()
  202. try {
  203. console.log('[1/3] Vite build...')
  204. execSync('npm run build', { cwd: projectRoot, stdio: 'inherit' })
  205. } catch (e) {
  206. console.error('\n[错误] 第 1 步 Vite build 失败,已停止打包。')
  207. console.error(e.message || e)
  208. process.exit(1)
  209. }
  210. try {
  211. console.log('[2/3] Electron builder win x64...')
  212. await runElectronBuilder(projectRoot, getBuilderEnv())
  213. } catch (e) {
  214. console.error('\n[错误] 第 2 步 electron-builder 失败。')
  215. console.error(e.message || e)
  216. process.exit(1)
  217. }
  218. try {
  219. const unpackedDir = path.join(projectRoot, X64_OUTPUT_DIR, X64_UNPACKED_DIR)
  220. const cp = require('child_process')
  221. // 先复制 web 到 win-unpacked/dist,再清理,避免删掉 package/build/web
  222. const webSrc = path.join(buildDir, 'web')
  223. const webDest = path.join(unpackedDir, 'dist')
  224. if (fs.existsSync(webSrc) && fs.statSync(webSrc).isDirectory()) {
  225. try {
  226. if (!fs.existsSync(webDest)) fs.mkdirSync(webDest, { recursive: true })
  227. if (process.platform === 'win32') {
  228. cp.execSync(`xcopy /E /I /Y "${webSrc}\\*" "${webDest}"`, { stdio: 'ignore' })
  229. } else {
  230. cp.execSync(`cp -R "${webSrc}"/* "${webDest}"`, { stdio: 'ignore' })
  231. }
  232. } catch (_) {}
  233. }
  234. console.log('[3/3] 清理 ' + X64_OUTPUT_DIR + '...')
  235. cleanDist(X64_OUTPUT_DIR, X64_UNPACKED_DIR)
  236. // 将 nodejs/node 打包进输出,复制到 win-unpacked/node/ 供运行时使用
  237. const nodeDir = path.join(unpackedDir, 'node')
  238. const nodeExeName = process.platform === 'win32' ? 'node.exe' : 'node'
  239. const portableNodeExe = path.join(NODE_PORTABLE_DIR, nodeExeName)
  240. if (fs.existsSync(portableNodeExe)) {
  241. try {
  242. if (!fs.existsSync(nodeDir)) fs.mkdirSync(nodeDir, { recursive: true })
  243. if (process.platform === 'win32') {
  244. cp.execSync(`xcopy /E /I /Y "${NODE_PORTABLE_DIR}\\*" "${nodeDir}"`, { stdio: 'ignore' })
  245. } else {
  246. cp.execSync(`cp -R "${NODE_PORTABLE_DIR}"/* "${nodeDir}"`, { stdio: 'ignore' })
  247. }
  248. console.log('[3/3] 已打包 nodejs/node → ' + X64_UNPACKED_DIR + '/node/')
  249. } catch (e) {
  250. console.warn('[3/3] 复制 nodejs/node 失败:', e.message)
  251. }
  252. }
  253. // adb:拷贝 lib/scrcpy-adb 到打包文件夹下,config 里配置为 projectRoot/scrcpy-adb/adb.exe
  254. const scrcpyAdbSrc = path.join(projectRoot, 'lib', 'scrcpy-adb')
  255. const scrcpyAdbDest = path.join(unpackedDir, 'scrcpy-adb')
  256. if (fs.existsSync(scrcpyAdbSrc) && fs.statSync(scrcpyAdbSrc).isDirectory()) {
  257. try {
  258. if (!fs.existsSync(scrcpyAdbDest)) fs.mkdirSync(scrcpyAdbDest, { recursive: true })
  259. if (process.platform === 'win32') {
  260. cp.execSync(`xcopy /E /I /Y "${scrcpyAdbSrc}\\*" "${scrcpyAdbDest}"`, { stdio: 'ignore' })
  261. } else {
  262. cp.execSync(`cp -R "${scrcpyAdbSrc}"/* "${scrcpyAdbDest}"`, { stdio: 'ignore' })
  263. }
  264. console.log('[3/3] 已打包 lib/scrcpy-adb → ' + X64_UNPACKED_DIR + '/scrcpy-adb/')
  265. } catch (e) {
  266. console.warn('[3/3] 复制 scrcpy-adb 失败:', e.message)
  267. }
  268. }
  269. const staticSrc = path.join(pkgResDir, 'static')
  270. const staticDest = path.join(unpackedDir, 'static')
  271. if (fs.existsSync(staticSrc) && fs.statSync(staticSrc).isDirectory()) {
  272. try {
  273. if (process.platform === 'win32') {
  274. cp.execSync(`xcopy /E /I /Y "${staticSrc}\\*" "${staticDest}"`, { stdio: 'ignore' })
  275. } else {
  276. if (!fs.existsSync(staticDest)) fs.mkdirSync(staticDest, { recursive: true })
  277. cp.execSync(`cp -R "${staticSrc}"/* "${staticDest}"`, { stdio: 'ignore' })
  278. }
  279. console.log('[3/3] 已打包 package/pack-resources/static → ' + X64_UNPACKED_DIR + '/static/')
  280. } catch (e) {
  281. console.warn('[3/3] 复制 static 失败:', e.message)
  282. }
  283. }
  284. // Python 整个目录拷贝到 win-unpacked/python/(含 scripts、x64、虚拟环境 x64/env)。用 robocopy 避免路径超长(260)静默失败
  285. const pythonSrcRoot = path.join(projectRoot, 'python')
  286. const pythonDestRoot = path.join(unpackedDir, 'python')
  287. if (fs.existsSync(pythonSrcRoot) && fs.statSync(pythonSrcRoot).isDirectory()) {
  288. try {
  289. if (!fs.existsSync(pythonDestRoot)) fs.mkdirSync(pythonDestRoot, { recursive: true })
  290. if (process.platform === 'win32') {
  291. // robocopy 支持长路径,/E 含子目录,虚拟环境 env 会一并复制
  292. const rc = cp.spawnSync('robocopy', [
  293. pythonSrcRoot,
  294. pythonDestRoot,
  295. '/E', '/R:1', '/W:1', '/NFL', '/NDL', '/NJH', '/NJS'
  296. ], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] })
  297. const exitCode = rc.status
  298. if (exitCode >= 8) {
  299. const err = (rc.stderr || rc.stdout || '').trim()
  300. throw new Error(`robocopy 退出码 ${exitCode}${err ? ': ' + err : ''}`)
  301. }
  302. } else {
  303. cp.execSync(`cp -R "${pythonSrcRoot}"/* "${pythonDestRoot}"`, { stdio: 'inherit' })
  304. }
  305. console.log('[3/3] 已打包 python(含 scripts、py)→ ' + X64_UNPACKED_DIR + '/python/')
  306. const bundledPy = path.join(unpackedDir, 'python', 'py', 'python.exe')
  307. if (!fs.existsSync(bundledPy)) {
  308. console.warn('[3/3] 警告: 未找到 python/py/python.exe,请确保源码下已安装嵌入式 Python 与依赖后重新打包')
  309. }
  310. } catch (e) {
  311. console.warn('[3/3] 复制 python 失败:', e.message)
  312. }
  313. }
  314. // 写入打包用 config:python 在 exe 同目录 python/py
  315. const packagedConfigPath = path.join(unpackedDir, 'config.js')
  316. const packagedConfig = `// 打包后使用:python 在 exe 同目录 python/py,不依赖系统 Python
  317. const path = require('path')
  318. const projectRoot = path.dirname(process.execPath)
  319. const isWin = process.platform === 'win32'
  320. const pythonDir = path.join(projectRoot, 'python', 'py')
  321. const pythonExePath = path.join(pythonDir, isWin ? 'python.exe' : 'python')
  322. module.exports = {
  323. projectRoot,
  324. window: { width: 800, height: 600, autoHideMenuBar: true },
  325. devTools: { enabled: false },
  326. vite: { port: 9527, host: 'localhost' },
  327. pythonPath: { path: pythonDir },
  328. pythonDir,
  329. pythonExePath,
  330. adbPath: { path: path.join(projectRoot, 'scrcpy-adb', isWin ? 'adb.exe' : 'adb') },
  331. nodejsPath: path.join(projectRoot, 'node', isWin ? 'node.exe' : 'node')
  332. }
  333. `
  334. try {
  335. fs.writeFileSync(packagedConfigPath, packagedConfig, 'utf8')
  336. console.log('[3/3] 已写入 ' + X64_UNPACKED_DIR + '/config.js(python 路径: python/py)')
  337. } catch (e) {
  338. console.warn('[3/3] 写入 config.js 失败:', e.message)
  339. }
  340. const readmePath = path.join(unpackedDir, '使用说明.txt')
  341. const readme = [
  342. 'AndroidRemoteController - 开包即用',
  343. '',
  344. '使用:直接双击运行 AndroidRemoteController.exe',
  345. '首次运行会在本目录自动创建 static 文件夹(用于存放运行时数据)。',
  346. '',
  347. '分发:可将本「win-unpacked」整个文件夹复制到任意 Windows 电脑使用,无需安装。',
  348. '请勿单独只复制 .exe,必须复制整个文件夹。',
  349. ''
  350. ].join('\r\n')
  351. try {
  352. fs.writeFileSync(readmePath, readme, 'utf8')
  353. } catch (_) {}
  354. const runJs = `/**
  355. * 在本目录启动 exe,若有报错则写入 启动报错.txt
  356. * 用法:node run.js 或双击 run.bat
  357. */
  358. const path = require('path')
  359. const fs = require('fs')
  360. const { spawn } = require('child_process')
  361. const dir = __dirname
  362. const errorLogPath = path.join(dir, '启动报错.txt')
  363. const exeNames = ['AndroidRemoteController.exe', 'electron-react-vite-app.exe']
  364. let exePath = null
  365. for (const name of exeNames) {
  366. const p = path.join(dir, name)
  367. if (fs.existsSync(p)) {
  368. exePath = p
  369. break
  370. }
  371. }
  372. if (!exePath) {
  373. const msg = \`[\${new Date().toISOString()}] 未找到 exe(\${exeNames.join(' / ')})\n\`
  374. fs.writeFileSync(errorLogPath, msg, 'utf8')
  375. console.error(msg.trim())
  376. process.exit(1)
  377. }
  378. const chunks = []
  379. function writeErrorLog(extra) {
  380. const header = \`[\${new Date().toISOString()}] 运行 \${path.basename(exePath)} 异常\n\`
  381. const body = chunks.length ? Buffer.concat(chunks).toString('utf8') : ''
  382. const tail = extra ? \`\n\${extra}\` : ''
  383. fs.writeFileSync(errorLogPath, header + body + tail, 'utf8')
  384. }
  385. const child = spawn(exePath, [], {
  386. cwd: dir,
  387. stdio: ['ignore', 'pipe', 'pipe'],
  388. windowsHide: false
  389. })
  390. child.stdout.on('data', (data) => {
  391. process.stdout.write(data)
  392. })
  393. child.stderr.on('data', (data) => {
  394. chunks.push(data)
  395. process.stderr.write(data)
  396. })
  397. child.on('error', (err) => {
  398. writeErrorLog(\`进程启动失败: \${err.message}\`)
  399. console.error(err)
  400. process.exit(1)
  401. })
  402. child.on('exit', (code, signal) => {
  403. if (code !== 0 && code != null) {
  404. writeErrorLog(\`退出码: \${code}\${signal ? \` 信号: \${signal}\` : ''}\`)
  405. }
  406. })
  407. `
  408. // 使用同目录下 node/node.exe,不依赖系统 PATH 的 node(别人电脑未装 Node 也能用)
  409. const runBat = '@echo off\r\ncd /d "%~dp0"\r\n"%~dp0node\\node.exe" run.js\r\npause\r\n'
  410. try {
  411. fs.writeFileSync(path.join(unpackedDir, 'run.js'), runJs, 'utf8')
  412. fs.writeFileSync(path.join(unpackedDir, 'run.bat'), runBat, 'utf8')
  413. } catch (_) {}
  414. // 若本目录存在 run.js、run.bat、config.js,拷贝到 win-unpacked 覆盖生成结果
  415. const pkgRunJs = path.join(pkgResDir, 'run.js')
  416. const pkgRunBat = path.join(pkgResDir, 'run.bat')
  417. const pkgConfigJs = path.join(pkgResDir, 'config.js')
  418. if (fs.existsSync(pkgRunJs)) {
  419. try {
  420. fs.copyFileSync(pkgRunJs, path.join(unpackedDir, 'run.js'))
  421. console.log('[3/3] 已拷贝 package/pack-resources/run.js → ' + X64_UNPACKED_DIR + '/run.js')
  422. } catch (e) { console.warn('[3/3] 拷贝 run.js 失败:', e.message) }
  423. }
  424. if (fs.existsSync(pkgRunBat)) {
  425. try {
  426. fs.copyFileSync(pkgRunBat, path.join(unpackedDir, 'run.bat'))
  427. console.log('[3/3] 已拷贝 package/pack-resources/run.bat → ' + X64_UNPACKED_DIR + '/run.bat')
  428. } catch (e) { console.warn('[3/3] 拷贝 run.bat 失败:', e.message) }
  429. }
  430. if (fs.existsSync(pkgConfigJs)) {
  431. try {
  432. fs.copyFileSync(pkgConfigJs, path.join(unpackedDir, 'config.js'))
  433. console.log('[3/3] 已拷贝 package/pack-resources/config.js → ' + X64_UNPACKED_DIR + '/config.js')
  434. } catch (e) { console.warn('[3/3] 拷贝 config.js 失败:', e.message) }
  435. }
  436. const pkgKillPs1 = path.join(pkgResDir, 'kill-all-process.ps1')
  437. if (fs.existsSync(pkgKillPs1)) {
  438. try {
  439. fs.copyFileSync(pkgKillPs1, path.join(unpackedDir, 'kill-all-process.ps1'))
  440. console.log('[3/3] 已拷贝 package/pack-resources/kill-all-process.ps1 → ' + X64_UNPACKED_DIR + '/kill-all-process.ps1')
  441. } catch (e) { console.warn('[3/3] 拷贝 kill-all-process.ps1 失败:', e.message) }
  442. }
  443. console.log('[完成] ' + X64_OUTPUT_DIR + '\\' + X64_UNPACKED_DIR)
  444. } catch (e) {
  445. console.error('\n[错误] 第 3 步 清理 ' + X64_OUTPUT_DIR + ' 失败。')
  446. console.error(e.message || e)
  447. process.exit(1)
  448. }
  449. }
  450. if (require.main === module) {
  451. main().catch((e) => {
  452. console.error(e)
  453. process.exit(1)
  454. })
  455. } else {
  456. module.exports = { cleanDist }
  457. }