#!/usr/bin/env node /** * Copies platform-specific native modules to app/src-tauri/resources/native/ * so they are bundled alongside service.js in the Tauri app. * * Run after build-sidecar.mjs, before `tauri build`. * Auto-detects platform from TARGET_ARCH env or process.platform + process.arch. */ import fs from 'node:fs'; import path from 'node:path'; import { execFileSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const root = path.resolve(__dirname, '..'); const nativeDir = path.join(root, 'app', 'src-tauri', 'resources', 'native'); const platform = process.platform; const arch = process.env.TARGET_ARCH || process.arch; // macOS "universal" is not a valid staging target: sqlite-vec and onnxruntime // ship per-arch binaries (no darwin/universal path exists), so a universal run // silently falls through to the x64 variant and mis-stages the arm64 half. // Build each arch separately and lipo the app bundle instead. if (arch === 'universal') { console.error( '[bundle-native-deps] FATAL — TARGET_ARCH=universal is not supported.\n' + ' Native modules (sqlite-vec, onnxruntime-node) are per-arch. Build each\n' + ' arch separately: TARGET_ARCH=arm64 (aarch64-apple-darwin) and\n' + ' TARGET_ARCH=x64 (x86_64-apple-darwin) — see release.yml\'s macOS matrix\n' + ' and the app tauri:build:mac:arm64 / :x64 scripts.', ); process.exit(1); } if (platform === 'darwin' && arch !== 'arm64' && arch !== 'x64') { throw new Error(`[bundle-native-deps] FATAL — unsupported macOS target architecture: ${arch}`); } console.log(`[bundle-native-deps] Platform: ${platform}-${arch}`); // A previous matrix leg must never satisfy this build with stale native files. fs.rmSync(nativeDir, { recursive: true, force: true }); fs.mkdirSync(nativeDir, { recursive: true }); fs.mkdirSync(path.join(nativeDir, 'onnxruntime'), { recursive: true }); fs.writeFileSync(path.join(nativeDir, '.gitkeep'), ''); fs.writeFileSync(path.join(nativeDir, 'onnxruntime', '.gitkeep'), ''); let totalFiles = 0; let totalBytes = 0; function assertDarwinArchitecture(srcPath) { if (platform !== 'darwin' || !/\.(?:dylib|node)$/i.test(srcPath)) return; const expected = arch === 'x64' ? 'x86_64' : 'arm64'; const architectures = execFileSync('/usr/bin/lipo', ['-archs', srcPath], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], }).trim().split(/\s+/); if (!architectures.includes(expected)) { throw new Error( `[bundle-native-deps] FATAL — ${path.relative(root, srcPath)} has Mach-O architecture ` + `${architectures.join(', ') || 'unknown'}, expected ${expected}`, ); } } function copyFile(src, destName) { const srcPath = path.join(root, src); const destPath = path.join(nativeDir, destName); if (!fs.existsSync(srcPath)) { throw new Error(`[bundle-native-deps] FATAL — required native dependency ${src} is missing`); } const sourceSize = fs.statSync(srcPath).size; if (sourceSize === 0) throw new Error(`[bundle-native-deps] FATAL — required native dependency ${src} is empty`); assertDarwinArchitecture(srcPath); fs.copyFileSync(srcPath, destPath); const size = fs.statSync(destPath).size; totalBytes += size; totalFiles++; console.log(` ${destName} (${(size / 1024 / 1024).toFixed(1)} MB)`); return true; } function copyDir(srcDir, destSubDir) { const srcPath = path.join(root, srcDir); if (!fs.existsSync(srcPath)) { throw new Error(`[bundle-native-deps] FATAL — required native dependency directory ${srcDir} is missing`); } const destPath = path.join(nativeDir, destSubDir); fs.mkdirSync(destPath, { recursive: true }); let copied = 0; for (const file of fs.readdirSync(srcPath)) { const fullSrc = path.join(srcPath, file); const stat = fs.statSync(fullSrc); if (stat.isFile()) { if (stat.size === 0) throw new Error(`[bundle-native-deps] FATAL — required native dependency ${fullSrc} is empty`); assertDarwinArchitecture(fullSrc); const destFile = path.join(destPath, file); fs.copyFileSync(fullSrc, destFile); totalBytes += stat.size; totalFiles++; copied++; console.log(` ${destSubDir}/${file} (${(stat.size / 1024 / 1024).toFixed(1)} MB)`); } } if (copied === 0) throw new Error(`[bundle-native-deps] FATAL — required native dependency directory ${srcDir} is empty`); } // 1. better-sqlite3 console.log('[bundle-native-deps] better-sqlite3:'); copyFile('node_modules/better-sqlite3/build/Release/better_sqlite3.node', 'better_sqlite3.node'); // 2. sqlite-vec console.log('[bundle-native-deps] sqlite-vec:'); const vecOs = platform === 'win32' ? 'windows' : platform === 'darwin' ? 'darwin' : 'linux'; const vecExt = platform === 'win32' ? 'dll' : platform === 'darwin' ? 'dylib' : 'so'; copyFile( `node_modules/sqlite-vec-${vecOs}-${arch}/vec0.${vecExt}`, `vec0.${vecExt}`, ); // 3. onnxruntime-node (multiple files — binding + shared libraries) console.log('[bundle-native-deps] onnxruntime-node:'); const ortOs = platform === 'win32' ? 'win32' : platform === 'darwin' ? 'darwin' : 'linux'; const ortDir = `node_modules/onnxruntime-node/bin/napi-v3/${ortOs}/${arch}`; copyDir(ortDir, 'onnxruntime'); console.log(`[bundle-native-deps] Copied ${totalFiles} files (${(totalBytes / 1024 / 1024).toFixed(1)} MB total) to resources/native/`);