aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/Android/android.py
diff options
context:
space:
mode:
Diffstat (limited to 'Android/android.py')
-rwxr-xr-xAndroid/android.py202
1 files changed, 202 insertions, 0 deletions
diff --git a/Android/android.py b/Android/android.py
new file mode 100755
index 00000000000..5c57e53c415
--- /dev/null
+++ b/Android/android.py
@@ -0,0 +1,202 @@
+#!/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()