stockfish
Version:
The Stockfish chess engine in Web Assembly (WASM)
593 lines (515 loc) • 22 kB
JavaScript
//! Chess.com (c) 2023
;
var runSpawnSync = require("child_process").spawnSync;
var runExecFileSync = require("child_process").execFileSync;
var params = getParams({booleans: ["no-chesscom", "debug-js", "h", "help", "help-all", "f", "force", "force-linking", "bin", "colors", "no-color", "no-minify", "v", "verbose", "no-simd", "grow-mem", "debug-wasm", "non-nested", "all", "skip-em-check", "single-threaded"]});
var args = ["build", "-j", require("os").cpus().length];
//var args = ["-j", require("os").cpus().length];
//var args = []; ///NOTE: Can't use multi-threading with emscripten_copy_files
var fs = require("fs");
var p = require("path");
var stockfishPath = p.join(__dirname, "src", "stockfish");
var stockfishWASMPath = p.join(__dirname, "src", "stockfish.wasm");
var stockfishWASMLoaderPath = p.join(__dirname, "src", "stockfish.js");
var stockfishWorkerThreadPath = p.join(__dirname, "src", "stockfish.worker.js");
var data;
var workerData;
var preface;
var postscript;
var buildToWASM;
var child;
var stockfishVersion = "16";
var expectedEmscripten = "2.0.26";
var fistRun;
var basename;
var buildingSingleThreaded = false;
function getParams(options, argv)
{
var i,
params = {_: []},
last,
len,
match;
if (Array.isArray(options)) {
args = options;
options = {};
}
options = options || {};
if (!options.booleans) {
options.booleans = [];
}
argv = argv || process.argv;
len = argv.length;
for (i = 2; i < len; i += 1) {
if (argv[i][0] === "-") {
if (argv[i][1] === "-") {
last = argv[i].substr(2);
match = last.match(/([^=]*)=(.*)/);
if (match) {
last = match[1];
params[last] = match[2];
last = "";
} else {
params[last] = true;
}
} else {
/// E.g., -hav should indicate h, a, and v as TRUE.
argv[i].split("").slice(1).forEach(function oneach(letter)
{
params[letter] = true;
last = letter;
});
}
} else if (last) {
params[last] = argv[i];
last = "";
} else {
params._.push(argv[i]);
last = "";
}
/// Handle booleans.
if (last && options.booleans.indexOf(last) > -1) {
last = "";
}
}
return params;
}
function minify(code)
{
var initComment;
if (params["no-minify"] || params["debug-wasm"]) {
return code;
}
initComment = code.match(/\/\*![\s\S]*?\*\//)[0];
return initComment + require("uglify-js").minify(code).code;
}
function color(color_code, str)
{
if (!params["no-colors"] && (process.stdout.isTTY || params.colors)) {
str = "\u001B[" + color_code + "m" + str + "\u001B[0m";
}
return str;
}
function warn(str)
{
console.warn(color(33, "WARN: " + str));
}
function highlight(str)
{
return color(33, str);
}
function note(str)
{
return color(36, str);
}
function bold(str)
{
return color(1, str);
}
function beep()
{
if (process.stdout.isTTY && !params.s && !params.silent) {
process.stdout.write("\u0007");
}
}
function changeVersion(version)
{
var filePath = p.join(__dirname, "src", "misc.cpp");
var data = fs.readFileSync(filePath, "utf8");
data = data.replace(/( version = ")[^\"]*(";)/, "$1" + version + "$2");
try {
fs.writeFileSync(filePath, data);
} catch (e) {
console.error(e);
}
}
function determineBestArch()
{
var cpuData = "";
var cpuArch = "";
var arch;
try {
cpuArch = execFileSync("uname", ["-m"], {encoding: "utf8", env: process.env, cwd: __dirname}).trim();
} catch (e) {}
if (cpuArch === "i686" || cpuArch === "i386") {
arch = "general-32";
} else {
if (cpuArch !== "x86_64" || cpuArch === "amd64") {
warn("Unrecognized cpu architechure. Defaulting to x86_64.");
}
try {
cpuData = execFileSync("cat", ["/proc/cpuinfo"], {encoding: "utf8", env: process.env, cwd: __dirname}).trim();
} catch (e) {}
if (!cpuData) {
try {
cpuData = execFileSync("lscpu", {encoding: "utf8", env: process.env, cwd: __dirname}).trim();
} catch (e) {}
}
if (!cpuData) {
try {
cpuData = execFileSync("lshw", ["-C", "cpu"], {encoding: "utf8", env: process.env, cwd: __dirname}).trim();
} catch (e) {}
}
if (!cpuData) {
try {
/// FreeBSD & macOS?
cpuData = execFileSync("sysctl", ["-a"], {encoding: "utf8", env: process.env, cwd: __dirname}).trim();
} catch (e) {}
}
if (/\bavx512\b/i.test(cpuData)) {
arch = "x86-64-avx512";
} else if (/\bbmi2\b/i.test(cpuData)) {
arch = "x86-64-bmi2";
} else if (/\bavx2\b/i.test(cpuData)) {
arch = "x86-64-avx2";
} else if (/\bpopcnt\b/i.test(cpuData)) {
arch = "x86-64-modern";
} else {
arch = "general-64";
}
}
console.log(note("Building " + arch));
args.push("ARCH=" + arch);
}
function spawnSync(command, args, options)
{
if (params.v || params.verbose) {
console.log(highlight(" - Running command: " + command + " " + (args && args.length ? "\"" + args.join("\" \"") + "\"" : "")));
}
return runSpawnSync(command, args, options);
}
function execFileSync(command, args, options)
{
if (params.v || params.verbose) {
console.log(highlight(" - Running command: " + command + " " + (args && args.length ? "\"" + args.join("\" \"") + "\"" : "")));
}
return runExecFileSync(command, args, options);
}
function addNNSymLink()
{
/// Adds NN symlink for testing code to use, if it doesn't exist already.
try {
var path = fs.readFileSync(p.join(__dirname, "src", "evaluate.h"), "utf8").match(/EvalFileDefaultName\s+["']([^"']+)/)[1];
execFileSync("ln", ["-s", "../../" + path], {cwd: p.join(__dirname, "src", "emscripten", "public"), stdio: "pipe"});
} catch (e) {
if (!e.message || e.message.indexOf("File exists") === -1) {
console.error("Creating symlink failed");
console.error(e);
}
}
}
function checkEmscriptenVersion()
{
var versionInfo;
var exec = params.emcc || "emcc";
try {
versionInfo = execFileSync(exec, ["--version"], {encoding: "utf8", env: process.env, cwd: __dirname});
if (versionInfo.indexOf(expectedEmscripten) === -1) {
console.error(highlight("Warning:"));
console.error("\nEmscripten version does not match.\nExpected " + note(expectedEmscripten) + ". See " + note(exec + " --version") + " for your currently installed version.");
console.error("\n" + "To install the expected verions, try:\n\n > " + note("emsdk install " + expectedEmscripten) + "\n > " + note("emsdk activate " + expectedEmscripten));
console.error("\nOr add " + note("--skip-em-check") + " to bypass this check.\n");
}
} catch (e) {
console.error(e);
console.error(highlight("Warning:"));
console.error("\nCould not confirm emscripten version. Set your " + note("emcc") + " path with the " + note("--emcc") +" flag, or add " + not("--skip-em-check") + " to bypass this check.\n");
}
}
function renameAndSymlink(origPath, newPath)
{
fs.renameSync(origPath, newPath);
fs.symlinkSync(newPath, origPath);
}
if (!params.make) {
params.make = "make";
}
if (params.arch) {
if (params.b || params.bin) {
warn("Cannot user --bin (or -b) with --arch");
process.exit(1);
}
args.push("ARCH=" + params.arch);
if (params.arch === "wasm") {
buildToWASM = true;
}
} else if (params.b || params.bin) {
determineBestArch()
} else {
buildToWASM = true;
params.arch = "wasm";
args.push("ARCH=wasm");
}
if (params.help || params["help-all"] || params.h) {
console.log("");
console.log(bold("Build the Stockfish Chess Engine"));
console.log("Usage: ./build.js [" + highlight("options") + "]");
console.log("");
console.log(" " + highlight("--all") + " Build all flavors of emscripten engines");
console.log(" " + highlight("-f --force") + " Always rebuild the entire project");
console.log(" " + highlight("--force-linking") + " Always preforming the final linking step");
// console.log(" " + highlight("--variants") + " Comma separated list of variants to include (default: " + note("none") + ")");
// console.log( " Possible values are " + note("all") + ", " + note("none") + " (no variants, except for Chess960),");
// console.log( " " + note("anti") + ", " + note("atomic") + ", " + note("crazyhouse") + ", " + note("horde") + ", " + note("kingofthehill") + ", " + note("race") + ", " + note("relay") + ", or " + note("3check"));
console.log(" " + highlight("--no-chesscom") + " Disable changes made specifically for Chess.com");
console.log(" " + highlight("--static") + " Link libaries statically (not avaiable for WASM)");
console.log(" " + highlight("--debug-wasm") + " Compile WASM in debug mode (adds ASSERTIONS=2 and SAFE_HEAP=1)");
console.log(" " + highlight("--no-minify") + " Disable closure compiler of JS code");
console.log(" " + highlight("--no-simd") + " Compile without WASM SIMD");
console.log(" " + highlight("--grow-mem") + " Allow WASM memory to grow (might be less performant, not sure)");
console.log(" " + highlight("--non-nested") + " Do not proxy to threads in WASM (for Chrome 109 only)");
console.log(" " + highlight("--arch") + " Architecture to build to (default: " + note("wasm") + ")");
console.log( " See " + highlight("--help-all") + " for more options, or use " + highlight("--bin") + " instead");
console.log(" " + highlight("--basename") + " The filename for the engine (default: " + note ("stockfish") + ")");
console.log( " This will not only rename the files, it will also rewrite the base JS file");
console.log( " to load the correct WASM engine");
console.log(" " + highlight("--bin") + " Attempt to build a binary engine that is the most suitable for this system");
console.log(" " + highlight("--make") + " Path to program used to make Stockfish (default: " + note("make") + ")");
console.log(" " + highlight("--comp") + " Compiler to build C code with");
console.log(" " + highlight("--compcxx") + " Compiler to build C++ code with");
console.log(" " + highlight("--version") + " Specify Stockfish version number (default: " + note(stockfishVersion) + ")");
console.log( " Use " + note("date") + " to use the current date");
console.log( " Use " + note("timestamp") + " to use the current Unix timestamp");
console.log( " Use " + note("hash") + " to use the current git commit hash");
console.log(" " + highlight("--single-threaded") + " Compile the engine without pthreads");
console.log(" " + highlight("--skip-em-check") + " Do not check Emscripten version (expected version: " + note(expectedEmscripten) + ")");
console.log(" " + highlight("--emcc") + " Path to " + note("emcc") + " (used to ensure version compatibility)");
console.log(" " + highlight("--colors") + " Always colorize the output, even through a pipe");
console.log(" " + highlight("--no-colors") + " Never colorize the output");
console.log(" " + highlight("-v --verbose") + " Print extra info");
console.log(" " + highlight("-h --help") + " Show build.js's help");
console.log(" " + highlight("--help-all") + " Show Stockfish's Makefile help as well");
console.log("");
console.log("Examples:");
console.log("");
console.log(" Default: include Chess.com modifications and compile to WASM");
console.log(" ./build.js");
console.log("");
console.log(" Vanilla Stockfish: no modifications, no variants, native binary");
console.log(" ./build.js " + highlight("--no-chesscom") + " " + highlight("--bin"));
console.log("");
if (params["help-all"]) {
console.log("");
console.log(bold("******** Makefile Help ********"));
console.log("");
spawnSync(params.make, {stdio: [0,1,2], env: process.env, cwd: p.join(__dirname, "src")});
}
process.exit();
}
if (!params["skip-em-check"]) {
checkEmscriptenVersion();
}
if (params.all) {
(function ()
{
var newArgs = [process.argv[1], "-f", "--skip-em-check"];
Object.keys(params).forEach(function (key)
{
var val = params[key];
var flag;
if (key === "all" || key === "basename" || key === "no-simd" || key === "non-nested" || key === "arch" || key === "_") {
return;
}
if (key.length === 1) {
flag = "-" + key;
} else {
flag = "--" + key;
}
if (val === true) {
newArgs.push(flag)
} else {
newArgs.push(flag, val);
}
});
if (params._ && params._.length) {
newArgs = newArgs.concat(params._);
}
console.log(highlight(" -- (1/4) Building non-nested worker multithreaded engine..."));
spawnSync(process.execPath, newArgs.concat(["--non-nested"]), {encoding: "utf8", env: process.env, cwd: __dirname, stdio: [0,1,2]});
console.log(highlight(" -- (2/4) Building non-simd multithreaded engine..."));
spawnSync(process.execPath, newArgs.concat(["--no-simd"]), {encoding: "utf8", env: process.env, cwd: __dirname, stdio: [0,1,2]});
console.log(highlight(" -- (3/4) Building single-threaded engine..."));
spawnSync(process.execPath, newArgs.concat(["--single-threaded"]), {encoding: "utf8", env: process.env, cwd: __dirname, stdio: [0,1,2]});
console.log(highlight(" -- (4/4) Building standard multithreaded engine..."));
spawnSync(process.execPath, newArgs.concat(["--basename=stockfish-nnue-" + stockfishVersion]), {encoding: "utf8", env: process.env, cwd: __dirname, stdio: [0,1,2]});
console.log(highlight(" -- Finished building all engines"));
process.exit();
}());
}
if (typeof params.basename === "string") {
basename = params.basename.replace(/\.(?:js|wasm)$/i, "");
}
if (params.force || params.f) {
args.push("--always-make");
} else if (params["force-linking"]) {
///NOTE: --force will also link as well, so both are not needed.
if (buildToWASM) {
try {
fs.unlinkSync(stockfishJSWASMPath);
} catch (e) {}
try {
fs.unlinkSync(stockfishJSWASMLoaderPath);
} catch (e) {}
if (basename) {
try {
fs.unlinkSync(p.join(__dirname, "src", basename + ".js"));
} catch (e) {}
}
} else {
try {
fs.unlinkSync(stockfishPath);
} catch (e) {}
}
}
/*
if (params.variants && params.variants.toLowerCase() !== "all") {
args.push("VARIANTS=" + params.variants.toUpperCase());
} else if (!params.variants) {
args.push("VARIANTS=NONE");
}
*/
if (!params["no-chesscom"]) {
args.push("CHESSCOM=1");
}
if (params["no-minify"]) {
args.push("NOJSMINIFY=yes");
}
if (params["no-simd"]) {
args.push("wasm_simd_post_mvp=no");
args.push("wasm_simd=no");
args.push("WASMLOWMEM=yes");
if (!basename) {
basename = "stockfish-nnue-" + stockfishVersion + "-no-simd";
}
} else {
args.push("wasm_simd_post_mvp=yes");
}
if (params["grow-mem"]) {
args.push("WASMGROWMEM=yes");
}
if (params["non-nested"]) {
args.push("WASMNONNESTED=yes");
if (!basename) {
basename = "stockfish-nnue-" + stockfishVersion + "-no-Worker";
}
}
if (params["single-threaded"]) {
buildingSingleThreaded = true;
args.push("use_wasm_pthreads=no");
args.push("wasm_simd_post_mvp=no");
args.push("wasm_simd=no");
args.push("WASMNONNESTED=no");
if (!basename) {
basename = "stockfish-nnue-" + stockfishVersion + "-single";
}
}
if (params["debug-wasm"]) {
if (buildToWASM) {
args.push("DEBUGWASM=1");
args.push("optimize=no");
} else {
warn("Ignoring --debug-wasm");
}
}
/*
if (params.simd) {
args.push("SIMD=1");
}
*/
/*
if (params.static) {
if (buildToJs) {
warn("Ignoring --static");
} else {
args.push("STATIC=1");
}
}
*/
if (params.comp) {
args.push("COMP=" + params.comp);
}
if (params.compcxx) {
args.push("COMPCXX=" + params.compcxx);
}
if (String(params.version).toLowerCase() === "timestamp") {
params.version = Date.now();
}
if (String(params.version).toLowerCase() === "hash") {
params.version = execFileSync("git", ["rev-parse", "--short=0", "HEAD"], {encoding: "utf8", env: process.env, cwd: __dirname}).trim();
}
///NOTE: Stockfish will insert the date automatically if no version number is given.
if (String(params.version).toLowerCase() !== "date") {
changeVersion(params.version === true || !params.version ? stockfishVersion : params.version);
}
if (buildToWASM) {
child = spawnSync(params.make, ["-C", ".."].concat(args), {stdio: [0,1,2], env: process.env, cwd: p.join(__dirname, "src", "emscripten")});
// ///TODO: Just do "build"
//child = spawnSync(params.make, ["-C", "..", "emscripten_build", "wasm_simd_post_mvp=yes"].concat(args), {stdio: [0,1,2], env: process.env, cwd: p.join(__dirname, "src", "emscripten")});
} else {
child = spawnSync(params.make, args, {stdio: [0,1,2], env: process.env, cwd: p.join(__dirname, "src")});
//child = spawnSync(params.make, ["build"].concat(args), {stdio: [0,1,2], env: process.env, cwd: p.join(__dirname, "src")});
}
/// Reset version string.
if (String(params.version).toLowerCase() !== "date") {
changeVersion("dev");
}
/// `make` does not throw an error when encountering errors, so we need to do that manually.
if (Number(child.status) !== 0) {
process.exit(Number(child.status));
}
if (!buildToWASM && basename) {
fs.renameSync(stockfishPath, p.join(__dirname, "src", basename));
}
if (buildToWASM) {
data = fs.readFileSync(stockfishWASMLoaderPath, "utf8");
if (!buildingSingleThreaded) {
workerData = fs.readFileSync(stockfishWorkerThreadPath, "utf8").trim();
try {
fs.unlinkSync(stockfishWorkerThreadPath);
} catch (e) {};
if (params["debug-wasm"]) {
data = "//HACK: This build requires some hacks to run\nif (typeof global === 'undefined') {\n if (typeof window !== 'undefined') { window.global=window }\n else { self.global=self }\n}\nglobal.ENVIRONMENT_IS_FETCH_WORKER=true;global.indexedDB={open:function(){return{}}};\n" + data;
}
}
if (!buildingSingleThreaded) {
/// Append the hacky custom post message code for the asyncify.
workerData += "\n" + fs.readFileSync(p.join(__dirname, "src", "emscripten", "worker-postamble.js"), "utf8").trim();
/// Run the init function instead of using emscripten's ugly importScripts hack.
///NOTE: Could remove other ugly hacks.
workerData = workerData.replace(/if\s*\([^)]+urlOrBlob[\s\S]*?else\s*\{[^}]+\}/, "Stockfish=INIT_ENGINE();");
data = data.replace("/// Insert worker here", workerData).trim();
}
/// Prevent throwing on 0 exit code.
data = data.replace(/(function\s*[a-zA-Z0-9]*)\(([^,)]+),([^)]+)\)\s*\{\s*throw\s*\3/, "$1($2,$3){if($2!==0)throw $3");
/// Prevent errors when exiting.
data = data.replace(/(apply[^{}]+})\s*finally/, "$1catch(e){if(e.message.indexOf(\"unreachable\")===-1)throw e}finally");
data = data.replace("__YEAR__", (new Date()).getFullYear());
data = data.replace("__VERSION__", stockfishVersion);
if (params["non-nested"]) {
if (params["debug-wasm"]) {
/// Replace safeSetTimeout()'s setTimeout() with setImmediate() in --debug-wasm mode.
data = data.replace(/(function\s+safeSetTimeout[\s\S]+?)setTimeout/, "$1quickTimeout");
} else {
/// Replace safeSetTimeout()'s setTimeout() with setImmediate() in regular mode.
data = data.replace(/;\s*setTimeout\s*\(\s*function\s*\(\s*\)/, ";quickTimeout(function()");
}
}
if (basename) {
data = data.replace(/(["'])stockfish.wasm["']/g, function (full, quote)
{
return quote + basename + ".wasm" + quote;
});
}
data = minify(data);
fs.writeFileSync(stockfishWASMLoaderPath, data);
if (basename) {
renameAndSymlink(stockfishWASMLoaderPath, p.join(__dirname, "src", basename + ".js"));
renameAndSymlink(stockfishWASMPath, p.join(__dirname, "src", basename + ".wasm"));
try {
renameAndSymlink(stockfishWASMPath + ".map", p.join(__dirname, "src", basename + ".wasm" + ".map"));
} catch (e) {}
}
addNNSymLink();
}
beep();