@simple-software/simplebuild
Version:
A build system for JS/TS that simply builds your project in the fastest way possible, with zero effort from you.
229 lines (189 loc) • 6.56 kB
JavaScript
#!/usr/bin/env node
const path = require('path');
const { spawn } = require('child_process');
const fs = require('fs');
const os = require('os');
/**
* Generate WASI runner script with template substitution
* @param {string} wasmPath - Path to the WASM file
* @param {string} workingDir - Working directory
* @returns {string} Generated script content
*/
function generateWasiRunner(wasmPath, workingDir) {
return `// Node ≥ 20 (host shim for WASM command execution)
import { WASI } from 'node:wasi';
import { spawnSync } from 'node:child_process';
import { readFileSync } from 'fs';
import { chdir } from 'node:process';
// Change to the target working directory
chdir('${workingDir}');
const wasi = new WASI({
version: 'preview1',
args: process.argv.slice(2),
env: process.env,
preopens: {
'.': '.',
'/': '/',
'${workingDir}': '${workingDir}'
}
});
// Memory management for WASM<->JS communication
let memory;
let wasmInstance;
let bump = 0x10000;
let lastExitCode = 0;
const enc = new TextEncoder();
const dec = new TextDecoder();
function readString(ptr, len) {
return dec.decode(new Uint8Array(memory.buffer, ptr, len));
}
function allocateMemory(size) {
const currentMemorySize = memory.buffer.byteLength;
if (bump + size > currentMemorySize) {
const pagesNeeded = Math.ceil((bump + size - currentMemorySize) / 65536);
memory.grow(pagesNeeded);
}
const ptr = bump;
bump += size;
bump = (bump + 3) & ~3; // Align to 4-byte boundary
return ptr;
}
function allocString(str) {
const bytes = enc.encode(str);
const ptr = allocateMemory(bytes.length);
new Uint8Array(memory.buffer, ptr, bytes.length).set(bytes);
return ptr;
}
function getExitCode() {
return lastExitCode;
}
function hostExec(cmdPtr, cmdLen, argvPtr, argvLen) {
const cmd = readString(cmdPtr, cmdLen);
const argvStr = readString(argvPtr, argvLen);
const argv = argvStr.split('\0').filter(arg => arg.length > 0);
const { status = 255, stdout = '', stderr = '' } =
spawnSync(cmd, argv, { encoding: 'utf8', cwd: '${workingDir}' });
lastExitCode = status;
const output = stdout + stderr;
const outputBytes = enc.encode(output);
const structPtr = allocateMemory(4 + outputBytes.length);
new DataView(memory.buffer).setUint32(structPtr, status, true);
new Uint8Array(memory.buffer, structPtr + 4, outputBytes.length).set(outputBytes);
const structLen = 4 + outputBytes.length;
return (BigInt(structLen) << 32n) | BigInt(structPtr);
}
function hostExecWithCwd(cmdPtr, cmdLen, argvPtr, argvLen, cwdPtr, cwdLen) {
const cmd = readString(cmdPtr, cmdLen);
const argvStr = readString(argvPtr, argvLen);
const cwd = readString(cwdPtr, cwdLen);
const argv = argvStr.split('\0').filter(arg => arg.length > 0);
const { status = 255, stdout = '', stderr = '' } =
spawnSync(cmd, argv, { encoding: 'utf8', cwd: cwd });
lastExitCode = status;
const output = stdout + stderr;
const outputBytes = enc.encode(output);
const structPtr = allocateMemory(4 + outputBytes.length);
new DataView(memory.buffer).setUint32(structPtr, status, true);
new Uint8Array(memory.buffer, structPtr + 4, outputBytes.length).set(outputBytes);
const structLen = 4 + outputBytes.length;
return (BigInt(structLen) << 32n) | BigInt(structPtr);
}
const wasm = readFileSync('${wasmPath}');
const wasmModule = new WebAssembly.Module(wasm);
wasmInstance = new WebAssembly.Instance(wasmModule, {
...wasi.getImportObject(),
host: {
allocString,
readString,
getExitCode,
exec: hostExec,
execWithCwd: hostExecWithCwd
},
});
memory = wasmInstance.exports.memory;
wasi.initialize(wasmInstance); `;
}
/**
* Show help message
*/
function showHelp() {
console.log(`Simple Build - A build system for JS/TS that simply builds your project
Usage: simplebuild [options]
Options:
--working-dir <path> Working directory to build (default: current directory)
--simplebuild-dir <name> Directory name for build artifacts (default: .simplebuild)
--help, -h Show this help message
Examples:
simplebuild
simplebuild --working-dir=/path/to/project
simplebuild --working-dir=/path/to/project --simplebuild-dir=build`);
}
/**
* Parse command line arguments
* @param {string[]} args - Command line arguments
* @returns {{workingDir: string, simpleBuildDir: string}}
*/
function parseArgs(args) {
let workingDir = process.cwd();
let simpleBuildDir = '.simplebuild';
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg.startsWith('--working-dir=')) {
workingDir = arg.split('=')[1];
} else if (arg === '--working-dir' && i + 1 < args.length) {
workingDir = args[i + 1];
i++;
} else if (arg.startsWith('--simplebuild-dir=')) {
simpleBuildDir = arg.split('=')[1];
} else if (arg === '--simplebuild-dir' && i + 1 < args.length) {
simpleBuildDir = args[i + 1];
i++;
} else if (arg === '--help' || arg === '-h') {
showHelp();
process.exit(0);
}
}
return { workingDir: path.resolve(workingDir), simpleBuildDir };
}
/**
* Run the WASM binary with proper cleanup
* @param {string} wasmPath - Path to the WASM file
* @param {string} workingDir - Working directory
* @param {string} simpleBuildDir - Simple build directory
*/
function runWasmBinary(wasmPath, workingDir, simpleBuildDir) {
const tempDir = os.tmpdir();
const tempScript = path.join(tempDir, `simplebuild-runner-${Date.now()}.mjs`);
const wasiScript = generateWasiRunner(wasmPath, workingDir);
fs.writeFileSync(tempScript, wasiScript);
const wasmArgs = [`--working-dir=${workingDir}`];
if (simpleBuildDir !== '.simplebuild') {
wasmArgs.push(`--simplebuild-dir=${simpleBuildDir}`);
}
const child = spawn('node', ['--no-warnings', tempScript, ...wasmArgs], {
stdio: 'inherit',
cwd: workingDir
});
child.on('close', (code) => {
try {
fs.unlinkSync(tempScript);
} catch (e) {
// Ignore cleanup errors
}
process.exit(code);
});
child.on('error', (error) => {
try {
fs.unlinkSync(tempScript);
} catch (e) {
// Ignore cleanup errors
}
console.error('Error running Simple Build:', error.message);
process.exit(1);
});
}
// Main execution
const args = process.argv.slice(2);
const { workingDir, simpleBuildDir } = parseArgs(args);
const wasmPath = path.join(__dirname, '..', 'wasm', 'Simple-Build-wasm-wasi.wasm');
runWasmBinary(wasmPath, workingDir, simpleBuildDir);