#!/usr/bin/env node /** * Downloads the correct Node.js binary for the current platform and places it * in app/src-tauri/resources/ for Tauri bundling. * * Defaults to the Node version running this script so copied native modules * from node_modules match the bundled runtime ABI. Set * WAGGLE_BUNDLED_NODE_VERSION to pin a different version intentionally. * * Uses Node.js official distribution (https://nodejs.org/dist/). * Caches in scripts/.cache/ to avoid re-downloading. */ 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 resourcesDir = path.join(root, 'app', 'src-tauri', 'resources'); const cacheDir = path.join(__dirname, '.cache'); const NODE_VERSION = process.env.WAGGLE_BUNDLED_NODE_VERSION ?? process.versions.node; if (!/^\d+\.\d+\.\d+$/.test(NODE_VERSION)) { console.error(`[bundle-node] FATAL — invalid Node.js version: ${NODE_VERSION}`); process.exit(1); } const platform = process.platform; const arch = process.env.TARGET_ARCH || process.arch; // macOS "universal" is not a valid download target: nodejs.org ships per-arch // binaries (node-vX-darwin-arm64 / -x64), and a universal run would fall // through to the x64 tarball and ship an x64-only node in an arm64 bundle. // Build each arch separately and lipo the app bundle instead. if (arch === 'universal') { console.error( '[bundle-node] FATAL — TARGET_ARCH=universal is not supported.\n' + ' Node.js ships per-arch binaries. Build each arch separately:\n' + ' TARGET_ARCH=arm64 (aarch64-apple-darwin) and TARGET_ARCH=x64\n' + ' (x86_64-apple-darwin) — see release.yml\'s macOS matrix and the app\n' + ' tauri:build:mac:arm64 / :x64 scripts.', ); process.exit(1); } fs.mkdirSync(cacheDir, { recursive: true }); fs.mkdirSync(resourcesDir, { recursive: true }); const destBinary = platform === 'win32' ? path.join(resourcesDir, 'node.exe') : path.join(resourcesDir, 'node'); // Check cache const cacheKey = `node-v${NODE_VERSION}-${platform}-${arch}`; const cachedBinary = path.join(cacheDir, platform === 'win32' ? `${cacheKey}.exe` : cacheKey); if (fs.existsSync(cachedBinary)) { console.log(`[bundle-node] Using cached Node.js v${NODE_VERSION} (${platform}-${arch})`); fs.copyFileSync(cachedBinary, destBinary); if (platform !== 'win32') { fs.chmodSync(destBinary, 0o755); } const size = (fs.statSync(destBinary).size / 1024 / 1024).toFixed(1); console.log(`[bundle-node] → ${destBinary} (${size} MB)`); process.exit(0); } // Download if (platform === 'win32') { // Windows: direct .exe download const url = `https://nodejs.org/dist/v${NODE_VERSION}/win-${arch}/node.exe`; console.log(`[bundle-node] Downloading Node.js v${NODE_VERSION} (${platform}-${arch})...`); console.log(` ${url}`); const response = await fetch(url); if (!response.ok) { console.error(`[bundle-node] Download failed: HTTP ${response.status}`); process.exit(1); } const buffer = Buffer.from(await response.arrayBuffer()); fs.writeFileSync(cachedBinary, buffer); fs.copyFileSync(cachedBinary, destBinary); } else { // macOS/Linux: download .tar.gz and extract bin/node const nodeArch = arch === 'arm64' ? 'arm64' : 'x64'; const osPart = platform === 'darwin' ? 'darwin' : 'linux'; const archiveName = `node-v${NODE_VERSION}-${osPart}-${nodeArch}.tar.gz`; const url = `https://nodejs.org/dist/v${NODE_VERSION}/${archiveName}`; console.log(`[bundle-node] Downloading Node.js v${NODE_VERSION} (${osPart}-${nodeArch})...`); console.log(` ${url}`); const archivePath = path.join(cacheDir, archiveName); const response = await fetch(url); if (!response.ok) { console.error(`[bundle-node] Download failed: HTTP ${response.status}`); process.exit(1); } const buffer = Buffer.from(await response.arrayBuffer()); fs.writeFileSync(archivePath, buffer); // Extract bin/node from the tarball. The archive root is the versioned dir // (node-vX-os-arch/), so the member must include that prefix; --strip- // components=1 then drops it so the file lands at /bin/node. // BSD tar (macOS) matches members against the FULL archived path, so a bare // "bin/node" matches nothing → "tar: bin/node: Not found in archive". // (safe — no user input in args) const extractDir = path.join(cacheDir, 'extract'); fs.mkdirSync(extractDir, { recursive: true }); const member = `node-v${NODE_VERSION}-${osPart}-${nodeArch}/bin/node`; execFileSync('tar', ['xzf', archivePath, '-C', extractDir, '--strip-components=1', member], { stdio: 'inherit', }); const extractedNode = path.join(extractDir, 'bin', 'node'); fs.copyFileSync(extractedNode, cachedBinary); fs.copyFileSync(cachedBinary, destBinary); fs.chmodSync(destBinary, 0o755); // Cleanup extracted files fs.rmSync(extractDir, { recursive: true, force: true }); } const size = (fs.statSync(destBinary).size / 1024 / 1024).toFixed(1); console.log(`[bundle-node] Node.js v${NODE_VERSION} (${platform}-${arch}) → ${destBinary} (${size} MB)`);