Index: Makefile.in ================================================================== --- Makefile.in +++ Makefile.in @@ -72,10 +72,21 @@ USE_SEE = @USE_SEE@ APPNAME = fossil # # APPNAME = fossil-fuzz # may be more appropriate for fuzzing. + +#### Emscripten stuff for optionally doing in-tree builds +# of any WASM components. We store precompiled WASM in the the SCM, so +# this is only useful for people who actively work on WASM +# components. EMSDK_ENV refers to the "environment" script which comes +# with the Emscripten SDK package: +# https://emscripten.org/docs/getting_started/downloads.html +EMSDK_HOME = @EMSDK_HOME@ +EMSDK_ENV = @EMSDK_ENV@ +EMCC_OPT = @EMCC_OPT@ +EMCC_WRAPPER = $(SRCDIR_tools)/emcc.sh .PHONY: all tags include $(SRCDIR)/main.mk Index: auto.def ================================================================== --- auto.def +++ auto.def @@ -28,10 +28,11 @@ static=0 => {Link a static executable} fusefs=1 => {Disable the Fuse Filesystem} fossil-debug=0 => {Build with fossil debugging enabled} no-opt=0 => {Build without optimization} json=0 => {Build with fossil JSON API enabled} + with-emsdk:path => {Directory containing the Emscripten SDK} } # Update the minimum required SQLite version number here, and also # in src/main.c near the sqlite3_libversion_number() call. Take care # that both places agree! @@ -632,10 +633,37 @@ set tclconfig(TCL_LD_FLAGS) [string map [list -ldl ""] \ $tclconfig(TCL_LD_FLAGS)] define-append EXTRA_LDFLAGS $tclconfig(TCL_LD_FLAGS) define FOSSIL_ENABLE_TCL } + +# Emscripten is a purely optional component used only for doing +# in-tree builds of WASM stuff, as opposed to WASM binaries we import +# from other places. This is only set up for Unix-style OSes and is +# untested anywhere but Linux. +set emsdkHome [opt-val with-emsdk] +define EMSDK_HOME "" +define EMSDK_ENV "" +define EMCC_OPT "-Oz" +if {$emsdkHome eq "" && [info exists ::env(EMSDK)]} { + # Fall back to checking the environment. $EMSDK gets set + # by sourcing emsdk_env.sh. + set emsdkHome $::env(EMSDK) +} +if {$emsdkHome ne ""} { + define EMSDK_HOME $emsdkHome + set emsdkEnv "$emsdkHome/emsdk_env.sh" + if {[file exists $emsdkEnv]} { + puts "Using Emscripten SDK environment from $emsdkEnv." + define EMSDK_ENV $emsdkEnv + if {[info exists ::env(EMCC_OPT)]} { + define EMCC_OPT $::env(EMCC_OPT) + } + } else { + puts "emsdk_env.sh not found. Assuming emcc is in the PATH." + } +} # Network functions require libraries on some systems cc-check-function-in-lib gethostbyname nsl if {![cc-check-function-in-lib socket {socket network}]} { # Last resort, may be Windows @@ -726,8 +754,16 @@ # Linux can only infer the dependency on pthread from OpenSSL when # doing dynamic linkage. define-append LIBS -lpthread } +if {[get-define EMSDK_HOME] ne ""} { + define EMCC_WRAPPER $::autosetup(dir)/../tools/emcc.sh + make-template tools/emcc.sh.in + catch {exec chmod u+x tools/emcc.sh} +} else { + define EMCC_WRAPPER "" + catch {exec rm -f tools/emcc.sh} +} make-template Makefile.in make-config-header autoconfig.h -auto {USE_* FOSSIL_*} ADDED extsrc/pikchr-worker.js Index: extsrc/pikchr-worker.js ================================================================== --- /dev/null +++ extsrc/pikchr-worker.js @@ -0,0 +1,221 @@ +/* + 2022-05-20 + + The author disclaims copyright to this source code. In place of a + legal notice, here is a blessing: + + * May you do good and not evil. + * May you find forgiveness for yourself and forgive others. + * May you share freely, never taking more than you give. + + *********************************************************************** + + This is a JS Worker file for use with the pikchr wasm build. It + loads the pikchr wasm module and offers access to it via the Worker + message-passing interface. + + Because we can have only a single message handler, as opposed to an + arbitrary number of discrete event listeners like with DOM elements, + we have to define a lower-level message API. Messages abstractly + look like: + + { type: string, data: type-specific value } + + Where 'type' is used for dispatching and 'data' is a + 'type'-dependent value. + + The 'type' values expected by each side of the main/worker + connection vary. The types are described below but subject to + change at any time as this experiment evolves. + + Main-to-Worker message types: + + - pikchr: data=pikchr-format text to render or an object: + + { + pikchr: source code for the pikchr, + darkMode: boolean true to adjust colors for a dark color scheme, + cssClass: CSS class name to add to the SVG + } + + Workers-to-Main types + + - stdout, stderr: indicate stdout/stderr output from the wasm + layer. The data property is the string of the output, noting + that the emscripten binding emits these one line at a time. Thus, + if a C-side puts() emits multiple lines in a single call, the JS + side will see that as multiple calls. Example: + + {type:'stdout', data: 'Hi, world.'} + + - module: Status text. This is intended to alert the main thread + about module loading status so that, e.g., the main thread can + update a progress widget and DTRT when the module is finished + loading and available for work. Status messages come in the form + + {type:'module', data:{ + type:'status', + data: {text:string|null, step:1-based-integer} + } + + with an incrementing step value for each subsequent message. When + the module loading is complete, a message with a text value of + null is posted. + + - pikchr: + + {type: 'pikchr', + data:{ + pikchr: input text, + result: rendered result (SVG on success, HTML on error), + isError: bool, true if .result holds an error report, + flags: integer: flags used to configure the pikchr rendering, + width: if !isError, width (integer pixels) of the SVG, + height: if !isError, height (integer pixels) of the SVG + } + } + +*/ + +"use strict"; +(function(){ + /** + Posts a message in the form {type,data} unless passed more than + 2 args, in which case it posts {type, data:[arg1...argN]}. + */ + const wMsg = function(type,data){ + postMessage({ + type, + data: arguments.length<3 + ? data + : Array.prototype.slice.call(arguments,1) + }); + }; + + const stderr = function(){wMsg('stderr', Array.prototype.slice.call(arguments));}; + + self.onerror = function(/*message, source, lineno, colno, error*/) { + const err = arguments[4]; + if(err && 'ExitStatus'==err.name){ + /* This "cannot happen" for this wasm binding, but just in + case... */ + pikchrModule.isDead = true; + stderr("FATAL ERROR:", err.message); + stderr("Restarting the app requires reloading the page."); + wMsg('error', err); + } + pikchrModule.setStatus('Exception thrown, see JavaScript console: '+err); + }; + + self.onmessage = function f(ev){ + ev = ev.data; + switch(ev.type){ + /** + Runs the given text through pikchr and emits a 'pikchr' + message result (output format documented above). + + Fires a working/start event before it starts and + working/end event when it finishes. + */ + case 'pikchr': + if(pikchrModule.isDead){ + stderr("wasm module has exit()ed. Cannot pikchr."); + return; + } + if(!f._){ + f._ = pikchrModule.cwrap('pikchr', 'string', [ + 'string'/*script*/, 'string'/*CSS class*/, 'number'/*flags*/, + 'number'/*output: SVG width*/, 'number'/*output: SVG height*/ + ]); + } + wMsg('working','start'); + const stack = pikchrModule.stackSave(); + try { + const pnWidth = pikchrModule.stackAlloc(4), + pnHeight = pikchrModule.stackAlloc(4); + let script = '', flags = 0, cssClass = null; + if('string'===typeof ev.data){ + script = ev.data; + }else if(ev.data && 'object'===typeof ev.data){ + script = ev.data.pikchr; + flags = ev.data.darkMode ? 0x02 : 0; + if(ev.data.cssClass) cssClass = ev.data.cssClass; + } + pikchrModule.setValue(pnWidth, 0, "i32"); + pikchrModule.setValue(pnHeight, 0, "i32"); + const msg = { + pikchr: script, + result: (f._(script, cssClass, flags, pnWidth, pnHeight) || "").trim(), + flags: flags + }; + msg.isError = !!(msg.result && msg.result.startsWith('