| #!/usr/bin/env python3 |
| |
| import argparse |
| import os |
| import re |
| import shutil |
| import subprocess |
| import sys |
| import sysconfig |
| from os.path import relpath |
| from pathlib import Path |
| |
| SCRIPT_NAME = Path(__file__).name |
| CHECKOUT = Path(__file__).resolve().parent.parent |
| CROSS_BUILD_DIR = CHECKOUT / "cross-build" |
| |
| |
| def delete_if_exists(path): |
| if path.exists(): |
| print(f"Deleting {path} ...") |
| shutil.rmtree(path) |
| |
| |
| def subdir(name, *, clean=None): |
| path = CROSS_BUILD_DIR / name |
| if clean: |
| delete_if_exists(path) |
| if not path.exists(): |
| if clean is None: |
| sys.exit( |
| f"{path} does not exist. Create it by running the appropriate " |
| f"`configure` subcommand of {SCRIPT_NAME}.") |
| else: |
| path.mkdir(parents=True) |
| return path |
| |
| |
| def run(command, *, host=None, **kwargs): |
| env = os.environ.copy() |
| if host: |
| env_script = CHECKOUT / "Android/android-env.sh" |
| env_output = subprocess.run( |
| f"set -eu; " |
| f"HOST={host}; " |
| f"PREFIX={subdir(host)}/prefix; " |
| f". {env_script}; " |
| f"export", |
| check=True, shell=True, text=True, stdout=subprocess.PIPE |
| ).stdout |
| |
| for line in env_output.splitlines(): |
| # We don't require every line to match, as there may be some other |
| # output from installing the NDK. |
| if match := re.search( |
| "^(declare -x |export )?(\\w+)=['\"]?(.*?)['\"]?$", line |
| ): |
| key, value = match[2], match[3] |
| if env.get(key) != value: |
| print(line) |
| env[key] = value |
| |
| if env == os.environ: |
| raise ValueError(f"Found no variables in {env_script.name} output:\n" |
| + env_output) |
| |
| print(">", " ".join(map(str, command))) |
| try: |
| subprocess.run(command, check=True, env=env, **kwargs) |
| except subprocess.CalledProcessError as e: |
| sys.exit(e) |
| |
| |
| def build_python_path(): |
| """The path to the build Python binary.""" |
| build_dir = subdir("build") |
| binary = build_dir / "python" |
| if not binary.is_file(): |
| binary = binary.with_suffix(".exe") |
| if not binary.is_file(): |
| raise FileNotFoundError("Unable to find `python(.exe)` in " |
| f"{build_dir}") |
| |
| return binary |
| |
| |
| def configure_build_python(context): |
| os.chdir(subdir("build", clean=context.clean)) |
| |
| command = [relpath(CHECKOUT / "configure")] |
| if context.args: |
| command.extend(context.args) |
| run(command) |
| |
| |
| def make_build_python(context): |
| os.chdir(subdir("build")) |
| run(["make", "-j", str(os.cpu_count())]) |
| |
| |
| def unpack_deps(host): |
| deps_url = "https://github.com/beeware/cpython-android-source-deps/releases/download" |
| for name_ver in ["bzip2-1.0.8-1", "libffi-3.4.4-2", "openssl-3.0.13-1", |
| "sqlite-3.45.1-0", "xz-5.4.6-0"]: |
| filename = f"{name_ver}-{host}.tar.gz" |
| run(["wget", f"{deps_url}/{name_ver}/{filename}"]) |
| run(["tar", "-xf", filename]) |
| os.remove(filename) |
| |
| |
| def configure_host_python(context): |
| host_dir = subdir(context.host, clean=context.clean) |
| |
| prefix_dir = host_dir / "prefix" |
| if not prefix_dir.exists(): |
| prefix_dir.mkdir() |
| os.chdir(prefix_dir) |
| unpack_deps(context.host) |
| |
| build_dir = host_dir / "build" |
| build_dir.mkdir(exist_ok=True) |
| os.chdir(build_dir) |
| |
| command = [ |
| # Basic cross-compiling configuration |
| relpath(CHECKOUT / "configure"), |
| f"--host={context.host}", |
| f"--build={sysconfig.get_config_var('BUILD_GNU_TYPE')}", |
| f"--with-build-python={build_python_path()}", |
| "--without-ensurepip", |
| |
| # Android always uses a shared libpython. |
| "--enable-shared", |
| "--without-static-libpython", |
| |
| # Dependent libraries. The others are found using pkg-config: see |
| # android-env.sh. |
| f"--with-openssl={prefix_dir}", |
| ] |
| |
| if context.args: |
| command.extend(context.args) |
| run(command, host=context.host) |
| |
| |
| def make_host_python(context): |
| host_dir = subdir(context.host) |
| os.chdir(host_dir / "build") |
| run(["make", "-j", str(os.cpu_count())], host=context.host) |
| run(["make", "install", f"prefix={host_dir}/prefix"], host=context.host) |
| |
| |
| def build_all(context): |
| steps = [configure_build_python, make_build_python, configure_host_python, |
| make_host_python] |
| for step in steps: |
| step(context) |
| |
| |
| def clean_all(context): |
| delete_if_exists(CROSS_BUILD_DIR) |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser() |
| subcommands = parser.add_subparsers(dest="subcommand") |
| build = subcommands.add_parser("build", help="Build everything") |
| configure_build = subcommands.add_parser("configure-build", |
| help="Run `configure` for the " |
| "build Python") |
| make_build = subcommands.add_parser("make-build", |
| help="Run `make` for the build Python") |
| configure_host = subcommands.add_parser("configure-host", |
| help="Run `configure` for Android") |
| make_host = subcommands.add_parser("make-host", |
| help="Run `make` for Android") |
| clean = subcommands.add_parser("clean", help="Delete files and directories " |
| "created by this script") |
| for subcommand in build, configure_build, configure_host: |
| subcommand.add_argument( |
| "--clean", action="store_true", default=False, dest="clean", |
| help="Delete any relevant directories before building") |
| for subcommand in build, configure_host, make_host: |
| subcommand.add_argument( |
| "host", metavar="HOST", |
| choices=["aarch64-linux-android", "x86_64-linux-android"], |
| help="Host triplet: choices=[%(choices)s]") |
| for subcommand in build, configure_build, configure_host: |
| subcommand.add_argument("args", nargs="*", |
| help="Extra arguments to pass to `configure`") |
| |
| context = parser.parse_args() |
| dispatch = {"configure-build": configure_build_python, |
| "make-build": make_build_python, |
| "configure-host": configure_host_python, |
| "make-host": make_host_python, |
| "build": build_all, |
| "clean": clean_all} |
| dispatch[context.subcommand](context) |
| |
| |
| if __name__ == "__main__": |
| main() |