]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-71052: Add Android build script and instructions (#116426)
authorMalcolm Smith <smith@chaquo.com>
Thu, 21 Mar 2024 23:52:29 +0000 (23:52 +0000)
committerGitHub <noreply@github.com>
Thu, 21 Mar 2024 23:52:29 +0000 (00:52 +0100)
Android/README.md [new file with mode: 0644]
Android/android-env.sh [new file with mode: 0644]
Android/android.py [new file with mode: 0755]
Include/cpython/pystate.h
Misc/NEWS.d/next/Build/2024-03-06-17-26-55.gh-issue-71052.vLbu9u.rst [new file with mode: 0644]
configure
configure.ac

diff --git a/Android/README.md b/Android/README.md
new file mode 100644 (file)
index 0000000..5ed186e
--- /dev/null
@@ -0,0 +1,64 @@
+# Python for Android
+
+These instructions are only needed if you're planning to compile Python for
+Android yourself. Most users should *not* need to do this. If you're looking to
+use Python on Android, one of the following tools will provide a much more
+approachable user experience:
+
+* [Briefcase](https://briefcase.readthedocs.io), from the BeeWare project
+* [Buildozer](https://buildozer.readthedocs.io), from the Kivy project
+* [Chaquopy](https://chaquo.com/chaquopy/)
+
+
+## Prerequisites
+
+Export the `ANDROID_HOME` environment variable to point at your Android SDK. If
+you don't already have the SDK, here's how to install it:
+
+* Download the "Command line tools" from <https://developer.android.com/studio>.
+* Create a directory `android-sdk/cmdline-tools`, and unzip the command line
+  tools package into it.
+* Rename `android-sdk/cmdline-tools/cmdline-tools` to
+  `android-sdk/cmdline-tools/latest`.
+* `export ANDROID_HOME=/path/to/android-sdk`
+
+
+## Building
+
+Building for Android requires doing a cross-build where you have a "build"
+Python to help produce an Android build of CPython. This procedure has been
+tested on Linux and macOS.
+
+The easiest way to do a build is to use the `android.py` script. You can either
+have it perform the entire build process from start to finish in one step, or
+you can do it in discrete steps that mirror running `configure` and `make` for
+each of the two builds of Python you end up producing.
+
+The discrete steps for building via `android.py` are:
+
+```sh
+./android.py configure-build
+./android.py make-build
+./android.py configure-host HOST
+./android.py make-host HOST
+```
+
+To see the possible values of HOST, run `./android.py configure-host --help`.
+
+Or to do it all in a single command, run:
+
+```sh
+./android.py build HOST
+```
+
+In the end you should have a build Python in `cross-build/build`, and an Android
+build in `cross-build/HOST`.
+
+You can use `--` as a separator for any of the `configure`-related commands –
+including `build` itself – to pass arguments to the underlying `configure`
+call. For example, if you want a pydebug build that also caches the results from
+`configure`, you can do:
+
+```sh
+./android.py build HOST -- -C --with-pydebug
+```
diff --git a/Android/android-env.sh b/Android/android-env.sh
new file mode 100644 (file)
index 0000000..3ce3e03
--- /dev/null
@@ -0,0 +1,87 @@
+# This script must be sourced with the following variables already set:
+: ${ANDROID_HOME:?}  # Path to Android SDK
+: ${HOST:?}  # GNU target triplet
+
+# You may also override the following:
+: ${api_level:=21}  # Minimum Android API level the build will run on
+: ${PREFIX:-}  # Path in which to find required libraries
+
+
+# Print all messages on stderr so they're visible when running within build-wheel.
+log() {
+    echo "$1" >&2
+}
+
+fail() {
+    log "$1"
+    exit 1
+}
+
+# When moving to a new version of the NDK, carefully review the following:
+#
+# * https://developer.android.com/ndk/downloads/revision_history
+#
+# * https://android.googlesource.com/platform/ndk/+/ndk-rXX-release/docs/BuildSystemMaintainers.md
+#   where XX is the NDK version. Do a diff against the version you're upgrading from, e.g.:
+#   https://android.googlesource.com/platform/ndk/+/ndk-r25-release..ndk-r26-release/docs/BuildSystemMaintainers.md
+ndk_version=26.2.11394342
+
+ndk=$ANDROID_HOME/ndk/$ndk_version
+if ! [ -e $ndk ]; then
+    log "Installing NDK: this may take several minutes"
+    yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "ndk;$ndk_version"
+fi
+
+if [ $HOST = "arm-linux-androideabi" ]; then
+    clang_triplet=armv7a-linux-androideabi
+else
+    clang_triplet=$HOST
+fi
+
+# These variables are based on BuildSystemMaintainers.md above, and
+# $ndk/build/cmake/android.toolchain.cmake.
+toolchain=$(echo $ndk/toolchains/llvm/prebuilt/*)
+export AR="$toolchain/bin/llvm-ar"
+export AS="$toolchain/bin/llvm-as"
+export CC="$toolchain/bin/${clang_triplet}${api_level}-clang"
+export CXX="${CC}++"
+export LD="$toolchain/bin/ld"
+export NM="$toolchain/bin/llvm-nm"
+export RANLIB="$toolchain/bin/llvm-ranlib"
+export READELF="$toolchain/bin/llvm-readelf"
+export STRIP="$toolchain/bin/llvm-strip"
+
+# The quotes make sure the wildcard in the `toolchain` assignment has been expanded.
+for path in "$AR" "$AS" "$CC" "$CXX" "$LD" "$NM" "$RANLIB" "$READELF" "$STRIP"; do
+    if ! [ -e "$path" ]; then
+        fail "$path does not exist"
+    fi
+done
+
+export CFLAGS=""
+export LDFLAGS="-Wl,--build-id=sha1 -Wl,--no-rosegment"
+
+# Many packages get away with omitting -lm on Linux, but Android is stricter.
+LDFLAGS="$LDFLAGS -lm"
+
+# -mstackrealign is included where necessary in the clang launcher scripts which are
+# pointed to by $CC, so we don't need to include it here.
+if [ $HOST = "arm-linux-androideabi" ]; then
+    CFLAGS="$CFLAGS -march=armv7-a -mthumb"
+fi
+
+if [ -n "${PREFIX:-}" ]; then
+    abs_prefix=$(realpath $PREFIX)
+    CFLAGS="$CFLAGS -I$abs_prefix/include"
+    LDFLAGS="$LDFLAGS -L$abs_prefix/lib"
+
+    export PKG_CONFIG="pkg-config --define-prefix"
+    export PKG_CONFIG_LIBDIR="$abs_prefix/lib/pkgconfig"
+fi
+
+# Use the same variable name as conda-build
+if [ $(uname) = "Darwin" ]; then
+    export CPU_COUNT=$(sysctl -n hw.ncpu)
+else
+    export CPU_COUNT=$(nproc)
+fi
diff --git a/Android/android.py b/Android/android.py
new file mode 100755 (executable)
index 0000000..5c57e53
--- /dev/null
@@ -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()
index 38d0897ea131611a6b8dfcbe6f8694f2fff745e6..f1540fde925921c421d5324613b04d6a3d860aa8 100644 (file)
@@ -211,6 +211,10 @@ struct _ts {
 #  define Py_C_RECURSION_LIMIT 800
 #elif defined(_WIN32)
 #  define Py_C_RECURSION_LIMIT 3000
+#elif defined(__ANDROID__)
+   // On an ARM64 emulator, API level 34 was OK with 10000, but API level 21
+   // crashed in test_compiler_recursion_limit.
+#  define Py_C_RECURSION_LIMIT 3000
 #elif defined(_Py_ADDRESS_SANITIZER)
 #  define Py_C_RECURSION_LIMIT 4000
 #else
diff --git a/Misc/NEWS.d/next/Build/2024-03-06-17-26-55.gh-issue-71052.vLbu9u.rst b/Misc/NEWS.d/next/Build/2024-03-06-17-26-55.gh-issue-71052.vLbu9u.rst
new file mode 100644 (file)
index 0000000..53776c0
--- /dev/null
@@ -0,0 +1 @@
+Add Android build script and instructions.
index 229f0d32d322dd2c39d5dbd710957aa1ded46329..0493d382f48481928a8b0a3c17e77fe19c3ff39c 100755 (executable)
--- a/configure
+++ b/configure
 if test "$ac_sys_system" = "Linux-android"; then
   # When these functions are used in an unprivileged process, they crash rather
   # than returning an error.
-  privileged_funcs="chroot initgroups setegid seteuid setgid setregid setresgid
-    setresuid setreuid setuid"
-
-  # These functions are unimplemented and always return an error.
-  unimplemented_funcs="sem_open sem_unlink"
+  blocked_funcs="chroot initgroups setegid seteuid setgid sethostname
+    setregid setresgid setresuid setreuid setuid"
+
+  # These functions are unimplemented and always return an error
+  # (https://android.googlesource.com/platform/system/sepolicy/+/refs/heads/android13-release/public/domain.te#1044)
+  blocked_funcs="$blocked_funcs sem_open sem_unlink"
+
+  # Before API level 23, when fchmodat is called with the unimplemented flag
+  # AT_SYMLINK_NOFOLLOW, instead of returning ENOTSUP as it should, it actually
+  # follows the symlink.
+  if test "$ANDROID_API_LEVEL" -lt 23; then
+    blocked_funcs="$blocked_funcs fchmodat"
+  fi
 
-  for name in $privileged_funcs $unimplemented_funcs; do
+  for name in $blocked_funcs; do
     as_func_var=`printf "%s\n" "ac_cv_func_$name" | $as_tr_sh`
 
     eval "$as_func_var=no"
 done
 fi
 
+# On Android before API level 23, clock_nanosleep returns the wrong value when
+# interrupted by a signal (https://issuetracker.google.com/issues/216495770).
+if ! { test "$ac_sys_system" = "Linux-android" &&
+       test "$ANDROID_API_LEVEL" -lt 23; }; then
 
   for ac_func in clock_nanosleep
 do :
@@ -22166,7 +22178,7 @@ then :
 
 else $as_nop
 
-    { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for clock_nanosleep in -lrt" >&5
+      { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for clock_nanosleep in -lrt" >&5
 printf %s "checking for clock_nanosleep in -lrt... " >&6; }
 if test ${ac_cv_lib_rt_clock_nanosleep+y}
 then :
@@ -22204,7 +22216,7 @@ printf "%s\n" "$ac_cv_lib_rt_clock_nanosleep" >&6; }
 if test "x$ac_cv_lib_rt_clock_nanosleep" = xyes
 then :
 
-        printf "%s\n" "#define HAVE_CLOCK_NANOSLEEP 1" >>confdefs.h
+          printf "%s\n" "#define HAVE_CLOCK_NANOSLEEP 1" >>confdefs.h
 
 
 fi
@@ -22213,6 +22225,7 @@ fi
 fi
 
 done
+fi
 
 
   for ac_func in nanosleep
index cd17977738482de1c59bf75fd80311a5530e4d62..cdfafc2d7272f19a0b6e18799202b6064417ded1 100644 (file)
@@ -4934,13 +4934,21 @@ fi
 if test "$ac_sys_system" = "Linux-android"; then
   # When these functions are used in an unprivileged process, they crash rather
   # than returning an error.
-  privileged_funcs="chroot initgroups setegid seteuid setgid setregid setresgid
-    setresuid setreuid setuid"
-
-  # These functions are unimplemented and always return an error.
-  unimplemented_funcs="sem_open sem_unlink"
+  blocked_funcs="chroot initgroups setegid seteuid setgid sethostname
+    setregid setresgid setresuid setreuid setuid"
+
+  # These functions are unimplemented and always return an error
+  # (https://android.googlesource.com/platform/system/sepolicy/+/refs/heads/android13-release/public/domain.te#1044)
+  blocked_funcs="$blocked_funcs sem_open sem_unlink"
+
+  # Before API level 23, when fchmodat is called with the unimplemented flag
+  # AT_SYMLINK_NOFOLLOW, instead of returning ENOTSUP as it should, it actually
+  # follows the symlink.
+  if test "$ANDROID_API_LEVEL" -lt 23; then
+    blocked_funcs="$blocked_funcs fchmodat"
+  fi
 
-  for name in $privileged_funcs $unimplemented_funcs; do
+  for name in $blocked_funcs; do
     AS_VAR_PUSHDEF([func_var], [ac_cv_func_$name])
     AS_VAR_SET([func_var], [no])
     AS_VAR_POPDEF([func_var])
@@ -5303,11 +5311,16 @@ then
   ])
 fi
 
-AC_CHECK_FUNCS([clock_nanosleep], [], [
-    AC_CHECK_LIB([rt], [clock_nanosleep], [
-        AC_DEFINE([HAVE_CLOCK_NANOSLEEP], [1])
-    ])
-])
+# On Android before API level 23, clock_nanosleep returns the wrong value when
+# interrupted by a signal (https://issuetracker.google.com/issues/216495770).
+if ! { test "$ac_sys_system" = "Linux-android" &&
+       test "$ANDROID_API_LEVEL" -lt 23; }; then
+  AC_CHECK_FUNCS([clock_nanosleep], [], [
+      AC_CHECK_LIB([rt], [clock_nanosleep], [
+          AC_DEFINE([HAVE_CLOCK_NANOSLEEP], [1])
+      ])
+  ])
+fi
 
 AC_CHECK_FUNCS([nanosleep], [], [
     AC_CHECK_LIB([rt], [nanosleep], [