diff options
Diffstat (limited to 'packages/bun-npm/src')
-rw-r--r-- | packages/bun-npm/src/install.ts | 159 | ||||
-rw-r--r-- | packages/bun-npm/src/platform.ts | 100 | ||||
-rw-r--r-- | packages/bun-npm/src/util.ts | 191 |
3 files changed, 450 insertions, 0 deletions
diff --git a/packages/bun-npm/src/install.ts b/packages/bun-npm/src/install.ts new file mode 100644 index 000000000..9eabd2c41 --- /dev/null +++ b/packages/bun-npm/src/install.ts @@ -0,0 +1,159 @@ +import { fetch, chmod, join, rename, rm, tmp, write, spawn } from "./util"; +import { unzipSync } from "zlib"; +import type { Platform } from "./platform"; +import { os, arch, supportedPlatforms } from "./platform"; + +declare const npmVersion: string; +declare const npmPackage: string; +declare const npmOwner: string; + +export async function importBun(): Promise<string> { + if (!supportedPlatforms.length) { + throw new Error(`Unsupported platform: ${os} ${arch}`); + } + for (const platform of supportedPlatforms) { + try { + return await requireBun(platform); + } catch (error) { + console.debug("requireBun failed", error); + } + } + throw new Error(`Failed to install package "${npmPackage}"`); +} + +async function requireBun(platform: Platform): Promise<string> { + const npmPackage = `${npmOwner}/${platform.bin}`; + function resolveBun() { + const exe = require.resolve(join(npmPackage, platform.exe)); + const { exitCode, stderr, stdout } = spawn(exe, ["--version"]); + if (exitCode === 0) { + return exe; + } + throw new Error(stderr || stdout); + } + try { + return resolveBun(); + } catch (error) { + console.debug("resolveBun failed", error); + console.error( + `Failed to find package "${npmPackage}".`, + `You may have used the "--no-optional" flag when running "npm install".`, + ); + } + const cwd = join("node_modules", npmPackage); + try { + installBun(platform, cwd); + } catch (error) { + console.debug("installBun failed", error); + console.error( + `Failed to install package "${npmPackage}" using "npm install".`, + error, + ); + try { + await downloadBun(platform, cwd); + } catch (error) { + console.debug("downloadBun failed", error); + console.error( + `Failed to download package "${npmPackage}" from "registry.npmjs.org".`, + error, + ); + } + } + return resolveBun(); +} + +function installBun(platform: Platform, dst: string): void { + const npmPackage = `${npmOwner}/${platform.bin}`; + const cwd = tmp(); + try { + write(join(cwd, "package.json"), "{}"); + const { exitCode } = spawn( + "npm", + [ + "install", + "--loglevel=error", + "--prefer-offline", + "--no-audit", + "--progress=false", + `${npmPackage}@${npmVersion}`, + ], + { + cwd, + stdio: "pipe", + env: { + ...process.env, + npm_config_global: undefined, + }, + }, + ); + if (exitCode === 0) { + rename(join(cwd, "node_modules", npmPackage), dst); + } + } finally { + try { + rm(cwd); + } catch (error) { + console.debug("rm failed", error); + // There is nothing to do if the directory cannot be cleaned up. + } + } +} + +async function downloadBun(platform: Platform, dst: string): Promise<void> { + const response = await fetch( + `https://registry.npmjs.org/${npmOwner}/${platform.bin}/-/${platform.bin}-${npmVersion}.tgz`, + ); + const tgz = await response.arrayBuffer(); + let buffer: Buffer; + try { + buffer = unzipSync(tgz); + } catch (cause) { + throw new Error("Invalid gzip data", { cause }); + } + function str(i: number, n: number): string { + return String.fromCharCode(...buffer.subarray(i, i + n)).replace( + /\0.*$/, + "", + ); + } + let offset = 0; + while (offset < buffer.length) { + const name = str(offset, 100).replace("package/", ""); + const size = parseInt(str(offset + 124, 12), 8); + offset += 512; + if (!isNaN(size)) { + write(join(dst, name), buffer.subarray(offset, offset + size)); + if (name === platform.exe) { + try { + chmod(join(dst, name), 0o755); + } catch (error) { + console.debug("chmod failed", error); + } + } + offset += (size + 511) & ~511; + } + } +} + +export function optimizeBun(path: string): void { + if (os === "win32") { + throw new Error( + "You must use Windows Subsystem for Linux, aka. WSL, to run bun. Learn more: https://learn.microsoft.com/en-us/windows/wsl/install", + ); + } + const { npm_config_user_agent } = process.env; + if (npm_config_user_agent && /\byarn\//.test(npm_config_user_agent)) { + throw new Error( + "Yarn does not support bun, because it does not allow linking to binaries. To use bun, install using the following command: curl -fsSL https://bun.sh/install | bash", + ); + } + try { + rename(path, join(__dirname, "bin", "bun")); + return; + } catch (error) { + console.debug("optimizeBun failed", error); + } + throw new Error( + "Your package manager doesn't seem to support bun. To use bun, install using the following command: curl -fsSL https://bun.sh/install | bash", + ); +} diff --git a/packages/bun-npm/src/platform.ts b/packages/bun-npm/src/platform.ts new file mode 100644 index 000000000..a01cc3ddc --- /dev/null +++ b/packages/bun-npm/src/platform.ts @@ -0,0 +1,100 @@ +import { read, spawn } from "./util"; + +export const os = process.platform; + +export const arch = + os === "darwin" && process.arch === "x64" && isRosetta2() + ? "arm64" + : process.arch; + +export const avx2 = + (arch === "x64" && os === "linux" && isLinuxAVX2()) || + (os === "darwin" && isDarwinAVX2()); + +export type Platform = { + os: string; + arch: string; + avx2?: boolean; + bin: string; + exe: string; +}; + +export const platforms: Platform[] = [ + { + os: "darwin", + arch: "arm64", + bin: "bun-darwin-aarch64", + exe: "bin/bun", + }, + { + os: "darwin", + arch: "x64", + avx2: true, + bin: "bun-darwin-x64", + exe: "bin/bun", + }, + { + os: "darwin", + arch: "x64", + bin: "bun-darwin-x64-baseline", + exe: "bin/bun", + }, + { + os: "linux", + arch: "arm64", + bin: "bun-linux-aarch64", + exe: "bin/bun", + }, + { + os: "linux", + arch: "x64", + avx2: true, + bin: "bun-linux-x64", + exe: "bin/bun", + }, + { + os: "linux", + arch: "x64", + bin: "bun-linux-x64-baseline", + exe: "bin/bun", + }, +]; + +export const supportedPlatforms: Platform[] = platforms + .filter( + (platform) => + platform.os === os && platform.arch === arch && (!platform.avx2 || avx2), + ) + .sort((a, b) => (a.avx2 === b.avx2 ? 0 : a.avx2 ? -1 : 1)); + +function isLinuxAVX2(): boolean { + try { + return read("/proc/cpuinfo").includes("avx2"); + } catch (error) { + console.debug("isLinuxAVX2 failed", error); + return false; + } +} + +function isDarwinAVX2(): boolean { + try { + const { exitCode, stdout } = spawn("sysctl", ["-n", "machdep.cpu"]); + return exitCode === 0 && stdout.includes("AVX2"); + } catch (error) { + console.debug("isDarwinAVX2 failed", error); + return false; + } +} + +function isRosetta2(): boolean { + try { + const { exitCode, stdout } = spawn("sysctl", [ + "-n", + "sysctl.proc_translated", + ]); + return exitCode === 0 && stdout.includes("1"); + } catch (error) { + console.debug("isRosetta2 failed", error); + return false; + } +} diff --git a/packages/bun-npm/src/util.ts b/packages/bun-npm/src/util.ts new file mode 100644 index 000000000..c36bda2b7 --- /dev/null +++ b/packages/bun-npm/src/util.ts @@ -0,0 +1,191 @@ +import fs from "fs"; +import path, { dirname } from "path"; +import { tmpdir } from "os"; +import child_process from "child_process"; + +if (process.env["DEBUG"] !== "1") { + console.debug = () => {}; +} + +export function join(...paths: (string | string[])[]): string { + return path.join(...paths.flat(2)); +} + +export function tmp(): string { + const path = fs.mkdtempSync(join(tmpdir(), "bun-")); + console.debug("tmp", path); + return path; +} + +export function rm(path: string): void { + console.debug("rm", path); + try { + fs.rmSync(path, { recursive: true }); + return; + } catch (error) { + console.debug("rmSync failed", error); + // Did not exist before Node.js v14. + // Attempt again with older, slower implementation. + } + let stats: fs.Stats; + try { + stats = fs.lstatSync(path); + } catch (error) { + console.debug("lstatSync failed", error); + // The file was likely deleted, so return early. + return; + } + if (!stats.isDirectory()) { + fs.unlinkSync(path); + return; + } + try { + fs.rmdirSync(path, { recursive: true }); + return; + } catch (error) { + console.debug("rmdirSync failed", error); + // Recursive flag did not exist before Node.js X. + // Attempt again with older, slower implementation. + } + for (const filename of fs.readdirSync(path)) { + rm(join(path, filename)); + } + fs.rmdirSync(path); +} + +export function rename(path: string, newPath: string): void { + console.debug("rename", path, newPath); + try { + fs.renameSync(path, newPath); + return; + } catch (error) { + console.debug("renameSync failed", error); + // If there is an error, delete the new path and try again. + } + try { + rm(newPath); + } catch (error) { + console.debug("rm failed", error); + // The path could have been deleted already. + } + fs.renameSync(path, newPath); +} + +export function write( + path: string, + content: string | ArrayBuffer | ArrayBufferView, +): void { + console.debug("write", path); + try { + fs.writeFileSync(path, content); + return; + } catch (error) { + console.debug("writeFileSync failed", error); + // If there is an error, ensure the parent directory + // exists and try again. + try { + fs.mkdirSync(dirname(path), { recursive: true }); + } catch (error) { + console.debug("mkdirSync failed", error); + // The directory could have been created already. + } + fs.writeFileSync(path, content); + } +} + +export function read(path: string): string { + console.debug("read", path); + return fs.readFileSync(path, "utf-8"); +} + +export function chmod(path: string, mode: fs.Mode): void { + console.debug("chmod", path, mode); + fs.chmodSync(path, mode); +} + +export function spawn( + cmd: string, + args: string[], + options: child_process.SpawnOptions = {}, +): { + exitCode: number; + stdout: string; + stderr: string; +} { + console.debug("spawn", [cmd, ...args].join(" ")); + const { status, stdout, stderr } = child_process.spawnSync(cmd, args, { + stdio: "pipe", + encoding: "utf-8", + ...options, + }); + return { + exitCode: status ?? 1, + stdout, + stderr, + }; +} + +export type Response = { + readonly status: number; + arrayBuffer(): Promise<ArrayBuffer>; + json<T>(): Promise<T>; +}; + +export const fetch = "fetch" in globalThis ? webFetch : nodeFetch; + +async function webFetch(url: string, assert?: boolean): Promise<Response> { + const response = await globalThis.fetch(url); + console.debug("fetch", url, response.status); + if (assert !== false && !isOk(response.status)) { + throw new Error(`${response.status}: ${url}`); + } + return response; +} + +async function nodeFetch(url: string, assert?: boolean): Promise<Response> { + const { get } = await import("node:http"); + return new Promise((resolve, reject) => { + get(url, (response) => { + console.debug("get", url, response.statusCode); + const status = response.statusCode ?? 501; + if (response.headers.location && isRedirect(status)) { + return nodeFetch(url).then(resolve, reject); + } + if (assert !== false && !isOk(status)) { + return reject(new Error(`${status}: ${url}`)); + } + const body: Buffer[] = []; + response.on("data", (chunk) => { + body.push(chunk); + }); + response.on("end", () => { + resolve({ + status, + async arrayBuffer() { + return Buffer.concat(body).buffer as ArrayBuffer; + }, + async json() { + const text = Buffer.concat(body).toString("utf-8"); + return JSON.parse(text); + }, + }); + }); + }).on("error", reject); + }); +} + +function isOk(status: number): boolean { + return status === 200; +} + +function isRedirect(status: number): boolean { + switch (status) { + case 301: // Moved Permanently + case 308: // Permanent Redirect + case 302: // Found + case 307: // Temporary Redirect + case 303: // See Other + return true; + } + return false; +} |