quickjs-done/standalone.js
Sneed Group Holder e18d08e951
Some checks are pending
ci / Linux (Ubuntu) (push) Waiting to run
ci / linux-asan (push) Waiting to run
ci / linux-msan (push) Waiting to run
ci / linux-ubsan (push) Waiting to run
ci / macOS (push) Waiting to run
ci / macos-asan (push) Waiting to run
ci / macos-ubsan (push) Waiting to run
ci / freebsd (push) Waiting to run
ci / qemu-alpine (arm32v6) (push) Waiting to run
ci / qemu-alpine (arm32v7) (push) Waiting to run
ci / qemu-alpine (arm64v8) (push) Waiting to run
ci / qemu-alpine (i386) (push) Waiting to run
ci / qemu-alpine (s390x) (push) Waiting to run
rebase on nextgen
2025-01-06 14:48:15 -06:00

129 lines
3.7 KiB
JavaScript

import * as std from "qjs:std";
import * as os from "qjs:os";
import * as bjson from "qjs:bjson";
// See quickjs.h
const JS_READ_OBJ_BYTECODE = 1 << 0;
const JS_READ_OBJ_REFERENCE = 1 << 3;
const JS_WRITE_OBJ_BYTECODE = 1 << 0;
const JS_WRITE_OBJ_REFERENCE = 1 << 3;
const JS_WRITE_OBJ_STRIP_SOURCE = 1 << 4;
/**
* Trailer for standalone binaries. When some code gets bundled with the qjs
* executable we add a 12 byte trailer. The first 8 bytes are the magic
* string that helps us understand this is a standalone binary, and the
* remaining 4 are the offset (from the beginning of the binary) where the
* bundled data is located.
*
* The offset is stored as a 32bit little-endian number.
*/
const Trailer = {
Magic: 'quickjs2',
MagicSize: 8,
DataSize: 4,
Size: 12
};
function encodeAscii(txt) {
return new Uint8Array(txt.split('').map(c => c.charCodeAt(0)));
}
function decodeAscii(buf) {
return Array.from(buf).map(c => String.fromCharCode(c)).join('')
}
export function compileStandalone(inFile, outFile, targetExe) {
// Step 1: compile the source file to bytecode
const js = std.loadFile(inFile);
if (!js) {
throw new Error(`failed to open ${inFile}`);
}
const code = std.evalScript(js, {
compile_only: true,
compile_module: true
});
const bytecode = new Uint8Array(bjson.write(code, JS_WRITE_OBJ_BYTECODE | JS_WRITE_OBJ_REFERENCE | JS_WRITE_OBJ_STRIP_SOURCE));
// Step 2: copy the bytecode to the end of the executable and add a marker.
const exeFileName = targetExe ?? globalThis.argv0;
const exe = std.loadFile(exeFileName, { binary: true });
if (!exe) {
throw new Error(`failed to open executable: ${exeFileName}`);
}
const exeSize = exe.length;
const newBuffer = exe.buffer.transfer(exeSize + bytecode.length + Trailer.Size);
const newExe = new Uint8Array(newBuffer);
newExe.set(bytecode, exeSize);
newExe.set(encodeAscii(Trailer.Magic), exeSize + bytecode.length);
const dw = new DataView(newBuffer, exeSize + bytecode.length + Trailer.MagicSize, Trailer.DataSize);
dw.setUint32(0, exeSize, true /* little-endian */);
// We use os.open() so we can set the permissions mask.
const newFd = os.open(outFile, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o755);
if (newFd < 0) {
throw new Error(`failed to create ${outFile}`);
}
if (os.write(newFd, newBuffer, 0, newBuffer.byteLength) < 0) {
os.close(newFd);
throw new Error(`failed to write to output file`);
}
os.close(newFd);
}
export function runStandalone() {
const file = globalThis.argv0;
const exe = std.open(file, 'rb');
if (!exe) {
throw new Error(`failed to open executable: ${file}`);
}
let r = exe.seek(-Trailer.Size, std.SEEK_END);
if (r < 0) {
throw new Error(`seek error: ${-r}`);
}
const trailer = new Uint8Array(Trailer.Size);
exe.read(trailer.buffer, 0, Trailer.Size);
const magic = new Uint8Array(trailer.buffer, 0, Trailer.MagicSize);
// Shouldn't happen since qjs.c checks for it.
if (decodeAscii(magic) !== Trailer.Magic) {
exe.close();
throw new Error('corrupted binary, magic mismatch');
}
const dw = new DataView(trailer.buffer, Trailer.MagicSize, Trailer.DataSize);
const offset = dw.getUint32(0, true /* little-endian */);
const bytecode = new Uint8Array(offset - Trailer.Size);
r = exe.seek(offset, std.SEEK_SET);
if (r < 0) {
exe.close();
throw new Error(`seek error: ${-r}`);
}
exe.read(bytecode.buffer, 0, bytecode.length);
if (exe.error()) {
exe.close();
throw new Error('read error');
}
exe.close();
const code = bjson.read(bytecode.buffer, 0, bytecode.length, JS_READ_OBJ_BYTECODE | JS_READ_OBJ_REFERENCE);
return std.evalScript(code, {
eval_module: true
});
}