| __lazy_modules__ = [ | |
| "datetime", | |
| "os", | |
| "pathlib", | |
| "shutil", | |
| "subprocess", | |
| "sys", | |
| "_shared", | |
| ] | |
| import datetime | |
| import os | |
| import pathlib | |
| import shutil | |
| import subprocess | |
| import sys | |
| import _shared | |
| def wasmtime_script(context): | |
| return f"""\ | |
| #!/bin/sh | |
| set -eu | |
| script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd -P) | |
| root=$(CDPATH= cd "$script_dir/.." && pwd -P) | |
| wasm_file="$root/lib/python{python_version(context)}/lib-wasm/python{python_version(context, debug_ok=True)}.wasm" | |
| exec wasmtime run \\ | |
| --argv0 "$wasm_file" \\ | |
| --config "$root/etc/python{python_version(context)}/wasmtime.toml" \\ | |
| --dir "/" \\ | |
| "$wasm_file" "$@" | |
| """ | |
| def python_version(context, debug_ok=False): | |
| """Calculate the M.N part of Python's version. | |
| If *debug_ok* is True, then "d" is appended as appropriate. | |
| """ | |
| details = context.wasi_build_details | |
| major = details["language"]["version_info"]["major"] | |
| minor = details["language"]["version_info"]["minor"] | |
| version = f"{major}.{minor}" | |
| if debug_ok and context.is_debug: | |
| version += "d" | |
| return version | |
| def lib_python(context): | |
| return pathlib.PurePath("lib") / f"python{python_version(context)}" | |
| def license_file(context): | |
| """Have <src>/LICENSE end up as lib/pythonXY/LICENSE.txt.""" | |
| return (lib_python(context) / "LICENSE.txt", context.checkout / "LICENSE") | |
| def build_dir_files(context): | |
| """Have build/lib.* files end up in lib/pythonXY. | |
| Symlinks are skipped as those files are covered by handling | |
| <build>/Modules. | |
| """ | |
| return [ | |
| (lib_python(context) / path.name, path) | |
| for path in context.wasi_pybuilddir.iterdir() | |
| if path.is_file(follow_symlinks=False) | |
| ] | |
| def stdlib_files(context): | |
| """Have <src>/Lib files end up in lib/pythonXY.""" | |
| lib_dir = context.checkout / "Lib" | |
| lib_files = [] | |
| for root, dirs, files in lib_dir.walk(): | |
| try: | |
| dirs.remove("__pycache__") | |
| except ValueError: | |
| pass | |
| for file in files: | |
| file_path = pathlib.Path(root) / file | |
| details = ( | |
| lib_python(context) / file_path.relative_to(lib_dir), | |
| file_path, | |
| ) | |
| lib_files.append(details) | |
| return lib_files | |
| def pkgconfig_files(context): | |
| """Have <src>/Misc/python*.pc end up in lib/pkgconfig. | |
| Each file ends up being listed under `python3` and `python-3.N`. | |
| """ | |
| misc_dir = context.wasi_build_path / "Misc" | |
| pkgconfig = pathlib.PurePath("lib") / "pkgconfig" | |
| return [ | |
| ( | |
| pkgconfig / f"python-{python_version(context, debug_ok=True)}.pc", | |
| misc_dir / "python.pc", | |
| ), | |
| ( | |
| pkgconfig | |
| / f"python-{python_version(context, debug_ok=True)}-embed.pc", | |
| misc_dir / "python-embed.pc", | |
| ), | |
| ] | |
| def pkgconfig_symlinks(pkgconfig_files, context): | |
| assert len(pkgconfig_files) == 2 | |
| embed, plain = ( | |
| (0, 1) if pkgconfig_files[0].name.endswith("-embed.pc") else (1, 0) | |
| ) | |
| embed_path, plain_path = pkgconfig_files[embed], pkgconfig_files[plain] | |
| major = context.wasi_build_details["language"]["version_info"]["major"] | |
| paths = [ | |
| (embed_path.parent / f"python{major}-embed.pc", embed_path), | |
| (plain_path.parent / f"python{major}.pc", plain_path), | |
| ] | |
| if context.is_debug: | |
| paths += [ | |
| ( | |
| embed_path.parent | |
| / f"python-{python_version(context)}-embed.pc", | |
| embed_path, | |
| ), | |
| ( | |
| plain_path.parent / f"python-{python_version(context)}.pc", | |
| plain_path, | |
| ), | |
| ] | |
| return paths | |
| def pyconfig_file(context): | |
| """Have <build>/pyconfig.h end up in include/pythonXYd?/pyconfig.h.""" | |
| return ( | |
| pathlib.PurePath("include") | |
| / f"python{python_version(context, debug_ok=True)}" | |
| / "pyconfig.h", | |
| context.wasi_build_path / "pyconfig.h", | |
| ) | |
| def header_files(context): | |
| """Have <build>/Include/*.h end up in include/pythonXYd?/.""" | |
| include_dir = context.checkout / "Include" | |
| files = [] | |
| for root, dirs, filenames in include_dir.walk(): | |
| for filename in filenames: | |
| file_path = pathlib.Path(root) / filename | |
| details = ( | |
| pathlib.PurePath("include") | |
| / f"python{python_version(context, debug_ok=True)}" | |
| / file_path.relative_to(include_dir), | |
| file_path, | |
| ) | |
| files.append(details) | |
| return files | |
| def man_file(context): | |
| """Have Misc/python.man end up in share/man/man1/.""" | |
| man_dir = pathlib.PurePath("share", "man", "man1") | |
| man_file = context.checkout / "Misc" / "python.man" | |
| return ( | |
| man_dir / f"python{python_version(context)}.1", | |
| man_file, | |
| ) | |
| def man_symlink(man_path, context): | |
| """Symlink pythonN.M.1 to pythonN.1.""" | |
| major = context.wasi_build_details["language"]["version_info"]["major"] | |
| return (man_path.parent / f"python{major}.1", man_path) | |
| def wasmtime_config_file(context): | |
| """Have wasmtime.toml end up in etc/pythonXY/.""" | |
| config = context.checkout / "Platforms" / "WASI" / "wasmtime.toml" | |
| return ( | |
| pathlib.PurePath("etc") | |
| / f"python{python_version(context)}" | |
| / "wasmtime.toml", | |
| config, | |
| ) | |
| def wasm_file(context): | |
| """Have python.wasm end up at lib/pythonXY/lib-wasm/pythonX.Yd?.wasm.""" | |
| return ( | |
| lib_python(context) | |
| / "lib-wasm" | |
| / f"python{python_version(context, debug_ok=True)}.wasm", | |
| context.wasi_build_path / "python.wasm", | |
| ) | |
| def python_wasmtime_script(base, context): | |
| """Create bin/pythonN.Md?.wasmtime.""" | |
| script = wasmtime_script(context) | |
| path = ( | |
| base / f"bin/python{python_version(context, debug_ok=True)}.wasmtime" | |
| ) | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| with open(path, "w") as f: | |
| f.write(script) | |
| path.chmod(0o755) | |
| return path | |
| def python_wasmtime_symlink(path, context): | |
| """Symlink bin/pythonN.Md?.wasmtime. | |
| - bin/pythonN.M.wasmtime (if debug build) | |
| - bin/pythonN.wasmtime (if debug build) | |
| """ | |
| symlinks = [ | |
| path.parent / f"python{context.wasi_build_version['major']}.wasmtime" | |
| ] | |
| if context.is_debug: | |
| # The file already has the debug name, so the missing symlink is the non-debug name. | |
| symlinks.append( | |
| path.parent / f"python{python_version(context)}.wasmtime" | |
| ) | |
| return [(symlink, path) for symlink in symlinks] | |
| def config_file(context): | |
| """Have Misc/config.sh end up at bin/pythonN.Md?.config.""" | |
| return ( | |
| pathlib.PurePath("bin") | |
| / f"python{python_version(context, debug_ok=True)}-config", | |
| context.wasi_build_path / "Misc" / "python-config.sh", | |
| ) | |
| def config_symlink(config_path, context): | |
| """Symlink bin/pythonN.Md?-config. | |
| - bin/pythonN.M-config (if debug build) | |
| - bin/pythonNd?-config | |
| - bin/pythonN-config (if debug build) | |
| """ | |
| symlinks = [ | |
| config_path.parent | |
| / f"python{context.wasi_build_version['major']}-config" | |
| ] | |
| if context.is_debug: | |
| # The file already has the debug name, so the missing symlink is the non-debug name. | |
| symlinks.append( | |
| config_path.parent / f"python{python_version(context)}-config" | |
| ) | |
| return [(symlink, config_path) for symlink in symlinks] | |
| def copy_files(files, base): | |
| for dest, src in files: | |
| target = base / dest | |
| target.parent.mkdir(parents=True, exist_ok=True) | |
| src.copy(target) | |
| def symlink_files(files, base): | |
| for target, source in files: | |
| target_path = base / target | |
| source_path = base / source | |
| target_path.symlink_to( | |
| os.path.relpath(source_path, start=target_path.parent) | |
| ) | |
| def gather(context): | |
| py_version = python_version(context) | |
| py_d_version = python_version(context, debug_ok=True) | |
| dist = context.dist_path | |
| if dist.exists(): | |
| _shared.log("๐งน", f"Deleting {dist} ...") | |
| shutil.rmtree(dist) | |
| indent = " " | |
| base = context.archive_dir | |
| _shared.log("๐", f"Copying files to {base} ...") | |
| _shared.log("๐", "bin/", spacing=indent * 2) | |
| _shared.log("๐", f"python{py_d_version}-config", spacing=indent * 3) | |
| config_location = config_file(context) | |
| copy_files([config_location], base) | |
| symlink_files(config_symlink(config_location[0], context), base) | |
| _shared.log("๐", f"python{py_d_version}.wasmtime", spacing=indent * 3) | |
| script_path = python_wasmtime_script(base, context) | |
| symlink_files(python_wasmtime_symlink(script_path, context), base) | |
| _shared.log("๐", "etc/", spacing=indent * 2) | |
| _shared.log("๐", f"python{py_version}/", spacing=indent * 3) | |
| _shared.log("๐", "wasmtime.toml", spacing=indent * 4) | |
| copy_files([wasmtime_config_file(context)], base) | |
| _shared.log("๐", "include/", spacing=indent * 2) | |
| _shared.log("๐", f"python{py_version}/", spacing=indent * 3) | |
| _shared.log("๐", "pyconfig.h", spacing=indent * 4) | |
| copy_files([pyconfig_file(context)], base) | |
| _shared.log("๐", "**/*.h", spacing=indent * 4) | |
| copy_files(header_files(context), base) | |
| _shared.log("๐", "lib/", spacing=indent * 2) | |
| _shared.log("๐", f"python{py_version}/", spacing=indent * 3) | |
| _shared.log("๐", "lib-dynload/", spacing=indent * 4) | |
| (base / lib_python(context) / "lib-dynload").mkdir( | |
| parents=True, exist_ok=True | |
| ) | |
| _shared.log("๐", "<empty>", spacing=indent * 5) | |
| _shared.log("๐", "lib-wasm/", spacing=indent * 4) | |
| _shared.log("๐", f"python{py_d_version}.wasm", spacing=indent * 5) | |
| copy_files([wasm_file(context)], base) | |
| _shared.log("๐", "LICENSE.txt", spacing=indent * 4) | |
| copy_files([license_file(context)], base) | |
| _shared.log("๐", "files in `cat pybuilddir.txt`", spacing=indent * 4) | |
| copy_files(build_dir_files(context), base) | |
| _shared.log("๐", "**/*.py", spacing=indent * 4) | |
| copy_files(stdlib_files(context), base) | |
| _shared.log("๐", "pkgconfig/", spacing=indent * 3) | |
| _shared.log("๐", "python*.pc", spacing=indent * 4) | |
| pkgconfig_paths = pkgconfig_files(context) | |
| copy_files(pkgconfig_paths, base) | |
| symlink_files( | |
| pkgconfig_symlinks([path for path, _ in pkgconfig_paths], context), | |
| base, | |
| ) | |
| _shared.log("๐", "share/", spacing=indent * 2) | |
| _shared.log("๐", "man/", spacing=indent * 3) | |
| _shared.log("๐", "man1/", spacing=indent * 4) | |
| _shared.log("๐", "python*.1", spacing=indent * 5) | |
| man_path = man_file(context) | |
| copy_files([man_path], base) | |
| symlink_files([man_symlink(man_path[0], context)], base) | |
| return base | |
| def archive(context): | |
| file_name = f"{context.archive_stem}.tar.xz" | |
| file_path = context.dist_path / file_name | |
| if file_path.exists(): | |
| _shared.log("๐งน", f"Deleting {file_path} ...") | |
| file_path.unlink() | |
| to_compress = context.archive_dir | |
| _shared.log("๐๏ธ", f"Archiving to {file_path} ...") | |
| mtime_format = "%Y-%m-%dT%H:%M:%SZ" | |
| if source_date_epoch := os.environ.get("SOURCE_DATE_EPOCH"): | |
| mtime = datetime.datetime.fromtimestamp( | |
| int(source_date_epoch), datetime.UTC | |
| ).strftime(mtime_format) | |
| else: | |
| try: | |
| mtime = subprocess.run( | |
| [ | |
| "git", | |
| "log", | |
| "-1", | |
| "--format=tformat:%cd", | |
| f"--date=format:{mtime_format}", | |
| os.fsdecode(context.checkout), | |
| ], | |
| env={"TZ": "UTC0"}, | |
| capture_output=True, | |
| text=True, | |
| check=True, | |
| ).stdout.strip() | |
| except subprocess.CalledProcessError as error: | |
| print(error.output) | |
| sys.exit(error.returncode) | |
| try: | |
| subprocess.run( | |
| [ | |
| "tar", | |
| "-c", | |
| "-f", | |
| os.fsdecode(file_path), | |
| "--sort=name", | |
| "--mtime", | |
| mtime, | |
| "--clamp-mtime", | |
| "--owner=0", | |
| "--group=0", | |
| "--numeric-owner", | |
| "--pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime", | |
| "--mode=go+u,go-w", | |
| # Explicitly using `-T` because if you don't compress with threads you can't | |
| # uncompress with them and the size difference is negligible when using | |
| # single-threaded compression. | |
| "--use-compress-program", | |
| "xz -T 0", | |
| to_compress.name, | |
| ], | |
| cwd=to_compress.parent, | |
| capture_output=True, | |
| text=True, | |
| check=True, | |
| ) | |
| except subprocess.CalledProcessError as error: | |
| print(error.output) | |
| sys.exit(error.returncode) |