Some checks failed
deploy-site / deploy (push) Failing after 14m16s
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.
117 lines
No EOL
4.3 KiB
JavaScript
117 lines
No EOL
4.3 KiB
JavaScript
// scripts/ci/verify-native-abi.mjs
|
|
// 패키징된 Electron 앱의 네이티브 모이 "Electron ABI"로 빌드됐는지 검증한다.
|
|
//
|
|
// 배경(실측 사고): 설치본에 Node ABI(131)로 빌드된 better_sqlite3.node가 들어가
|
|
// 앱이 시작하자마자 "NODE_MODULE_VERSION 131 ... requires 130"으로 죽었다.
|
|
// 원인은 패키징에서 네이티브 재빌드를 건너뛴 것(npmRebuild=false)이었고, 조용히 지나갔다.
|
|
//
|
|
// 검증 방법: 호스트 Node로 모듈을 로드해 본다.
|
|
// - 로드 성공 → 호스트 Node ABI로 빌드된 것 = Electron용이 아님 → 실패
|
|
// - NODE_MODULE_VERSION 불일치로 거부 → 다른 런타임(Electron)용 = 통과
|
|
// - 파일 없음 → 실패
|
|
//
|
|
// 사용:
|
|
// node scripts/ci/verify-native-abi.mjs # release/<version>/win-unpacked 자동 탐색
|
|
// node scripts/ci/verify-native-abi.mjs --dir <packagedDir>
|
|
|
|
import { spawnSync } from 'node:child_process'
|
|
import { existsSync, readFileSync, readdirSync } 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')
|
|
let packagedDir = dirFlagIndex >= 0 ? process.argv[dirFlagIndex + 1] : null
|
|
|
|
if (!packagedDir) {
|
|
const version = JSON.parse(
|
|
readFileSync(join(root, 'release', 'product-version.json'), 'utf8'),
|
|
).version
|
|
const releaseDir = join(root, 'apps', 'desktop', 'release', version)
|
|
if (existsSync(releaseDir)) {
|
|
const candidates = readdirSync(releaseDir).filter((name) =>
|
|
/unpacked$/.test(name) || name === 'win-unpacked' || name === 'mac-arm64',
|
|
)
|
|
if (candidates.length > 0) {
|
|
packagedDir = join(releaseDir, candidates[0])
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!packagedDir || !existsSync(packagedDir)) {
|
|
console.error(
|
|
'패키징 산출물 디렉토리를 찾을 수 없습니다. --dir로 지정하세요 (예: apps/desktop/release/1.3.3/win-unpacked).',
|
|
)
|
|
process.exit(1)
|
|
}
|
|
|
|
const unpackedRoot = join(packagedDir, 'resources', 'app.asar.unpacked', 'node_modules')
|
|
|
|
/** Electron ABI(V8 내부 API)에 의존해 재빌드가 반드시 필요한 모듈 */
|
|
const REQUIRED_ELECTRON_ABI = [
|
|
{
|
|
name: 'better-sqlite3',
|
|
binary: join('better-sqlite3', 'build', 'Release', 'better_sqlite3.node'),
|
|
},
|
|
]
|
|
|
|
/** N-API 기반이라 타임 무관 — 존재만 확인 */
|
|
const NAPI_MODULES = [
|
|
{ name: 'uiohook-napi', binary: join('uiohook-napi', 'build', 'Release', 'uiohook_napi.node') },
|
|
]
|
|
|
|
const failures = []
|
|
const notes = []
|
|
|
|
for (const module of REQUIRED_ELECTRON_ABI) {
|
|
const binaryPath = join(unpackedRoot, module.binary)
|
|
if (!existsSync(binaryPath)) {
|
|
failures.push(`${module.name}: 패키징된 네이티브 바이너리가 없습니다 → ${binaryPath}`)
|
|
continue
|
|
}
|
|
|
|
// 호스트 Node로 로드 시도: 성공하면 Electron ABI가 아니다.
|
|
const probe = spawnSync(
|
|
process.execPath,
|
|
['-e', `require(${JSON.stringify(binaryPath.replace(/\\/g, '\\\\'))})`],
|
|
{ encoding: 'utf8' },
|
|
)
|
|
const output = `${probe.stdout ?? ''}${probe.stderr ?? ''}`
|
|
|
|
if (probe.status === 0) {
|
|
failures.push(
|
|
[
|
|
`${module.name}: 호스트 Node에서 로드됩니다 = Electron ABI가 아니다.`,
|
|
' 패키징 전에 Electron용으로 재빌드해야 합니다 (electron-builder npmRebuild=true,',
|
|
' 또는 `npx @electron/rebuild -v <electronVersion>`).',
|
|
].join('\n'),
|
|
)
|
|
continue
|
|
}
|
|
|
|
if (/NODE_MODULE_VERSION/.test(output)) {
|
|
notes.push(`${module.name}: Electron ABI 확인 (${output.split('\n')[0].slice(0, 80)})`)
|
|
} else {
|
|
failures.push(`${module.name}: 알 수 없는 오류로 로드 실패 → ${output.split('\n')[0]}`)
|
|
}
|
|
}
|
|
|
|
for (const module of NAPI_MODULES) {
|
|
const binaryPath = join(unpackedRoot, module.binary)
|
|
if (!existsSync(binaryPath)) {
|
|
notes.push(`${module.name}: 바이너리 없음(선택) — ${binaryPath}`)
|
|
} else {
|
|
notes.push(`${module.name}: 존재 확인 (N-API)`)
|
|
}
|
|
}
|
|
|
|
for (const note of notes) console.log(`[native-abi] ${note}`)
|
|
|
|
if (failures.length > 0) {
|
|
console.error('[native-abi] 검증 실패:')
|
|
for (const failure of failures) console.error(` - ${failure}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
console.log('[native-abi] GREEN — 패키징된 네이티브 모듈이 Electron에서 실행 가능한 ABI입니다.') |