This commit is contained in:
130
scripts/bundle-node.mjs
Normal file
130
scripts/bundle-node.mjs
Normal file
@@ -0,0 +1,130 @@
|
||||
#!/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 <extractDir>/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)`);
|
||||
Reference in New Issue
Block a user