Skip to content

Commit 4a2f3d3

Browse files
authored
Support pyodide xbuildenv install-emscripten on Windows (#322)
* Support `pyodide xbuildenv install-emscripten` on Windows * test_install_emscripten - update tests * Add Windows basic integration test CI * windows-integration-test - test emcc compilation * windows-integration-test - add integration test trigger condition * windows-integration-test - bump `test` job node to 24 for consistency * build_env - document emsdk_env script usage for both platforms * windows-integration-test - use `pwsh` instead of `powershell` `powershell` is using powershell 5.1, instead of powershell 7.4 and 5.1 is using utf16 by default, while 7.4 is utf8 and utf8 is more unix compatible and doesn't conflict with emcc. * xbuildenv - use`GIT_DIR=.` when applying emscripten patches In `git apply` there's no `--no-index` option or any other to prevent `git` from crawling up from `emsdk/upstream/emscripten`to `emsdk` folder to find `.git` there and then fail to apply patch. Annoyingly, it doesn't produce errors and even doesn't produce any logs without `--verbose`. Providing `GIT_DIR=.` explicitly prevent `.git` from searching git repo and applies patch cleanly. * windows-integration-test - use Python 3.13 To test against the most recent pyodide - missed some patches not being applied, because they weren't present in pyodide 0.27.7 that's used for Python 3.12. * xbuildenv - sort patches before applying Since `Path.glob` doesn't guarantee consistent order.
1 parent 03e7e63 commit 4a2f3d3

5 files changed

Lines changed: 171 additions & 36 deletions

File tree

.github/workflows/main.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,53 @@ jobs:
6464
if-no-files-found: error
6565
include-hidden-files: true
6666

67+
windows-integration-test:
68+
runs-on: windows-latest
69+
needs: [check-integration-test-trigger]
70+
if: needs.check-integration-test-trigger.outputs.run-integration-test
71+
defaults:
72+
run:
73+
shell: pwsh
74+
steps:
75+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
76+
with:
77+
fetch-depth: 0
78+
79+
- name: Setup Python
80+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
81+
with:
82+
python-version: "3.13"
83+
84+
- name: Set up Node.js
85+
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
86+
with:
87+
node-version: "24"
88+
89+
- name: Install package
90+
run: |
91+
python -m pip install --upgrade pip
92+
pip install -e .
93+
94+
- name: Install xbuildenv and emscripten
95+
run: |
96+
pyodide xbuildenv install
97+
pyodide xbuildenv install-emscripten
98+
99+
- name: Test emcc
100+
run: |
101+
$EMSDK_DIR = pyodide config get emsdk_dir
102+
. "$EMSDK_DIR\emsdk_env.ps1"
103+
$code = @"
104+
#include <stdio.h>
105+
int main() {
106+
printf("emcc works\n");
107+
return 0;
108+
}
109+
"@
110+
$code | Out-File test.c
111+
emcc test.c -o test.js
112+
node test.js
113+
67114
check-integration-test-trigger:
68115
name: test-integration-test-trigger
69116
runs-on: ubuntu-latest

pyodide_build/build_env.py

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import functools
55
import os
66
import re
7+
import shutil
78
import subprocess
89
import sys
910
from collections.abc import Iterator, Sequence
@@ -14,7 +15,12 @@
1415
from packaging.tags import Tag, compatible_tags, cpython_tags
1516

1617
from pyodide_build import __version__
17-
from pyodide_build.common import default_xbuildenv_path, search_pyproject_toml, to_bool
18+
from pyodide_build.common import (
19+
IS_WIN,
20+
default_xbuildenv_path,
21+
search_pyproject_toml,
22+
to_bool,
23+
)
1824

1925
RUST_BUILD_PRELUDE = """
2026
rustup default ${RUST_TOOLCHAIN}
@@ -300,8 +306,11 @@ def emscripten_version() -> str:
300306

301307
def get_emscripten_version_info() -> str:
302308
"""Extracted for testing purposes."""
309+
emcc = shutil.which("emcc")
310+
if not emcc:
311+
raise FileNotFoundError
303312
return subprocess.run(
304-
["emcc", "-v"], capture_output=True, encoding="utf8", check=True
313+
[emcc, "-v"], capture_output=True, encoding="utf8", check=True
305314
).stderr
306315

307316

@@ -317,35 +326,51 @@ def get_emscripten_version_info() -> str:
317326

318327
def activate_emscripten_env(emsdk_dir: Path) -> dict[str, str]:
319328
"""
320-
Source emsdk_env.sh and return the resulting environment variables.
329+
Source the emsdk_env script (emsdk_env.sh on Unix, emsdk_env.bat on Windows)
330+
and return the resulting environment variables.
321331
322332
Parameters
323333
----------
324334
emsdk_dir
325-
Path to the emsdk directory containing emsdk_env.sh
335+
Path to the emsdk directory containing the emsdk_env script
326336
327337
Returns
328338
-------
329339
dict[str, str]
330-
Dictionary of environment variables set by emsdk_env.sh
340+
Dictionary of environment variables set by the emsdk_env script
331341
"""
332-
emsdk_env_script = emsdk_dir / "emsdk_env.sh"
342+
emsdk_env_script_filename = "emsdk_env.bat" if IS_WIN else "emsdk_env.sh"
343+
emsdk_env_script = emsdk_dir / emsdk_env_script_filename
333344
if not emsdk_env_script.exists():
334-
raise FileNotFoundError(f"emsdk_env.sh not found at {emsdk_env_script}")
335-
336-
# Source emsdk_env.sh and capture the resulting environment
337-
result = subprocess.run(
338-
["bash", "-c", f"source {emsdk_env_script} > /dev/null 2>&1 && env"],
339-
capture_output=True,
340-
encoding="utf8",
341-
check=True,
342-
)
345+
raise FileNotFoundError(
346+
f"{emsdk_env_script_filename} not found at {emsdk_env_script}"
347+
)
348+
349+
# Source the emsdk_env script and capture the resulting environment
350+
if IS_WIN:
351+
# Passing args as a string, as otherwise shell is misinterpreting the command with quotes,
352+
# resulting in no output.
353+
result = subprocess.run(
354+
f'cmd /c call "{emsdk_env_script}" > nul 2>&1 && set',
355+
capture_output=True,
356+
encoding="utf8",
357+
check=True,
358+
)
359+
else:
360+
result = subprocess.run(
361+
["bash", "-c", f'source "{emsdk_env_script}" > /dev/null 2>&1 && env'],
362+
capture_output=True,
363+
encoding="utf8",
364+
check=True,
365+
)
343366

344367
# Parse the environment variables from output
345368
env_vars: dict[str, str] = {}
346369
for line in result.stdout.splitlines():
347370
if "=" in line:
348371
key, _, value = line.partition("=")
372+
# On Windows it's 'Path'.
373+
key = key.upper()
349374
if key in EMSDK_ENV_VARS:
350375
env_vars[key] = value
351376

pyodide_build/cli/xbuildenv.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import click
44

55
from pyodide_build.build_env import get_build_flag, local_versions
6-
from pyodide_build.common import default_xbuildenv_path
6+
from pyodide_build.common import IS_WIN, default_xbuildenv_path
77
from pyodide_build.views import MetadataView
88
from pyodide_build.xbuildenv import CrossBuildEnvManager
99
from pyodide_build.xbuildenv_releases import (
@@ -295,4 +295,8 @@ def _install_emscripten(
295295
emsdk_dir = manager.install_emscripten(version, force=force)
296296

297297
print("Installing emsdk complete.")
298-
print(f"Use `source {emsdk_dir}/emsdk_env.sh` to set up the environment.")
298+
if IS_WIN:
299+
cmd = f"{emsdk_dir}/emsdk_env.bat"
300+
else:
301+
cmd = f"source {emsdk_dir}/emsdk_env.sh"
302+
print(f"Use `{cmd}` to set up the environment.")

0 commit comments

Comments
 (0)