Skip to content
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
214 changes: 170 additions & 44 deletions Tools/wasm/emscripten/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import contextlib
import functools
import hashlib
import json
import os
import shutil
import subprocess
Expand All @@ -14,6 +15,8 @@
from textwrap import dedent
from urllib.request import urlopen

import tomllib

try:
from os import process_cpu_count as cpu_count
except ImportError:
Expand All @@ -22,48 +25,62 @@

EMSCRIPTEN_DIR = Path(__file__).parent
CHECKOUT = EMSCRIPTEN_DIR.parent.parent.parent
EMSCRIPTEN_VERSION_FILE = EMSCRIPTEN_DIR / "emscripten_version.txt"
CONFIG_FILE = EMSCRIPTEN_DIR / "config.toml"

DEFAULT_CROSS_BUILD_DIR = CHECKOUT / "cross-build"
HOST_TRIPLE = "wasm32-emscripten"


def get_build_paths(cross_build_dir=None):
@functools.cache
def load_config_toml():
with CONFIG_FILE.open("rb") as file:
return tomllib.load(file)


@functools.cache
def required_emscripten_version():
return load_config_toml()["emscripten-version"]


@functools.cache
def emsdk_cache_root(emsdk_cache):
required_version = required_emscripten_version()
return Path(emsdk_cache).absolute() / required_version


@functools.cache
def emsdk_activate_path(emsdk_cache):
return emsdk_cache_root(emsdk_cache) / "emsdk/emsdk_env.sh"


def get_build_paths(cross_build_dir=None, emsdk_cache=None):
"""Compute all build paths from the given cross-build directory."""
if cross_build_dir is None:
cross_build_dir = DEFAULT_CROSS_BUILD_DIR
cross_build_dir = Path(cross_build_dir).absolute()
host_triple_dir = cross_build_dir / HOST_TRIPLE
prefix_dir = host_triple_dir / "prefix"
if emsdk_cache:
prefix_dir = emsdk_cache_root(emsdk_cache) / "prefix"

return {
"cross_build_dir": cross_build_dir,
"native_build_dir": cross_build_dir / "build",
"host_triple_dir": host_triple_dir,
"host_build_dir": host_triple_dir / "build",
"host_dir": host_triple_dir / "build" / "python",
Comment on lines 70 to 71
Copy link
Contributor

Choose a reason for hiding this comment

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

This is the only part that I'd like to clean up - dropping the build part of the path. With this is place, we end up with cross-build/wasm32-emscripten/build/python and cross-build/wasm32-emscripten/build/mpdec etc - but nothing else at the same level as build.

However, we can't remove that level without also changing the buildbot config, so we might want to include that as part of the "move to Platforms" change.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Does the buildbot config hard code build as part of the path somewhere? I suppose when it goes to run tests?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes - it's hardcoded in the buildbot config.

One option to consider would be to add a python3 Tools/wasm/emscripten test target (and/or python3 Tools/wasm/emscripten run target). That would break the dependency on the internals of the build location at least.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let's add python3 Tools/wasm/emscripten run.

"prefix_dir": host_triple_dir / "prefix",
"prefix_dir": prefix_dir,
}


LOCAL_SETUP = CHECKOUT / "Modules" / "Setup.local"
LOCAL_SETUP_MARKER = b"# Generated by Tools/wasm/emscripten.py\n"


@functools.cache
def get_required_emscripten_version():
"""Read the required emscripten version from emscripten_version.txt."""
return EMSCRIPTEN_VERSION_FILE.read_text().strip()


@functools.cache
def get_emsdk_activate_path(emsdk_cache):
required_version = get_required_emscripten_version()
return Path(emsdk_cache) / required_version / "emsdk_env.sh"


def validate_emsdk_version(emsdk_cache):
"""Validate that the emsdk cache contains the required emscripten version."""
required_version = get_required_emscripten_version()
emsdk_env = get_emsdk_activate_path(emsdk_cache)
required_version = required_emscripten_version()
emsdk_env = emsdk_activate_path(emsdk_cache)
if not emsdk_env.is_file():
print(
f"Required emscripten version {required_version} not found in {emsdk_cache}",
Expand All @@ -90,7 +107,7 @@ def get_emsdk_environ(emsdk_cache):
[
"bash",
"-c",
f"EMSDK_QUIET=1 source {get_emsdk_activate_path(emsdk_cache)} && env",
f"EMSDK_QUIET=1 source {emsdk_activate_path(emsdk_cache)} && env",
],
text=True,
)
Expand Down Expand Up @@ -207,6 +224,35 @@ def build_python_path(context):
return binary


def install_emscripten(context):
emsdk_cache = context.emsdk_cache
if emsdk_cache is None:
print("install-emscripten requires --emsdk-cache", file=sys.stderr)
sys.exit(1)
version = required_emscripten_version()
emsdk_target = emsdk_cache_root(emsdk_cache) / "emsdk"
if emsdk_target.exists():
if not context.quiet:
print(f"Emscripten version {version} already installed")
return
if not context.quiet:
print(f"Installing emscripten version {version}")
emsdk_target.mkdir(parents=True)
call(
[
"git",
"clone",
"https://github.com/emscripten-core/emsdk.git",
emsdk_target,
],
quiet=context.quiet,
)
call([emsdk_target / "emsdk", "install", version], quiet=context.quiet)
call([emsdk_target / "emsdk", "activate", version], quiet=context.quiet)
if not context.quiet:
print(f"Installed emscripten version {version}")


@subdir("native_build_dir", clean_ok=True)
def configure_build_python(context, working_dir):
"""Configure the build/host Python."""
Expand Down Expand Up @@ -258,43 +304,95 @@ def download_and_unpack(working_dir: Path, url: str, expected_shasum: str):
shutil.unpack_archive(tmp_file.name, working_dir)


def should_build_library(prefix, name, config, quiet):
cached_config = prefix / (name + ".json")
if not cached_config.exists():
if not quiet:
print(
f"No cached build of {name} version {config['version']} found, building"
)
return True

try:
with cached_config.open("rb") as f:
cached_config = json.load(f)
except json.JSONDecodeError:
if not quiet:
print(f"Cached data for {name} invalid, rebuilding")
return True
if config == cached_config:
if not quiet:
print(
f"Found cached build of {name} version {config['version']}, not rebuilding"
)
return False

if not quiet:
print(
f"Found cached build of {name} version {config['version']} but it's out of date, rebuilding"
)
return True


def write_library_config(prefix, name, config, quiet):
cached_config = prefix / (name + ".json")
with cached_config.open("w") as f:
json.dump(config, f)
if not quiet:
print(f"Succeded building {name}, wrote config to {cached_config}")


@subdir("host_build_dir", clean_ok=True)
def make_emscripten_libffi(context, working_dir):
ver = "3.4.6"
libffi_dir = working_dir / f"libffi-{ver}"
prefix = context.build_paths["prefix_dir"]
libffi_config = load_config_toml()["libffi"]
if not should_build_library(
prefix, "libffi", libffi_config, context.quiet
):
return
url = libffi_config["url"]
version = libffi_config["version"]
shasum = libffi_config["shasum"]
libffi_dir = working_dir / f"libffi-{version}"
shutil.rmtree(libffi_dir, ignore_errors=True)
download_and_unpack(
working_dir,
f"https://github.com/libffi/libffi/releases/download/v{ver}/libffi-{ver}.tar.gz",
"b0dea9df23c863a7a50e825440f3ebffabd65df1497108e5d437747843895a4e",
url.format(version=version),
shasum,
)
call(
[EMSCRIPTEN_DIR / "make_libffi.sh"],
env=updated_env(
{"PREFIX": context.build_paths["prefix_dir"]}, context.emsdk_cache
),
env=updated_env({"PREFIX": prefix}, context.emsdk_cache),
cwd=libffi_dir,
quiet=context.quiet,
)
write_library_config(prefix, "libffi", libffi_config, context.quiet)


@subdir("host_build_dir", clean_ok=True)
def make_mpdec(context, working_dir):
ver = "4.0.1"
mpdec_dir = working_dir / f"mpdecimal-{ver}"
prefix = context.build_paths["prefix_dir"]
mpdec_config = load_config_toml()["mpdec"]
if not should_build_library(prefix, "mpdec", mpdec_config, context.quiet):
return

url = mpdec_config["url"]
version = mpdec_config["version"]
shasum = mpdec_config["shasum"]
mpdec_dir = working_dir / f"mpdecimal-{version}"
shutil.rmtree(mpdec_dir, ignore_errors=True)
download_and_unpack(
working_dir,
f"https://www.bytereef.org/software/mpdecimal/releases/mpdecimal-{ver}.tar.gz",
"96d33abb4bb0070c7be0fed4246cd38416188325f820468214471938545b1ac8",
url.format(version=version),
shasum,
)
call(
[
"emconfigure",
mpdec_dir / "configure",
"CFLAGS=-fPIC",
"--prefix",
context.build_paths["prefix_dir"],
prefix,
"--disable-shared",
],
cwd=mpdec_dir,
Expand All @@ -306,6 +404,7 @@ def make_mpdec(context, working_dir):
cwd=mpdec_dir,
quiet=context.quiet,
)
write_library_config(prefix, "mpdec", mpdec_config, context.quiet)


@subdir("host_dir", clean_ok=True)
Expand Down Expand Up @@ -436,16 +535,24 @@ def make_emscripten_python(context, working_dir):
subprocess.check_call([exec_script, "--version"])


def build_all(context):
"""Build everything."""
steps = [
configure_build_python,
make_build_python,
make_emscripten_libffi,
make_mpdec,
configure_emscripten_python,
make_emscripten_python,
]
def build_target(context):
"""Build one or more targets."""
steps = []
if context.target in {"all"}:
steps.append(install_emscripten)
Comment on lines +541 to +542
Copy link
Contributor

Choose a reason for hiding this comment

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

Should install_emscripten really be added as a step for build? There is a subcommand for it already with: install-emscripten.

I've a CI pipeline with a pre-installed emsdk build (with a newer version than the currently pinned 4.0.12). So to build I now need to run two commands if I don't want to install a different emsdk version. This feels wrong, especially as I wouldn't expect a emsdk install for the build subcommand in the first place.

python3 Tools/wasm/emscripten build build
python3 Tools/wasm/emscripten build host

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess that argument makes sense.

I included it because conceptually, "build all" should be a "do everything that is needed to build", which may include getting EMSDK.

However, the usage in the CPython buildbot will likely be explicitly broken up into explicit build build, and build host calls; having an additional install_emscripten won't be a big overhead (and would make sense to have broken out as a separate command anyway for logging purposes).

On the other end - a local, manual build of build all --emsdk-cache ... won't need to download Emscripten on most builds - and if EMSDK isn't there, it's an easy manual invocation.

So - I guess it makes sense to drop this addition. @hoodmane: do you think we should slip this into #145806?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It also doesn't even work because validate_emsdk_version(context.emsdk_cache) is called if --emsdk-cache is set and will bail if it's not installed.

if context.target in {"build", "all"}:
steps.extend([
configure_build_python,
make_build_python,
])
if context.target in {"host", "all"}:
steps.extend([
make_emscripten_libffi,
make_mpdec,
configure_emscripten_python,
make_emscripten_python,
])

for step in steps:
step(context)

Expand Down Expand Up @@ -475,7 +582,22 @@ def main():

parser = argparse.ArgumentParser()
subcommands = parser.add_subparsers(dest="subcommand")
install_emscripten_cmd = subcommands.add_parser(
"install-emscripten",
help="Install the appropriate version of Emscripten",
)
build = subcommands.add_parser("build", help="Build everything")
build.add_argument(
"target",
nargs="?",
default="all",
choices=["all", "host", "build"],
help=(
"What should be built. 'build' for just the build platform, or "
"'host' for the host platform, or 'all' for both. Defaults to 'all'."
),
)

configure_build = subcommands.add_parser(
"configure-build-python", help="Run `configure` for the build Python"
)
Expand Down Expand Up @@ -512,6 +634,7 @@ def main():
)

for subcommand in (
install_emscripten_cmd,
build,
configure_build,
make_libffi_cmd,
Expand Down Expand Up @@ -568,22 +691,25 @@ def main():

context = parser.parse_args()

context.build_paths = get_build_paths(context.cross_build_dir)

if context.emsdk_cache:
if context.emsdk_cache and context.subcommand != "install-emscripten":
validate_emsdk_version(context.emsdk_cache)
context.emsdk_cache = Path(context.emsdk_cache).absolute()
else:
print("Build will use EMSDK from current environment.")

context.build_paths = get_build_paths(
context.cross_build_dir, context.emsdk_cache
)

dispatch = {
"install-emscripten": install_emscripten,
"make-libffi": make_emscripten_libffi,
"make-mpdec": make_mpdec,
"configure-build-python": configure_build_python,
"make-build-python": make_build_python,
"configure-host": configure_emscripten_python,
"make-host": make_emscripten_python,
"build": build_all,
"build": build_target,
"clean": clean_contents,
}

Expand Down
14 changes: 14 additions & 0 deletions Tools/wasm/emscripten/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Any data that can vary between Python versions is to be kept in this file.
# This allows for blanket copying of the Emscripten build code between supported
# Python versions.
emscripten-version = "4.0.12"

[libffi]
url = "https://github.com/libffi/libffi/releases/download/v{version}/libffi-{version}.tar.gz"
version = "3.4.6"
shasum = "b0dea9df23c863a7a50e825440f3ebffabd65df1497108e5d437747843895a4e"

[mpdec]
url = "https://www.bytereef.org/software/mpdecimal/releases/mpdecimal-{version}.tar.gz"
version = "4.0.1"
shasum = "96d33abb4bb0070c7be0fed4246cd38416188325f820468214471938545b1ac8"
1 change: 0 additions & 1 deletion Tools/wasm/emscripten/emscripten_version.txt

This file was deleted.

Loading