377 lines
11 KiB
JavaScript
377 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
|
// Immediate log to verify script execution
|
|
console.log("[setup-bun] Script loaded");
|
|
|
|
/**
|
|
* Postinstall script to set up bun from platform-specific optional dependencies.
|
|
* Handles Windows ARM64 by downloading x64-baseline via emulation.
|
|
*/
|
|
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
symlinkSync,
|
|
unlinkSync,
|
|
copyFileSync,
|
|
chmodSync,
|
|
writeFileSync,
|
|
statSync,
|
|
} from "fs";
|
|
import { join, dirname } from "path";
|
|
import { spawnSync } from "child_process";
|
|
import { fileURLToPath } from "url";
|
|
import { get } from "https";
|
|
import { createGunzip } from "zlib";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const projectRoot = join(__dirname, "..");
|
|
const nodeModules = join(projectRoot, "node_modules");
|
|
const binDir = join(nodeModules, ".bin");
|
|
|
|
const os = process.platform;
|
|
const arch = process.arch;
|
|
const isWindows = os === "win32";
|
|
const bunExe = isWindows ? "bun.exe" : "bun";
|
|
|
|
// Detect libc type on Linux (glibc vs musl)
|
|
function detectLibc() {
|
|
if (os !== "linux") return null;
|
|
|
|
// Check for musl-specific loader
|
|
const muslLoaders = [
|
|
`/lib/ld-musl-${arch === "arm64" ? "aarch64" : "x86_64"}.so.1`,
|
|
"/lib/ld-musl-x86_64.so.1",
|
|
"/lib/ld-musl-aarch64.so.1",
|
|
];
|
|
|
|
for (const loader of muslLoaders) {
|
|
if (existsSync(loader)) {
|
|
console.log(` Detected musl libc (found ${loader})`);
|
|
return "musl";
|
|
}
|
|
}
|
|
|
|
// Default to glibc on Linux
|
|
console.log(" Detected glibc (no musl loader found)");
|
|
return "glibc";
|
|
}
|
|
|
|
// Platform to package mapping (matches @oven/bun-* package names)
|
|
// For Linux, separate glibc and musl packages
|
|
const platformPackages = {
|
|
darwin: {
|
|
arm64: ["bun-darwin-aarch64"],
|
|
x64: ["bun-darwin-x64", "bun-darwin-x64-baseline"],
|
|
},
|
|
linux: {
|
|
arm64: {
|
|
glibc: ["bun-linux-aarch64"],
|
|
musl: ["bun-linux-aarch64-musl"],
|
|
},
|
|
x64: {
|
|
glibc: ["bun-linux-x64", "bun-linux-x64-baseline"],
|
|
musl: ["bun-linux-x64-musl", "bun-linux-x64-musl-baseline"],
|
|
},
|
|
},
|
|
win32: {
|
|
x64: ["bun-windows-x64", "bun-windows-x64-baseline"],
|
|
arm64: ["bun-windows-x64-baseline"], // x64 runs via emulation on ARM64
|
|
},
|
|
};
|
|
|
|
function findBunBinary() {
|
|
let packages = platformPackages[os]?.[arch];
|
|
|
|
// For Linux, select packages based on libc type
|
|
if (os === "linux" && packages && typeof packages === "object") {
|
|
const libc = detectLibc();
|
|
packages = packages[libc] || [];
|
|
}
|
|
|
|
packages = packages || [];
|
|
console.log(
|
|
`Looking for bun packages: ${packages.join(", ") || "(none for this platform)"}`,
|
|
);
|
|
|
|
for (const pkg of packages) {
|
|
const binPath = join(nodeModules, "@oven", pkg, "bin", bunExe);
|
|
console.log(` Checking: ${binPath}`);
|
|
if (existsSync(binPath)) {
|
|
console.log(` Found bun at: ${binPath}`);
|
|
return binPath;
|
|
} else {
|
|
console.log(` Not found`);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async function downloadBunForWindowsArm64() {
|
|
// Windows ARM64 can run x64 binaries via emulation
|
|
const pkg = "bun-windows-x64-baseline";
|
|
const version = "1.2.21";
|
|
const url = `https://registry.npmjs.org/@oven/${pkg}/-/${pkg}-${version}.tgz`;
|
|
const destDir = join(nodeModules, "@oven", pkg);
|
|
|
|
console.log(`Downloading ${pkg} for Windows ARM64 emulation...`);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
get(url, (response) => {
|
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
get(response.headers.location, handleResponse).on("error", reject);
|
|
} else {
|
|
handleResponse(response);
|
|
}
|
|
|
|
function handleResponse(res) {
|
|
if (res.statusCode !== 200) {
|
|
reject(new Error(`Failed to download: ${res.statusCode}`));
|
|
return;
|
|
}
|
|
|
|
const chunks = [];
|
|
const gunzip = createGunzip();
|
|
|
|
res.pipe(gunzip);
|
|
|
|
gunzip.on("data", (chunk) => chunks.push(chunk));
|
|
gunzip.on("end", () => {
|
|
try {
|
|
extractTar(Buffer.concat(chunks), destDir);
|
|
const binPath = join(destDir, "bin", bunExe);
|
|
if (existsSync(binPath)) {
|
|
resolve(binPath);
|
|
} else {
|
|
reject(new Error("Binary not found after extraction"));
|
|
}
|
|
} catch (err) {
|
|
reject(err);
|
|
}
|
|
});
|
|
gunzip.on("error", reject);
|
|
}
|
|
}).on("error", reject);
|
|
});
|
|
}
|
|
|
|
function extractTar(buffer, destDir) {
|
|
// Simple tar extraction (512-byte blocks)
|
|
let offset = 0;
|
|
while (offset < buffer.length) {
|
|
const name = buffer
|
|
.toString("utf-8", offset, offset + 100)
|
|
.replace(/\0.*$/, "")
|
|
.replace("package/", "");
|
|
const size = parseInt(
|
|
buffer.toString("utf-8", offset + 124, offset + 136).trim(),
|
|
8,
|
|
);
|
|
|
|
offset += 512;
|
|
|
|
if (!isNaN(size) && size > 0 && name) {
|
|
const filePath = join(destDir, name);
|
|
const fileDir = dirname(filePath);
|
|
if (!existsSync(fileDir)) {
|
|
mkdirSync(fileDir, { recursive: true });
|
|
}
|
|
const content = buffer.subarray(offset, offset + size);
|
|
writeFileSync(filePath, content);
|
|
|
|
// Make executable
|
|
if (name.endsWith(bunExe) || name === "bin/bun") {
|
|
try {
|
|
chmodSync(filePath, 0o755);
|
|
} catch {}
|
|
}
|
|
|
|
offset += Math.ceil(size / 512) * 512;
|
|
}
|
|
}
|
|
}
|
|
|
|
function copyFileWithRetry(source, dest, maxRetries = 3, delay = 100) {
|
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
try {
|
|
copyFileSync(source, dest);
|
|
return true;
|
|
} catch (err) {
|
|
if (attempt === maxRetries) {
|
|
throw err;
|
|
}
|
|
// Wait a bit before retrying (exponential backoff)
|
|
const waitTime = delay * Math.pow(2, attempt - 1);
|
|
const start = Date.now();
|
|
while (Date.now() - start < waitTime) {
|
|
// Busy wait (simple delay without external dependencies)
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function setupBinLink(bunPath) {
|
|
if (!existsSync(binDir)) {
|
|
mkdirSync(binDir, { recursive: true });
|
|
}
|
|
|
|
const bunLink = join(binDir, bunExe);
|
|
const bunxLink = join(binDir, isWindows ? "bunx.exe" : "bunx");
|
|
|
|
// Check if files already exist and are valid (same size as source)
|
|
// This can help avoid unnecessary copy operations that might fail
|
|
let needsCopy = true;
|
|
if (existsSync(bunLink) && existsSync(bunxLink)) {
|
|
try {
|
|
const sourceStat = statSync(bunPath);
|
|
const linkStat = statSync(bunLink);
|
|
if (sourceStat.size === linkStat.size) {
|
|
console.log(
|
|
"Bun binaries already exist and appear valid, skipping copy",
|
|
);
|
|
needsCopy = false;
|
|
}
|
|
} catch {
|
|
// If stat fails, proceed with copy
|
|
needsCopy = true;
|
|
}
|
|
}
|
|
|
|
if (needsCopy) {
|
|
// Remove existing links
|
|
for (const link of [bunLink, bunxLink]) {
|
|
try {
|
|
unlinkSync(link);
|
|
} catch {}
|
|
}
|
|
} else {
|
|
console.log(`Bun linked to: ${bunLink}`);
|
|
return;
|
|
}
|
|
|
|
if (isWindows) {
|
|
// On Windows, copy the binary (symlinks may not work without admin)
|
|
// Use retry logic to handle file locking issues
|
|
try {
|
|
copyFileWithRetry(bunPath, bunLink);
|
|
copyFileWithRetry(bunPath, bunxLink);
|
|
} catch (err) {
|
|
// If copy fails, try using Windows copy command as fallback
|
|
console.log(`Copy failed, trying Windows copy command: ${err.message}`);
|
|
try {
|
|
if (isWindows) {
|
|
// Use cmd /c copy for Windows with proper path quoting
|
|
// Paths with spaces need to be quoted, and we need to handle backslashes
|
|
const sourceQuoted = `"${bunPath}"`;
|
|
const destQuoted = `"${bunLink}"`;
|
|
const destxQuoted = `"${bunxLink}"`;
|
|
|
|
// Try copying with a small delay between attempts
|
|
const result1 = spawnSync(
|
|
"cmd.exe",
|
|
["/c", "copy", "/Y", sourceQuoted, destQuoted],
|
|
{ shell: false, stdio: "pipe" },
|
|
);
|
|
|
|
// Small delay before second copy
|
|
const start = Date.now();
|
|
while (Date.now() - start < 50) {}
|
|
|
|
const result2 = spawnSync(
|
|
"cmd.exe",
|
|
["/c", "copy", "/Y", sourceQuoted, destxQuoted],
|
|
{ shell: false, stdio: "pipe" },
|
|
);
|
|
|
|
if (result1.status !== 0) {
|
|
const errorMsg =
|
|
result1.stderr?.toString() ||
|
|
result1.stdout?.toString() ||
|
|
"Unknown error";
|
|
throw new Error(
|
|
`Windows copy command failed for bun.exe: ${errorMsg}`,
|
|
);
|
|
}
|
|
if (result2.status !== 0) {
|
|
const errorMsg =
|
|
result2.stderr?.toString() ||
|
|
result2.stdout?.toString() ||
|
|
"Unknown error";
|
|
throw new Error(
|
|
`Windows copy command failed for bunx.exe: ${errorMsg}`,
|
|
);
|
|
}
|
|
|
|
// Verify files were created
|
|
if (!existsSync(bunLink) || !existsSync(bunxLink)) {
|
|
throw new Error("Files were not created after copy command");
|
|
}
|
|
} else {
|
|
// Fallback to original copyFileSync if not Windows
|
|
copyFileSync(bunPath, bunLink);
|
|
copyFileSync(bunPath, bunxLink);
|
|
}
|
|
} catch (fallbackErr) {
|
|
// If all copy methods fail, log the error but don't throw
|
|
// The script will exit gracefully and bun can be installed manually
|
|
console.error(
|
|
`All copy methods failed. Bun setup incomplete: ${fallbackErr.message}`,
|
|
);
|
|
console.error(
|
|
"You may need to install bun manually or run this script with appropriate permissions.",
|
|
);
|
|
throw fallbackErr;
|
|
}
|
|
}
|
|
} else {
|
|
// On Unix, use symlinks
|
|
symlinkSync(bunPath, bunLink);
|
|
symlinkSync(bunPath, bunxLink);
|
|
}
|
|
|
|
console.log(`Bun linked to: ${bunLink}`);
|
|
}
|
|
|
|
// Force immediate output
|
|
process.stdout.write("[setup-bun] Script starting...\n");
|
|
|
|
async function main() {
|
|
process.stdout.write(`[setup-bun] Setting up bun for ${os} ${arch}...\n`);
|
|
process.stdout.write(`[setup-bun] Project root: ${projectRoot}\n`);
|
|
process.stdout.write(`[setup-bun] Node modules: ${nodeModules}\n`);
|
|
|
|
let bunPath = findBunBinary();
|
|
|
|
if (!bunPath && os === "win32" && arch === "arm64") {
|
|
try {
|
|
bunPath = await downloadBunForWindowsArm64();
|
|
} catch (err) {
|
|
console.error("Failed to download bun for Windows ARM64:", err.message);
|
|
}
|
|
}
|
|
|
|
if (!bunPath) {
|
|
console.log("No bun binary found in optional dependencies.");
|
|
console.log("Bun will need to be installed separately.");
|
|
console.log("See: https://bun.sh/docs/installation");
|
|
process.exit(0); // Don't fail the install
|
|
}
|
|
|
|
try {
|
|
setupBinLink(bunPath);
|
|
|
|
// Verify installation
|
|
const result = spawnSync(bunPath, ["--version"], { encoding: "utf-8" });
|
|
if (result.status === 0) {
|
|
console.log(`Bun ${result.stdout.trim()} installed successfully!`);
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to set up bun:", err.message);
|
|
process.exit(0); // Don't fail the install
|
|
}
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(0); // Don't fail the install
|
|
});
|