From bdd45edd42c136e5fd4b35cd6dedf11326e4d3ce Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 26 Mar 2026 13:59:40 +0800 Subject: [PATCH 1/8] Allow for cleaning a subset of targets. --- Android/android.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/Android/android.py b/Android/android.py index b644be9cc64c7a..221f1619ec10d2 100755 --- a/Android/android.py +++ b/Android/android.py @@ -289,9 +289,15 @@ def clean(host): delete_glob(CROSS_BUILD_DIR / host) -def clean_all(context): - for host in HOSTS + ["build"]: - clean(host) +def clean_targets(context): + if context.target in {"all", "build"}: + clean("build") + + if context.target == "hosts": + for host in HOSTS: + clean(host) + else: + clean(context.target) def setup_ci(): @@ -864,7 +870,19 @@ def add_parser(*args, **kwargs): make_host = add_parser( "make-host", help="Run `make` for Android") - add_parser("clean", help="Delete all build directories") + clean = add_parser("clean", help="Delete all build directories") + clean.add_argument( + "target", + nargs="?", + default="all", + help=( + "The host triple to clean (e.g., aarch64-linux-android), " + "or 'build' for just the build platform, or 'hosts' for all " + "host platforms, or 'all' for the build platform and all " + "hosts. Defaults to 'all'" + ), + ) + add_parser("build-testbed", help="Build the testbed app") test = add_parser("test", help="Run the testbed app") package = add_parser("package", help="Make a release package") @@ -945,7 +963,7 @@ def main(): "configure-host": configure_host_python, "make-host": make_host_python, "build": build_all, - "clean": clean_all, + "clean": clean_targets, "build-testbed": build_testbed, "test": run_testbed, "package": package, From 57dd50f6f8260058de35de0ce006f63ecc0b2879 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 26 Mar 2026 14:07:09 +0800 Subject: [PATCH 2/8] Allow the download cache dir to be customized. --- Android/android.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Android/android.py b/Android/android.py index 221f1619ec10d2..d6eb42a6e19e25 100755 --- a/Android/android.py +++ b/Android/android.py @@ -205,20 +205,20 @@ def make_build_python(context): # # If you're a member of the Python core team, and you'd like to be able to push # these tags yourself, please contact Malcolm Smith or Russell Keith-Magee. -def unpack_deps(host, prefix_dir): +def unpack_deps(host, prefix_dir, cache_dir): os.chdir(prefix_dir) deps_url = "https://github.com/beeware/cpython-android-source-deps/releases/download" for name_ver in ["bzip2-1.0.8-3", "libffi-3.4.4-3", "openssl-3.5.5-0", "sqlite-3.50.4-0", "xz-5.4.6-1", "zstd-1.5.7-1"]: filename = f"{name_ver}-{host}.tar.gz" - download(f"{deps_url}/{name_ver}/{filename}") + download(f"{deps_url}/{name_ver}/{filename}", cache_dir) shutil.unpack_archive(filename) os.remove(filename) -def download(url, target_dir="."): - out_path = f"{target_dir}/{basename(url)}" - run(["curl", "-Lf", "--retry", "5", "--retry-all-errors", "-o", out_path, url]) +def download(url, cache_dir): + out_path = cache_dir / basename(url) + run(["curl", "-Lf", "--retry", "5", "--retry-all-errors", "-o", str(out_path), url]) return out_path @@ -230,7 +230,8 @@ def configure_host_python(context): prefix_dir = host_dir / "prefix" if not prefix_dir.exists(): prefix_dir.mkdir() - unpack_deps(context.host, prefix_dir) + cache_dir = context.cache_dir or CROSS_BUILD_DIR / "downloads" + unpack_deps(context.host, prefix_dir, cache_dir) os.chdir(host_dir) command = [ @@ -890,6 +891,15 @@ def add_parser(*args, **kwargs): env = add_parser("env", help="Print environment variables") # Common arguments + # --cache-dir option + for cmd in [configure_host, build, ci]: + cmd.add_argument( + "--cache-dir", + default=os.environ.get("CACHE_DIR"), + help="The directory to store cached downloads.", + ) + + # --clean option for subcommand in [build, configure_build, configure_host, ci]: subcommand.add_argument( "--clean", action="store_true", default=False, dest="clean", From 283debeddeb053a124c4ec9cabf268f11dacfd6b Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 26 Mar 2026 14:08:58 +0800 Subject: [PATCH 3/8] Make the cross-build dir customizable. --- Android/android.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/Android/android.py b/Android/android.py index d6eb42a6e19e25..1aae981194d343 100755 --- a/Android/android.py +++ b/Android/android.py @@ -864,7 +864,7 @@ def add_parser(*args, **kwargs): "make-host") configure_build = add_parser( "configure-build", help="Run `configure` for the build Python") - add_parser( + make_build = add_parser( "make-build", help="Run `make` for the build Python") configure_host = add_parser( "configure-host", help="Run `configure` for Android") @@ -891,6 +891,31 @@ def add_parser(*args, **kwargs): env = add_parser("env", help="Print environment variables") # Common arguments + # --cross-build-dir argument + for cmd in [ + clean, + configure_build, + make_build, + configure_host, + make_host, + build, + package, + test, + ci, + ]: + cmd.add_argument( + "--cross-build-dir", + action="store", + default=os.environ.get("CROSS_BUILD_DIR"), + dest="cross_build_dir", + type=Path, + help=( + "Path to the cross-build directory " + f"(default: {CROSS_BUILD_DIR}). Can also be set " + "with the CROSS_BUILD_DIR environment variable." + ), + ) + # --cache-dir option for cmd in [configure_host, build, ci]: cmd.add_argument( @@ -967,6 +992,12 @@ def main(): stream.reconfigure(line_buffering=True) context = parse_args() + + # Set the CROSS_BUILD_DIR if an argument was provided + if context.cross_build_dir: + global CROSS_BUILD_DIR + CROSS_BUILD_DIR = context.cross_build_dir.resolve() + dispatch = { "configure-build": configure_build_python, "make-build": make_build_python, From 46da12452c2f28d3580f95fcc318382fcee4d290 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 26 Mar 2026 14:36:03 +0800 Subject: [PATCH 4/8] Allow build command to target all, build and hosts. --- Android/android.py | 79 ++++++++++++++++++++++++++++------------------ 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/Android/android.py b/Android/android.py index 1aae981194d343..0f8fa606018437 100755 --- a/Android/android.py +++ b/Android/android.py @@ -211,33 +211,37 @@ def unpack_deps(host, prefix_dir, cache_dir): for name_ver in ["bzip2-1.0.8-3", "libffi-3.4.4-3", "openssl-3.5.5-0", "sqlite-3.50.4-0", "xz-5.4.6-1", "zstd-1.5.7-1"]: filename = f"{name_ver}-{host}.tar.gz" - download(f"{deps_url}/{name_ver}/{filename}", cache_dir) - shutil.unpack_archive(filename) - os.remove(filename) + out_path = download(f"{deps_url}/{name_ver}/{filename}", cache_dir) + shutil.unpack_archive(out_path) def download(url, cache_dir): out_path = cache_dir / basename(url) - run(["curl", "-Lf", "--retry", "5", "--retry-all-errors", "-o", str(out_path), url]) + if not out_path.is_file(): + run(["curl", "-Lf", "--retry", "5", "--retry-all-errors", "-o", str(out_path), url]) + else: + print(f"Using cached version of {basename(url)}") return out_path -def configure_host_python(context): +def configure_host_python(context, host=None): if context.clean: clean(context.host) + if host is None: + host = context.host - host_dir = subdir(context.host, create=True) + host_dir = subdir(host, create=True) prefix_dir = host_dir / "prefix" if not prefix_dir.exists(): prefix_dir.mkdir() - cache_dir = context.cache_dir or CROSS_BUILD_DIR / "downloads" - unpack_deps(context.host, prefix_dir, cache_dir) + cache_dir = Path(context.cache_dir).resolve() or CROSS_BUILD_DIR / "downloads" + unpack_deps(host, prefix_dir, cache_dir) os.chdir(host_dir) command = [ # Basic cross-compiling configuration relpath(PYTHON_DIR / "configure"), - f"--host={context.host}", + f"--host={host}", f"--build={sysconfig.get_config_var('BUILD_GNU_TYPE')}", f"--with-build-python={build_python_path()}", "--without-ensurepip", @@ -253,14 +257,16 @@ def configure_host_python(context): if context.args: command.extend(context.args) - run(command, host=context.host) + run(command, host=host) -def make_host_python(context): +def make_host_python(context, host=None): + if host is None: + host = context.host # The CFLAGS and LDFLAGS set in android-env include the prefix dir, so # delete any previous Python installation to prevent it being used during # the build. - host_dir = subdir(context.host) + host_dir = subdir(host) prefix_dir = host_dir / "prefix" for pattern in ("include/python*", "lib/libpython*", "lib/python*"): delete_glob(f"{prefix_dir}/{pattern}") @@ -279,11 +285,18 @@ def make_host_python(context): ) -def build_all(context): - steps = [configure_build_python, make_build_python, configure_host_python, - make_host_python] - for step in steps: - step(context) +def build_targets(context): + if context.target in {"all", "build"}: + configure_build_python(context) + make_build_python(context) + + if context.target == "hosts": + for host in HOSTS: + configure_host_python(context, host) + make_host_python(context, host) + elif context.target not in {"all", "build"}: + configure_host_python(context, context.target) + make_host_python(context, context.target) def clean(host): @@ -297,7 +310,7 @@ def clean_targets(context): if context.target == "hosts": for host in HOSTS: clean(host) - else: + elif context.target not in {"all", "build"}: clean(context.target) @@ -872,17 +885,6 @@ def add_parser(*args, **kwargs): "make-host", help="Run `make` for Android") clean = add_parser("clean", help="Delete all build directories") - clean.add_argument( - "target", - nargs="?", - default="all", - help=( - "The host triple to clean (e.g., aarch64-linux-android), " - "or 'build' for just the build platform, or 'hosts' for all " - "host platforms, or 'all' for the build platform and all " - "hosts. Defaults to 'all'" - ), - ) add_parser("build-testbed", help="Build the testbed app") test = add_parser("test", help="Run the testbed app") @@ -930,7 +932,22 @@ def add_parser(*args, **kwargs): "--clean", action="store_true", default=False, dest="clean", help="Delete the relevant build directories first") - host_commands = [build, configure_host, make_host, package, ci] + # Allow "all" and "hosts" options + for subcommand in [clean, build]: + subcommand.add_argument( + "target", + nargs="?", + default="all", + choices=["all", "build", "hosts"] + HOSTS, + help=( + "The host triple to build (e.g., aarch64-linux-android), " + "or 'build' for just the build platform, or 'hosts' for all " + "host platforms, or 'all' for the build platform and all " + "hosts. Defaults to 'all'" + ), + ) + + host_commands = [configure_host, make_host, package, ci] if in_source_tree: host_commands.append(env) for subcommand in host_commands: @@ -1003,7 +1020,7 @@ def main(): "make-build": make_build_python, "configure-host": configure_host_python, "make-host": make_host_python, - "build": build_all, + "build": build_targets, "clean": clean_targets, "build-testbed": build_testbed, "test": run_testbed, From afabb8f48cf78e18d39e6c7c245607fcbba57545 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 26 Mar 2026 14:36:17 +0800 Subject: [PATCH 5/8] Add blurb. --- .../next/Build/2026-03-26-14-35-29.gh-issue-146450.9Kmp5Q.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Build/2026-03-26-14-35-29.gh-issue-146450.9Kmp5Q.rst diff --git a/Misc/NEWS.d/next/Build/2026-03-26-14-35-29.gh-issue-146450.9Kmp5Q.rst b/Misc/NEWS.d/next/Build/2026-03-26-14-35-29.gh-issue-146450.9Kmp5Q.rst new file mode 100644 index 00000000000000..32cb5b8221a926 --- /dev/null +++ b/Misc/NEWS.d/next/Build/2026-03-26-14-35-29.gh-issue-146450.9Kmp5Q.rst @@ -0,0 +1,2 @@ +The Android build script was modified to improve parity with other platform +build scripts. From 7215162f9a6ea90628a60557c9ad750d82bfa8fa Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 26 Mar 2026 14:46:44 +0800 Subject: [PATCH 6/8] Correct handling of fallback cache_dir --- Android/android.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Android/android.py b/Android/android.py index 0f8fa606018437..c4ba5ad320a08b 100755 --- a/Android/android.py +++ b/Android/android.py @@ -234,7 +234,11 @@ def configure_host_python(context, host=None): prefix_dir = host_dir / "prefix" if not prefix_dir.exists(): prefix_dir.mkdir() - cache_dir = Path(context.cache_dir).resolve() or CROSS_BUILD_DIR / "downloads" + cache_dir = ( + Path(context.cache_dir).resolve() + if context.cache_dir + else CROSS_BUILD_DIR / "downloads" + ) unpack_deps(host, prefix_dir, cache_dir) os.chdir(host_dir) @@ -932,7 +936,7 @@ def add_parser(*args, **kwargs): "--clean", action="store_true", default=False, dest="clean", help="Delete the relevant build directories first") - # Allow "all" and "hosts" options + # Allow "all", "build" and "hosts" targets for some commands for subcommand in [clean, build]: subcommand.add_argument( "target", From 3f0595aba3200c70d54d5787ae2e6e22bd59de38 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 26 Mar 2026 15:06:48 +0800 Subject: [PATCH 7/8] Ensure download cache directory exists before downloading. --- Android/android.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Android/android.py b/Android/android.py index c4ba5ad320a08b..ed22461b357859 100755 --- a/Android/android.py +++ b/Android/android.py @@ -217,6 +217,7 @@ def unpack_deps(host, prefix_dir, cache_dir): def download(url, cache_dir): out_path = cache_dir / basename(url) + cache_dir.mkdir(parents=True, exist_ok=True) if not out_path.is_file(): run(["curl", "-Lf", "--retry", "5", "--retry-all-errors", "-o", str(out_path), url]) else: From e1fa7eb5f7ac31a8b404c6d2c8fac685ac952d67 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 27 Mar 2026 09:47:51 +0800 Subject: [PATCH 8/8] Correct handling of 'clean all' --- Android/android.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Android/android.py b/Android/android.py index ed22461b357859..5161ad02efded0 100755 --- a/Android/android.py +++ b/Android/android.py @@ -312,10 +312,10 @@ def clean_targets(context): if context.target in {"all", "build"}: clean("build") - if context.target == "hosts": + if context.target in {"all", "hosts"}: for host in HOSTS: clean(host) - elif context.target not in {"all", "build"}: + elif context.target != "build": clean(context.target)