Skip to content

Commit 4e440bd

Browse files
authored
Build with wasm exception handling (#81)
For now, we need to use a custom rust toolchain for this. Apparently all alternatives to this don't work. -Zbuild-std doesn't work with panic=abort (rust-lang/cargo#7359) and my attempts to use a custom sysroot with either https://github.com/RalfJung/rustc-build-sysroot/ or https://github.com/DianaNites/cargo-sysroot/ seem to hit the same problem as with `-Zbuild-std`. Thus, I think the only reasonable way to go is to build the sysroot from the rust source directory. This is done by https://github.com/pyodide/rust-emscripten-wasm-eh-sysroot/
1 parent 75c0e51 commit 4e440bd

File tree

6 files changed

+61
-15
lines changed

6 files changed

+61
-15
lines changed

pyodide_build/common.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,9 @@ def to_bool(value: str) -> bool:
451451
return value.lower() not in {"", "0", "false", "no", "off"}
452452

453453

454-
def download_and_unpack_archive(url: str, path: Path, descr: str) -> None:
454+
def download_and_unpack_archive(
455+
url: str, path: Path, descr: str, *, exists_ok: bool = False
456+
) -> None:
455457
"""
456458
Download the cross-build environment from the given URL and extract it to the given path.
457459
@@ -465,7 +467,7 @@ def download_and_unpack_archive(url: str, path: Path, descr: str) -> None:
465467
"""
466468
logger.info("Downloading %s from %s", descr, url)
467469

468-
if path.exists():
470+
if not exists_ok and path.exists():
469471
raise FileExistsError(f"Path {path} already exists")
470472

471473
try:

pyodide_build/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ def _get_make_environment_vars(self) -> Mapping[str, str]:
179179
"cpythoninstall": "CPYTHONINSTALL",
180180
"rustflags": "RUSTFLAGS",
181181
"rust_toolchain": "RUST_TOOLCHAIN",
182+
"rust_emscripten_target_url": "RUST_EMSCRIPTEN_TARGET_URL",
182183
"cflags": "SIDE_MODULE_CFLAGS",
183184
"cxxflags": "SIDE_MODULE_CXXFLAGS",
184185
"ldflags": "SIDE_MODULE_LDFLAGS",
@@ -210,6 +211,7 @@ def _get_make_environment_vars(self) -> Mapping[str, str]:
210211
"cxxflags",
211212
"ldflags",
212213
"rust_toolchain",
214+
"rust_emscripten_target_url",
213215
"meson_cross_file",
214216
"skip_emscripten_version_check",
215217
"build_dependency_index_url",
@@ -229,6 +231,7 @@ def _get_make_environment_vars(self) -> Mapping[str, str]:
229231
"cargo_build_target": "wasm32-unknown-emscripten",
230232
"cargo_target_wasm32_unknown_emscripten_linker": "emcc",
231233
"rust_toolchain": "nightly-2025-02-01",
234+
"rust_emscripten_target_url": "",
232235
# Other configuration
233236
"pyodide_jobs": "1",
234237
"skip_emscripten_version_check": "0",

pyodide_build/pypabuild.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ def get_build_env(
295295
args["orig__name__"] = __name__
296296
args["pythoninclude"] = get_build_flag("PYTHONINCLUDE")
297297
args["PATH"] = env["PATH"]
298+
args["abi"] = get_build_flag("PYODIDE_ABI_VERSION")
298299

299300
pywasmcross_env = json.dumps(args)
300301
# Store into environment variable and to disk. In most cases we will

pyodide_build/pywasmcross.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ class CrossCompileArgs(NamedTuple):
7777
target_install_dir: str = "" # The path to the target Python installation
7878
pythoninclude: str = "" # path to the cross-compiled Python include directory
7979
exports: Literal["whole_archive", "requested", "pyinit"] | list[str] = "pyinit"
80+
# Pyodide abi, e.g., 2025_0
81+
# Sometimes we have to inject compile flags only for certain abis.
82+
abi: str = ""
8083

8184

8285
def is_link_cmd(line: list[str]) -> bool:
@@ -93,7 +96,7 @@ def is_link_cmd(line: list[str]) -> bool:
9396
return False
9497

9598

96-
def replay_genargs_handle_dashl(arg: str, used_libs: set[str]) -> str | None:
99+
def replay_genargs_handle_dashl(arg: str, used_libs: set[str], abi: str) -> str | None:
97100
"""
98101
Figure out how to replace a `-lsomelib` argument.
99102
@@ -118,6 +121,13 @@ def replay_genargs_handle_dashl(arg: str, used_libs: set[str]) -> str | None:
118121
if arg == "-lgfortran":
119122
return None
120123

124+
# Some Emscripten libraries that use setjmp/longjmp.
125+
# The Emscripten linker should automatically know to use these variants so
126+
# this shouldn't be necessary.
127+
# This suffix will need to change soon to `-legacysjlj`.
128+
if abi > "2025" and arg in ["-lfreetype", "-lpng"]:
129+
arg += "-wasm-sjlj"
130+
121131
# WASM link doesn't like libraries being included twice
122132
# skip second one
123133
if arg in used_libs:
@@ -555,7 +565,7 @@ def handle_command_generate_args( # noqa: C901
555565
continue
556566

557567
if arg.startswith("-l"):
558-
result = replay_genargs_handle_dashl(arg, used_libs)
568+
result = replay_genargs_handle_dashl(arg, used_libs, build_args.abi)
559569
elif arg.startswith("-I"):
560570
result = replay_genargs_handle_dashI(arg, build_args.target_install_dir)
561571
elif arg.startswith("-Wl"):
@@ -638,6 +648,7 @@ def compiler_main():
638648
target_install_dir=PYWASMCROSS_ARGS["target_install_dir"],
639649
pythoninclude=PYWASMCROSS_ARGS["pythoninclude"],
640650
exports=PYWASMCROSS_ARGS["exports"],
651+
abi=PYWASMCROSS_ARGS["abi"],
641652
)
642653
basename = Path(sys.argv[0]).name
643654
args = list(sys.argv)

pyodide_build/recipe/graph_builder.py

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from pyodide_build import build_env
3131
from pyodide_build.build_env import BuildArgs
3232
from pyodide_build.common import (
33+
download_and_unpack_archive,
3334
exit_with_stdio,
3435
extract_wheel_metadata_file,
3536
find_matching_wheels,
@@ -700,26 +701,54 @@ def run(self, n_jobs: int, already_built: set[str]) -> None:
700701
self.build_queue.put((job_priority(dependent), dependent))
701702

702703

704+
def _run(cmd, *args, check=False, **kwargs):
705+
result = subprocess.run(cmd, *args, **kwargs, check=check)
706+
if result.returncode != 0:
707+
logger.error("ERROR: command failed %s", " ".join(cmd))
708+
exit_with_stdio(result)
709+
return result
710+
711+
703712
def _ensure_rust_toolchain():
704713
rust_toolchain = build_env.get_build_flag("RUST_TOOLCHAIN")
705-
result = subprocess.run(
706-
["rustup", "toolchain", "install", rust_toolchain], check=False
707-
)
708-
if result.returncode == 0:
709-
result = subprocess.run(
714+
_run(["rustup", "toolchain", "install", rust_toolchain])
715+
_run(["rustup", "default", rust_toolchain])
716+
717+
url = build_env.get_build_flag("RUST_EMSCRIPTEN_TARGET_URL")
718+
if not url:
719+
# Install target with rustup target add
720+
_run(
710721
[
711722
"rustup",
712723
"target",
713724
"add",
714725
"wasm32-unknown-emscripten",
715726
"--toolchain",
716727
rust_toolchain,
717-
],
718-
check=False,
728+
]
719729
)
720-
if result.returncode != 0:
721-
logger.error("ERROR: rustup toolchain install failed")
722-
exit_with_stdio(result)
730+
return
731+
732+
# Now we are going to delete the normal wasm32-unknown-emscripten sysroot
733+
# and replace it with our wasm-eh version.
734+
# We place the "install_token" to indicate that our custom sysroot has been
735+
# installed and which URL we got it from.
736+
result = _run(
737+
["rustup", "which", "--toolchain", rust_toolchain, "rustc"],
738+
capture_output=True,
739+
text=True,
740+
)
741+
742+
toolchain_root = Path(result.stdout).parents[1]
743+
rustlib = toolchain_root / "lib/rustlib"
744+
install_token = rustlib / "wasm32-unknown-emscripten_install-url.txt"
745+
if install_token.exists() and install_token.read_text() == url:
746+
return
747+
shutil.rmtree(rustlib / "wasm32-unknown-emscripten", ignore_errors=True)
748+
download_and_unpack_archive(
749+
url, rustlib, "wasm32-unknown-emscripten target", exists_ok=True
750+
)
751+
install_token.write_text(url)
723752

724753

725754
def build_from_graph(

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ lint.select = [
133133

134134
lint.logger-objects = ["pyodide_build.logger.logger"]
135135

136-
lint.ignore = ["E402", "E501", "E731", "E741", "PERF401", "PLW2901", "UP038"]
136+
lint.ignore = ["E402", "E501", "E731", "E741", "PERF401", "PLW2901", "UP038", "PLR0912"]
137137
# line-length = 219 # E501: Recommended goal is 88 to match black
138138
target-version = "py312"
139139

0 commit comments

Comments
 (0)