2024-03-22 00:52:29 +01:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
import argparse
|
2024-07-31 02:21:43 +02:00
|
|
|
from glob import glob
|
2024-03-22 00:52:29 +01:00
|
|
|
import os
|
|
|
|
import re
|
|
|
|
import shutil
|
|
|
|
import subprocess
|
|
|
|
import sys
|
|
|
|
import sysconfig
|
2024-05-01 08:36:45 +02:00
|
|
|
from os.path import basename, relpath
|
2024-03-22 00:52:29 +01:00
|
|
|
from pathlib import Path
|
2024-05-01 08:36:45 +02:00
|
|
|
from tempfile import TemporaryDirectory
|
2024-03-22 00:52:29 +01:00
|
|
|
|
|
|
|
SCRIPT_NAME = Path(__file__).name
|
|
|
|
CHECKOUT = Path(__file__).resolve().parent.parent
|
|
|
|
CROSS_BUILD_DIR = CHECKOUT / "cross-build"
|
|
|
|
|
|
|
|
|
2024-07-31 02:21:43 +02:00
|
|
|
def delete_glob(pattern):
|
|
|
|
# Path.glob doesn't accept non-relative patterns.
|
|
|
|
for path in glob(str(pattern)):
|
|
|
|
path = Path(path)
|
2024-03-22 00:52:29 +01:00
|
|
|
print(f"Deleting {path} ...")
|
2024-07-31 02:21:43 +02:00
|
|
|
if path.is_dir() and not path.is_symlink():
|
|
|
|
shutil.rmtree(path)
|
|
|
|
else:
|
|
|
|
path.unlink()
|
2024-03-22 00:52:29 +01:00
|
|
|
|
|
|
|
|
|
|
|
def subdir(name, *, clean=None):
|
|
|
|
path = CROSS_BUILD_DIR / name
|
|
|
|
if clean:
|
2024-07-31 02:21:43 +02:00
|
|
|
delete_glob(path)
|
2024-03-22 00:52:29 +01:00
|
|
|
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"
|
2024-05-01 08:36:45 +02:00
|
|
|
download(f"{deps_url}/{name_ver}/{filename}")
|
2024-03-22 00:52:29 +01:00
|
|
|
run(["tar", "-xf", filename])
|
|
|
|
os.remove(filename)
|
|
|
|
|
|
|
|
|
2024-05-01 08:36:45 +02:00
|
|
|
def download(url, target_dir="."):
|
|
|
|
out_path = f"{target_dir}/{basename(url)}"
|
|
|
|
run(["curl", "-Lf", "-o", out_path, url])
|
|
|
|
return out_path
|
|
|
|
|
|
|
|
|
2024-03-22 00:52:29 +01:00
|
|
|
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):
|
2024-07-31 02:21:43 +02:00
|
|
|
# The CFLAGS and LDFLAGS set in android-env include the prefix dir, so
|
|
|
|
# delete any previously-installed Python libs and include files to prevent
|
|
|
|
# them being used during the build.
|
2024-03-22 00:52:29 +01:00
|
|
|
host_dir = subdir(context.host)
|
2024-07-31 02:21:43 +02:00
|
|
|
prefix_dir = host_dir / "prefix"
|
|
|
|
delete_glob(f"{prefix_dir}/include/python*")
|
|
|
|
delete_glob(f"{prefix_dir}/lib/libpython*")
|
|
|
|
|
2024-03-22 00:52:29 +01:00
|
|
|
os.chdir(host_dir / "build")
|
|
|
|
run(["make", "-j", str(os.cpu_count())], host=context.host)
|
2024-07-31 02:21:43 +02:00
|
|
|
run(["make", "install", f"prefix={prefix_dir}"], host=context.host)
|
2024-03-22 00:52:29 +01:00
|
|
|
|
|
|
|
|
|
|
|
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):
|
2024-07-31 02:21:43 +02:00
|
|
|
delete_glob(CROSS_BUILD_DIR)
|
2024-03-22 00:52:29 +01:00
|
|
|
|
|
|
|
|
2024-05-01 08:36:45 +02:00
|
|
|
# To avoid distributing compiled artifacts without corresponding source code,
|
|
|
|
# the Gradle wrapper is not included in the CPython repository. Instead, we
|
|
|
|
# extract it from the Gradle release.
|
|
|
|
def setup_testbed(context):
|
|
|
|
ver_long = "8.7.0"
|
|
|
|
ver_short = ver_long.removesuffix(".0")
|
|
|
|
testbed_dir = CHECKOUT / "Android/testbed"
|
|
|
|
|
|
|
|
for filename in ["gradlew", "gradlew.bat"]:
|
|
|
|
out_path = download(
|
|
|
|
f"https://raw.githubusercontent.com/gradle/gradle/v{ver_long}/{filename}",
|
|
|
|
testbed_dir)
|
|
|
|
os.chmod(out_path, 0o755)
|
|
|
|
|
|
|
|
with TemporaryDirectory(prefix=SCRIPT_NAME) as temp_dir:
|
|
|
|
os.chdir(temp_dir)
|
|
|
|
bin_zip = download(
|
|
|
|
f"https://services.gradle.org/distributions/gradle-{ver_short}-bin.zip")
|
|
|
|
outer_jar = f"gradle-{ver_short}/lib/plugins/gradle-wrapper-{ver_short}.jar"
|
|
|
|
run(["unzip", bin_zip, outer_jar])
|
|
|
|
run(["unzip", "-o", "-d", f"{testbed_dir}/gradle/wrapper", outer_jar,
|
|
|
|
"gradle-wrapper.jar"])
|
|
|
|
|
|
|
|
|
2024-03-22 00:52:29 +01:00
|
|
|
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")
|
2024-05-01 08:36:45 +02:00
|
|
|
subcommands.add_parser(
|
|
|
|
"clean", help="Delete the cross-build directory")
|
|
|
|
subcommands.add_parser(
|
|
|
|
"setup-testbed", help="Download the testbed Gradle wrapper")
|
|
|
|
|
2024-03-22 00:52:29 +01:00
|
|
|
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,
|
2024-05-01 08:36:45 +02:00
|
|
|
"clean": clean_all,
|
|
|
|
"setup-testbed": setup_testbed}
|
2024-03-22 00:52:29 +01:00
|
|
|
dispatch[context.subcommand](context)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|