-
-
Notifications
You must be signed in to change notification settings - Fork 31.9k
gh-71052: Add Android build script #115576
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
+456
−70
Closed
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
07fab81
Fix Android linker issues
mhsmith 0da8532
Merge branch 'main' into android-link
mhsmith 5f7c39e
More Android linker fixes
mhsmith d0ad0df
Add Android build script and README
mhsmith 90eec1e
Fix build on Linux
mhsmith ec59dd1
Add news entry
mhsmith 87f85b9
Merge branch 'main' into android-link
mhsmith a646e2b
Fix typos
mhsmith File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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-1", "openssl-3.0.13-0", | ||
"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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 2 additions & 0 deletions
2
Misc/NEWS.d/next/Build/2024-02-16-22-26-00.gh-issue-71052.HEfpsm.rst
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
Add instructions and a script to build for Android, and fix related build | ||
issues. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these dependencies patched? Could you use https://github.com/python/cpython-source-deps/ instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They have a couple of small patches, which are in the cpython-android-source-deps repository along with build scripts for each library.
cpython-source-deps and cpython-bin-deps are currently focused only on the Windows releases for python.org. But we're not planning to make such a release for Android, at least not in the Python 3.13 cycle. The main purposes of the android.py script are:
So I don't think we need to follow the same level of rigor around the dependencies as we do for Windows. Anyone who doesn't want to use the BeeWare binary releases is free to examine the scripts and build the libraries from source.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right that
cpython-source-deps
is currently being used by the Windows installer only, but there's been talk about using it for WASM and/or macOS as well.That also true for both macOS and Windows.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW: I've got an analogous project for iOS binary dependencies. There are some small patches for libFFI (which may not be required any more with the release of 3.4.5), XZ, and OpenSSL; the real complication is the build process itself, because of the need to produce fat binaries. libFFI is the only dependency that ships with explicit iOS build tooling, and even then, it requires a lot more work than
./configure && make
It hasn't come up in the iOS patch process yet; but for the PEP 730 work, these dependencies are only required as part of the buildbot, so my intention was to avoid the issue with a Jedi-mind-trick-esque "you'll need a build of libFFI etc" in the docs - essentially treating these artefacts are part of the tooling producing BeeWare artefacts, which is used for builedbot validation process. My thinking was if/when we get to the point where CPython is producing official iOS binaries the BeeWare repo would be donated upstream (or retooled to fit into the existing cpython-source-deps/cpython-bin-deps repos)