package-x64.js 15 KB

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