diff --git a/.gitignore b/.gitignore index 8e868c8..c610b76 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ src/assets/ui.ts src/assets/swagger.ts *.tgz /release +/.electron-node-modules +/.electron-build-temp diff --git a/package.json b/package.json index b06b743..5d51104 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "electron": "bun run build:lib && electron ./electron-main.js", "electron:dev": "bun run scripts/electron-dev.ts", "electron:build": "bun run scripts/electron-build.ts", - "electron:build:all": "bun run build:lib && electron-builder -mwl", + "electron:build:all": "bun run scripts/electron-build.ts --all", "prepublishOnly": "bun scripts/build.ts --lib", "push": "npm publish --registry=https://registry.npmjs.org", "build": "bun scripts/build.ts --single", @@ -73,11 +73,14 @@ "appId": "com.github.xxnuo.mtranserver", "productName": "MTranServer", "executableName": "mtranserver", + "electronVersion": "39.2.7", + "npmRebuild": false, + "buildDependenciesFromSource": false, "copyright": "Copyright © 2025", "extraMetadata": { "main": "electron-main.js" }, - "asar": true, + "asar": false, "asarUnpack": [ "dist/mtranserver*", "dist/*.exe" diff --git a/scripts/electron-build.ts b/scripts/electron-build.ts index 5339615..2d5e407 100644 --- a/scripts/electron-build.ts +++ b/scripts/electron-build.ts @@ -2,9 +2,13 @@ import { spawn } from 'child_process' import fs from 'fs' import path from 'path' -function run(command: string, args: string[]) { +function run(command: string, args: string[], options?: { cwd?: string; env?: NodeJS.ProcessEnv }) { return new Promise((resolve, reject) => { - const proc = spawn(command, args, { stdio: 'inherit', env: process.env }) + const proc = spawn(command, args, { + stdio: 'inherit', + env: options?.env ?? process.env, + cwd: options?.cwd + }) proc.on('exit', (code) => { if (code === 0) resolve() else reject(new Error(`exit ${code}`)) @@ -12,34 +16,133 @@ function run(command: string, args: string[]) { }) } -async function main() { - const bunBin = process.execPath - await run(bunBin, ['run', 'build:lib']) - - const distDir = path.join(process.cwd(), 'dist') - if (fs.existsSync(distDir)) { - for (const entry of fs.readdirSync(distDir)) { - if (entry.startsWith('mtranserver') || entry.endsWith('.exe')) { - try { - fs.rmSync(path.join(distDir, entry), { recursive: true, force: true }) - } catch { - continue - } +function cleanDistBinaries(distDir: string) { + if (!fs.existsSync(distDir)) return + for (const entry of fs.readdirSync(distDir)) { + if (entry.startsWith('mtranserver') || entry.endsWith('.exe')) { + try { + fs.rmSync(path.join(distDir, entry), { recursive: true, force: true }) + } catch { + continue } } } +} - const platform = process.platform - const builderArgs = ['node_modules/.bin/electron-builder'] - if (platform === 'linux') { - builderArgs.push('--linux', 'AppImage') - } else if (platform === 'darwin') { - builderArgs.push('--mac') - } else if (platform === 'win32') { - builderArgs.push('--win') +async function prepareNpmModules(root: string) { + const tempDir = path.join(root, '.electron-build-temp') + fs.rmSync(tempDir, { recursive: true, force: true }) + fs.mkdirSync(tempDir, { recursive: true }) + fs.copyFileSync(path.join(root, 'package.json'), path.join(tempDir, 'package.json')) + if (fs.existsSync(path.join(root, 'package-lock.json'))) { + fs.copyFileSync(path.join(root, 'package-lock.json'), path.join(tempDir, 'package-lock.json')) } + await run('npm', ['install', '--omit=dev', '--ignore-scripts', '--install-strategy=hoisted'], { cwd: tempDir }) + const targetModules = path.join(root, 'node_modules_prod') + fs.rmSync(targetModules, { recursive: true, force: true }) + fs.renameSync(path.join(tempDir, 'node_modules'), targetModules) + fs.rmSync(tempDir, { recursive: true, force: true }) + return targetModules +} - await run(bunBin, builderArgs) +function getUnpackedDir(root: string) { + const platform = process.platform + const releaseDir = path.join(root, 'release') + if (platform === 'linux') { + return path.join(releaseDir, 'linux-unpacked') + } else if (platform === 'darwin') { + const entries = fs.readdirSync(releaseDir).filter(e => e.endsWith('.app')) + if (entries.length > 0) { + return path.join(releaseDir, 'mac-arm64', entries[0]) + } + return path.join(releaseDir, 'mac') + } else if (platform === 'win32') { + return path.join(releaseDir, 'win-unpacked') + } + return path.join(releaseDir, 'linux-unpacked') +} + +async function repackAsar(unpackedDir: string) { + const appDir = path.join(unpackedDir, 'resources', 'app') + const asarFile = path.join(unpackedDir, 'resources', 'app.asar') + const asarUnpackDir = path.join(unpackedDir, 'resources', 'app.asar.unpacked') + const binDir = path.join(appDir, 'node_modules', '.bin') + if (fs.existsSync(binDir)) { + fs.rmSync(binDir, { recursive: true }) + } + if (fs.existsSync(asarFile)) { + fs.rmSync(asarFile) + } + if (fs.existsSync(asarUnpackDir)) { + fs.rmSync(asarUnpackDir, { recursive: true }) + } + await run('npx', ['@electron/asar', 'pack', appDir, asarFile]) + fs.rmSync(appDir, { recursive: true }) +} + +async function main() { + const bunBin = process.execPath + const root = process.cwd() + + await run(bunBin, ['run', 'build:lib']) + + const distDir = path.join(root, 'dist') + cleanDistBinaries(distDir) + + console.log('Installing production dependencies with npm...') + const prodModules = await prepareNpmModules(root) + + const originalModules = path.join(root, 'node_modules') + const backupModules = path.join(root, 'node_modules_bun') + + fs.renameSync(originalModules, backupModules) + fs.renameSync(prodModules, originalModules) + + const electronBuilderPath = path.join(backupModules, '.bin/electron-builder') + const env = { ...process.env, NODE_PATH: backupModules } + + try { + const platform = process.platform + const dirArgs = [electronBuilderPath, '--dir'] + if (process.argv.includes('--all')) { + dirArgs.push('-mwl') + } else if (platform === 'linux') { + dirArgs.push('--linux') + } else if (platform === 'darwin') { + dirArgs.push('--mac') + } else if (platform === 'win32') { + dirArgs.push('--win') + } + + console.log('Packaging directory...') + await run(bunBin, dirArgs, { env }) + + const unpackedDir = getUnpackedDir(root) + const appNodeModules = path.join(unpackedDir, 'resources', 'app', 'node_modules') + + console.log('Copying all node_modules...') + fs.rmSync(appNodeModules, { recursive: true, force: true }) + fs.cpSync(originalModules, appNodeModules, { recursive: true }) + + console.log('Repacking asar...') + await repackAsar(unpackedDir) + + console.log('Building final package...') + const finalArgs = [electronBuilderPath, '--prepackaged', unpackedDir] + if (process.argv.includes('--all')) { + finalArgs.push('-mwl') + } else if (platform === 'linux') { + finalArgs.push('--linux', 'AppImage') + } else if (platform === 'darwin') { + finalArgs.push('--mac') + } else if (platform === 'win32') { + finalArgs.push('--win') + } + await run(bunBin, finalArgs, { env }) + } finally { + fs.rmSync(originalModules, { recursive: true, force: true }) + fs.renameSync(backupModules, originalModules) + } } main().catch(() => process.exit(1))