// scripts/ci/fix-native-abi.mjs // 패키징된 앱 트리에 Electron ABI 네이티브 모듈을 보장한다. // // 필요한가: // - better-sqlite3는 V8 내부 API에 의존해 런타임별 ABI(NODE_MODULE_VERSION)가 다르다. // 개발 PC에서 `npm install`을 돌리면 Node ABI로 재빌드되고, 그 상태로 패키징하면 // 설치본이 시작하자마자 "NODE_MODULE_VERSION 131 ... requires 130"으로 죽는다(실측 사고). // - CI에서는 electron-builder의 npmRebuild가 이를 처리하지만, 로컬에서는 실행 중인 Electron이 // node_modules 파일을 잠그고 있어 재빌드가 EPERM으로 실패할 수 있다. // 그래서 "패키징된 트리"에 정확한 ABI 바이너리를 직접 넣는다(원본 node_modules는 건드리지 않는다). // // 동작: 임시 사본에서 prebuild-install로 Electron용 프리빌드를 받아 패키징 트리에 복사한 뒤, // 호스트 Node가 그 모듈을 거부하는지(= Electron ABI) 확인한다. // // 사용: // node scripts/ci/fix-native-abi.mjs --dir import { spawnSync } from 'node:child_process' import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..') const dirFlagIndex = process.argv.indexOf('--dir') const packagedDir = dirFlagIndex >= 0 ? process.argv[dirFlagIndex + 1] : null if (!packagedDir || !existsSync(packagedDir)) { console.error('사용: node scripts/ci/fix-native-abi.mjs --dir ') process.exit(1) } const sourcePackageJson = JSON.parse( readFileSync(join(root, 'apps', 'desktop', 'package.json'), 'utf8'), ) const electronVersion = sourcePackageJson.devDependencies?.electron?.replace(/[^0-9.]/g, '') ?? '33.4.11' const arch = process.arch === 'arm64' ? 'arm64' : 'x64' const relativeBinary = join( 'node_modules', 'better-sqlite3', 'build', 'Release', 'better_sqlite3.node', ) const targetBinary = join(packagedDir, 'resources', 'app.asar.unpacked', relativeBinary) if (!existsSync(targetBinary)) { console.error(`[native-abi] 패키징 트리에 better-sqlite3 바이너리가 없습니다: ${targetBinary}`) process.exit(1) } /** 호스트 Node로 로드되면 Electron ABI가 아니다 */ function hostNodeLoads(binaryPath) { const probe = spawnSync(process.execPath, ['-e', `require(${JSON.stringify(binaryPath)})`], { encoding: 'utf8', }) return probe.status === 0 } if (!hostNodeLoads(targetBinary)) { console.log('[native-abi] 이미 Electron ABI 바이너리입니다 — 수정 불필요') process.exit(0) } console.log('[native-abi] 호스트 Node ABI로 빌드된 모듈을 찾았습니다 → Electron ABI로 교체합니다') const scratchRoot = join(root, '.tmp', 'native-abi') const scratchModule = join(scratchRoot, 'better-sqlite3') // 임시 사본 준비 (원본 node_modules는 잠겨 있을 수 있으므로 복사해서 작업) rmSync(scratchRoot, { recursive: true, force: true }) mkdirSync(scratchRoot, { recursive: true }) const copy = spawnSync( process.platform === 'win32' ? 'cmd' : 'cp', process.platform === 'win32' ? ['/c', 'xcopy', join(root, 'node_modules', 'better-sqlite3'), scratchModule, '/E', '/I', '/Q', '/Y'] : ['-r', join(root, 'node_modules', 'better-sqlite3'), scratchModule], { stdio: 'inherit' }, ) if (copy.status !== 0) { console.error('[native-abi] better-sqlite3 사본 생성 실패') process.exit(copy.status ?? 1) } const prebuild = spawnSync( process.platform === 'win32' ? 'npx.cmd' : 'npx', [ '--no-install', 'prebuild-install', `--runtime=electron`, `--target=${electronVersion}`, `--arch=${arch}`, '--force', ], { cwd: scratchModule, stdio: 'inherit', shell: process.platform === 'win32' }, ) const scratchBinary = join(scratchModule, 'build', 'Release', 'better_sqlite3.node') if (prebuild.status !== 0 || !existsSync(scratchBinary)) { console.error( [ `[native-abi] Electron ${electronVersion} 프리빌드를 받지 못했습니다.`, ' 대안: CI에서 electron-builder의 npmRebuild=true로 빌드하세요(권장).', ' 로컬에서 계속하려면 실행 중인 Electron을 모두 종료한 뒤 다음을 실행하세요:', ` npx @electron/rebuild -v ${electronVersion} -m better-sqlite3`, ].join('\n'), ) process.exit(1) } if (hostNodeLoads(scratchBinary)) { console.error('[native-abi] 받은 프리빌드가 Electron ABI가 아닙니다(prebuild-install 결과를 확인).') process.exit(1) } copyFileSync(scratchBinary, targetBinary) console.log(`[native-abi] 교체 완료: ${targetBinary}`) rmSync(scratchRoot, { recursive: true, force: true })