bebop-tools
Version:
Bebop compiler for Node projects.
210 lines (198 loc) • 5.44 kB
text/typescript
import path from "path";
import fs from "fs";
import os from "os";
import child_process from "child_process";
import nodeBindings from "wasi-js/dist/bindings/node";
import { WASIExitError, WASIKillError } from "wasi-js/dist/types";
import WASI from "wasi-js";
const supportedCpuArchitectures = ["x64", "arm64"];
const supportedPlatforms = ["win32", "darwin", "linux"];
const isWebContainer = (() => {
const isStackblitz =
process.env.SHELL === "/bin/jsh" && process.versions.webcontainer != null;
if (isStackblitz) {
return true;
}
// codesandbox
if (process.env.CSB?.includes("true")) {
return true;
}
return false;
})();
/**
* Ensures the current host OS is supported by the Bebop compiler
* @param {NodeJS.Architecture} arch the host arch
* @param {NodeJS.Platform} platform the host os
*/
function ensureHostSupport(arch: string, platform: string) {
if (isWebContainer) return;
if (!supportedCpuArchitectures.includes(arch))
throw new Error(`Unsupported CPU arch: ${arch}`);
if (!supportedPlatforms.includes(platform))
throw new Error(`Unsupported platform: ${platform}`);
}
/**
* Gets information about the current compiler host
* @returns
*/
function getHostInfo() {
const arch = process.arch;
const platform = process.platform;
if (isWebContainer) {
return {
arch: "wasm",
os: "wasi",
exeSuffix: ".wasm",
};
}
ensureHostSupport(arch, platform);
const osName = (() => {
switch (platform) {
case "win32":
return "windows";
case "darwin":
return "macos";
case "linux":
return "linux";
default:
throw new Error(`Unknown platform name: ${platform}`);
}
})();
return {
arch: arch,
os: osName,
exeSuffix: osName === "windows" ? ".exe" : "",
};
}
/**
* Gets the fully qualified and normalized path to correct bundled version of the Bebop compiler
*/
const resolveBebopcPath = () => {
const toolsDir = path.resolve(__dirname, "../tools");
if (!fs.existsSync(toolsDir)) {
throw new Error(`The root 'tools' directory does not exist: ${toolsDir}`);
}
const info = getHostInfo();
const executable = path.normalize(
`${path.resolve(
toolsDir,
`${info.os}/${info.arch}/bebopc${info.exeSuffix}`
)}`
);
if (!fs.existsSync(executable)) {
throw new Error(`${executable} does not exist`);
}
return executable;
};
/**
* Ensures that bebopc binary is executable
* @param {string} executable the path to the executable
*/
const setExecutableBit = (executable: string) => {
if (isWebContainer) return;
if (process.platform === "win32") {
child_process.execSync(`Unblock-File -Path "${executable}"`, {
stdio: "ignore",
shell: "powershell.exe",
});
} else {
child_process.execSync(`chmod +x "${executable}"`, { stdio: "ignore" });
}
};
const launchBebopc = async (args: string[]): Promise<number> => {
if (!isWebContainer) {
const executable = resolveBebopcPath();
const child = child_process.spawn(executable, args, {
stdio: "inherit",
});
return await new Promise((resolve, reject) => {
child.on("exit", (code) => {
if (code === 0) {
resolve(code);
} else {
reject(new Error(`bebopc exited with code ${code}`));
}
});
});
} else {
return await launchWasi(args);
}
};
const decoder = new TextDecoder();
const launchWasi = async (args: string[]): Promise<number> => {
// add the executable name to the front
args.unshift("bebopc");
const module = await WebAssembly.compile(
await new Promise((resolve, reject) => {
fs.readFile(resolveBebopcPath(), (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
})
);
let sab: Int32Array | undefined;
const workingDir = process.cwd();
let standardOutput = "";
let standardError = "";
const wasi = new WASI({
args,
env: {
RUST_BACKTRACE: "1",
},
bindings: {
...nodeBindings,
exit: (code: number | null) => {
throw new WASIExitError(code);
},
kill: (signal: string) => {
throw new WASIKillError(signal);
},
},
preopens: {
// we're giving the wasi module access to the current working directory
"/": workingDir,
"/tmp": os.tmpdir(),
},
sendStdout: (data: Uint8Array): void => {
standardOutput += decoder.decode(data);
},
sendStderr: (data: Uint8Array) => {
standardError += decoder.decode(data);
},
sleep: (ms: number) => {
sab ??= new Int32Array(new SharedArrayBuffer(4));
Atomics.wait(sab, 0, 0, Math.max(ms, 1));
},
});
let imports = wasi.getImports(module);
imports = {
wasi_snapshot_preview1: {
...imports.wasi_snapshot_preview1,
sock_accept: () => -1,
},
};
const instance = await WebAssembly.instantiate(module, imports);
let exitCode = 0;
try {
wasi.start(instance);
} catch (e) {
if (e instanceof WASIExitError) {
exitCode = e.code ?? 127;
} else {
throw e;
}
}
standardOutput = standardOutput.trim();
if (standardOutput) {
console.log(standardOutput);
}
standardError = standardError.trim();
if (standardError) {
console.error(standardError);
}
return exitCode;
};
export { resolveBebopcPath, setExecutableBit, launchBebopc };