Skip to content

Commit 3ec5730

Browse files
authored
pythongh-71052: Add Android build script and instructions (python#116426)
1 parent 50f9b0b commit 3ec5730

File tree

7 files changed

+403
-19
lines changed

7 files changed

+403
-19
lines changed

Android/README.md

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Python for Android
2+
3+
These instructions are only needed if you're planning to compile Python for
4+
Android yourself. Most users should *not* need to do this. If you're looking to
5+
use Python on Android, one of the following tools will provide a much more
6+
approachable user experience:
7+
8+
* [Briefcase](https://briefcase.readthedocs.io), from the BeeWare project
9+
* [Buildozer](https://buildozer.readthedocs.io), from the Kivy project
10+
* [Chaquopy](https://chaquo.com/chaquopy/)
11+
12+
13+
## Prerequisites
14+
15+
Export the `ANDROID_HOME` environment variable to point at your Android SDK. If
16+
you don't already have the SDK, here's how to install it:
17+
18+
* Download the "Command line tools" from <https://developer.android.com/studio>.
19+
* Create a directory `android-sdk/cmdline-tools`, and unzip the command line
20+
tools package into it.
21+
* Rename `android-sdk/cmdline-tools/cmdline-tools` to
22+
`android-sdk/cmdline-tools/latest`.
23+
* `export ANDROID_HOME=/path/to/android-sdk`
24+
25+
26+
## Building
27+
28+
Building for Android requires doing a cross-build where you have a "build"
29+
Python to help produce an Android build of CPython. This procedure has been
30+
tested on Linux and macOS.
31+
32+
The easiest way to do a build is to use the `android.py` script. You can either
33+
have it perform the entire build process from start to finish in one step, or
34+
you can do it in discrete steps that mirror running `configure` and `make` for
35+
each of the two builds of Python you end up producing.
36+
37+
The discrete steps for building via `android.py` are:
38+
39+
```sh
40+
./android.py configure-build
41+
./android.py make-build
42+
./android.py configure-host HOST
43+
./android.py make-host HOST
44+
```
45+
46+
To see the possible values of HOST, run `./android.py configure-host --help`.
47+
48+
Or to do it all in a single command, run:
49+
50+
```sh
51+
./android.py build HOST
52+
```
53+
54+
In the end you should have a build Python in `cross-build/build`, and an Android
55+
build in `cross-build/HOST`.
56+
57+
You can use `--` as a separator for any of the `configure`-related commands –
58+
including `build` itself – to pass arguments to the underlying `configure`
59+
call. For example, if you want a pydebug build that also caches the results from
60+
`configure`, you can do:
61+
62+
```sh
63+
./android.py build HOST -- -C --with-pydebug
64+
```

Android/android-env.sh

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# This script must be sourced with the following variables already set:
2+
: ${ANDROID_HOME:?} # Path to Android SDK
3+
: ${HOST:?} # GNU target triplet
4+
5+
# You may also override the following:
6+
: ${api_level:=21} # Minimum Android API level the build will run on
7+
: ${PREFIX:-} # Path in which to find required libraries
8+
9+
10+
# Print all messages on stderr so they're visible when running within build-wheel.
11+
log() {
12+
echo "$1" >&2
13+
}
14+
15+
fail() {
16+
log "$1"
17+
exit 1
18+
}
19+
20+
# When moving to a new version of the NDK, carefully review the following:
21+
#
22+
# * https://developer.android.com/ndk/downloads/revision_history
23+
#
24+
# * https://android.googlesource.com/platform/ndk/+/ndk-rXX-release/docs/BuildSystemMaintainers.md
25+
# where XX is the NDK version. Do a diff against the version you're upgrading from, e.g.:
26+
# https://android.googlesource.com/platform/ndk/+/ndk-r25-release..ndk-r26-release/docs/BuildSystemMaintainers.md
27+
ndk_version=26.2.11394342
28+
29+
ndk=$ANDROID_HOME/ndk/$ndk_version
30+
if ! [ -e $ndk ]; then
31+
log "Installing NDK: this may take several minutes"
32+
yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "ndk;$ndk_version"
33+
fi
34+
35+
if [ $HOST = "arm-linux-androideabi" ]; then
36+
clang_triplet=armv7a-linux-androideabi
37+
else
38+
clang_triplet=$HOST
39+
fi
40+
41+
# These variables are based on BuildSystemMaintainers.md above, and
42+
# $ndk/build/cmake/android.toolchain.cmake.
43+
toolchain=$(echo $ndk/toolchains/llvm/prebuilt/*)
44+
export AR="$toolchain/bin/llvm-ar"
45+
export AS="$toolchain/bin/llvm-as"
46+
export CC="$toolchain/bin/${clang_triplet}${api_level}-clang"
47+
export CXX="${CC}++"
48+
export LD="$toolchain/bin/ld"
49+
export NM="$toolchain/bin/llvm-nm"
50+
export RANLIB="$toolchain/bin/llvm-ranlib"
51+
export READELF="$toolchain/bin/llvm-readelf"
52+
export STRIP="$toolchain/bin/llvm-strip"
53+
54+
# The quotes make sure the wildcard in the `toolchain` assignment has been expanded.
55+
for path in "$AR" "$AS" "$CC" "$CXX" "$LD" "$NM" "$RANLIB" "$READELF" "$STRIP"; do
56+
if ! [ -e "$path" ]; then
57+
fail "$path does not exist"
58+
fi
59+
done
60+
61+
export CFLAGS=""
62+
export LDFLAGS="-Wl,--build-id=sha1 -Wl,--no-rosegment"
63+
64+
# Many packages get away with omitting -lm on Linux, but Android is stricter.
65+
LDFLAGS="$LDFLAGS -lm"
66+
67+
# -mstackrealign is included where necessary in the clang launcher scripts which are
68+
# pointed to by $CC, so we don't need to include it here.
69+
if [ $HOST = "arm-linux-androideabi" ]; then
70+
CFLAGS="$CFLAGS -march=armv7-a -mthumb"
71+
fi
72+
73+
if [ -n "${PREFIX:-}" ]; then
74+
abs_prefix=$(realpath $PREFIX)
75+
CFLAGS="$CFLAGS -I$abs_prefix/include"
76+
LDFLAGS="$LDFLAGS -L$abs_prefix/lib"
77+
78+
export PKG_CONFIG="pkg-config --define-prefix"
79+
export PKG_CONFIG_LIBDIR="$abs_prefix/lib/pkgconfig"
80+
fi
81+
82+
# Use the same variable name as conda-build
83+
if [ $(uname) = "Darwin" ]; then
84+
export CPU_COUNT=$(sysctl -n hw.ncpu)
85+
else
86+
export CPU_COUNT=$(nproc)
87+
fi

Android/android.py

+202
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
import os
5+
import re
6+
import shutil
7+
import subprocess
8+
import sys
9+
import sysconfig
10+
from os.path import relpath
11+
from pathlib import Path
12+
13+
SCRIPT_NAME = Path(__file__).name
14+
CHECKOUT = Path(__file__).resolve().parent.parent
15+
CROSS_BUILD_DIR = CHECKOUT / "cross-build"
16+
17+
18+
def delete_if_exists(path):
19+
if path.exists():
20+
print(f"Deleting {path} ...")
21+
shutil.rmtree(path)
22+
23+
24+
def subdir(name, *, clean=None):
25+
path = CROSS_BUILD_DIR / name
26+
if clean:
27+
delete_if_exists(path)
28+
if not path.exists():
29+
if clean is None:
30+
sys.exit(
31+
f"{path} does not exist. Create it by running the appropriate "
32+
f"`configure` subcommand of {SCRIPT_NAME}.")
33+
else:
34+
path.mkdir(parents=True)
35+
return path
36+
37+
38+
def run(command, *, host=None, **kwargs):
39+
env = os.environ.copy()
40+
if host:
41+
env_script = CHECKOUT / "Android/android-env.sh"
42+
env_output = subprocess.run(
43+
f"set -eu; "
44+
f"HOST={host}; "
45+
f"PREFIX={subdir(host)}/prefix; "
46+
f". {env_script}; "
47+
f"export",
48+
check=True, shell=True, text=True, stdout=subprocess.PIPE
49+
).stdout
50+
51+
for line in env_output.splitlines():
52+
# We don't require every line to match, as there may be some other
53+
# output from installing the NDK.
54+
if match := re.search(
55+
"^(declare -x |export )?(\\w+)=['\"]?(.*?)['\"]?$", line
56+
):
57+
key, value = match[2], match[3]
58+
if env.get(key) != value:
59+
print(line)
60+
env[key] = value
61+
62+
if env == os.environ:
63+
raise ValueError(f"Found no variables in {env_script.name} output:\n"
64+
+ env_output)
65+
66+
print(">", " ".join(map(str, command)))
67+
try:
68+
subprocess.run(command, check=True, env=env, **kwargs)
69+
except subprocess.CalledProcessError as e:
70+
sys.exit(e)
71+
72+
73+
def build_python_path():
74+
"""The path to the build Python binary."""
75+
build_dir = subdir("build")
76+
binary = build_dir / "python"
77+
if not binary.is_file():
78+
binary = binary.with_suffix(".exe")
79+
if not binary.is_file():
80+
raise FileNotFoundError("Unable to find `python(.exe)` in "
81+
f"{build_dir}")
82+
83+
return binary
84+
85+
86+
def configure_build_python(context):
87+
os.chdir(subdir("build", clean=context.clean))
88+
89+
command = [relpath(CHECKOUT / "configure")]
90+
if context.args:
91+
command.extend(context.args)
92+
run(command)
93+
94+
95+
def make_build_python(context):
96+
os.chdir(subdir("build"))
97+
run(["make", "-j", str(os.cpu_count())])
98+
99+
100+
def unpack_deps(host):
101+
deps_url = "https://github.com/beeware/cpython-android-source-deps/releases/download"
102+
for name_ver in ["bzip2-1.0.8-1", "libffi-3.4.4-2", "openssl-3.0.13-1",
103+
"sqlite-3.45.1-0", "xz-5.4.6-0"]:
104+
filename = f"{name_ver}-{host}.tar.gz"
105+
run(["wget", f"{deps_url}/{name_ver}/{filename}"])
106+
run(["tar", "-xf", filename])
107+
os.remove(filename)
108+
109+
110+
def configure_host_python(context):
111+
host_dir = subdir(context.host, clean=context.clean)
112+
113+
prefix_dir = host_dir / "prefix"
114+
if not prefix_dir.exists():
115+
prefix_dir.mkdir()
116+
os.chdir(prefix_dir)
117+
unpack_deps(context.host)
118+
119+
build_dir = host_dir / "build"
120+
build_dir.mkdir(exist_ok=True)
121+
os.chdir(build_dir)
122+
123+
command = [
124+
# Basic cross-compiling configuration
125+
relpath(CHECKOUT / "configure"),
126+
f"--host={context.host}",
127+
f"--build={sysconfig.get_config_var('BUILD_GNU_TYPE')}",
128+
f"--with-build-python={build_python_path()}",
129+
"--without-ensurepip",
130+
131+
# Android always uses a shared libpython.
132+
"--enable-shared",
133+
"--without-static-libpython",
134+
135+
# Dependent libraries. The others are found using pkg-config: see
136+
# android-env.sh.
137+
f"--with-openssl={prefix_dir}",
138+
]
139+
140+
if context.args:
141+
command.extend(context.args)
142+
run(command, host=context.host)
143+
144+
145+
def make_host_python(context):
146+
host_dir = subdir(context.host)
147+
os.chdir(host_dir / "build")
148+
run(["make", "-j", str(os.cpu_count())], host=context.host)
149+
run(["make", "install", f"prefix={host_dir}/prefix"], host=context.host)
150+
151+
152+
def build_all(context):
153+
steps = [configure_build_python, make_build_python, configure_host_python,
154+
make_host_python]
155+
for step in steps:
156+
step(context)
157+
158+
159+
def clean_all(context):
160+
delete_if_exists(CROSS_BUILD_DIR)
161+
162+
163+
def main():
164+
parser = argparse.ArgumentParser()
165+
subcommands = parser.add_subparsers(dest="subcommand")
166+
build = subcommands.add_parser("build", help="Build everything")
167+
configure_build = subcommands.add_parser("configure-build",
168+
help="Run `configure` for the "
169+
"build Python")
170+
make_build = subcommands.add_parser("make-build",
171+
help="Run `make` for the build Python")
172+
configure_host = subcommands.add_parser("configure-host",
173+
help="Run `configure` for Android")
174+
make_host = subcommands.add_parser("make-host",
175+
help="Run `make` for Android")
176+
clean = subcommands.add_parser("clean", help="Delete files and directories "
177+
"created by this script")
178+
for subcommand in build, configure_build, configure_host:
179+
subcommand.add_argument(
180+
"--clean", action="store_true", default=False, dest="clean",
181+
help="Delete any relevant directories before building")
182+
for subcommand in build, configure_host, make_host:
183+
subcommand.add_argument(
184+
"host", metavar="HOST",
185+
choices=["aarch64-linux-android", "x86_64-linux-android"],
186+
help="Host triplet: choices=[%(choices)s]")
187+
for subcommand in build, configure_build, configure_host:
188+
subcommand.add_argument("args", nargs="*",
189+
help="Extra arguments to pass to `configure`")
190+
191+
context = parser.parse_args()
192+
dispatch = {"configure-build": configure_build_python,
193+
"make-build": make_build_python,
194+
"configure-host": configure_host_python,
195+
"make-host": make_host_python,
196+
"build": build_all,
197+
"clean": clean_all}
198+
dispatch[context.subcommand](context)
199+
200+
201+
if __name__ == "__main__":
202+
main()

Include/cpython/pystate.h

+4
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,10 @@ struct _ts {
211211
# define Py_C_RECURSION_LIMIT 800
212212
#elif defined(_WIN32)
213213
# define Py_C_RECURSION_LIMIT 3000
214+
#elif defined(__ANDROID__)
215+
// On an ARM64 emulator, API level 34 was OK with 10000, but API level 21
216+
// crashed in test_compiler_recursion_limit.
217+
# define Py_C_RECURSION_LIMIT 3000
214218
#elif defined(_Py_ADDRESS_SANITIZER)
215219
# define Py_C_RECURSION_LIMIT 4000
216220
#else
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add Android build script and instructions.

0 commit comments

Comments
 (0)