Skip to content

gh-124621: Emscripten: Support pyrepl in browser #136931

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jul 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions Lib/_pyrepl/trace.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import os
import sys

# types
if False:
Expand All @@ -12,10 +13,22 @@
trace_file = open(trace_filename, "a")


def trace(line: str, *k: object, **kw: object) -> None:
if trace_file is None:
return
if k or kw:
line = line.format(*k, **kw)
trace_file.write(line + "\n")
trace_file.flush()

if sys.platform == "emscripten":
from posix import _emscripten_log

def trace(line: str, *k: object, **kw: object) -> None:
if "PYREPL_TRACE" not in os.environ:
return
if k or kw:
line = line.format(*k, **kw)
_emscripten_log(line)

else:
def trace(line: str, *k: object, **kw: object) -> None:
if trace_file is None:
return
if k or kw:
line = line.format(*k, **kw)
trace_file.write(line + "\n")
trace_file.flush()
5 changes: 4 additions & 1 deletion Lib/test/test_fcntl.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import sys
import unittest
from test.support import (
cpython_only, get_pagesize, is_apple, requires_subprocess, verbose
cpython_only, get_pagesize, is_apple, requires_subprocess, verbose, is_emscripten
)
from test.support.import_helper import import_module
from test.support.os_helper import TESTFN, unlink, make_bad_fd
Expand Down Expand Up @@ -211,6 +211,7 @@ def test_fcntl_f_getpath(self):
@unittest.skipUnless(
hasattr(fcntl, "F_SETPIPE_SZ") and hasattr(fcntl, "F_GETPIPE_SZ"),
"F_SETPIPE_SZ and F_GETPIPE_SZ are not available on all platforms.")
@unittest.skipIf(is_emscripten, "Emscripten pipefs doesn't support these")
def test_fcntl_f_pipesize(self):
test_pipe_r, test_pipe_w = os.pipe()
try:
Expand Down Expand Up @@ -265,12 +266,14 @@ def _check_fcntl_not_mutate_len(self, nbytes=None):
@unittest.skipUnless(
hasattr(fcntl, "F_SETOWN_EX") and hasattr(fcntl, "F_GETOWN_EX"),
"requires F_SETOWN_EX and F_GETOWN_EX")
@unittest.skipIf(is_emscripten, "Emscripten doesn't actually support these")
def test_fcntl_small_buffer(self):
self._check_fcntl_not_mutate_len()

@unittest.skipUnless(
hasattr(fcntl, "F_SETOWN_EX") and hasattr(fcntl, "F_GETOWN_EX"),
"requires F_SETOWN_EX and F_GETOWN_EX")
@unittest.skipIf(is_emscripten, "Emscripten doesn't actually support these")
def test_fcntl_large_buffer(self):
self._check_fcntl_not_mutate_len(2024)

Expand Down
54 changes: 42 additions & 12 deletions Makefile.pre.in
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,7 @@ build_wasm: check-clean-src $(BUILDPYTHON) platform sharedmods \
python-config checksharedmods

.PHONY: build_emscripten
build_emscripten: build_wasm web_example
build_emscripten: build_wasm web_example web_example_pyrepl_jspi

# Check that the source is clean when building out of source.
.PHONY: check-clean-src
Expand Down Expand Up @@ -1095,26 +1095,28 @@ $(DLLLIBRARY) libpython$(LDVERSION).dll.a: $(LIBRARY_OBJS)

# wasm32-emscripten browser web example

WEBEX_DIR=$(srcdir)/Tools/wasm/emscripten/web_example/
EMSCRIPTEN_DIR=$(srcdir)/Tools/wasm/emscripten
WEBEX_DIR=$(EMSCRIPTEN_DIR)/web_example/

ZIP_STDLIB=python$(VERSION)$(ABI_THREAD).zip
$(ZIP_STDLIB): $(srcdir)/Lib/*.py $(srcdir)/Lib/*/*.py \
$(EMSCRIPTEN_DIR)/wasm_assets.py \
Makefile pybuilddir.txt Modules/Setup.local
$(PYTHON_FOR_BUILD) $(EMSCRIPTEN_DIR)/wasm_assets.py \
--buildroot . --prefix $(prefix) -o $@

web_example/index.html: $(WEBEX_DIR)/index.html
@mkdir -p web_example
@cp $< $@

web_example/python.worker.mjs: $(WEBEX_DIR)/python.worker.mjs
web_example/server.py: $(WEBEX_DIR)/server.py
@mkdir -p web_example
@cp $< $@

web_example/server.py: $(WEBEX_DIR)/server.py
web_example/$(ZIP_STDLIB): $(ZIP_STDLIB)
@mkdir -p web_example
@cp $< $@

WEB_STDLIB=web_example/python$(VERSION)$(ABI_THREAD).zip
$(WEB_STDLIB): $(srcdir)/Lib/*.py $(srcdir)/Lib/*/*.py \
$(WEBEX_DIR)/wasm_assets.py \
Makefile pybuilddir.txt Modules/Setup.local
$(PYTHON_FOR_BUILD) $(WEBEX_DIR)/wasm_assets.py \
--buildroot . --prefix $(prefix) -o $@

web_example/python.mjs web_example/python.wasm: $(BUILDPYTHON)
@if test $(HOST_GNU_TYPE) != 'wasm32-unknown-emscripten' ; then \
echo "Can only build web_example when target is Emscripten" ;\
Expand All @@ -1124,7 +1126,35 @@ 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/index.html web_example/server.py $(WEB_STDLIB)
web_example: web_example/python.mjs web_example/index.html web_example/server.py web_example/$(ZIP_STDLIB)

WEBEX2=web_example_pyrepl_jspi
WEBEX2_DIR=$(EMSCRIPTEN_DIR)/$(WEBEX2)/

$(WEBEX2)/python.mjs $(WEBEX2)/python.wasm: $(BUILDPYTHON)
@if test $(HOST_GNU_TYPE) != 'wasm32-unknown-emscripten' ; then \
echo "Can only build web_example when target is Emscripten" ;\
exit 1 ;\
fi
@mkdir -p $(WEBEX2)
@cp python.mjs $(WEBEX2)/python.mjs
@cp python.wasm $(WEBEX2)/python.wasm

$(WEBEX2)/index.html: $(WEBEX2_DIR)/index.html
@mkdir -p $(WEBEX2)
@cp $< $@

$(WEBEX2)/src.mjs: $(WEBEX2_DIR)/src.mjs
@mkdir -p $(WEBEX2)
@cp $< $@

$(WEBEX2)/$(ZIP_STDLIB): $(ZIP_STDLIB)
@mkdir -p $(WEBEX2)
@cp $< $@

.PHONY: web_example_pyrepl_jspi
web_example_pyrepl_jspi: $(WEBEX2)/python.mjs $(WEBEX2)/index.html $(WEBEX2)/src.mjs $(WEBEX2)/$(ZIP_STDLIB)


############################################################################
# Header files
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pyrepl now works in Emscripten.
80 changes: 79 additions & 1 deletion Modules/clinic/posixmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions Modules/posixmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -16971,6 +16971,25 @@ os__emscripten_debugger_impl(PyObject *module)
emscripten_debugger();
Py_RETURN_NONE;
}

EM_JS(void, emscripten_log_impl_js, (const char* arg), {
console.warn(UTF8ToString(arg));
});

/*[clinic input]
os._emscripten_log
arg: str

Log something to the JS console. Emscripten only.
[clinic start generated code]*/

static PyObject *
os__emscripten_log_impl(PyObject *module, const char *arg)
/*[clinic end generated code: output=9749e5e293c42784 input=350aa1f70bc1e905]*/
{
emscripten_log_impl_js(arg);
Py_RETURN_NONE;
}
#endif /* __EMSCRIPTEN__ */


Expand Down Expand Up @@ -17190,6 +17209,7 @@ static PyMethodDef posix_methods[] = {
OS__IS_INPUTHOOK_INSTALLED_METHODDEF
OS__CREATE_ENVIRON_METHODDEF
OS__EMSCRIPTEN_DEBUGGER_METHODDEF
OS__EMSCRIPTEN_LOG_METHODDEF
{NULL, NULL} /* Sentinel */
};

Expand Down
Loading
Loading