gh-131531: Add android.py package command (#131532)

Adds a `package` entry point to the `android.py` build script to support
creating an Android distribution artefact.
This commit is contained in:
Malcolm Smith 2025-04-01 01:46:29 +01:00 committed by GitHub
parent 45a3ab5a81
commit fe5c4c53e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 287 additions and 124 deletions

View file

@ -1,19 +1,22 @@
# Python for Android # Python for Android
These instructions are only needed if you're planning to compile Python for If you obtained this README as part of a release package, then the only
Android yourself. Most users should *not* need to do this. Instead, use one of applicable sections are "Prerequisites", "Testing", and "Using in your own app".
the tools listed in `Doc/using/android.rst`, which will provide a much easier
experience. If you obtained this README as part of the CPython source tree, then you can
also follow the other sections to compile Python for Android yourself.
However, most app developers should not need to do any of these things manually.
Instead, use one of the tools listed
[here](https://docs.python.org/3/using/android.html), which will provide a much
easier experience.
## Prerequisites ## Prerequisites
First, make sure you have all the usual tools and libraries needed to build If you already have an Android SDK installed, export the `ANDROID_HOME`
Python for your development machine. environment variable to point at its location. Otherwise, here's how to install
it:
Second, you'll need an Android SDK. If you already have the SDK installed,
export the `ANDROID_HOME` environment variable to point at its location.
Otherwise, here's how to install it:
* Download the "Command line tools" from <https://developer.android.com/studio>. * Download the "Command line tools" from <https://developer.android.com/studio>.
* Create a directory `android-sdk/cmdline-tools`, and unzip the command line * Create a directory `android-sdk/cmdline-tools`, and unzip the command line
@ -27,15 +30,16 @@ The `android.py` script also requires the following commands to be on the `PATH`
* `curl` * `curl`
* `java` (or set the `JAVA_HOME` environment variable) * `java` (or set the `JAVA_HOME` environment variable)
* `tar` * `tar`
* `unzip`
## Building ## Building
Python can be built for Android on any POSIX platform supported by the Android Python can be built for Android on any POSIX platform supported by the Android
development tools, which currently means Linux or macOS. This involves doing a development tools, which currently means Linux or macOS.
cross-build where you use a "build" Python (for your development machine) to
help produce a "host" Python for Android. First we'll make a "build" Python (for your development machine), then use it to
help produce a "host" Python for Android. So make sure you have all the usual
tools and libraries needed to build Python for your development machine.
The easiest way to do a build is to use the `android.py` script. You can either 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 have it perform the entire build process from start to finish in one step, or
@ -60,8 +64,8 @@ To do all steps in a single command, run:
./android.py build HOST ./android.py build HOST
``` ```
In the end you should have a build Python in `cross-build/build`, and an Android In the end you should have a build Python in `cross-build/build`, and a host
build in `cross-build/HOST`. Python in `cross-build/HOST`.
You can use `--` as a separator for any of the `configure`-related commands You can use `--` as a separator for any of the `configure`-related commands
including `build` itself to pass arguments to the underlying `configure` including `build` itself to pass arguments to the underlying `configure`
@ -73,14 +77,27 @@ call. For example, if you want a pydebug build that also caches the results from
``` ```
## Packaging
After building an architecture as described in the section above, you can
package it for release with this command:
```sh
./android.py package HOST
```
`HOST` is defined in the section above.
This will generate a tarball in `cross-build/HOST/dist`, whose structure is
similar to the `Android` directory of the CPython source tree.
## Testing ## Testing
The test suite can be run on Linux, macOS, or Windows: The Python test suite can be run on Linux, macOS, or Windows:
* On Linux, the emulator needs access to the KVM virtualization interface, and * On Linux, the emulator needs access to the KVM virtualization interface, and
a DISPLAY environment variable pointing at an X server. a DISPLAY environment variable pointing at an X server.
* On Windows, you won't be able to do the build on the same machine, so you'll
have to copy the `cross-build/HOST` directory from somewhere else.
The test suite can usually be run on a device with 2 GB of RAM, but this is The test suite can usually be run on a device with 2 GB of RAM, but this is
borderline, so you may need to increase it to 4 GB. As of Android borderline, so you may need to increase it to 4 GB. As of Android
@ -90,9 +107,16 @@ and find `hw.ramSize` in both config.ini and hardware-qemu.ini. Either set these
manually to the same value, or use the Android Studio Device Manager, which will manually to the same value, or use the Android Studio Device Manager, which will
update both files. update both files.
Before running the test suite, follow the instructions in the previous section You can run the test suite either:
to build the architecture you want to test. Then run the test script in one of
the following modes: * Within the CPython repository, after doing a build as described above. On
Windows, you won't be able to do the build on the same machine, so you'll have
to copy the `cross-build/HOST/prefix` directory from somewhere else.
* Or by taking a release package built using the `package` command, extracting
it wherever you want, and using its own copy of `android.py`.
The test script supports the following modes:
* In `--connected` mode, it runs on a device or emulator you have already * In `--connected` mode, it runs on a device or emulator you have already
connected to the build machine. List the available devices with connected to the build machine. List the available devices with
@ -133,4 +157,4 @@ until you re-run `android.py make-host` or `build`.
## Using in your own app ## Using in your own app
See `Doc/using/android.rst`. See https://docs.python.org/3/using/android.html.

View file

@ -2,7 +2,6 @@
import asyncio import asyncio
import argparse import argparse
from glob import glob
import os import os
import re import re
import shlex import shlex
@ -13,6 +12,8 @@ import sys
import sysconfig import sysconfig
from asyncio import wait_for from asyncio import wait_for
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from datetime import datetime, timezone
from glob import glob
from os.path import basename, relpath from os.path import basename, relpath
from pathlib import Path from pathlib import Path
from subprocess import CalledProcessError from subprocess import CalledProcessError
@ -20,11 +21,12 @@ from tempfile import TemporaryDirectory
SCRIPT_NAME = Path(__file__).name SCRIPT_NAME = Path(__file__).name
CHECKOUT = Path(__file__).resolve().parent.parent ANDROID_DIR = Path(__file__).resolve().parent
ANDROID_DIR = CHECKOUT / "Android" CHECKOUT = ANDROID_DIR.parent
TESTBED_DIR = ANDROID_DIR / "testbed" TESTBED_DIR = ANDROID_DIR / "testbed"
CROSS_BUILD_DIR = CHECKOUT / "cross-build" CROSS_BUILD_DIR = CHECKOUT / "cross-build"
HOSTS = ["aarch64-linux-android", "x86_64-linux-android"]
APP_ID = "org.python.testbed" APP_ID = "org.python.testbed"
DECODE_ARGS = ("UTF-8", "backslashreplace") DECODE_ARGS = ("UTF-8", "backslashreplace")
@ -58,12 +60,10 @@ def delete_glob(pattern):
path.unlink() path.unlink()
def subdir(name, *, clean=None): def subdir(*parts, create=False):
path = CROSS_BUILD_DIR / name path = CROSS_BUILD_DIR.joinpath(*parts)
if clean:
delete_glob(path)
if not path.exists(): if not path.exists():
if clean is None: if not create:
sys.exit( sys.exit(
f"{path} does not exist. Create it by running the appropriate " f"{path} does not exist. Create it by running the appropriate "
f"`configure` subcommand of {SCRIPT_NAME}.") f"`configure` subcommand of {SCRIPT_NAME}.")
@ -123,7 +123,9 @@ def build_python_path():
def configure_build_python(context): def configure_build_python(context):
os.chdir(subdir("build", clean=context.clean)) if context.clean:
clean("build")
os.chdir(subdir("build", create=True))
command = [relpath(CHECKOUT / "configure")] command = [relpath(CHECKOUT / "configure")]
if context.args: if context.args:
@ -153,18 +155,17 @@ def download(url, target_dir="."):
def configure_host_python(context): def configure_host_python(context):
host_dir = subdir(context.host, clean=context.clean) if context.clean:
clean(context.host)
host_dir = subdir(context.host, create=True)
prefix_dir = host_dir / "prefix" prefix_dir = host_dir / "prefix"
if not prefix_dir.exists(): if not prefix_dir.exists():
prefix_dir.mkdir() prefix_dir.mkdir()
os.chdir(prefix_dir) os.chdir(prefix_dir)
unpack_deps(context.host) unpack_deps(context.host)
build_dir = host_dir / "build" os.chdir(host_dir)
build_dir.mkdir(exist_ok=True)
os.chdir(build_dir)
command = [ command = [
# Basic cross-compiling configuration # Basic cross-compiling configuration
relpath(CHECKOUT / "configure"), relpath(CHECKOUT / "configure"),
@ -193,11 +194,10 @@ def make_host_python(context):
# the build. # the build.
host_dir = subdir(context.host) host_dir = subdir(context.host)
prefix_dir = host_dir / "prefix" prefix_dir = host_dir / "prefix"
delete_glob(f"{prefix_dir}/include/python*") for pattern in ("include/python*", "lib/libpython*", "lib/python*"):
delete_glob(f"{prefix_dir}/lib/libpython*") delete_glob(f"{prefix_dir}/{pattern}")
delete_glob(f"{prefix_dir}/lib/python*")
os.chdir(host_dir / "build") os.chdir(host_dir)
run(["make", "-j", str(os.cpu_count())], host=context.host) run(["make", "-j", str(os.cpu_count())], host=context.host)
run(["make", "install", f"prefix={prefix_dir}"], host=context.host) run(["make", "install", f"prefix={prefix_dir}"], host=context.host)
@ -209,8 +209,13 @@ def build_all(context):
step(context) step(context)
def clean(host):
delete_glob(CROSS_BUILD_DIR / host)
def clean_all(context): def clean_all(context):
delete_glob(CROSS_BUILD_DIR) for host in HOSTS + ["build"]:
clean(host)
def setup_sdk(): def setup_sdk():
@ -234,31 +239,27 @@ def setup_sdk():
# To avoid distributing compiled artifacts without corresponding source code, # To avoid distributing compiled artifacts without corresponding source code,
# the Gradle wrapper is not included in the CPython repository. Instead, we # the Gradle wrapper is not included in the CPython repository. Instead, we
# extract it from the Gradle release. # extract it from the Gradle GitHub repository.
def setup_testbed(): def setup_testbed():
if all((TESTBED_DIR / path).exists() for path in [ # The Gradle version used for the build is specified in
"gradlew", "gradlew.bat", "gradle/wrapper/gradle-wrapper.jar", # testbed/gradle/wrapper/gradle-wrapper.properties. This wrapper version
]): # doesn't need to match, as any version of the wrapper can download any
# version of Gradle.
version = "8.9.0"
paths = ["gradlew", "gradlew.bat", "gradle/wrapper/gradle-wrapper.jar"]
if all((TESTBED_DIR / path).exists() for path in paths):
return return
ver_long = "8.7.0" for path in paths:
ver_short = ver_long.removesuffix(".0") out_path = TESTBED_DIR / path
out_path.parent.mkdir(exist_ok=True)
for filename in ["gradlew", "gradlew.bat"]: download(
out_path = download( f"https://raw.githubusercontent.com/gradle/gradle/v{version}/{path}",
f"https://raw.githubusercontent.com/gradle/gradle/v{ver_long}/{filename}", out_path.parent,
TESTBED_DIR) )
os.chmod(out_path, 0o755) os.chmod(out_path, 0o755)
with TemporaryDirectory(prefix=SCRIPT_NAME) as temp_dir:
bin_zip = download(
f"https://services.gradle.org/distributions/gradle-{ver_short}-bin.zip",
temp_dir)
outer_jar = f"gradle-{ver_short}/lib/plugins/gradle-wrapper-{ver_short}.jar"
run(["unzip", "-d", temp_dir, bin_zip, outer_jar])
run(["unzip", "-o", "-d", f"{TESTBED_DIR}/gradle/wrapper",
f"{temp_dir}/{outer_jar}", "gradle-wrapper.jar"])
# run_testbed will build the app automatically, but it's useful to have this as # run_testbed will build the app automatically, but it's useful to have this as
# a separate command to allow running the app outside of this script. # a separate command to allow running the app outside of this script.
@ -538,6 +539,73 @@ async def run_testbed(context):
raise e.exceptions[0] raise e.exceptions[0]
def package_version(prefix_dir):
patchlevel_glob = f"{prefix_dir}/include/python*/patchlevel.h"
patchlevel_paths = glob(patchlevel_glob)
if len(patchlevel_paths) != 1:
sys.exit(f"{patchlevel_glob} matched {len(patchlevel_paths)} paths.")
for line in open(patchlevel_paths[0]):
if match := re.fullmatch(r'\s*#define\s+PY_VERSION\s+"(.+)"\s*', line):
version = match[1]
break
else:
sys.exit(f"Failed to find Python version in {patchlevel_paths[0]}.")
# If not building against a tagged commit, add a timestamp to the version.
# Follow the PyPA version number rules, as this will make it easier to
# process with other tools.
if version.endswith("+"):
version += datetime.now(timezone.utc).strftime("%Y%m%d.%H%M%S")
return version
def package(context):
prefix_dir = subdir(context.host, "prefix")
version = package_version(prefix_dir)
with TemporaryDirectory(prefix=SCRIPT_NAME) as temp_dir:
temp_dir = Path(temp_dir)
# Include all tracked files from the Android directory.
for line in run(
["git", "ls-files"],
cwd=ANDROID_DIR, capture_output=True, text=True, log=False,
).stdout.splitlines():
src = ANDROID_DIR / line
dst = temp_dir / line
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dst, follow_symlinks=False)
# Include anything from the prefix directory which could be useful
# either for embedding Python in an app, or building third-party
# packages against it.
for rel_dir, patterns in [
("include", ["openssl*", "python*", "sqlite*"]),
("lib", ["engines-3", "libcrypto*.so", "libpython*", "libsqlite*",
"libssl*.so", "ossl-modules", "python*"]),
("lib/pkgconfig", ["*crypto*", "*ssl*", "*python*", "*sqlite*"]),
]:
for pattern in patterns:
for src in glob(f"{prefix_dir}/{rel_dir}/{pattern}"):
dst = temp_dir / relpath(src, prefix_dir.parent)
dst.parent.mkdir(parents=True, exist_ok=True)
if Path(src).is_dir():
shutil.copytree(
src, dst, symlinks=True,
ignore=lambda *args: ["__pycache__"]
)
else:
shutil.copy2(src, dst, follow_symlinks=False)
dist_dir = subdir(context.host, "dist", create=True)
package_path = shutil.make_archive(
f"{dist_dir}/python-{version}-{context.host}", "gztar", temp_dir
)
print(f"Wrote {package_path}")
# Handle SIGTERM the same way as SIGINT. This ensures that if we're terminated # Handle SIGTERM the same way as SIGINT. This ensures that if we're terminated
# by the buildbot worker, we'll make an attempt to clean up our subprocesses. # by the buildbot worker, we'll make an attempt to clean up our subprocesses.
def install_signal_handler(): def install_signal_handler():
@ -550,6 +618,8 @@ def install_signal_handler():
def parse_args(): def parse_args():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
subcommands = parser.add_subparsers(dest="subcommand") subcommands = parser.add_subparsers(dest="subcommand")
# Subcommands
build = subcommands.add_parser("build", help="Build everything") build = subcommands.add_parser("build", help="Build everything")
configure_build = subcommands.add_parser("configure-build", configure_build = subcommands.add_parser("configure-build",
help="Run `configure` for the " help="Run `configure` for the "
@ -561,25 +631,27 @@ def parse_args():
make_host = subcommands.add_parser("make-host", make_host = subcommands.add_parser("make-host",
help="Run `make` for Android") help="Run `make` for Android")
subcommands.add_parser( subcommands.add_parser(
"clean", help="Delete the cross-build directory") "clean", help="Delete all build and prefix directories")
subcommands.add_parser(
"build-testbed", help="Build the testbed app")
test = subcommands.add_parser(
"test", help="Run the test suite")
package = subcommands.add_parser("package", help="Make a release package")
# Common arguments
for subcommand in build, configure_build, configure_host: for subcommand in build, configure_build, configure_host:
subcommand.add_argument( subcommand.add_argument(
"--clean", action="store_true", default=False, dest="clean", "--clean", action="store_true", default=False, dest="clean",
help="Delete any relevant directories before building") help="Delete the relevant build and prefix directories first")
for subcommand in build, configure_host, make_host: for subcommand in [build, configure_host, make_host, package]:
subcommand.add_argument( subcommand.add_argument(
"host", metavar="HOST", "host", metavar="HOST", choices=HOSTS,
choices=["aarch64-linux-android", "x86_64-linux-android"],
help="Host triplet: choices=[%(choices)s]") help="Host triplet: choices=[%(choices)s]")
for subcommand in build, configure_build, configure_host: for subcommand in build, configure_build, configure_host:
subcommand.add_argument("args", nargs="*", subcommand.add_argument("args", nargs="*",
help="Extra arguments to pass to `configure`") help="Extra arguments to pass to `configure`")
subcommands.add_parser( # Test arguments
"build-testbed", help="Build the testbed app")
test = subcommands.add_parser(
"test", help="Run the test suite")
test.add_argument( test.add_argument(
"-v", "--verbose", action="count", default=0, "-v", "--verbose", action="count", default=0,
help="Show Gradle output, and non-Python logcat messages. " help="Show Gradle output, and non-Python logcat messages. "
@ -608,14 +680,17 @@ def main():
stream.reconfigure(line_buffering=True) stream.reconfigure(line_buffering=True)
context = parse_args() context = parse_args()
dispatch = {"configure-build": configure_build_python, dispatch = {
"make-build": make_build_python, "configure-build": configure_build_python,
"configure-host": configure_host_python, "make-build": make_build_python,
"make-host": make_host_python, "configure-host": configure_host_python,
"build": build_all, "make-host": make_host_python,
"clean": clean_all, "build": build_all,
"build-testbed": build_testbed, "clean": clean_all,
"test": run_testbed} "build-testbed": build_testbed,
"test": run_testbed,
"package": package,
}
try: try:
result = dispatch[context.subcommand](context) result = dispatch[context.subcommand](context)

View file

@ -1,18 +1,19 @@
# The Gradle wrapper should be downloaded by running `../android.py setup-testbed`. # The Gradle wrapper can be downloaded by running the `test` or `build-testbed`
# commands of android.py.
/gradlew /gradlew
/gradlew.bat /gradlew.bat
/gradle/wrapper/gradle-wrapper.jar /gradle/wrapper/gradle-wrapper.jar
# The repository's top-level .gitignore file ignores all .idea directories, but
# we want to keep any files which can't be regenerated from the Gradle
# configuration.
!.idea/
/.idea/*
!/.idea/inspectionProfiles
*.iml *.iml
.gradle .gradle
/local.properties /local.properties
/.idea/caches
/.idea/deploymentTargetDropdown.xml
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store .DS_Store
/build /build
/captures /captures

View file

@ -0,0 +1,8 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="AndroidLintGradleDependency" enabled="true" level="WEAK WARNING" enabled_by_default="true" editorAttributes="INFO_ATTRIBUTES" />
<inspection_tool class="AndroidLintOldTargetApi" enabled="true" level="WEAK WARNING" enabled_by_default="true" editorAttributes="INFO_ATTRIBUTES" />
<inspection_tool class="UnstableApiUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true" editorAttributes="INFO_ATTRIBUTES" />
</profile>
</component>

View file

@ -6,28 +6,71 @@ plugins {
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
} }
val PYTHON_DIR = file("../../..").canonicalPath val ANDROID_DIR = file("../..")
val PYTHON_CROSS_DIR = "$PYTHON_DIR/cross-build" val PYTHON_DIR = ANDROID_DIR.parentFile!!
val PYTHON_CROSS_DIR = file("$PYTHON_DIR/cross-build")
val inSourceTree = (
ANDROID_DIR.name == "Android" && file("$PYTHON_DIR/pyconfig.h.in").exists()
)
val ABIS = mapOf( val KNOWN_ABIS = mapOf(
"arm64-v8a" to "aarch64-linux-android", "aarch64-linux-android" to "arm64-v8a",
"x86_64" to "x86_64-linux-android", "x86_64-linux-android" to "x86_64",
).filter { file("$PYTHON_CROSS_DIR/${it.value}").exists() } )
if (ABIS.isEmpty()) {
// Discover prefixes.
val prefixes = ArrayList<File>()
if (inSourceTree) {
for ((triplet, _) in KNOWN_ABIS.entries) {
val prefix = file("$PYTHON_CROSS_DIR/$triplet/prefix")
if (prefix.exists()) {
prefixes.add(prefix)
}
}
} else {
// Testbed is inside a release package.
val prefix = file("$ANDROID_DIR/prefix")
if (prefix.exists()) {
prefixes.add(prefix)
}
}
if (prefixes.isEmpty()) {
throw GradleException( throw GradleException(
"No Android ABIs found in $PYTHON_CROSS_DIR: see Android/README.md " + "No Android prefixes found: see README.md for testing instructions"
"for building instructions."
) )
} }
val PYTHON_VERSION = file("$PYTHON_DIR/Include/patchlevel.h").useLines { // Detect Python versions and ABIs.
for (line in it) { lateinit var pythonVersion: String
val match = """#define PY_VERSION\s+"(\d+\.\d+)""".toRegex().find(line) var abis = HashMap<File, String>()
if (match != null) { for ((i, prefix) in prefixes.withIndex()) {
return@useLines match.groupValues[1] val libDir = file("$prefix/lib")
val version = run {
for (filename in libDir.list()!!) {
"""python(\d+\.\d+)""".toRegex().matchEntire(filename)?.let {
return@run it.groupValues[1]
}
} }
throw GradleException("Failed to find Python version in $libDir")
} }
throw GradleException("Failed to find Python version") if (i == 0) {
pythonVersion = version
} else if (pythonVersion != version) {
throw GradleException(
"${prefixes[0]} is Python $pythonVersion, but $prefix is Python $version"
)
}
val libPythonDir = file("$libDir/python$pythonVersion")
val triplet = run {
for (filename in libPythonDir.list()!!) {
"""_sysconfigdata__android_(.+).py""".toRegex().matchEntire(filename)?.let {
return@run it.groupValues[1]
}
}
throw GradleException("Failed to find Python triplet in $libPythonDir")
}
abis[prefix] = KNOWN_ABIS[triplet]!!
} }
@ -53,10 +96,16 @@ android {
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
ndk.abiFilters.addAll(ABIS.keys) ndk.abiFilters.addAll(abis.values)
externalNativeBuild.cmake.arguments( externalNativeBuild.cmake.arguments(
"-DPYTHON_CROSS_DIR=$PYTHON_CROSS_DIR", "-DPYTHON_PREFIX_DIR=" + if (inSourceTree) {
"-DPYTHON_VERSION=$PYTHON_VERSION", // AGP uses the ${} syntax for its own purposes, so use a Jinja style
// placeholder.
"$PYTHON_CROSS_DIR/{{triplet}}/prefix"
} else {
prefixes[0]
},
"-DPYTHON_VERSION=$pythonVersion",
"-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON",
) )
@ -133,24 +182,25 @@ dependencies {
// Create some custom tasks to copy Python and its standard library from // Create some custom tasks to copy Python and its standard library from
// elsewhere in the repository. // elsewhere in the repository.
androidComponents.onVariants { variant -> androidComponents.onVariants { variant ->
val pyPlusVer = "python$PYTHON_VERSION" val pyPlusVer = "python$pythonVersion"
generateTask(variant, variant.sources.assets!!) { generateTask(variant, variant.sources.assets!!) {
into("python") { into("python") {
// Include files such as pyconfig.h are used by some of the tests.
into("include/$pyPlusVer") { into("include/$pyPlusVer") {
for (triplet in ABIS.values) { for (prefix in prefixes) {
from("$PYTHON_CROSS_DIR/$triplet/prefix/include/$pyPlusVer") from("$prefix/include/$pyPlusVer")
} }
duplicatesStrategy = DuplicatesStrategy.EXCLUDE duplicatesStrategy = DuplicatesStrategy.EXCLUDE
} }
into("lib/$pyPlusVer") { into("lib/$pyPlusVer") {
// To aid debugging, the source directory takes priority. // To aid debugging, the source directory takes priority when
from("$PYTHON_DIR/Lib") // running inside a CPython source tree.
if (inSourceTree) {
// The cross-build directory provides ABI-specific files such as from("$PYTHON_DIR/Lib")
// sysconfigdata. }
for (triplet in ABIS.values) { for (prefix in prefixes) {
from("$PYTHON_CROSS_DIR/$triplet/prefix/lib/$pyPlusVer") from("$prefix/lib/$pyPlusVer")
} }
into("site-packages") { into("site-packages") {
@ -164,9 +214,9 @@ androidComponents.onVariants { variant ->
} }
generateTask(variant, variant.sources.jniLibs!!) { generateTask(variant, variant.sources.jniLibs!!) {
for ((abi, triplet) in ABIS.entries) { for ((prefix, abi) in abis.entries) {
into(abi) { into(abi) {
from("$PYTHON_CROSS_DIR/$triplet/prefix/lib") from("$prefix/lib")
include("libpython*.*.so") include("libpython*.*.so")
include("lib*_python.so") include("lib*_python.so")
} }

View file

@ -1,9 +1,14 @@
cmake_minimum_required(VERSION 3.4.1) cmake_minimum_required(VERSION 3.4.1)
project(testbed) project(testbed)
set(PREFIX_DIR ${PYTHON_CROSS_DIR}/${CMAKE_LIBRARY_ARCHITECTURE}/prefix) # Resolve variables from the command line.
include_directories(${PREFIX_DIR}/include/python${PYTHON_VERSION}) string(
link_directories(${PREFIX_DIR}/lib) REPLACE {{triplet}} ${CMAKE_LIBRARY_ARCHITECTURE}
PYTHON_PREFIX_DIR ${PYTHON_PREFIX_DIR}
)
include_directories(${PYTHON_PREFIX_DIR}/include/python${PYTHON_VERSION})
link_directories(${PYTHON_PREFIX_DIR}/lib)
link_libraries(log python${PYTHON_VERSION}) link_libraries(log python${PYTHON_VERSION})
add_library(main_activity SHARED main_activity.c) add_library(main_activity SHARED main_activity.c)

View file

@ -27,9 +27,8 @@ details.
Adding Python to an Android app Adding Python to an Android app
------------------------------- -------------------------------
These instructions are only needed if you're planning to compile Python for Most app developers should use one of the following tools, which will provide a
Android yourself. Most users should *not* need to do this. Instead, use one of much easier experience:
the following tools, which will provide a much easier experience:
* `Briefcase <https://briefcase.readthedocs.io>`__, from the BeeWare project * `Briefcase <https://briefcase.readthedocs.io>`__, from the BeeWare project
* `Buildozer <https://buildozer.readthedocs.io>`__, from the Kivy project * `Buildozer <https://buildozer.readthedocs.io>`__, from the Kivy project
@ -42,10 +41,11 @@ If you're sure you want to do all of this manually, read on. You can use the
link to the relevant file. link to the relevant file.
* Build Python by following the instructions in :source:`Android/README.md`. * Build Python by following the instructions in :source:`Android/README.md`.
This will create the directory ``cross-build/HOST/prefix``.
* Add code to your :source:`build.gradle <Android/testbed/app/build.gradle.kts>` * Add code to your :source:`build.gradle <Android/testbed/app/build.gradle.kts>`
file to copy the following items into your project. All except your own Python file to copy the following items into your project. All except your own Python
code can be copied from ``cross-build/HOST/prefix/lib``: code can be copied from ``prefix/lib``:
* In your JNI libraries: * In your JNI libraries: