mirror of
https://github.com/HeyPuter/node-worker.git
synced 2026-11-13 20:12:45 -10:00
694 lines
22 KiB
JavaScript
694 lines
22 KiB
JavaScript
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { spawnSync, spawn } from 'node:child_process';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const rootDir = path.dirname(fileURLToPath(import.meta.url));
|
|
const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8'));
|
|
const nodeConfig = packageJson.nodeCore;
|
|
|
|
const EMSDK_CONFIG = {
|
|
repository: 'https://github.com/emscripten-core/emsdk.git',
|
|
commit: '3.1.56',
|
|
checkoutDir: 'emsdk',
|
|
toolVersion: '3.1.56',
|
|
};
|
|
|
|
const nodeCheckoutDir = path.resolve(rootDir, nodeConfig.checkoutDir);
|
|
const emsdkDir = path.resolve(rootDir, EMSDK_CONFIG.checkoutDir);
|
|
const wasmBuildDir = path.join(nodeCheckoutDir, 'wasm-build');
|
|
const wasmOutputFile = path.join(wasmBuildDir, 'node-worker.wasm');
|
|
const wasmModuleFile = path.join(wasmBuildDir, 'node-worker.wasm.js');
|
|
const nodeWasmDir = path.join(rootDir, 'src', 'worker', 'node-wasm');
|
|
const llhttpShimFile = path.join(nodeWasmDir, 'llhttp-shim.c');
|
|
const zlibShimFile = path.join(nodeWasmDir, 'zlib-shim.c');
|
|
const brotliShimFile = path.join(nodeWasmDir, 'brotli-shim.c');
|
|
const cryptoShimFile = path.join(nodeWasmDir, 'crypto-shim.c');
|
|
const nghttp2ShimFile = path.join(nodeWasmDir, 'nghttp2-shim.c');
|
|
const patchesDir = path.join(rootDir, 'node-patches');
|
|
|
|
const nodeRustDir = path.join(rootDir, 'src', 'worker', 'node-rust');
|
|
// The wasm-bindgen bits live in the `wasm` crate (which depends on the pure-Rust
|
|
// `rewriter` core crate); build.sh + the wasm-bindgen output dir live there too.
|
|
const rewriterWasmCrateDir = path.join(nodeRustDir, 'wasm');
|
|
const rewriterOutDir = path.join(rewriterWasmCrateDir, 'out');
|
|
const rewriterWasmFile = path.join(rewriterOutDir, 'rewriter_bg.wasm');
|
|
const rewriterGlueFile = path.join(rewriterOutDir, 'rewriter.js');
|
|
const rewriterDtsFile = path.join(rewriterOutDir, 'rewriter.d.ts');
|
|
const rewriterBgDtsFile = path.join(rewriterOutDir, 'rewriter_bg.wasm.d.ts');
|
|
const wasmBuildRewriterGlueFile = path.join(wasmBuildDir, 'rewriter.js');
|
|
const wasmBuildRewriterDtsFile = path.join(wasmBuildDir, 'rewriter.d.ts');
|
|
const wasmBuildRewriterBgDtsFile = path.join(wasmBuildDir, 'rewriter_bg.wasm.d.ts');
|
|
const wasmBuildRewriterModuleFile = path.join(wasmBuildDir, 'rewriter.wasm.js');
|
|
|
|
// OpenSSL libcrypto is built straight from node's vendored tree so the version
|
|
// (3.5.5) and headers match. `linux-x32` is an ILP32 target that maps cleanly
|
|
// onto wasm32; `no-asm` is mandatory. The CROSS_COMPILE blanking fixes the
|
|
// emconfigure-mangled CC path. Only `libcrypto.a` is built (no TLS/libssl).
|
|
const opensslSourceDir = path.join(nodeCheckoutDir, 'deps', 'openssl', 'openssl');
|
|
const opensslBuildDir = path.join(nodeCheckoutDir, 'deps', 'openssl', 'wasm-build');
|
|
const opensslLibCrypto = path.join(opensslBuildDir, 'libcrypto.a');
|
|
|
|
function run(command, args, cwd = rootDir, extra = {}) {
|
|
const result = spawnSync(command, args, {
|
|
cwd,
|
|
stdio: 'inherit',
|
|
env: {
|
|
...process.env,
|
|
...extra.env,
|
|
},
|
|
});
|
|
|
|
if (result.status !== 0) {
|
|
throw new Error(`Command failed: ${command} ${args.join(' ')}`);
|
|
}
|
|
}
|
|
|
|
function runAsync(command, args, extra = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(command, args, {
|
|
cwd: rootDir,
|
|
stdio: ['ignore', 'inherit', 'inherit'],
|
|
env: { ...process.env, ...extra.env },
|
|
});
|
|
child.on('error', reject);
|
|
child.on('close', (status) => {
|
|
if (status === 0) resolve();
|
|
else reject(new Error(`Command failed: ${command} ${args.join(' ')}`));
|
|
});
|
|
});
|
|
}
|
|
|
|
// Run an array of thunks with bounded concurrency (one in flight per core).
|
|
async function runPool(thunks, concurrency) {
|
|
let next = 0;
|
|
const worker = async () => {
|
|
while (next < thunks.length) {
|
|
await thunks[next++]();
|
|
}
|
|
};
|
|
await Promise.all(
|
|
Array.from({ length: Math.min(concurrency, thunks.length) }, worker),
|
|
);
|
|
}
|
|
|
|
function capture(command, args, cwd = rootDir, extra = {}) {
|
|
const result = spawnSync(command, args, {
|
|
cwd,
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
env: {
|
|
...process.env,
|
|
...extra.env,
|
|
},
|
|
});
|
|
|
|
if (result.status !== 0) {
|
|
const stderr = result.stderr?.trim();
|
|
throw new Error(stderr || `Command failed: ${command} ${args.join(' ')}`);
|
|
}
|
|
|
|
return result.stdout.trim();
|
|
}
|
|
|
|
function ensureParentDir(filePath) {
|
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
}
|
|
|
|
function tryResolveRevision(checkoutDir, revision) {
|
|
for (const candidate of [revision, `refs/tags/${revision}`, 'FETCH_HEAD']) {
|
|
const result = spawnSync('git', ['rev-parse', '--verify', candidate], {
|
|
cwd: checkoutDir,
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
if (result.status === 0) {
|
|
return result.stdout.trim();
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function ensureGitCheckout(config, checkoutDir) {
|
|
if (!fs.existsSync(checkoutDir)) {
|
|
fs.mkdirSync(path.dirname(checkoutDir), { recursive: true });
|
|
run('git', ['clone', config.repository, checkoutDir, '--depth=1']);
|
|
}
|
|
|
|
let target = tryResolveRevision(checkoutDir, config.commit);
|
|
if (target === null) {
|
|
run('git', ['fetch', '--depth=1', 'origin', config.commit], checkoutDir);
|
|
target = tryResolveRevision(checkoutDir, config.commit);
|
|
}
|
|
if (target === null) {
|
|
run('git', ['fetch', '--depth=1', 'origin', 'tag', config.commit], checkoutDir);
|
|
target = tryResolveRevision(checkoutDir, config.commit);
|
|
}
|
|
if (target === null) {
|
|
throw new Error(`Unable to resolve ${config.commit} in ${checkoutDir}`);
|
|
}
|
|
|
|
const head = capture('git', ['rev-parse', 'HEAD'], checkoutDir);
|
|
if (head !== target) {
|
|
run('git', ['checkout', '--detach', target], checkoutDir);
|
|
}
|
|
}
|
|
|
|
function applyNodePatches() {
|
|
if (!fs.existsSync(patchesDir)) {
|
|
return;
|
|
}
|
|
|
|
const patches = fs.readdirSync(patchesDir)
|
|
.filter((file) => file.endsWith('.patch'))
|
|
.sort();
|
|
|
|
for (const patch of patches) {
|
|
const patchPath = path.join(patchesDir, patch);
|
|
const forward = spawnSync('git', ['apply', '--check', patchPath], { cwd: nodeCheckoutDir });
|
|
if (forward.status === 0) {
|
|
run('git', ['apply', patchPath], nodeCheckoutDir);
|
|
continue;
|
|
}
|
|
|
|
const reverse = spawnSync('git', ['apply', '--reverse', '--check', patchPath], { cwd: nodeCheckoutDir });
|
|
if (reverse.status !== 0) {
|
|
throw new Error(`Patch does not apply cleanly: ${patch}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function ensureEmsdkInstalled() {
|
|
const emsdkScript = path.join(emsdkDir, 'emsdk');
|
|
run(emsdkScript, ['install', EMSDK_CONFIG.toolVersion], emsdkDir);
|
|
run(emsdkScript, ['activate', EMSDK_CONFIG.toolVersion], emsdkDir);
|
|
}
|
|
|
|
function buildOpenSSL() {
|
|
// Skip if already built — the libcrypto.a compile is the slowest step.
|
|
if (fs.existsSync(opensslLibCrypto)) {
|
|
return;
|
|
}
|
|
|
|
const emConfigure = path.join(emsdkDir, 'upstream', 'emscripten', 'emconfigure');
|
|
const emMake = path.join(emsdkDir, 'upstream', 'emscripten', 'emmake');
|
|
const emConfig = path.join(emsdkDir, '.emscripten');
|
|
|
|
fs.mkdirSync(opensslBuildDir, { recursive: true });
|
|
|
|
const configure = path.join(opensslSourceDir, 'Configure');
|
|
run(
|
|
emConfigure,
|
|
[
|
|
configure,
|
|
'linux-x32',
|
|
// Seed the DRBG via getrandom()/getentropy() instead of the default
|
|
// OS path (which reads /dev/urandom — unavailable under FILESYSTEM=0).
|
|
// -D__ELF__ enables the getentropy weak-symbol branch in
|
|
// providers/.../seeding/rand_unix.c, which crypto-shim.c satisfies.
|
|
'--with-rand-seed=getrandom',
|
|
'no-asm', 'no-shared', 'no-dso', 'no-engine', 'no-afalgeng',
|
|
'no-tests', 'no-threads', 'no-ui-console', 'no-comp', 'no-ocsp',
|
|
'no-srp', 'no-cms', 'no-ts', 'no-ssl', 'no-tls', 'no-quic',
|
|
'-D__ELF__',
|
|
'-DOPENSSL_SYS_NETWARE', '-DHAVE_FORK=0', '-DOPENSSL_NO_DYNAMIC_ENGINE',
|
|
'-DOPENSSL_NO_SECURE_MEMORY',
|
|
],
|
|
opensslBuildDir,
|
|
{ env: { EM_CONFIG: emConfig } },
|
|
);
|
|
|
|
// emconfigure mangles CROSS_COMPILE into the CC path; blank it so CC is the
|
|
// bare absolute emcc path.
|
|
const makefilePath = path.join(opensslBuildDir, 'Makefile');
|
|
const makefile = fs.readFileSync(makefilePath, 'utf8')
|
|
.replace(/^CROSS_COMPILE.*$/m, 'CROSS_COMPILE=');
|
|
fs.writeFileSync(makefilePath, makefile);
|
|
|
|
run(
|
|
emMake,
|
|
['make', `-j${os.cpus().length}`, 'build_generated', 'libcrypto.a'],
|
|
opensslBuildDir,
|
|
{ env: { EM_CONFIG: emConfig } },
|
|
);
|
|
|
|
if (!fs.existsSync(opensslLibCrypto)) {
|
|
throw new Error(`OpenSSL build did not produce ${opensslLibCrypto}`);
|
|
}
|
|
}
|
|
|
|
async function compileNodeWorkerWasm() {
|
|
ensureParentDir(wasmOutputFile);
|
|
|
|
const emcc = path.join(emsdkDir, 'upstream', 'emscripten', 'emcc');
|
|
const emConfig = path.join(emsdkDir, '.emscripten');
|
|
if (!fs.existsSync(emcc)) {
|
|
throw new Error(`Missing emcc at ${emcc}`);
|
|
}
|
|
if (!fs.existsSync(emConfig)) {
|
|
throw new Error(`Missing emsdk config at ${emConfig}`);
|
|
}
|
|
|
|
const llhttpDir = path.join(nodeCheckoutDir, 'deps', 'llhttp');
|
|
const llhttpIncludeDir = path.join(llhttpDir, 'include');
|
|
const llhttpSourceDir = path.join(llhttpDir, 'src');
|
|
const zlibDir = path.join(nodeCheckoutDir, 'deps', 'zlib');
|
|
const brotliDir = path.join(nodeCheckoutDir, 'deps', 'brotli', 'c');
|
|
const brotliIncludeDir = path.join(brotliDir, 'include');
|
|
const nghttp2Dir = path.join(nodeCheckoutDir, 'deps', 'nghttp2', 'lib');
|
|
const nghttp2IncludeDir = path.join(nghttp2Dir, 'includes');
|
|
// Generated headers (configuration.h, opensslconf.h) live in the build dir;
|
|
// the rest of the public headers come from the source tree.
|
|
const opensslBuildIncludeDir = path.join(opensslBuildDir, 'include');
|
|
const opensslSrcIncludeDir = path.join(opensslSourceDir, 'include');
|
|
|
|
// Portable subset — leaves SIMD (adler32_simd, crc32_simd, crc_folding,
|
|
// slide_hash_simd, cpu_features) and unused file-I/O code (gz*, compress,
|
|
// uncompr, infback) out of the build.
|
|
const zlibSources = [
|
|
'adler32.c',
|
|
'crc32.c',
|
|
'deflate.c',
|
|
'inffast.c',
|
|
'inflate.c',
|
|
'inftrees.c',
|
|
'trees.c',
|
|
'zutil.c',
|
|
].map((name) => path.join(zlibDir, name));
|
|
|
|
// Mirrors `deps/brotli/brotli.gyp:brotli_sources`.
|
|
const brotliSources = [
|
|
'common/constants.c',
|
|
'common/context.c',
|
|
'common/dictionary.c',
|
|
'common/platform.c',
|
|
'common/shared_dictionary.c',
|
|
'common/transform.c',
|
|
'dec/bit_reader.c',
|
|
'dec/decode.c',
|
|
'dec/huffman.c',
|
|
'dec/prefix.c',
|
|
'dec/state.c',
|
|
'dec/static_init.c',
|
|
'enc/backward_references.c',
|
|
'enc/backward_references_hq.c',
|
|
'enc/bit_cost.c',
|
|
'enc/block_splitter.c',
|
|
'enc/brotli_bit_stream.c',
|
|
'enc/cluster.c',
|
|
'enc/command.c',
|
|
'enc/compound_dictionary.c',
|
|
'enc/compress_fragment.c',
|
|
'enc/compress_fragment_two_pass.c',
|
|
'enc/dictionary_hash.c',
|
|
'enc/encode.c',
|
|
'enc/encoder_dict.c',
|
|
'enc/entropy_encode.c',
|
|
'enc/fast_log.c',
|
|
'enc/histogram.c',
|
|
'enc/literal_cost.c',
|
|
'enc/memory.c',
|
|
'enc/metablock.c',
|
|
'enc/static_dict.c',
|
|
'enc/static_dict_lut.c',
|
|
'enc/static_init.c',
|
|
'enc/utf8_util.c',
|
|
].map((name) => path.join(brotliDir, name));
|
|
|
|
// Mirrors `deps/nghttp2/nghttp2.gyp:nghttp2_sources`.
|
|
const nghttp2Sources = [
|
|
'nghttp2_buf.c',
|
|
'nghttp2_callbacks.c',
|
|
'nghttp2_debug.c',
|
|
'nghttp2_extpri.c',
|
|
'nghttp2_frame.c',
|
|
'nghttp2_hd.c',
|
|
'nghttp2_hd_huffman.c',
|
|
'nghttp2_hd_huffman_data.c',
|
|
'nghttp2_helper.c',
|
|
'nghttp2_http.c',
|
|
'nghttp2_map.c',
|
|
'nghttp2_mem.c',
|
|
'nghttp2_alpn.c',
|
|
'nghttp2_option.c',
|
|
'nghttp2_outbound_item.c',
|
|
'nghttp2_pq.c',
|
|
'nghttp2_priority_spec.c',
|
|
'nghttp2_queue.c',
|
|
'nghttp2_ratelim.c',
|
|
'nghttp2_rcbuf.c',
|
|
'nghttp2_session.c',
|
|
'nghttp2_stream.c',
|
|
'nghttp2_submit.c',
|
|
'nghttp2_time.c',
|
|
'nghttp2_version.c',
|
|
'sfparse.c',
|
|
].map((name) => path.join(nghttp2Dir, name));
|
|
|
|
const exportedFunctions = [
|
|
'_malloc',
|
|
'_free',
|
|
|
|
// llhttp shim
|
|
'_llhttp_wasm_alloc',
|
|
'_llhttp_wasm_free',
|
|
'_llhttp_wasm_init',
|
|
'_llhttp_wasm_smoke_test',
|
|
'_llhttp_execute',
|
|
'_llhttp_finish',
|
|
'_llhttp_pause',
|
|
'_llhttp_resume',
|
|
'_llhttp_resume_after_upgrade',
|
|
'_llhttp_get_type',
|
|
'_llhttp_get_http_major',
|
|
'_llhttp_get_http_minor',
|
|
'_llhttp_get_method',
|
|
'_llhttp_get_status_code',
|
|
'_llhttp_get_upgrade',
|
|
'_llhttp_reset',
|
|
'_llhttp_should_keep_alive',
|
|
'_llhttp_get_errno',
|
|
'_llhttp_get_error_reason',
|
|
'_llhttp_get_error_pos',
|
|
'_llhttp_errno_name',
|
|
'_llhttp_set_lenient_headers',
|
|
'_llhttp_set_lenient_chunked_length',
|
|
'_llhttp_set_lenient_keep_alive',
|
|
'_llhttp_set_lenient_transfer_encoding',
|
|
'_llhttp_set_lenient_version',
|
|
'_llhttp_set_lenient_data_after_close',
|
|
'_llhttp_set_lenient_optional_lf_after_cr',
|
|
'_llhttp_set_lenient_optional_crlf_after_chunk',
|
|
'_llhttp_set_lenient_optional_cr_before_lf',
|
|
'_llhttp_set_lenient_spaces_after_chunk_size',
|
|
|
|
// zlib shim
|
|
'_zlib_alloc',
|
|
'_zlib_init',
|
|
'_zlib_ensure_in_buf',
|
|
'_zlib_ensure_out_buf',
|
|
'_zlib_write',
|
|
'_zlib_avail_in',
|
|
'_zlib_avail_out',
|
|
'_zlib_get_err',
|
|
'_zlib_get_msg',
|
|
'_zlib_params',
|
|
'_zlib_reset',
|
|
'_zlib_end',
|
|
'_zlib_crc32_buf',
|
|
'_zlib_smoke_test',
|
|
|
|
// brotli shim
|
|
'_brotli_alloc',
|
|
'_brotli_init',
|
|
'_brotli_ensure_in_buf',
|
|
'_brotli_ensure_out_buf',
|
|
'_brotli_write',
|
|
'_brotli_avail_in',
|
|
'_brotli_avail_out',
|
|
'_brotli_get_err',
|
|
'_brotli_get_msg',
|
|
'_brotli_end',
|
|
|
|
// crypto shim (OpenSSL libcrypto)
|
|
'_crypto_init',
|
|
'_crypto_last_error',
|
|
'_crypto_md_oneshot',
|
|
'_crypto_md_new',
|
|
'_crypto_md_copy',
|
|
'_crypto_md_update',
|
|
'_crypto_md_final',
|
|
'_crypto_md_free',
|
|
'_crypto_hmac_new',
|
|
'_crypto_hmac_update',
|
|
'_crypto_hmac_final',
|
|
'_crypto_hmac_free',
|
|
'_crypto_get_hashes',
|
|
'_crypto_get_ciphers',
|
|
'_crypto_get_curves',
|
|
'_crypto_pbkdf2',
|
|
'_crypto_scrypt',
|
|
'_crypto_hkdf',
|
|
'_crypto_cipher_new',
|
|
'_crypto_cipher_update',
|
|
'_crypto_cipher_final',
|
|
'_crypto_cipher_set_aad',
|
|
'_crypto_cipher_get_auth_tag',
|
|
'_crypto_cipher_set_auth_tag',
|
|
'_crypto_cipher_set_auto_padding',
|
|
'_crypto_cipher_free',
|
|
'_crypto_cipher_info',
|
|
'_crypto_pkey_parse',
|
|
'_crypto_pkey_free',
|
|
'_crypto_pkey_up_ref',
|
|
'_crypto_pkey_type',
|
|
'_crypto_pkey_export',
|
|
'_crypto_pkey_detail',
|
|
'_crypto_generate_rsa',
|
|
'_crypto_generate_ec',
|
|
'_crypto_generate_ed',
|
|
'_crypto_pkey_sign',
|
|
'_crypto_pkey_verify',
|
|
'_crypto_generate_secret',
|
|
'_crypto_x509_parse',
|
|
'_crypto_x509_free',
|
|
'_crypto_x509_name',
|
|
'_crypto_x509_fingerprint',
|
|
'_crypto_x509_valid',
|
|
'_crypto_x509_serial',
|
|
'_crypto_x509_subject_alt_name',
|
|
'_crypto_x509_info_access',
|
|
'_crypto_x509_sig_alg',
|
|
'_crypto_x509_raw',
|
|
'_crypto_x509_pem',
|
|
'_crypto_x509_public_key',
|
|
'_crypto_x509_check_host',
|
|
'_crypto_x509_check_email',
|
|
'_crypto_x509_check_ip',
|
|
'_crypto_x509_check_ca',
|
|
'_crypto_x509_verify',
|
|
'_crypto_x509_check_issued',
|
|
'_crypto_rand_bytes',
|
|
'_crypto_timing_safe_equal',
|
|
'_crypto_smoke_test',
|
|
|
|
// nghttp2 shim
|
|
'_h2_session_new',
|
|
'_h2_session_del',
|
|
'_h2_session_want_read',
|
|
'_h2_session_want_write',
|
|
'_h2_recv_buf',
|
|
'_h2_session_mem_recv',
|
|
'_h2_session_send',
|
|
'_h2_session_send_ptr',
|
|
'_h2_submit_request',
|
|
'_h2_submit_trailers',
|
|
'_h2_submit_rst_stream',
|
|
'_h2_submit_priority',
|
|
'_h2_resume_data',
|
|
'_h2_submit_settings',
|
|
'_h2_pack_settings',
|
|
'_h2_submit_ping',
|
|
'_h2_submit_goaway',
|
|
'_h2_set_next_stream_id',
|
|
'_h2_set_local_window_size',
|
|
'_h2_terminate',
|
|
'_h2_refresh_session_state',
|
|
'_h2_refresh_stream_state',
|
|
'_h2_get_settings',
|
|
'_h2_get_next_stream_id',
|
|
'_h2_get_ping_data',
|
|
'_h2_get_goaway_code',
|
|
'_h2_get_goaway_last_stream',
|
|
'_h2_get_goaway_opaque_ptr',
|
|
'_h2_get_goaway_opaque_len',
|
|
'_h2_strerror',
|
|
'_h2_smoke_test',
|
|
];
|
|
|
|
const sources = [
|
|
llhttpShimFile,
|
|
zlibShimFile,
|
|
brotliShimFile,
|
|
cryptoShimFile,
|
|
nghttp2ShimFile,
|
|
path.join(llhttpSourceDir, 'api.c'),
|
|
path.join(llhttpSourceDir, 'http.c'),
|
|
path.join(llhttpSourceDir, 'llhttp.c'),
|
|
...zlibSources,
|
|
...brotliSources,
|
|
...nghttp2Sources,
|
|
];
|
|
|
|
// Per-file compile flags (shared by every object). Passing the full include
|
|
// set to every source is harmless and keeps the job uniform.
|
|
const includes = [
|
|
`-I${llhttpIncludeDir}`,
|
|
`-I${zlibDir}`,
|
|
`-I${brotliIncludeDir}`,
|
|
`-I${opensslBuildIncludeDir}`,
|
|
`-I${opensslSrcIncludeDir}`,
|
|
];
|
|
const compileFlags = [...includes, '-O3', '-ffunction-sections', '-fdata-sections'];
|
|
|
|
// nghttp2's lib sources + our shim need the vendored headers and the same
|
|
// defines node's nghttp2.gyp uses. Applied only to those TUs so the shared
|
|
// object cache for llhttp/zlib/brotli/crypto isn't invalidated.
|
|
const nghttp2Flags = [
|
|
`-I${nghttp2IncludeDir}`,
|
|
`-I${nghttp2Dir}`,
|
|
'-DHAVE_CONFIG_H',
|
|
'-DBUILDING_NGHTTP2',
|
|
'-DNGHTTP2_STATICLIB',
|
|
'-D_U_=',
|
|
];
|
|
const nghttp2SrcSet = new Set([nghttp2ShimFile, ...nghttp2Sources]);
|
|
const extraFlagsFor = (src) => (nghttp2SrcSet.has(src) ? nghttp2Flags : []);
|
|
|
|
// Compile each source to an object file in parallel (one job per core),
|
|
// caching by source mtime. The whole object cache is invalidated whenever
|
|
// the compile flags change.
|
|
const objDir = path.join(wasmBuildDir, 'obj');
|
|
fs.mkdirSync(objDir, { recursive: true });
|
|
|
|
const flagsFile = path.join(objDir, '.compileflags');
|
|
const flagsKey = JSON.stringify([compileFlags, nghttp2Flags]);
|
|
if (!fs.existsSync(flagsFile) || fs.readFileSync(flagsFile, 'utf8') !== flagsKey) {
|
|
for (const f of fs.readdirSync(objDir)) {
|
|
if (f.endsWith('.o')) fs.rmSync(path.join(objDir, f));
|
|
}
|
|
fs.writeFileSync(flagsFile, flagsKey);
|
|
}
|
|
|
|
const objectFor = (src) =>
|
|
path.join(objDir, path.relative(rootDir, src).replace(/[\\/]/g, '__').replace(/\.c$/, '.o'));
|
|
|
|
const objects = sources.map(objectFor);
|
|
const jobs = [];
|
|
sources.forEach((src, i) => {
|
|
const obj = objects[i];
|
|
const fresh = fs.existsSync(obj) && fs.statSync(obj).mtimeMs >= fs.statSync(src).mtimeMs;
|
|
if (fresh) return;
|
|
jobs.push(() => runAsync(emcc, ['-c', src, ...compileFlags, ...extraFlagsFor(src), '-o', obj], { env: { EM_CONFIG: emConfig } }));
|
|
});
|
|
|
|
const cores = os.cpus().length;
|
|
if (jobs.length > 0) {
|
|
console.log(`Compiling ${jobs.length}/${sources.length} wasm objects with ${cores} workers (${sources.length - jobs.length} cached)...`);
|
|
await runPool(jobs, cores);
|
|
} else {
|
|
console.log(`All ${sources.length} wasm objects cached; linking only.`);
|
|
}
|
|
|
|
// Link the objects + libcrypto.a into the standalone wasm. libcrypto.a is
|
|
// last so the linker resolves the crypto_* references; --gc-sections drops
|
|
// the large unreferenced majority of libcrypto. This (binaryen -O3) is the
|
|
// serial floor, so skip it when the output is already newer than every input
|
|
// and the link flags are unchanged.
|
|
const linkFlags = [
|
|
'-O3',
|
|
'-Wl,--gc-sections',
|
|
'-sSTANDALONE_WASM=1',
|
|
'-sFILESYSTEM=0',
|
|
'-sALLOW_MEMORY_GROWTH=1',
|
|
'-sERROR_ON_UNDEFINED_SYMBOLS=1',
|
|
`-sEXPORTED_FUNCTIONS=${JSON.stringify(exportedFunctions)}`,
|
|
'-Wl,--no-entry',
|
|
];
|
|
const linkFile = path.join(objDir, '.linkflags');
|
|
const linkKey = JSON.stringify(linkFlags);
|
|
const linkFlagsChanged = !fs.existsSync(linkFile) || fs.readFileSync(linkFile, 'utf8') !== linkKey;
|
|
const newestInput = Math.max(
|
|
fs.statSync(opensslLibCrypto).mtimeMs,
|
|
...objects.map((o) => fs.statSync(o).mtimeMs),
|
|
);
|
|
const wasmFresh =
|
|
!linkFlagsChanged &&
|
|
fs.existsSync(wasmOutputFile) &&
|
|
fs.statSync(wasmOutputFile).mtimeMs >= newestInput;
|
|
if (wasmFresh) {
|
|
console.log('wasm is up to date; skipping link.');
|
|
return;
|
|
}
|
|
|
|
run(emcc, [...objects, opensslLibCrypto, ...linkFlags, '-o', wasmOutputFile], rootDir, {
|
|
env: { EM_CONFIG: emConfig },
|
|
});
|
|
fs.writeFileSync(linkFile, linkKey);
|
|
}
|
|
|
|
// Builds the Rust `wasm` crate to wasm via its own build.sh (cargo +
|
|
// wasm-bindgen — see src/worker/node-rust/wasm/build.sh), then copies the
|
|
// wasm-bindgen glue/types and base64-embeds the wasm binary into
|
|
// node_core/wasm-build/, mirroring emitWasmModule()'s pattern for the
|
|
// emscripten build. Unlike node-worker.wasm, wasm-bindgen's `--target web`
|
|
// output isn't a standalone module — src/worker/node-rust/loader.ts uses the
|
|
// copied glue's own `initSync` to instantiate it instead of a hand-rolled
|
|
// WebAssembly.Instance.
|
|
function buildRewriterWasm() {
|
|
const sourceFiles = [path.join(nodeRustDir, 'Cargo.toml')];
|
|
for (const crate of ['rewriter', 'transform', 'wasm']) {
|
|
const crateDir = path.join(nodeRustDir, crate);
|
|
sourceFiles.push(path.join(crateDir, 'Cargo.toml'));
|
|
const srcDir = path.join(crateDir, 'src');
|
|
for (const file of fs.readdirSync(srcDir)) {
|
|
if (file.endsWith('.rs')) sourceFiles.push(path.join(srcDir, file));
|
|
}
|
|
}
|
|
|
|
const newestSource = Math.max(...sourceFiles.map((f) => fs.statSync(f).mtimeMs));
|
|
const outputsFresh =
|
|
fs.existsSync(wasmBuildRewriterModuleFile) &&
|
|
fs.existsSync(wasmBuildRewriterGlueFile) &&
|
|
fs.statSync(wasmBuildRewriterModuleFile).mtimeMs >= newestSource &&
|
|
fs.statSync(wasmBuildRewriterGlueFile).mtimeMs >= newestSource;
|
|
if (outputsFresh) {
|
|
console.log('rewriter wasm is up to date; skipping build.');
|
|
return;
|
|
}
|
|
|
|
run(path.join(rewriterWasmCrateDir, 'build.sh'), [], rewriterWasmCrateDir);
|
|
|
|
ensureParentDir(wasmBuildRewriterModuleFile);
|
|
fs.copyFileSync(rewriterGlueFile, wasmBuildRewriterGlueFile);
|
|
if (fs.existsSync(rewriterDtsFile)) {
|
|
fs.copyFileSync(rewriterDtsFile, wasmBuildRewriterDtsFile);
|
|
}
|
|
if (fs.existsSync(rewriterBgDtsFile)) {
|
|
fs.copyFileSync(rewriterBgDtsFile, wasmBuildRewriterBgDtsFile);
|
|
}
|
|
|
|
const base64 = fs.readFileSync(rewriterWasmFile).toString('base64');
|
|
const contents = `const rewriterWasmBase64 = ${JSON.stringify(base64)};\n\nexport default rewriterWasmBase64;\n`;
|
|
fs.writeFileSync(wasmBuildRewriterModuleFile, contents);
|
|
}
|
|
|
|
function emitWasmModule() {
|
|
// Skip re-encoding when the base64 module is already newer than the wasm.
|
|
if (
|
|
fs.existsSync(wasmModuleFile) &&
|
|
fs.statSync(wasmModuleFile).mtimeMs >= fs.statSync(wasmOutputFile).mtimeMs
|
|
) {
|
|
return;
|
|
}
|
|
ensureParentDir(wasmModuleFile);
|
|
const base64 = fs.readFileSync(wasmOutputFile).toString('base64');
|
|
const contents = `const nodeWorkerWasmBase64 = ${JSON.stringify(base64)};\n\nexport default nodeWorkerWasmBase64;\n`;
|
|
fs.writeFileSync(wasmModuleFile, contents);
|
|
}
|
|
|
|
// WASM_ONLY skips the (already-done) checkout/emsdk steps for fast wasm
|
|
// rebuilds during shim development.
|
|
if (!process.env.WASM_ONLY) {
|
|
ensureGitCheckout(nodeConfig, nodeCheckoutDir);
|
|
applyNodePatches();
|
|
ensureGitCheckout(EMSDK_CONFIG, emsdkDir);
|
|
ensureEmsdkInstalled();
|
|
}
|
|
buildOpenSSL();
|
|
await compileNodeWorkerWasm();
|
|
emitWasmModule();
|
|
buildRewriterWasm();
|