fix(release): stop shipping native modules built for the wrong runtime
Some checks failed
deploy-site / deploy (push) Failing after 14m16s
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.
This commit is contained in:
parent
0fbbbc1756
commit
1af3cf75c7
42 changed files with 473 additions and 83 deletions
|
|
@ -78,6 +78,20 @@ if (!existsSync(join(desktopDir, 'sidecar-dist', 'sidecar'))) {
|
|||
process.exit(1)
|
||||
}
|
||||
|
||||
/** 현재 스크립트와 같은 Node로 CI 스크립트를 실행한다 */
|
||||
function runNodeScript(relativePath, scriptArgs) {
|
||||
console.log(`[portable] $ node ${relativePath} ${scriptArgs.join(' ')}`)
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[join(root, relativePath), ...scriptArgs],
|
||||
{ cwd: root, stdio: 'inherit' },
|
||||
)
|
||||
if (result.status !== 0) {
|
||||
console.error(`[portable] ${relativePath} 실패 (exit ${result.status ?? 'null'})`)
|
||||
process.exit(result.status ?? 1)
|
||||
}
|
||||
}
|
||||
|
||||
function resolve7za() {
|
||||
// electron-builder가 의존하는 7zip-bin이 플랫폼별 7za 실행 파일을 제공한다.
|
||||
const platformDir =
|
||||
|
|
@ -128,6 +142,11 @@ if (!existsSync(appDir)) {
|
|||
process.exit(1)
|
||||
}
|
||||
|
||||
// 네이티브 모듈 ABI 사고 방지: 호스트 Node ABI로 빌드된 모듈이 섞이면 설치본이 시작조차 못 한다.
|
||||
// (실측 사고: better_sqlite3.node가 NODE_MODULE_VERSION 131 → Electron 130 요구)
|
||||
runNodeScript('scripts/ci/fix-native-abi.mjs', ['--dir', appDir])
|
||||
runNodeScript('scripts/ci/verify-native-abi.mjs', ['--dir', appDir])
|
||||
|
||||
// 이전 볼륨 정리 (같은 버전 재드 시 잔여 볼륨이 섞이지 않도록)
|
||||
for (const name of readdirSync(releaseDir)) {
|
||||
if (name.startsWith(ARCHIVE_BASE)) {
|
||||
|
|
@ -251,6 +270,9 @@ const zipBuild = spawnSync(
|
|||
'npx',
|
||||
[
|
||||
'electron-builder',
|
||||
// 검증된 트리에서 바로 패키징한다(ne ABI가 확실한 디렉토리만 사용).
|
||||
'--prepackaged',
|
||||
appDir,
|
||||
'--win',
|
||||
'zip',
|
||||
'--x64',
|
||||
|
|
|
|||
119
scripts/ci/fix-native-abi.mjs
Normal file
119
scripts/ci/fix-native-abi.mjs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
// 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 })
|
||||
|
|
@ -133,28 +133,51 @@ async function forgejoFetch(url, init = {}) {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 원격 파일이 로컬 바이트와 같은지 판단한다.
|
||||
* Forgejo generic registry는 HEAD를 405로 거부하고 해시도 주지 않으므로,
|
||||
* Range GET으로 크기를 본 뒤 1MiB 이하는 실제 바이트까지 비교한다.
|
||||
* (크기만 비교하면 버전 문자열만 바뀐 latest.yml 같은 메타데이터를 놓친다 — 실측 사고.)
|
||||
*/
|
||||
async function remoteIsIdentical(url, body, fetchImpl) {
|
||||
const probe = await fetchImpl(url, { headers: { Range: 'bytes=0-0' } }).catch(() => null)
|
||||
if (!probe?.ok) return false
|
||||
const contentRange = probe.headers.get('content-range')
|
||||
const remoteSize = contentRange ? Number(contentRange.split('/')[1]) : NaN
|
||||
if (!Number.isFinite(remoteSize) || remoteSize !== body.length) return false
|
||||
if (body.length > 1024 * 1024) return true
|
||||
const full = await fetchImpl(url).catch(() => null)
|
||||
if (!full?.ok) return false
|
||||
const remoteBytes = Buffer.from(await full.arrayBuffer())
|
||||
return remoteBytes.length === body.length && remoteBytes.equals(body)
|
||||
}
|
||||
|
||||
async function upload(url, body, contentType) {
|
||||
if (check) {
|
||||
console.log(`[portable] (check) PUT ${url} (${body.length} bytes)`)
|
||||
return
|
||||
}
|
||||
// Forgejo의 generic registry는 HEAD를 405로 거부한다(실측) → Range GET으로 크기만 읽는다.
|
||||
const probe = await forgejoFetch(url, { headers: { Range: 'bytes=0-0' } }).catch(() => null)
|
||||
const contentRange = probe?.headers.get('content-range')
|
||||
const remoteLength = contentRange ? Number(contentRange.split('/')[1]) : NaN
|
||||
if (probe?.ok && Number.isFinite(remoteLength)) {
|
||||
if (remoteLength === body.length) {
|
||||
console.log(`[portable] 이미 동일한 파일이 있습니다(건너뜀): ${url}`)
|
||||
return
|
||||
}
|
||||
// 볼륨은 불변 자산이다 — 같은 버전 경로에 다른 바이트가 있으면 덮어쓰지 않고 중단한다.
|
||||
if (url.includes(`/portable-${version}/`) && url.includes('.7z.')) {
|
||||
|
||||
// 메타데이터는 크기가 아니라 내용까지 비교해야 한다.
|
||||
// 크기만 보면 버전 문자열만 바뀐 latest.yml/json을 "동일"로 오판한다 — 실측 사고.
|
||||
if (await remoteIsIdentical(url, body, forgejoFetch)) {
|
||||
console.log(`[portable] 이미 동일한 파일이 있습니다(건너): ${url}`)
|
||||
return
|
||||
}
|
||||
|
||||
const existing = await forgejoFetch(url, { headers: { Range: 'bytes=0-0' } }).catch(() => null)
|
||||
if (existing?.ok) {
|
||||
// 볼륨/부품은 불변 자산이다 — 같은 버전 경로에 다른 바이트가 있으면 덮어쓰지 않고 중단한다.
|
||||
const isImmutableAsset =
|
||||
url.includes(`/portable-${version}/`) && (url.includes('.7z.') || url.includes('.zip.'))
|
||||
if (isImmutableAsset) {
|
||||
console.error(
|
||||
`[portable] ${version} 볼륨에 다른 바이트가 이미 있습니다: ${url}\n` +
|
||||
' 이미 게시된 버전은 덮어쓰지 않습니다(불변). 새 버전으로 게시하세요.',
|
||||
`[portable] ${version} 자산에 다른 바이트가 이미 있습니다: ${url}\n` +
|
||||
' 이미 게시된 버전은 어쓰지 않습니다(불변). 새 버전으로 게시하세요.',
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
// 메타데이터와 latest 별칭은 최신을 반영해야 하므로 지우고 쓴다.
|
||||
await forgejoFetch(url, { method: 'DELETE' }).catch(() => null)
|
||||
}
|
||||
const response = await forgejoFetch(url, {
|
||||
|
|
|
|||
|
|
@ -41,13 +41,16 @@ const version = JSON.parse(
|
|||
const releaseDir = join(desktopDir, 'release', version)
|
||||
|
||||
if (build) {
|
||||
console.log('[updater] electron-builder NSIS 빌드 ( 전용 — 런타임 제외)')
|
||||
const appDir = join(releaseDir, 'win-unpacked')
|
||||
|
||||
// 1) 먼저 unpacked 트리빌드한다.
|
||||
console.log('[updater] electron-builder --dir (앱 전용 — 런타임 제외)')
|
||||
const result = spawnSync(
|
||||
'npx',
|
||||
[
|
||||
'electron-builder',
|
||||
'--win',
|
||||
'nsis',
|
||||
'dir',
|
||||
'--x64',
|
||||
'--config',
|
||||
'electron-builder.yml',
|
||||
|
|
@ -62,6 +65,48 @@ if (build) {
|
|||
console.error(`[updater] 빌드 실패 (exit ${result.status ?? 'null'})`)
|
||||
process.exit(result.status ?? 1)
|
||||
}
|
||||
|
||||
// 2) 네이티브 모듈 ABI 보장 + 검증 (호스트 Node ABI가 섞이면 이 시작조차 못 한다)
|
||||
runNodeScript('scripts/ci/fix-native-abi.mjs', ['--dir', appDir])
|
||||
runNodeScript('scripts/ci/verify-native-abi.mjs', ['--dir', appDir])
|
||||
|
||||
// 3) 검증된 트리에서 설치본 생성 (--prepackaged = 재빌드 없이 그대로 패키징)
|
||||
console.log('[updater] electron-builder --prepackaged (NSIS x64)')
|
||||
const packageResult = spawnSync(
|
||||
'npx',
|
||||
[
|
||||
'electron-builder',
|
||||
'--prepackaged',
|
||||
appDir,
|
||||
'--win',
|
||||
'nsis',
|
||||
'--x64',
|
||||
'--config',
|
||||
'electron-builder.yml',
|
||||
'--publish',
|
||||
'never',
|
||||
'-c.win.forceCodeSigning=false',
|
||||
'-c.npmRebuild=false',
|
||||
],
|
||||
{ cwd: desktopDir, stdio: 'inherit', shell: process.platform === 'win32' },
|
||||
)
|
||||
if (packageResult.status !== 0) {
|
||||
console.error(`[updater] 설치본 생성 실패 (exit ${packageResult.status ?? 'null'})`)
|
||||
process.exit(packageResult.status ?? 1)
|
||||
}
|
||||
}
|
||||
|
||||
/** 이 스크립트와 같은 Node로 CI 스크립트를 실행한다 */
|
||||
function runNodeScript(relativePath, scriptArgs) {
|
||||
console.log(`[updater] $ node ${relativePath} ${scriptArgs.join(' ')}`)
|
||||
const result = spawnSync(process.execPath, [join(root, relativePath), ...scriptArgs], {
|
||||
cwd: root,
|
||||
stdio: 'inherit',
|
||||
})
|
||||
if (result.status !== 0) {
|
||||
console.error(`[updater] ${relativePath} 실패 (exit ${result.status ?? 'null'})`)
|
||||
process.exit(result.status ?? 1)
|
||||
}
|
||||
}
|
||||
|
||||
const metadataPath = join(releaseDir, 'latest.yml')
|
||||
|
|
@ -143,6 +188,25 @@ if (!ackUnsigned) {
|
|||
|
||||
const authorization = forgejoAuthorization()
|
||||
|
||||
/**
|
||||
* 원격 파일이 로컬 바이트와 같은지 판단한다.
|
||||
* Forgejo generic registry는 HEAD를 405로 거부하고 해시도 주지 않으므로,
|
||||
* Range GET으로 크기를 본 뒤 1MiB 이하는 실제 바이트까지 비교한다.
|
||||
* (크기만 비교하면 버전 문자열만 바뀐 latest.yml 같은 메타데이터를 놓친다 — 실측 사고.)
|
||||
*/
|
||||
async function remoteIsIdentical(url, body, fetchImpl) {
|
||||
const probe = await fetchImpl(url, { headers: { Range: 'bytes=0-0' } }).catch(() => null)
|
||||
if (!probe?.ok) return false
|
||||
const contentRange = probe.headers.get('content-range')
|
||||
const remoteSize = contentRange ? Number(contentRange.split('/')[1]) : NaN
|
||||
if (!Number.isFinite(remoteSize) || remoteSize !== body.length) return false
|
||||
if (body.length > 1024 * 1024) return true
|
||||
const full = await fetchImpl(url).catch(() => null)
|
||||
if (!full?.ok) return false
|
||||
const remoteBytes = Buffer.from(await full.arrayBuffer())
|
||||
return remoteBytes.length === body.length && remoteBytes.equals(body)
|
||||
}
|
||||
|
||||
async function forgejoFetch(url, init = {}) {
|
||||
return fetch(url, {
|
||||
...init,
|
||||
|
|
@ -164,19 +228,16 @@ for (const target of targets) {
|
|||
const url = `${target}/${encodeURIComponent(payload.name)}`
|
||||
const body = await readFile(payload.path)
|
||||
|
||||
// Forgejo generic registry는 같은 경로에 다른 바이트가 있으면 409를 돌려준다.
|
||||
// 메타데이터(latest.yml / update-policy.json)는 최신을 가리켜야 하므로 먼저 지운다.
|
||||
// (설치본/블록맵은 파일명에 버전이 있어 충돌하지 않는다.)
|
||||
const existing = await forgejoFetch(url, { headers: { Range: 'bytes=0-0' } }).catch(
|
||||
() => null,
|
||||
)
|
||||
// Forgejo generic registry는 HEAD를 405로 거부하고 해시도 주지 않는다.
|
||||
// 크기만 비교하면 버전 문자열만 바뀐 latest.yml을 "동일"로 오판한다 — 내용까지 비교한다.
|
||||
if (await remoteIsIdentical(url, body, forgejoFetch)) {
|
||||
console.log(`[updater] 이미 동일한 파일이 있습니다(건너): ${url}`)
|
||||
continue
|
||||
}
|
||||
|
||||
// 메타데이터는 최신을 가리켜야 하므로 기존 파일을 지우고 쓴다(PUT은 409를 돌려준다).
|
||||
const existing = await forgejoFetch(url, { headers: { Range: 'bytes=0-0' } }).catch(() => null)
|
||||
if (existing?.ok) {
|
||||
const contentRange = existing.headers.get('content-range')
|
||||
const remoteSize = contentRange ? Number(contentRange.split('/')[1]) : NaN
|
||||
if (remoteSize === body.length) {
|
||||
console.log(`[updater] 이미 동일한 파일이 있습니다(건너뜀): ${url}`)
|
||||
continue
|
||||
}
|
||||
await forgejoFetch(url, { method: 'DELETE' }).catch(() => null)
|
||||
}
|
||||
|
||||
|
|
|
|||
117
scripts/ci/verify-native-abi.mjs
Normal file
117
scripts/ci/verify-native-abi.mjs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
// 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입니다.')
|
||||
Loading…
Add table
Add a link
Reference in a new issue