d3ro-voice/scripts/ci/fix-native-abi.mjs
Yun Chan 1af3cf75c7
Some checks failed
deploy-site / deploy (push) Failing after 14m16s
fix(release): stop shipping native modules built for the wrong runtime
The released installer could not start: it carried a better-sqlite3 build for the
host Node runtime instead of Electron, so the app died immediately with a module
version mismatch when it opened its database.

Packaging now proves the Electron build of every runtime-sensitive native module
before an installer or archive exists, and installers are produced only from that
verified tree, so the mistake cannot pass silently. The release pipelines run the
same check.

The default local model also pointed at a retired model: a *.gguf name that
Ollama cannot serve, while the settings, onboarding, and guide screens
recommended an older model. All of them now use the model the service code
already preferred.
2026-09-18 15:45:03 +09:00

119 lines
No EOL
4.7 KiB
JavaScript

// 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 <packagedDir>
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 <packagedDir>')
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 })