Skip to main content
Module

x/wasmbuild/lib/pre_build.ts

Build tool to use Rust code in Deno and the browser.
Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
import { BuildCommand, CheckCommand } from "./args.ts";import { base64, colors, path, Sha1 } from "./deps.ts";import { getCargoWorkspace, WasmCrate } from "./manifest.ts";import { verifyVersions } from "./versions.ts";import { BindgenOutput, generateBindgen } from "./bindgen.ts";import { pathExists } from "./helpers.ts";export type { BindgenOutput } from "./bindgen.ts";
export interface PreBuildOutput { bindgen: BindgenOutput; bindingJsText: string; bindingJsPath: string; sourceHash: string; wasmFileName: string | undefined;}
export async function runPreBuild( args: CheckCommand | BuildCommand,): Promise<PreBuildOutput> { const home = Deno.env.get("HOME"); const root = Deno.cwd(); if (!await pathExists(path.join(root, "Cargo.toml"))) { console.error( "%cConsider running `deno task wasmbuild new` to get started", "color: yellow", ); throw `Cargo.toml not found in ${root}`; } const workspace = await getCargoWorkspace(root, args.cargoFlags); const crate = workspace.getWasmCrate(args.project);
verifyVersions(crate);
console.log( `${ colors.bold(colors.green("Ensuring")) } wasm32-unknown-unknown target installed...`, );
const rustupAddWasm = new Deno.Command("rustup", { args: ["target", "add", "wasm32-unknown-unknown"], }); const rustupAddWasmOutput = await rustupAddWasm.output(); if (!rustupAddWasmOutput.success) { console.error(`adding wasm32-unknown-unknown target failed`); Deno.exit(1); }
console.log( `${colors.bold(colors.green("Building"))} ${crate.name} WebAssembly...`, );
const cargoBuildCmd = [ "build", "--lib", "-p", crate.name, "--target", "wasm32-unknown-unknown", ...args.cargoFlags, ];
if (args.profile === "release") { cargoBuildCmd.push("--release"); }
const RUSTFLAGS = Deno.env.get("RUSTFLAGS") || "" + `--remap-path-prefix='${root}'=. --remap-path-prefix='${home}'=~`; console.log(` ${colors.bold(colors.gray(cargoBuildCmd.join(" ")))}`); const cargoBuildReleaseCmdProcess = new Deno.Command("cargo", { args: cargoBuildCmd, env: { "SOURCE_DATE_EPOCH": "1600000000", "TZ": "UTC", "LC_ALL": "C", RUSTFLAGS, }, }).spawn(); const cargoBuildReleaseCmdOutput = await cargoBuildReleaseCmdProcess.status; if (!cargoBuildReleaseCmdOutput.success) { console.error(`cargo build failed`); Deno.exit(1); }
console.log(` ${colors.bold(colors.gray("Running wasm-bindgen..."))}`); const bindgenOutput = await generateBindgen( crate.libName, path.join( workspace.metadata.target_directory, `wasm32-unknown-unknown/${args.profile}/${crate.libName}.wasm`, ), );
console.log( `${colors.bold(colors.green("Generating"))} lib JS bindings...`, );
const bindingJsFileName = `${crate.libName}.generated.${args.bindingJsFileExt}`; const bindingJsPath = path.join(args.outDir, bindingJsFileName);
const { bindingJsText, sourceHash } = await getBindingJsOutput( args, crate, bindgenOutput, bindingJsPath, );
return { bindgen: bindgenOutput, bindingJsText, bindingJsPath, sourceHash, wasmFileName: args.loaderKind === "sync" ? undefined : getWasmFileNameFromCrate(crate), };}
async function getBindingJsOutput( args: CheckCommand | BuildCommand, crate: WasmCrate, bindgenOutput: BindgenOutput, bindingJsPath: string,) { const sourceHash = await getHash(); const header = `// @generated file from wasmbuild -- do not edit// @ts-nocheck: generated// deno-lint-ignore-file// deno-fmt-ignore-file`; const genText = bindgenOutput.js.replace( /\bconst\swasm_url\s.+/ms, getLoaderText(args, crate, bindgenOutput, bindingJsPath), ); const bodyText = await getFormattedText(`// source-hash: ${sourceHash}let wasm;${genText.includes("let cachedInt32Memory0") ? "" : "let cachedInt32Memory0;"}${genText.includes("let cachedUint8Memory0") ? "" : "let cachedUint8Memory0;"}${genText}`);
return { bindingJsText: `${header}\n${bodyText}`, sourceHash, };
async function getFormattedText(inputText: string) { const denoFmtCmdArgs = [ "fmt", "--quiet", "--ext", "js", "-", ]; console.log(` ${colors.bold(colors.gray(denoFmtCmdArgs.join(" ")))}`); const denoFmtCmd = new Deno.Command(Deno.execPath(), { args: denoFmtCmdArgs, stdin: "piped", stdout: "piped", }); const denoFmtChild = denoFmtCmd.spawn(); const stdin = denoFmtChild.stdin.getWriter(); await stdin.write(new TextEncoder().encode(inputText)); await stdin.close();
const output = await denoFmtChild.output(); if (!output.success) { console.error("deno fmt command failed"); Deno.exit(1); } return new TextDecoder().decode(output.stdout); }
async function getHash() { // Create a hash of all the sources, snippets, and local modules // in order to tell when the output has changed. const hasher = new Sha1(); const sourceHash = await crate.getSourcesHash(); hasher.update(sourceHash); for (const [identifier, list] of Object.entries(bindgenOutput.snippets)) { hasher.update(identifier); for (const text of list) { hasher.update(text.replace(/\r?\n/g, "\n")); } } for (const [name, text] of Object.entries(bindgenOutput.localModules)) { hasher.update(name); hasher.update(text.replace(/\r?\n/g, "\n")); } return hasher.hex(); }}
function getLoaderText( args: CheckCommand | BuildCommand, crate: WasmCrate, bindgenOutput: BindgenOutput, bindingJsPath: string,) { switch (args.loaderKind) { case "sync": return getSyncLoaderText(bindgenOutput); case "async": return getAsyncLoaderText( crate, bindgenOutput, false, bindingJsPath, ); case "async-with-cache": return getAsyncLoaderText( crate, bindgenOutput, true, bindingJsPath, ); }}
function getSyncLoaderText(bindgenOutput: BindgenOutput) { const exportNames = getExportNames(bindgenOutput); return `/** Instantiates an instance of the Wasm module returning its functions. * @remarks It is safe to call this multiple times and once successfully * loaded it will always return a reference to the same object. */export function instantiate() { return instantiateWithInstance().exports;}
let instanceWithExports;
/** Instantiates an instance of the Wasm module along with its exports. * @remarks It is safe to call this multiple times and once successfully * loaded it will always return a reference to the same object. * @returns {{ * instance: WebAssembly.Instance; * exports: { ${exportNames.map((n) => `${n}: typeof ${n}`).join("; ")} } * }} */export function instantiateWithInstance() { if (instanceWithExports == null) { const instance = instantiateInstance(); wasm = instance.exports; cachedInt32Memory0 = new Int32Array(wasm.memory.buffer); cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer); instanceWithExports = { instance, exports: { ${exportNames.join(", ")} }, }; } return instanceWithExports;}
/** Gets if the Wasm module has been instantiated. */export function isInstantiated() { return instanceWithExports != null;}
function instantiateInstance() { const wasmBytes = base64decode("\\\n${ base64.encode(new Uint8Array(bindgenOutput.wasmBytes)) .replace(/.{78}/g, "$&\\\n") }\\\n"); const wasmModule = new WebAssembly.Module(wasmBytes); return new WebAssembly.Instance(wasmModule, imports);}
function base64decode(b64) { const binString = atob(b64); const size = binString.length; const bytes = new Uint8Array(size); for (let i = 0; i < size; i++) { bytes[i] = binString.charCodeAt(i); } return bytes;} `;}
function parseRelativePath( fromFilePath: string, toRelativeSpecifier: string,): string { const specifier = import.meta.resolve(toRelativeSpecifier); if (!specifier.startsWith("file:")) return specifier;
const fromDirPath = path.join(Deno.cwd(), path.dirname(fromFilePath)); const toFilePath = path.fromFileUrl(specifier); const relativeFromTo = path.relative(fromDirPath, toFilePath) .replace(/\\/g, "/"); // The path might be absolute on the Windows CI because it uses a // different drive for the temp dir. In that case, just use the resolved // specifier. return path.isAbsolute(relativeFromTo) ? specifier : relativeFromTo;}
function getAsyncLoaderText( crate: WasmCrate, bindgenOutput: BindgenOutput, useCache: boolean, bindingJsFileName: string,) { const exportNames = getExportNames(bindgenOutput);
let loaderText = getWasmbuildLoaderText();
let cacheText = ""; if (useCache) { // If it's Deno or Node (via dnt), then use the cache. // It's ok that the Node path is importing a .ts file because // it will be transformed by dnt. loaderText += `const isNodeOrDeno = typeof Deno === "object" || (typeof process !== "undefined" && process.versions != null && process.versions.node != null);\n`; const cacheUrl = parseRelativePath(bindingJsFileName, "../loader/cache.ts"); cacheText += `isNodeOrDeno ? (await import("${cacheUrl}")).cacheToLocalDir : undefined`; } else { cacheText = "undefined"; }
loaderText += `const loader = new WasmBuildLoader({ imports, cache: ${cacheText},})`;
loaderText += `/** * Options for instantiating a Wasm instance. * @typedef {Object} InstantiateOptions * @property {URL=} url - Optional url to the Wasm file to instantiate. * @property {DecompressCallback=} decompress - Callback to decompress the * raw Wasm file bytes before instantiating. */
/** Instantiates an instance of the Wasm module returning its functions. * @remarks It is safe to call this multiple times and once successfully * loaded it will always return a reference to the same object. * @param {InstantiateOptions=} opts */export async function instantiate(opts) { return (await instantiateWithInstance(opts)).exports;}
/** Instantiates an instance of the Wasm module along with its exports. * @remarks It is safe to call this multiple times and once successfully * loaded it will always return a reference to the same object. * @param {InstantiateOptions=} opts * @returns {Promise<{ * instance: WebAssembly.Instance; * exports: { ${exportNames.map((n) => `${n}: typeof ${n}`).join("; ")} } * }>} */export async function instantiateWithInstance(opts) { const {instance } = await loader.load( opts?.url ?? new URL("${getWasmFileNameFromCrate(crate)}", import.meta.url), opts?.decompress, ); wasm = wasm ?? instance.exports; cachedInt32Memory0 = cachedInt32Memory0 ?? new Int32Array(wasm.memory.buffer); cachedUint8Memory0 = cachedUint8Memory0 ?? new Uint8Array(wasm.memory.buffer); return { instance, exports: getWasmInstanceExports(), };}
function getWasmInstanceExports() { return { ${exportNames.join(", ")} };}
/** Gets if the Wasm module has been instantiated. */export function isInstantiated() { return loader.instance != null;}`;
return loaderText;
function getWasmbuildLoaderText() { return `/*** @callback WasmBuildDecompressCallback* @param {Uint8Array} compressed* @returns {Uint8Array} decompressed*/
/*** @callback WasmBuildCacheCallback* @param {URL} url* @param {WasmBuildDecompressCallback | undefined} decompress* @returns {Promise<URL |Uint8Array>}*/
/*** @typedef WasmBuildLoaderOptions* @property {WebAssembly.Imports | undefined} imports - The Wasm module's imports.* @property {WasmBuildCacheCallback} [cache] - A function that caches the Wasm module to* a local path so that a network request isn't required on every load.** Returns an ArrayBuffer with the bytes on download success, but cache save failure.*/
class WasmBuildLoader { /** @type {WasmBuildLoaderOptions} */ #options; /** @type {Promise<WebAssembly.WebAssemblyInstantiatedSource> | undefined} */ #lastLoadPromise; /** @type {WebAssembly.WebAssemblyInstantiatedSource | undefined} */ #instantiated;
/** @param {WasmBuildLoaderOptions} options */ constructor(options) { this.#options = options; }
/** @returns {WebAssembly.Instance | undefined} */ get instance() { return this.#instantiated?.instance; }
/** @returns {WebAssembly.Module | undefined} */ get module() { return this.#instantiated?.module; }
/** * @param {URL} url * @param {WasmBuildDecompressCallback | undefined} decompress * @returns {Promise<WebAssembly.WebAssemblyInstantiatedSource>} */ load( url, decompress, ) { if (this.#instantiated) { return Promise.resolve(this.#instantiated); } else if (this.#lastLoadPromise == null) { this.#lastLoadPromise = (async () => { try { this.#instantiated = await this.#instantiate(url, decompress); return this.#instantiated; } finally { this.#lastLoadPromise = undefined; } })(); } return this.#lastLoadPromise; }
/** * @param {URL} url * @param {WasmBuildDecompressCallback | undefined} decompress */ async #instantiate(url, decompress) { const imports = this.#options.imports; if (this.#options.cache != null && url.protocol !== "file:") { try { const result = await this.#options.cache( url, decompress ?? ((bytes) => bytes), ); if (result instanceof URL) { url = result; decompress = undefined; // already decompressed } else if (result != null) { return WebAssembly.instantiate(result, imports); } } catch { // ignore if caching ever fails (ex. when on deploy) } }
const isFile = url.protocol === "file:";
// make file urls work in Node via dnt const isNode = (/** @type {any} */ (globalThis)).process?.versions?.node != null; if (isFile && typeof Deno !== "object") { throw new Error( "Loading local files are not supported in this environment", ); } if (isNode && isFile) { // the deno global will be shimmed by dnt const wasmCode = await Deno.readFile(url); return WebAssembly.instantiate( decompress ? decompress(wasmCode) : wasmCode, imports, ); }
switch (url.protocol) { case "file:": case "https:": case "http:": { const wasmResponse = await fetchWithRetries(url); if (decompress) { const wasmCode = new Uint8Array(await wasmResponse.arrayBuffer()); return WebAssembly.instantiate(decompress(wasmCode), imports); } if ( isFile || wasmResponse.headers.get("content-type")?.toLowerCase() .startsWith("application/wasm") ) { return WebAssembly.instantiateStreaming( // Cast to any so there's no type checking issues with dnt // (https://github.com/denoland/wasmbuild/issues/92) /** @type {any} */ (wasmResponse), imports, ); } else { return WebAssembly.instantiate( await wasmResponse.arrayBuffer(), imports, ); } } default: throw new Error(\`Unsupported protocol: \${url.protocol}\`); } }}
/** @param {URL | string} url */async function fetchWithRetries(url, maxRetries = 5) { let sleepMs = 250; let iterationCount = 0; while (true) { iterationCount++; try { const res = await fetch(url); if (res.ok || iterationCount > maxRetries) { return res; } } catch (err) { if (iterationCount > maxRetries) { throw err; } } console.warn(\`Failed fetching. Retrying in \${sleepMs}ms...\`); await new Promise((resolve) => setTimeout(resolve, sleepMs)); sleepMs = Math.min(sleepMs * 2, 10_000); }}`; }}
function getExportNames(bindgenOutput: BindgenOutput) { return Array.from(bindgenOutput.js.matchAll( /export (function|class) ([^({]+)[({]/g, )).map((m) => m[2]);}
function getWasmFileNameFromCrate(crate: WasmCrate) { return `${crate.libName}_bg.wasm`;}