mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
301 lines
8.0 KiB
JavaScript
Executable File
301 lines
8.0 KiB
JavaScript
Executable File
#! /usr/bin/env node
|
|
|
|
import {} from "node:fs/promises";
|
|
import { spawn, spawnSync } from "node:child_process";
|
|
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
import { basename, dirname, join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
|
|
const projectPath = dirname(import.meta.dirname);
|
|
const vendorPath = process.env.BUN_VENDOR_PATH || join(projectPath, "vendor");
|
|
|
|
const isWindows = process.platform === "win32";
|
|
const isMacOS = process.platform === "darwin";
|
|
const isLinux = process.platform === "linux";
|
|
|
|
const spawnSyncTimeout = 1000 * 60;
|
|
const spawnTimeout = 1000 * 60 * 3;
|
|
|
|
async function spawnSafe(command, args, options = {}) {
|
|
const result = new Promise((resolve, reject) => {
|
|
let stdout = "";
|
|
let stderr = "";
|
|
let subprocess;
|
|
try {
|
|
subprocess = spawn(command, args, {
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
timeout: spawnTimeout,
|
|
...options,
|
|
});
|
|
subprocess.on("error", reject);
|
|
subprocess.on("exit", (exitCode, signalCode) => {
|
|
if (exitCode !== 0 || signalCode) {
|
|
const reason = signalCode || `code ${exitCode}`;
|
|
const cause = stderr || stdout;
|
|
reject(new Error(`Process exited with ${reason}`, { cause }));
|
|
} else {
|
|
resolve({ exitCode, signalCode, stdout, stderr });
|
|
}
|
|
});
|
|
subprocess?.stdout?.on("data", chunk => {
|
|
process.stdout.write(chunk);
|
|
stdout += chunk.toString("utf-8");
|
|
});
|
|
subprocess?.stderr?.on("data", chunk => {
|
|
process.stderr.write(chunk);
|
|
stderr += chunk.toString("utf-8");
|
|
});
|
|
} catch (cause) {
|
|
reject(cause);
|
|
}
|
|
});
|
|
try {
|
|
return await result;
|
|
} catch (cause) {
|
|
if (options.throwOnError === false) {
|
|
return;
|
|
}
|
|
const description = `${command} ${args.join(" ")}`;
|
|
throw new Error(`Command failed: ${description}`, { cause });
|
|
}
|
|
}
|
|
|
|
function spawnSyncSafe(command, args, options = {}) {
|
|
try {
|
|
const { error, status, signal, stdout, stderr } = spawnSync(command, args, {
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
encoding: "utf-8",
|
|
timeout: spawnSyncTimeout,
|
|
...options,
|
|
});
|
|
if (error) {
|
|
throw error;
|
|
}
|
|
if (signal || status !== 0) {
|
|
const reason = signal || `code ${status}`;
|
|
const cause = stderr || stdout;
|
|
throw new Error(`Process exited with ${reason}`, { cause });
|
|
}
|
|
return stdout;
|
|
} catch (cause) {
|
|
if (options.throwOnError === false) {
|
|
return;
|
|
}
|
|
const description = `${command} ${args.join(" ")}`;
|
|
throw new Error(`Command failed: ${description}`, { cause });
|
|
}
|
|
}
|
|
|
|
async function fetchSafe(url, options = {}) {
|
|
let response;
|
|
try {
|
|
response = await fetch(url, options);
|
|
if (!response.ok) {
|
|
const { status, statusText } = response;
|
|
const body = await response.text();
|
|
throw new Error(`${status} ${statusText}`, { cause: body });
|
|
}
|
|
switch (options.format) {
|
|
case "json":
|
|
return await response.json();
|
|
case "text":
|
|
return await response.text();
|
|
case "bytes":
|
|
return new Uint8Array(await response.arrayBuffer());
|
|
default:
|
|
return response;
|
|
}
|
|
} catch (cause) {
|
|
if (options.throwOnError === false) {
|
|
return response;
|
|
}
|
|
throw new Error(`Fetch failed: ${url}`, { cause });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} command
|
|
* @param {string} [path]
|
|
* @returns {string | undefined}
|
|
*/
|
|
function which(command, path) {
|
|
const cmd = isWindows ? "where" : "which";
|
|
const result = spawnSyncSafe(cmd, [command], {
|
|
throwOnError: false,
|
|
env: {
|
|
PATH: path || process.env.PATH,
|
|
},
|
|
});
|
|
if (!result) {
|
|
return;
|
|
}
|
|
if (isWindows) {
|
|
// On Windows, multiple paths can be returned from `where`.
|
|
for (const line of result.split("\r\n")) {
|
|
return line;
|
|
}
|
|
}
|
|
return result.trimEnd();
|
|
}
|
|
|
|
function getZigTarget(os = process.platform, arch = process.arch) {
|
|
if (arch === "x64") {
|
|
if (os === "linux") return "linux-x86_64";
|
|
if (os === "darwin") return "macos-x86_64";
|
|
if (os === "win32") return "windows-x86_64";
|
|
}
|
|
if (arch === "arm64") {
|
|
if (os === "linux") return "linux-aarch64";
|
|
if (os === "darwin") return "macos-aarch64";
|
|
}
|
|
throw new Error(`Unsupported zig target: os=${os}, arch=${arch}`);
|
|
}
|
|
|
|
function getRecommendedZigVersion() {
|
|
const scriptPath = join(projectPath, "build.zig");
|
|
try {
|
|
const scriptContent = readFileSync(scriptPath, "utf-8");
|
|
const match = scriptContent.match(/recommended_zig_version = "([^"]+)"/);
|
|
if (!match) {
|
|
throw new Error("File does not contain string: 'recommended_zig_version'");
|
|
}
|
|
return match[1];
|
|
} catch (cause) {
|
|
throw new Error("Failed to find recommended Zig version", { cause });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @returns {Promise<string>}
|
|
*/
|
|
async function getLatestZigVersion() {
|
|
try {
|
|
const response = await fetchSafe("https://ziglang.org/download/index.json", { format: "json" });
|
|
const { master } = response;
|
|
const { version } = master;
|
|
return version;
|
|
} catch (cause) {
|
|
throw new Error("Failed to get latest Zig version", { cause });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} execPath
|
|
* @returns {string | undefined}
|
|
*/
|
|
function getVersion(execPath) {
|
|
const args = /(?:zig)(?:\.exe)?/i.test(execPath) ? ["version"] : ["--version"];
|
|
const result = spawnSyncSafe(execPath, args, { throwOnError: false });
|
|
if (!result) {
|
|
return;
|
|
}
|
|
return result.trim();
|
|
}
|
|
|
|
/**
|
|
* @returns {string}
|
|
*/
|
|
function getTmpdir() {
|
|
if (isMacOS && existsSync("/tmp")) {
|
|
return "/tmp";
|
|
}
|
|
return tmpdir();
|
|
}
|
|
|
|
/**
|
|
* @returns {string}
|
|
*/
|
|
function mkTmpdir() {
|
|
return mkdtempSync(join(getTmpdir(), "bun-"));
|
|
}
|
|
|
|
/**
|
|
* @param {string} url
|
|
* @param {string} [path]
|
|
* @returns {Promise<string>}
|
|
*/
|
|
async function downloadFile(url, path) {
|
|
const outPath = path || join(mkTmpdir(), basename(url));
|
|
const bytes = await fetchSafe(url, { format: "bytes" });
|
|
mkdirSync(dirname(outPath), { recursive: true });
|
|
writeFileSync(outPath, bytes);
|
|
return outPath;
|
|
}
|
|
|
|
/**
|
|
* @param {string} tarPath
|
|
* @param {string} [path]
|
|
* @returns {Promise<string>}
|
|
*/
|
|
async function extractFile(tarPath, path) {
|
|
const outPath = path || join(mkTmpdir(), basename(tarPath));
|
|
mkdirSync(outPath, { recursive: true });
|
|
await spawnSafe("tar", ["-xf", tarPath, "-C", outPath, "--strip-components=1"]);
|
|
return outPath;
|
|
}
|
|
|
|
const dependencies = [
|
|
{
|
|
name: "zig",
|
|
version: getRecommendedZigVersion(),
|
|
download: downloadZig,
|
|
},
|
|
];
|
|
|
|
async function getDependencyPath(name) {
|
|
let dependency;
|
|
for (const entry of dependencies) {
|
|
if (name === entry.name) {
|
|
dependency = entry;
|
|
break;
|
|
}
|
|
}
|
|
if (!dependency) {
|
|
throw new Error(`Unknown dependency: ${name}`);
|
|
}
|
|
const { version, download } = dependency;
|
|
mkdirSync(vendorPath, { recursive: true });
|
|
for (const path of readdirSync(vendorPath)) {
|
|
if (!path.startsWith(name)) {
|
|
continue;
|
|
}
|
|
const dependencyPath = join(vendorPath, path);
|
|
const dependencyVersion = getVersion(dependencyPath);
|
|
if (dependencyVersion === version) {
|
|
return dependencyPath;
|
|
}
|
|
}
|
|
if (!download) {
|
|
throw new Error(`Dependency not found: ${name}`);
|
|
}
|
|
return await download(version);
|
|
}
|
|
|
|
/**
|
|
* @param {string} [version]
|
|
*/
|
|
async function downloadZig(version) {
|
|
const target = getZigTarget();
|
|
const expectedVersion = version || getRecommendedZigVersion();
|
|
const url = `https://ziglang.org/builds/zig-${target}-${expectedVersion}.tar.xz`;
|
|
const tarPath = await downloadFile(url);
|
|
const extractedPath = await extractFile(tarPath);
|
|
const zigPath = join(extractedPath, exePath("zig"));
|
|
const actualVersion = getVersion(zigPath);
|
|
const outPath = join(vendorPath, exePath(`zig-${actualVersion}`));
|
|
mkdirSync(dirname(outPath), { recursive: true });
|
|
copyFileSync(zigPath, outPath);
|
|
return outPath;
|
|
}
|
|
|
|
/**
|
|
* @param {string} path
|
|
* @returns {string}
|
|
*/
|
|
function exePath(path) {
|
|
return isWindows ? `${path}.exe` : path;
|
|
}
|
|
|
|
const execPath = await getDependencyPath("zig");
|
|
console.log(execPath);
|