// scripts/ci/lib/runtime-index-builder.mjs // 로컬 AI 런타임 인덱스(runtime.json)를 만드는 순수 함수. // // 왜 분리하나: build-portable.mjs 가 인덱스를 즉석 객체로 만들면서 platform/arch 를 // 빠뜨렸다. 사이드카(PyInstaller)와 ffmpeg 는 빌드 호스트의 네이티브 실행 파일인데, // 인덱스가 그 사실을 밝히지 않아 macOS 앱이 Windows 엔진(sidecar.exe)을 받아 풀고 // 검증에서 떨어진 뒤 매번 ~160MB 를 다시 받았다. 인덱스 모양은 여기 한 곳에서 정하고, // 클라이언트(apps/desktop/src/main/services/runtime/runtime-index.ts)는 platform/arch 가 // 다르면 부품을 받기 전에 거부한다. export const RUNTIME_INDEX_SCHEMA_VERSION = 1; /** * @typedef {{ name: string, size: number, sha256: string }} RuntimePackedPart * @typedef {{ archive: string, sha256: string, totalSize: number, parts: RuntimePackedPart[] }} RuntimePackedComponent */ /** * @param {{ * version: string, * generatedAt: string, * platform: string, * arch: string, * partBaseUrl: string, * components: Record, * }} input */ export function buildRuntimeIndex({ version, generatedAt, platform, arch, partBaseUrl, components }) { if (typeof platform !== "string" || platform.length === 0) { throw new Error("runtime index requires a build platform (process.platform)"); } if (typeof arch !== "string" || arch.length === 0) { throw new Error("runtime index requires a build arch (process.arch)"); } const base = String(partBaseUrl).replace(/\/+$/, ""); return { schemaVersion: RUNTIME_INDEX_SCHEMA_VERSION, version, platform, arch, generatedAt, components: Object.fromEntries( Object.entries(components).map(([name, entry]) => [ name, { ...entry, parts: entry.parts.map((part) => ({ ...part, url: `${base}/${part.name}` })), }, ]), ), }; }