Skip to content
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

Build with wasm exception handling #81

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
30cbe32
Build with wasm exception handling
hoodmane Jan 13, 2025
a02dda7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 13, 2025
3186d20
Fix
hoodmane Jan 13, 2025
8256e72
Fix again
hoodmane Jan 13, 2025
5e46696
Only download rust toolchain once
hoodmane Jan 13, 2025
adda8a5
Fix
hoodmane Jan 13, 2025
b33d1c9
Always run rustup toolchain link
hoodmane Jan 14, 2025
f2d72e7
Add wasm-sjlj suffix to wasm-eh libs
hoodmane Jan 14, 2025
3c3dc79
Update Emscripten toolchain version
hoodmane Jan 14, 2025
30517bd
Merge branch 'main' into rust-wasm-eh
hoodmane Jan 15, 2025
0c88c0f
Merge branch 'main' into rust-wasm-eh
hoodmane Jan 15, 2025
663e78e
Remove duplication of toolchain name
hoodmane Jan 15, 2025
529a583
Pass recipe ldflags through library rewriting logic as well
hoodmane Jan 15, 2025
a63c7b9
Add ignore for too many branches lint
hoodmane Jan 15, 2025
b50d1c1
Only install Emscripten target from url
hoodmane Jan 15, 2025
3b1734b
Fix syntax error
hoodmane Jan 15, 2025
c93aab1
Move target url to config variable
hoodmane Jan 15, 2025
036626d
Set default rust_toolchain to nightly-2025-01-15
hoodmane Jan 15, 2025
7bbdde2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 15, 2025
114e477
Only change pywasmcross behavior for new abi
hoodmane Jan 15, 2025
330e762
Merge branch 'main' into rust-wasm-eh
hoodmane Jan 27, 2025
6a49a51
Merge branch 'main' into rust-wasm-eh
hoodmane Jan 30, 2025
194c463
Don't append None to new_args
hoodmane Jan 31, 2025
6fa5b9b
Merge branch 'main' into rust-wasm-eh
hoodmane Feb 23, 2025
4cb167e
Merge branch 'main' into rust-wasm-eh
hoodmane Feb 24, 2025
dc1c6a2
Merge branch 'main' into rust-wasm-eh
hoodmane Feb 26, 2025
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
6 changes: 4 additions & 2 deletions pyodide_build/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,9 @@ def to_bool(value: str) -> bool:
return value.lower() not in {"", "0", "false", "no", "off"}


def download_and_unpack_archive(url: str, path: Path, descr: str) -> None:
def download_and_unpack_archive(
url: str, path: Path, descr: str, *, exists_ok: bool = False
) -> None:
"""
Download the cross-build environment from the given URL and extract it to the given path.

Expand All @@ -465,7 +467,7 @@ def download_and_unpack_archive(url: str, path: Path, descr: str) -> None:
"""
logger.info("Downloading %s from %s", descr, url)

if path.exists():
if not exists_ok and path.exists():
raise FileExistsError(f"Path {path} already exists")

try:
Expand Down
5 changes: 4 additions & 1 deletion pyodide_build/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ def _get_make_environment_vars(self) -> Mapping[str, str]:
"cpythoninstall": "CPYTHONINSTALL",
"rustflags": "RUSTFLAGS",
"rust_toolchain": "RUST_TOOLCHAIN",
"rust_emscripten_target_url": "RUST_EMSCRIPTEN_TARGET_URL",
"cflags": "SIDE_MODULE_CFLAGS",
"cxxflags": "SIDE_MODULE_CXXFLAGS",
"ldflags": "SIDE_MODULE_LDFLAGS",
Expand Down Expand Up @@ -210,6 +211,7 @@ def _get_make_environment_vars(self) -> Mapping[str, str]:
"cxxflags",
"ldflags",
"rust_toolchain",
"rust_emscripten_target_url",
"meson_cross_file",
"skip_emscripten_version_check",
"build_dependency_index_url",
Expand All @@ -228,7 +230,8 @@ def _get_make_environment_vars(self) -> Mapping[str, str]:
"rustflags": "-C link-arg=-sSIDE_MODULE=2 -C link-arg=-sWASM_BIGINT -Z link-native-libraries=no",
"cargo_build_target": "wasm32-unknown-emscripten",
"cargo_target_wasm32_unknown_emscripten_linker": "emcc",
"rust_toolchain": "nightly-2024-01-29",
"rust_toolchain": "nightly-2025-01-15",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's mention the default rust toolchain update in the changelog.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change isn't really needed to make this work. Maybe I can revert it here and put it by itself in a separate pr as a follow-up.

"rust_emscripten_target_url": "",
# Other configuration
"pyodide_jobs": "1",
"skip_emscripten_version_check": "0",
Expand Down
1 change: 1 addition & 0 deletions pyodide_build/pypabuild.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ def get_build_env(
args["orig__name__"] = __name__
args["pythoninclude"] = get_build_flag("PYTHONINCLUDE")
args["PATH"] = env["PATH"]
args["abi"] = get_build_flag("PYODIDE_ABI_VERSION")

pywasmcross_env = json.dumps(args)
# Store into environment variable and to disk. In most cases we will
Expand Down
15 changes: 12 additions & 3 deletions pyodide_build/pywasmcross.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class CrossCompileArgs(NamedTuple):
target_install_dir: str = "" # The path to the target Python installation
pythoninclude: str = "" # path to the cross-compiled Python include directory
exports: Literal["whole_archive", "requested", "pyinit"] | list[str] = "pyinit"
abi: str = ""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a comment about this parameter.



def is_link_cmd(line: list[str]) -> bool:
Expand All @@ -93,7 +94,7 @@ def is_link_cmd(line: list[str]) -> bool:
return False


def replay_genargs_handle_dashl(arg: str, used_libs: set[str]) -> str | None:
def replay_genargs_handle_dashl(arg: str, used_libs: set[str], abi: str) -> str | None:
"""
Figure out how to replace a `-lsomelib` argument.

Expand All @@ -118,6 +119,9 @@ def replay_genargs_handle_dashl(arg: str, used_libs: set[str]) -> str | None:
if arg == "-lgfortran":
return None

if abi > "2025" and arg in ["-lfreetype", "-lpng"]:
arg += "-wasm-sjlj"
Comment on lines +122 to +123
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a comment about the context of this if condition.


# WASM link doesn't like libraries being included twice
# skip second one
if arg in used_libs:
Expand Down Expand Up @@ -555,7 +559,7 @@ def handle_command_generate_args( # noqa: C901
continue

if arg.startswith("-l"):
result = replay_genargs_handle_dashl(arg, used_libs)
result = replay_genargs_handle_dashl(arg, used_libs, build_args.abi)
elif arg.startswith("-I"):
result = replay_genargs_handle_dashI(arg, build_args.target_install_dir)
elif arg.startswith("-Wl"):
Expand All @@ -578,7 +582,11 @@ def handle_command_generate_args( # noqa: C901
# Better to fail at compile or link time.
if is_link_cmd(line):
new_args.append("-Wl,--fatal-warnings")
new_args.extend(build_args.ldflags.split())
for arg in build_args.ldflags.split():
if arg.startswith("-l"):
arg = replay_genargs_handle_dashl(arg, used_libs, build_args.abi)
Comment on lines +586 to +587
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a strong opinion, but as build_args.ldflags is passed by the user, I think the user is responsible for setting the proper flags.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have mixed feelings. I guess we can't fix static/dynamic libs not built with the python build system anyways so it's not that valuable. But it's pretty weird and annoying that linking -lpng successfully links but will break with opaque errors at runtime and the fix is to link with -lpng-wasm-sjlj. Maybe we can add some logic to audit wheel or somewhere appropriate that detects files linked against wrong libraries and explains as best we can what went wrong. Or people can just ask.

So yeah probably it makes sense to remove this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can add some logic to audit wheel or somewhere appropriate that detects files linked against wrong libraries and explains as best we can what went wrong.

Yeah, I think the problem is that we are not building libraries like libpng or libfreetype ourselves and use flags like -s USE_LIBPNG=1 instead. So, there exist multiple variants of those libraries, and it isn't easy to know what to use without looking into the emscripten code.

Ideally, I think it is better to drop all emscripten ports and build libraries ourselves. I think this would fix the issue with the Emscripten version and library version being fixed, and it would also fix the variant issues like the wasm exception.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, I noticed that the library suffices are changed in the recent version of Emscripten: https://github.com/emscripten-core/emscripten/blob/242af002f8dee0366e86002fec187d447dcd26e5/tools/ports/libpng.py#L12-L16

if arg:
new_args.append(arg)
new_args.extend(get_export_flags(line, build_args.exports))

if "-c" in line:
Expand Down Expand Up @@ -638,6 +646,7 @@ def compiler_main():
target_install_dir=PYWASMCROSS_ARGS["target_install_dir"],
pythoninclude=PYWASMCROSS_ARGS["pythoninclude"],
exports=PYWASMCROSS_ARGS["exports"],
abi=PYWASMCROSS_ARGS["abi"],
)
basename = Path(sys.argv[0]).name
args = list(sys.argv)
Expand Down
45 changes: 35 additions & 10 deletions pyodide_build/recipe/graph_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from pyodide_build import build_env
from pyodide_build.build_env import BuildArgs
from pyodide_build.common import (
download_and_unpack_archive,
exit_with_stdio,
extract_wheel_metadata_file,
find_matching_wheels,
Expand Down Expand Up @@ -700,26 +701,50 @@ def run(self, n_jobs: int, already_built: set[str]) -> None:
self.build_queue.put((job_priority(dependent), dependent))


def _run(cmd, *args, check=False, **kwargs):
result = subprocess.run(cmd, *args, **kwargs, check=check)
if result.returncode != 0:
logger.error("ERROR: command failed %s", " ".join(cmd))
exit_with_stdio(result)
return result


def _ensure_rust_toolchain():
rust_toolchain = build_env.get_build_flag("RUST_TOOLCHAIN")
result = subprocess.run(
["rustup", "toolchain", "install", rust_toolchain], check=False
)
if result.returncode == 0:
result = subprocess.run(
_run(["rustup", "toolchain", "install", rust_toolchain])
_run(["rustup", "default", rust_toolchain])

url = build_env.get_build_flag("RUST_EMSCRIPTEN_TARGET_URL")
if not url:
# Install target with rustup target add
_run(
[
"rustup",
"target",
"add",
"wasm32-unknown-emscripten",
"--toolchain",
rust_toolchain,
],
check=False,
]
)
if result.returncode != 0:
logger.error("ERROR: rustup toolchain install failed")
exit_with_stdio(result)
return

# Install target from url
result = _run(
["rustup", "which", "--toolchain", rust_toolchain, "rustc"],
capture_output=True,
text=True,
)
toolchain_root = Path(result.stdout).parents[1]
rustlib = toolchain_root / "lib/rustlib"
install_token = rustlib / "wasm32-unknown-emscripten_install-url.txt"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this file something you added manually in your custom toolchain? If so, let's add a comment about it.

if install_token.exists() and install_token.read_text() == url:
return
shutil.rmtree(rustlib / "wasm32-unknown-emscripten", ignore_errors=True)
download_and_unpack_archive(
url, rustlib, "wasm32-unknown-emscripten target", exists_ok=True
)
install_token.write_text(url)


def build_from_graph(
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ lint.select = [

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

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

Expand Down