mirror of
https://github.com/python/cpython.git
synced 2025-12-02 07:37:25 +00:00
gh-136251: Improvements to WASM demo REPL (GH-136252)
Co-authored-by: Hood Chatham <roberthoodchatham@gmail.com>
This commit is contained in:
parent
9c7b2af73d
commit
d1d526afe7
7 changed files with 327 additions and 56 deletions
|
|
@ -1096,7 +1096,7 @@ $(DLLLIBRARY) libpython$(LDVERSION).dll.a: $(LIBRARY_OBJS)
|
|||
# wasm32-emscripten browser web example
|
||||
|
||||
WEBEX_DIR=$(srcdir)/Tools/wasm/emscripten/web_example/
|
||||
web_example/python.html: $(WEBEX_DIR)/python.html
|
||||
web_example/index.html: $(WEBEX_DIR)/index.html
|
||||
@mkdir -p web_example
|
||||
@cp $< $@
|
||||
|
||||
|
|
@ -1124,7 +1124,7 @@ web_example/python.mjs web_example/python.wasm: $(BUILDPYTHON)
|
|||
cp python.wasm web_example/python.wasm
|
||||
|
||||
.PHONY: web_example
|
||||
web_example: web_example/python.mjs web_example/python.worker.mjs web_example/python.html web_example/server.py $(WEB_STDLIB)
|
||||
web_example: web_example/python.mjs web_example/python.worker.mjs web_example/index.html web_example/server.py $(WEB_STDLIB)
|
||||
|
||||
############################################################################
|
||||
# Header files
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
Fixes and usability improvements for ``Tools/wasm/emscripten/web_example``
|
||||
|
|
@ -100,7 +100,7 @@ EM_JS_MACROS(void, _emscripten_promising_main_js, (void), {
|
|||
return;
|
||||
}
|
||||
const origResolveGlobalSymbol = resolveGlobalSymbol;
|
||||
if (!Module.onExit && process?.exit) {
|
||||
if (!Module.onExit && globalThis?.process?.exit) {
|
||||
Module.onExit = (code) => process.exit(code);
|
||||
}
|
||||
// * wrap the main symbol with WebAssembly.promising,
|
||||
|
|
|
|||
|
|
@ -86,11 +86,11 @@ CLI you will need to write your own alternative to `node_entry.mjs`.
|
|||
|
||||
### The Web Example
|
||||
|
||||
When building for Emscripten, the web example will be built automatically. It is
|
||||
in the ``web_example`` directory. To run the web example, ``cd`` into the
|
||||
When building for Emscripten, the web example will be built automatically. It
|
||||
is in the ``web_example`` directory. To run the web example, ``cd`` into the
|
||||
``web_example`` directory, then run ``python server.py``. This will start a web
|
||||
server; you can then visit ``http://localhost:8000/python.html`` in a browser to
|
||||
see a simple REPL example.
|
||||
server; you can then visit ``http://localhost:8000/`` in a browser to see a
|
||||
simple REPL example.
|
||||
|
||||
The web example relies on a bug fix in Emscripten version 3.1.73 so if you build
|
||||
with earlier versions of Emscripten it may not work. The web example uses
|
||||
|
|
|
|||
|
|
@ -1,31 +1,39 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="author" content="Katie Bell" />
|
||||
<meta name="description" content="Simple REPL for Python WASM" />
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="author" content="Katie Bell, Adam Hartz">
|
||||
<meta name="description" content="Simple REPL for Python WASM">
|
||||
<title>wasm-python terminal</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/xterm@4.18.0/css/xterm.css"
|
||||
crossorigin
|
||||
integrity="sha384-4eEEn/eZgVHkElpKAzzPx/Kow/dTSgFk1BNe+uHdjHa+NkZJDh5Vqkq31+y7Eycd"
|
||||
/>
|
||||
>
|
||||
<style>
|
||||
body {
|
||||
font-family: arial;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
#code {
|
||||
#editor {
|
||||
padding: 5px;
|
||||
border: 1px solid black;
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
height: 300px;
|
||||
}
|
||||
#info {
|
||||
padding-top: 20px;
|
||||
}
|
||||
.error {
|
||||
border: 1px solid red;
|
||||
background-color: #ffd9d9;
|
||||
padding: 5px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
|
|
@ -41,8 +49,14 @@
|
|||
src="https://unpkg.com/xterm@4.18.0/lib/xterm.js"
|
||||
crossorigin
|
||||
integrity="sha384-yYdNmem1ioP5Onm7RpXutin5A8TimLheLNQ6tnMi01/ZpxXdAwIm2t4fJMx1Djs+"
|
||||
/>
|
||||
></script>
|
||||
<script
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.43.1/ace.js"
|
||||
crossorigin
|
||||
integrity="sha512-kmA5vhcxOkZI0ReiKJMGNb8/KKbgbExIlnt6aXuPtl86AgHBEi6OHHOz2wsTazBDGZKxe7fmiE+pIuZJQks4+A=="
|
||||
></script>
|
||||
<script type="module">
|
||||
const _magic_ctrlc_string = "__WASM_REPL_CTRLC_" + (Date.now()) + "__";
|
||||
class WorkerManager {
|
||||
constructor(
|
||||
workerURL,
|
||||
|
|
@ -132,11 +146,14 @@
|
|||
|
||||
class WasmTerminal {
|
||||
constructor() {
|
||||
this.inputBuffer = new BufferQueue();
|
||||
this.input = "";
|
||||
this.resolveInput = null;
|
||||
this.activeInput = false;
|
||||
this.inputStartCursor = null;
|
||||
try {
|
||||
this.history = JSON.parse(sessionStorage.getItem('__python_wasm_repl.history'));
|
||||
this.historyBuffer = this.history.slice();
|
||||
} catch(e) {
|
||||
this.history = [];
|
||||
this.historyBuffer = [];
|
||||
}
|
||||
this.reset();
|
||||
|
||||
this.xterm = new Terminal({
|
||||
scrollback: 10000,
|
||||
|
|
@ -155,6 +172,18 @@
|
|||
this.xterm.onData(this.handleTermData);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.inputBuffer = new BufferQueue();
|
||||
this.input = "";
|
||||
this.resolveInput = null;
|
||||
this.activeInput = false;
|
||||
this.inputStartCursor = null;
|
||||
|
||||
this.cursorPosition = 0;
|
||||
this.historyIndex = -1;
|
||||
this.beforeHistoryNav = "";
|
||||
}
|
||||
|
||||
open(container) {
|
||||
this.xterm.open(container);
|
||||
}
|
||||
|
|
@ -186,9 +215,34 @@
|
|||
if (!(ord === 0x1b || ord == 0x7f || ord < 32)) {
|
||||
this.inputBuffer.addData(data);
|
||||
}
|
||||
// TODO: Handle ANSI escape sequences
|
||||
// TODO: Handle more escape sequences?
|
||||
} else if (ord === 0x1b) {
|
||||
// Handle special characters
|
||||
switch (data.slice(1)) {
|
||||
case "[A": // up
|
||||
this.historyBack();
|
||||
break;
|
||||
case "[B": // down
|
||||
this.historyForward();
|
||||
break;
|
||||
case "[C": // right
|
||||
this.cursorRight();
|
||||
break;
|
||||
case "[D": // left
|
||||
this.cursorLeft();
|
||||
break;
|
||||
case "[H": // home key
|
||||
this.cursorHome(true);
|
||||
break;
|
||||
case "[F": // end key
|
||||
this.cursorEnd(true);
|
||||
break;
|
||||
case "[3~": // delete key
|
||||
this.deleteAtCursor();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if (ord < 32 || ord === 0x7f) {
|
||||
switch (data) {
|
||||
case "\x0c": // CTRL+L
|
||||
|
|
@ -201,8 +255,18 @@
|
|||
this.input + this.writeLine("\n"),
|
||||
);
|
||||
this.input = "";
|
||||
this.cursorPosition = 0;
|
||||
this.activeInput = false;
|
||||
break;
|
||||
case "\x03": // CTRL+C
|
||||
this.input = "";
|
||||
this.cursorPosition = 0;
|
||||
this.historyIndex = -1;
|
||||
this.resolveInput(_magic_ctrlc_string + "\n");
|
||||
break;
|
||||
case "\x09": // TAB
|
||||
this.handleTab();
|
||||
break;
|
||||
case "\x7F": // BACKSPACE
|
||||
case "\x08": // CTRL+H
|
||||
this.handleCursorErase(true);
|
||||
|
|
@ -211,14 +275,20 @@
|
|||
// Send empty input
|
||||
if (this.input === "") {
|
||||
this.resolveInput("");
|
||||
this.cursorPosition = 0;
|
||||
this.activeInput = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.handleCursorInsert(data);
|
||||
this.updateHistory();
|
||||
}
|
||||
};
|
||||
|
||||
clearLine() {
|
||||
this.xterm.write("\x1b[K");
|
||||
}
|
||||
|
||||
writeLine(line) {
|
||||
this.xterm.write(line.slice(0, -1));
|
||||
this.xterm.write("\r\n");
|
||||
|
|
@ -226,8 +296,36 @@
|
|||
}
|
||||
|
||||
handleCursorInsert(data) {
|
||||
this.input += data;
|
||||
const trailing = this.input.slice(this.cursorPosition);
|
||||
this.input =
|
||||
this.input.slice(0, this.cursorPosition) +
|
||||
data +
|
||||
trailing;
|
||||
this.cursorPosition += data.length;
|
||||
this.xterm.write(data);
|
||||
if (trailing.length !== 0) {
|
||||
this.xterm.write(trailing);
|
||||
this.xterm.write("\x1b[" + trailing.length + "D");
|
||||
}
|
||||
this.updateHistory();
|
||||
}
|
||||
|
||||
handleTab() {
|
||||
// handle tabs: from the current position, add spaces until
|
||||
// this.cursorPosition is a multiple of 4.
|
||||
const prefix = this.input.slice(0, this.cursorPosition);
|
||||
const suffix = this.input.slice(this.cursorPosition);
|
||||
const count = 4 - (this.cursorPosition % 4);
|
||||
const toAdd = " ".repeat(count);
|
||||
this.input = prefix + toAdd + suffix;
|
||||
this.cursorHome(false);
|
||||
this.clearLine();
|
||||
this.xterm.write(this.input);
|
||||
if (suffix) {
|
||||
this.xterm.write("\x1b[" + suffix.length + "D");
|
||||
}
|
||||
this.cursorPosition += count;
|
||||
this.updateHistory();
|
||||
}
|
||||
|
||||
handleCursorErase() {
|
||||
|
|
@ -238,9 +336,113 @@
|
|||
) {
|
||||
return;
|
||||
}
|
||||
this.input = this.input.slice(0, -1);
|
||||
this.xterm.write("\x1B[D");
|
||||
this.xterm.write("\x1B[P");
|
||||
const trailing = this.input.slice(this.cursorPosition);
|
||||
this.input =
|
||||
this.input.slice(0, this.cursorPosition - 1) + trailing;
|
||||
this.cursorLeft();
|
||||
this.clearLine();
|
||||
if (trailing.length !== 0) {
|
||||
this.xterm.write(trailing);
|
||||
this.xterm.write("\x1b[" + trailing.length + "D");
|
||||
}
|
||||
this.updateHistory();
|
||||
}
|
||||
|
||||
deleteAtCursor() {
|
||||
if (this.cursorPosition < this.input.length) {
|
||||
const trailing = this.input.slice(
|
||||
this.cursorPosition + 1,
|
||||
);
|
||||
this.input =
|
||||
this.input.slice(0, this.cursorPosition) + trailing;
|
||||
this.clearLine();
|
||||
if (trailing.length !== 0) {
|
||||
this.xterm.write(trailing);
|
||||
this.xterm.write("\x1b[" + trailing.length + "D");
|
||||
}
|
||||
this.updateHistory();
|
||||
}
|
||||
}
|
||||
|
||||
cursorRight() {
|
||||
if (this.cursorPosition < this.input.length) {
|
||||
this.cursorPosition += 1;
|
||||
this.xterm.write("\x1b[C");
|
||||
}
|
||||
}
|
||||
|
||||
cursorLeft() {
|
||||
if (this.cursorPosition > 0) {
|
||||
this.cursorPosition -= 1;
|
||||
this.xterm.write("\x1b[D");
|
||||
}
|
||||
}
|
||||
|
||||
cursorHome(updatePosition) {
|
||||
if (this.cursorPosition > 0) {
|
||||
this.xterm.write("\x1b[" + this.cursorPosition + "D");
|
||||
if (updatePosition) {
|
||||
this.cursorPosition = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cursorEnd() {
|
||||
if (this.cursorPosition < this.input.length) {
|
||||
this.xterm.write(
|
||||
"\x1b[" +
|
||||
(this.input.length - this.cursorPosition) +
|
||||
"C",
|
||||
);
|
||||
this.cursorPosition = this.input.length;
|
||||
}
|
||||
}
|
||||
|
||||
updateHistory() {
|
||||
if (this.historyIndex !== -1) {
|
||||
this.historyBuffer[this.historyIndex] = this.input;
|
||||
} else {
|
||||
this.beforeHistoryNav = this.input;
|
||||
}
|
||||
}
|
||||
|
||||
historyBack() {
|
||||
if (this.history.length === 0) {
|
||||
return;
|
||||
} else if (this.historyIndex === -1) {
|
||||
// we're not currently navigating the history; store
|
||||
// the current command and then look at the end of our
|
||||
// history buffer
|
||||
this.beforeHistoryNav = this.input;
|
||||
this.historyIndex = this.history.length - 1;
|
||||
} else if (this.historyIndex > 0) {
|
||||
this.historyIndex -= 1;
|
||||
}
|
||||
this.input = this.historyBuffer[this.historyIndex];
|
||||
this.cursorHome(false);
|
||||
this.clearLine();
|
||||
this.xterm.write(this.input);
|
||||
this.cursorPosition = this.input.length;
|
||||
}
|
||||
|
||||
historyForward() {
|
||||
if (this.history.length === 0 || this.historyIndex === -1) {
|
||||
// we're not currently navigating the history; NOP.
|
||||
return;
|
||||
} else if (this.historyIndex < this.history.length - 1) {
|
||||
this.historyIndex += 1;
|
||||
this.input = this.historyBuffer[this.historyIndex];
|
||||
} else if (this.historyIndex == this.history.length - 1) {
|
||||
// we're coming back from the last history value; reset
|
||||
// the input to whatever it was when we started going
|
||||
// through the history
|
||||
this.input = this.beforeHistoryNav;
|
||||
this.historyIndex = -1;
|
||||
}
|
||||
this.cursorHome(false);
|
||||
this.clearLine();
|
||||
this.xterm.write(this.input);
|
||||
this.cursorPosition = this.input.length;
|
||||
}
|
||||
|
||||
prompt = async () => {
|
||||
|
|
@ -263,12 +465,29 @@
|
|||
// Hack to ensure cursor input start doesn't end up after user input
|
||||
setTimeout(() => {
|
||||
this.handleCursorInsert(
|
||||
this.inputBuffer.nextLine(),
|
||||
this.inputBuffer.nextLine()
|
||||
);
|
||||
}, 1);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this.resolveInput = (value) => {
|
||||
if (
|
||||
value.replace(/\s/g, "").length != 0 &&
|
||||
value != _magic_ctrlc_string + "\n"
|
||||
) {
|
||||
if (this.historyIndex !== -1) {
|
||||
this.historyBuffer[this.historyIndex] =
|
||||
this.history[this.historyIndex];
|
||||
}
|
||||
this.history.push(value.slice(0, -1));
|
||||
this.historyBuffer.push(value.slice(0, -1));
|
||||
this.historyIndex = -1;
|
||||
this.cursorPosition = 0;
|
||||
try {
|
||||
sessionStorage.setItem('__python_wasm_repl.history', JSON.stringify(this.history));
|
||||
} catch(e) {
|
||||
}
|
||||
}
|
||||
resolve(value);
|
||||
};
|
||||
});
|
||||
|
|
@ -327,8 +546,6 @@
|
|||
const stopButton = document.getElementById("stop");
|
||||
const clearButton = document.getElementById("clear");
|
||||
|
||||
const codeBox = document.getElementById("codebox");
|
||||
|
||||
window.onload = () => {
|
||||
const terminal = new WasmTerminal();
|
||||
terminal.open(document.getElementById("terminal"));
|
||||
|
|
@ -362,8 +579,9 @@
|
|||
|
||||
runButton.addEventListener("click", (e) => {
|
||||
terminal.clear();
|
||||
terminal.reset(); // reset the history
|
||||
programRunning(true);
|
||||
const code = codeBox.value;
|
||||
const code = editor.getValue();
|
||||
pythonWorkerManager.run({
|
||||
args: ["main.py"],
|
||||
files: { "main.py": code },
|
||||
|
|
@ -372,10 +590,28 @@
|
|||
|
||||
replButton.addEventListener("click", (e) => {
|
||||
terminal.clear();
|
||||
terminal.reset(); // reset the history
|
||||
const REPL = `
|
||||
class WASMREPLKeyboardInterrupt(KeyboardInterrupt):
|
||||
pass
|
||||
|
||||
import sys
|
||||
import code
|
||||
import builtins
|
||||
|
||||
def _interrupt_aware_input(prompt=''):
|
||||
line = builtins.input(prompt)
|
||||
if line.strip() == "${_magic_ctrlc_string}":
|
||||
raise KeyboardInterrupt()
|
||||
return line
|
||||
|
||||
cprt = 'Type "help", "copyright", "credits" or "license" for more information.'
|
||||
banner = f'Python {sys.version} on {sys.platform}\\n{cprt}'
|
||||
|
||||
code.interact(banner=banner, readfunc=_interrupt_aware_input, exitmsg='')
|
||||
`;
|
||||
programRunning(true);
|
||||
// Need to use "-i -" to force interactive mode.
|
||||
// Looks like isatty always returns false in emscripten
|
||||
pythonWorkerManager.run({ args: ["-i", "-"], files: {} });
|
||||
pythonWorkerManager.run({ args: ["-c", REPL], files: {} });
|
||||
});
|
||||
|
||||
stopButton.addEventListener("click", (e) => {
|
||||
|
|
@ -395,6 +631,7 @@
|
|||
|
||||
const finishedCallback = () => {
|
||||
programRunning(false);
|
||||
pythonWorkerManager.reset();
|
||||
};
|
||||
|
||||
const pythonWorkerManager = new WorkerManager(
|
||||
|
|
@ -404,23 +641,27 @@
|
|||
finishedCallback,
|
||||
);
|
||||
};
|
||||
var editor;
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
editor = ace.edit("editor");
|
||||
editor.session.setMode("ace/mode/python");
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="repldemo">
|
||||
<h1>Simple REPL for Python WASM</h1>
|
||||
<textarea id="codebox" cols="108" rows="16">
|
||||
print('Welcome to WASM!')
|
||||
</textarea
|
||||
>
|
||||
<div id="editor">print('Welcome to WASM!')</div>
|
||||
<div class="button-container">
|
||||
<button id="run" disabled>Run</button>
|
||||
<button id="run" disabled>Run code</button>
|
||||
<button id="repl" disabled>Start REPL</button>
|
||||
<button id="stop" disabled>Stop</button>
|
||||
<button id="clear" disabled>Clear</button>
|
||||
</div>
|
||||
<div id="terminal"></div>
|
||||
<div id="info">
|
||||
The simple REPL provides a limited Python experience in the browser.
|
||||
The simple REPL provides a limited Python experience in the
|
||||
browser.
|
||||
<a
|
||||
href="https://github.com/python/cpython/blob/main/Tools/wasm/README.md"
|
||||
>
|
||||
|
|
@ -429,5 +670,34 @@ print('Welcome to WASM!')
|
|||
contains a list of known limitations and issues. Networking,
|
||||
subprocesses, and threading are not available.
|
||||
</div>
|
||||
</div>
|
||||
<div id="buffererror" class="error" style="display: none">
|
||||
<p>
|
||||
<code>SharedArrayBuffer</code>, which is required for this demo,
|
||||
is not available in your browser environment. One common cause
|
||||
of this failure is loading <code>index.html</code> directly in
|
||||
your browser instead of using <code>server.py</code> as
|
||||
described in
|
||||
<a
|
||||
href="https://github.com/python/cpython/blob/main/Tools/wasm/README.md#the-web-example"
|
||||
>
|
||||
Tools/wasm/README.md
|
||||
</a>.
|
||||
</p>
|
||||
<p>
|
||||
For more details about security requirements for
|
||||
<code>SharedArrayBuffer</code>, see
|
||||
<a
|
||||
href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements"
|
||||
>this MDN page</a
|
||||
>.
|
||||
</p>
|
||||
<script>
|
||||
if (typeof SharedArrayBuffer === 'undefined') {
|
||||
document.getElementById('repldemo').style.display = 'none';
|
||||
document.getElementById('buffererror').style.display = 'block';
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
2
configure
generated
vendored
2
configure
generated
vendored
|
|
@ -9603,7 +9603,7 @@ fi
|
|||
as_fn_append LDFLAGS_NODIST " -sWASM_BIGINT"
|
||||
|
||||
as_fn_append LINKFORSHARED " -sFORCE_FILESYSTEM -lidbfs.js -lnodefs.js -lproxyfs.js -lworkerfs.js"
|
||||
as_fn_append LINKFORSHARED " -sEXPORTED_RUNTIME_METHODS=FS,callMain,ENV"
|
||||
as_fn_append LINKFORSHARED " -sEXPORTED_RUNTIME_METHODS=FS,callMain,ENV,HEAPU32"
|
||||
as_fn_append LINKFORSHARED " -sEXPORTED_FUNCTIONS=_main,_Py_Version,__PyRuntime,__PyEM_EMSCRIPTEN_COUNT_ARGS_OFFSET,_PyGILState_GetThisThreadState,__Py_DumpTraceback"
|
||||
as_fn_append LINKFORSHARED " -sSTACK_SIZE=5MB"
|
||||
as_fn_append LINKFORSHARED " -sTEXTDECODER=2"
|
||||
|
|
|
|||
|
|
@ -2335,7 +2335,7 @@ AS_CASE([$ac_sys_system],
|
|||
|
||||
dnl Include file system support
|
||||
AS_VAR_APPEND([LINKFORSHARED], [" -sFORCE_FILESYSTEM -lidbfs.js -lnodefs.js -lproxyfs.js -lworkerfs.js"])
|
||||
AS_VAR_APPEND([LINKFORSHARED], [" -sEXPORTED_RUNTIME_METHODS=FS,callMain,ENV"])
|
||||
AS_VAR_APPEND([LINKFORSHARED], [" -sEXPORTED_RUNTIME_METHODS=FS,callMain,ENV,HEAPU32"])
|
||||
AS_VAR_APPEND([LINKFORSHARED], [" -sEXPORTED_FUNCTIONS=_main,_Py_Version,__PyRuntime,__PyEM_EMSCRIPTEN_COUNT_ARGS_OFFSET,_PyGILState_GetThisThreadState,__Py_DumpTraceback"])
|
||||
AS_VAR_APPEND([LINKFORSHARED], [" -sSTACK_SIZE=5MB"])
|
||||
dnl Avoid bugs in JS fallback string decoding path
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue