aboutsummaryrefslogtreecommitdiff
path: root/packages/bun-npm/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/bun-npm/src')
-rw-r--r--packages/bun-npm/src/install.ts159
-rw-r--r--packages/bun-npm/src/platform.ts100
-rw-r--r--packages/bun-npm/src/util.ts191
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;
+}