mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 05:31:20 +00:00 
			
		
		
		
	gh-71052: Add Android build script and instructions (#116426)
This commit is contained in:
		
							parent
							
								
									50f9b0b1e0
								
							
						
					
					
						commit
						3ec57307e7
					
				
					 7 changed files with 401 additions and 17 deletions
				
			
		
							
								
								
									
										64
									
								
								Android/README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								Android/README.md
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||
| ``` | ||||
							
								
								
									
										87
									
								
								Android/android-env.sh
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								Android/android-env.sh
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||
							
								
								
									
										202
									
								
								Android/android.py
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										202
									
								
								Android/android.py
									
										
									
									
									
										Executable file
									
								
							|  | @ -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() | ||||
|  | @ -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 | ||||
|  |  | |||
|  | @ -0,0 +1 @@ | |||
| Add Android build script and instructions. | ||||
							
								
								
									
										23
									
								
								configure
									
										
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										23
									
								
								configure
									
										
									
										generated
									
									
										vendored
									
									
								
							|  | @ -17595,13 +17595,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" | ||||
|   blocked_funcs="chroot initgroups setegid seteuid setgid sethostname | ||||
|     setregid setresgid setresuid setreuid setuid" | ||||
| 
 | ||||
|   # These functions are unimplemented and always return an error. | ||||
|   unimplemented_funcs="sem_open sem_unlink" | ||||
|   # 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" | ||||
| 
 | ||||
|   for name in $privileged_funcs $unimplemented_funcs; do | ||||
|   # 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 $blocked_funcs; do | ||||
|     as_func_var=`printf "%s\n" "ac_cv_func_$name" | $as_tr_sh` | ||||
| 
 | ||||
|     eval "$as_func_var=no" | ||||
|  | @ -22156,6 +22164,10 @@ fi | |||
| 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 : | ||||
|  | @ -22213,6 +22225,7 @@ fi | |||
| fi | ||||
| 
 | ||||
| done | ||||
| fi | ||||
| 
 | ||||
| 
 | ||||
|   for ac_func in nanosleep | ||||
|  |  | |||
							
								
								
									
										27
									
								
								configure.ac
									
										
									
									
									
								
							
							
						
						
									
										27
									
								
								configure.ac
									
										
									
									
									
								
							|  | @ -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" | ||||
|   blocked_funcs="chroot initgroups setegid seteuid setgid sethostname | ||||
|     setregid setresgid setresuid setreuid setuid" | ||||
| 
 | ||||
|   # These functions are unimplemented and always return an error. | ||||
|   unimplemented_funcs="sem_open sem_unlink" | ||||
|   # 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" | ||||
| 
 | ||||
|   for name in $privileged_funcs $unimplemented_funcs; do | ||||
|   # 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 $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], [], [ | ||||
| # 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], [ | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Malcolm Smith
						Malcolm Smith