Skip to content

Commit a467f0a

Browse files
committed
Add android.py package command
1 parent cabe9ec commit a467f0a

File tree

4 files changed

+140
-45
lines changed

4 files changed

+140
-45
lines changed

Android/README.md

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
# Python for Android
22

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. Instead, use one of
5-
the tools listed in `Doc/using/android.rst`, which will provide a much easier
6-
experience.
3+
If you obtained this README as part of a release package, then the only
4+
applicable sections are "Prerequisites", "Testing", and "Using in your own app".
75

6+
If you obtained this README as part of the CPython source tree, then you can
7+
also follow the other sections to compile Python for Android yourself.
8+
9+
However, most app developers should not need to do any of these things manually.
10+
Instead, use one of the tools listed
11+
[here](https://docs.python.org/3/using/android.html), which will provide a much
12+
easier experience.
813

9-
## Prerequisites
1014

11-
First, make sure you have all the usual tools and libraries needed to build
12-
Python for your development machine.
15+
## Prerequisites
1316

14-
Second, you'll need an Android SDK. If you already have the SDK installed,
15-
export the `ANDROID_HOME` environment variable to point at its location.
16-
Otherwise, here's how to install it:
17+
If you already have an Android SDK installed, export the `ANDROID_HOME`
18+
environment variable to point at its location. Otherwise, here's how to install
19+
it:
1720

1821
* Download the "Command line tools" from <https://developer.android.com/studio>.
1922
* Create a directory `android-sdk/cmdline-tools`, and unzip the command line
@@ -33,9 +36,11 @@ The `android.py` script also requires the following commands to be on the `PATH`
3336
## Building
3437

3538
Python can be built for Android on any POSIX platform supported by the Android
36-
development tools, which currently means Linux or macOS. This involves doing a
37-
cross-build where you use a "build" Python (for your development machine) to
38-
help produce a "host" Python for Android.
39+
development tools, which currently means Linux or macOS.
40+
41+
First we'll make a "build" Python (for your development machine), then use it to
42+
help produce a "host" Python for Android. So make sure you have all the usual
43+
tools and libraries needed to build Python for your development machine.
3944

4045
The easiest way to do a build is to use the `android.py` script. You can either
4146
have it perform the entire build process from start to finish in one step, or
@@ -73,9 +78,24 @@ call. For example, if you want a pydebug build that also caches the results from
7378
```
7479

7580

81+
## Packaging
82+
83+
After building an architecture as described in the section above, you can
84+
package it for release with this command:
85+
86+
```sh
87+
./android.py package HOST
88+
```
89+
90+
`HOST` is defined in the section above.
91+
92+
This will generate a tarball in the `dist` directory, whose structure is similar
93+
to the `Android` directory of the CPython source tree.
94+
95+
7696
## Testing
7797

78-
The test suite can be run on Linux, macOS, or Windows:
98+
The Python test suite can be run on Linux, macOS, or Windows:
7999

80100
* On Linux, the emulator needs access to the KVM virtualization interface, and
81101
a DISPLAY environment variable pointing at an X server.
@@ -90,7 +110,7 @@ and find `hw.ramSize` in both config.ini and hardware-qemu.ini. Either set these
90110
manually to the same value, or use the Android Studio Device Manager, which will
91111
update both files.
92112

93-
Before running the test suite, follow the instructions in the previous section
113+
Before running the test suite, follow the instructions in the section above
94114
to build the architecture you want to test. Then run the test script in one of
95115
the following modes:
96116

@@ -133,4 +153,4 @@ until you re-run `android.py make-host` or `build`.
133153

134154
## Using in your own app
135155

136-
See `Doc/using/android.rst`.
156+
See https://docs.python.org/3/using/android.html.

Android/android.py

Lines changed: 90 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import asyncio
44
import argparse
5-
from glob import glob
5+
import json
66
import os
77
import re
88
import shlex
@@ -13,15 +13,17 @@
1313
import sysconfig
1414
from asyncio import wait_for
1515
from contextlib import asynccontextmanager
16+
from datetime import datetime, timezone
17+
from glob import glob
1618
from os.path import basename, relpath
1719
from pathlib import Path
1820
from subprocess import CalledProcessError
1921
from tempfile import TemporaryDirectory
2022

2123

2224
SCRIPT_NAME = Path(__file__).name
23-
CHECKOUT = Path(__file__).resolve().parent.parent
24-
ANDROID_DIR = CHECKOUT / "Android"
25+
ANDROID_DIR = Path(__file__).resolve().parent
26+
CHECKOUT = ANDROID_DIR.parent
2527
BUILD_DIR = ANDROID_DIR / "build"
2628
DIST_DIR = ANDROID_DIR / "dist"
2729
PREFIX_DIR = ANDROID_DIR / "prefix"
@@ -544,6 +546,67 @@ async def run_testbed(context):
544546
raise e.exceptions[0]
545547

546548

549+
def package_version(prefix_subdir):
550+
vars_glob = f"{prefix_subdir}/lib/python*/_sysconfig_vars__android_*.json"
551+
vars_paths = glob(vars_glob)
552+
if len(vars_paths) != 1:
553+
sys.exit(f"{vars_glob} matched {len(vars_paths)} paths.")
554+
with open(vars_paths[0]) as vars_file:
555+
version = json.load(vars_file)["py_version"]
556+
557+
# If not building against a tagged commit, add a timestamp to the version.
558+
# Follow the PyPA version number rules, as this will make it easier to
559+
# process with other tools.
560+
if version.endswith("+"):
561+
version += datetime.now(timezone.utc).strftime("%Y%m%d.%H%M%S")
562+
563+
return version
564+
565+
566+
def package(context):
567+
prefix_subdir = subdir(PREFIX_DIR, context.host)
568+
version = package_version(prefix_subdir)
569+
570+
with TemporaryDirectory(prefix=SCRIPT_NAME) as temp_dir:
571+
temp_dir = Path(temp_dir)
572+
573+
# All tracked files in the Android directory.
574+
for line in run(
575+
["git", "ls-files"],
576+
cwd=ANDROID_DIR, capture_output=True, text=True, log=False,
577+
).stdout.splitlines():
578+
src = ANDROID_DIR / line
579+
dst = temp_dir / line
580+
dst.parent.mkdir(parents=True, exist_ok=True)
581+
shutil.copy2(src, dst, follow_symlinks=False)
582+
583+
# Anything in the prefix directory which could be useful either for
584+
# apps embedding Python, or packages built against it.
585+
for rel_dir, patterns in [
586+
("include", ["openssl*", "python*", "sqlite*"]),
587+
("lib", ["engines-3", "libcrypto*.so", "libpython*", "libsqlite*",
588+
"libssl*.so", "ossl-modules", "python*"]),
589+
("lib/pkgconfig", ["*crypto*", "*ssl*", "*python*", "*sqlite*"]),
590+
]:
591+
for pattern in patterns:
592+
for src in glob(f"{prefix_subdir}/{rel_dir}/{pattern}"):
593+
dst = temp_dir / relpath(src, ANDROID_DIR)
594+
dst.parent.mkdir(parents=True, exist_ok=True)
595+
if Path(src).is_dir():
596+
shutil.copytree(
597+
src, dst, symlinks=True,
598+
ignore=lambda *args: ["__pycache__"]
599+
)
600+
else:
601+
shutil.copy2(src, dst, follow_symlinks=False)
602+
603+
DIST_DIR.mkdir(exist_ok=True)
604+
package_path = shutil.make_archive(
605+
f"{DIST_DIR}/python-{version}-{context.host}", "gztar", temp_dir
606+
)
607+
print(f"Wrote {package_path}")
608+
609+
547610
# Handle SIGTERM the same way as SIGINT. This ensures that if we're terminated
548611
# by the buildbot worker, we'll make an attempt to clean up our subprocesses.
549612
def install_signal_handler():
@@ -556,6 +619,8 @@ def signal_handler(*args):
556619
def parse_args():
557620
parser = argparse.ArgumentParser()
558621
subcommands = parser.add_subparsers(dest="subcommand")
622+
623+
# Subcommands
559624
build = subcommands.add_parser("build", help="Build everything")
560625
configure_build = subcommands.add_parser("configure-build",
561626
help="Run `configure` for the "
@@ -567,25 +632,27 @@ def parse_args():
567632
make_host = subcommands.add_parser("make-host",
568633
help="Run `make` for Android")
569634
subcommands.add_parser(
570-
"clean", help="Delete the cross-build directory")
635+
"clean", help="Delete all build and prefix directories")
636+
subcommands.add_parser(
637+
"build-testbed", help="Build the testbed app")
638+
test = subcommands.add_parser(
639+
"test", help="Run the test suite")
640+
package = subcommands.add_parser("package", help="Make a release package")
571641

642+
# Common arguments
572643
for subcommand in build, configure_build, configure_host:
573644
subcommand.add_argument(
574645
"--clean", action="store_true", default=False, dest="clean",
575-
help="Delete any relevant directories before building")
576-
for subcommand in build, configure_host, make_host:
646+
help="Delete the relevant build and prefix directories first")
647+
for subcommand in [build, configure_host, make_host, package]:
577648
subcommand.add_argument(
578-
"host", metavar="HOST",
579-
choices=["aarch64-linux-android", "x86_64-linux-android"],
649+
"host", metavar="HOST", choices=HOSTS,
580650
help="Host triplet: choices=[%(choices)s]")
581651
for subcommand in build, configure_build, configure_host:
582652
subcommand.add_argument("args", nargs="*",
583653
help="Extra arguments to pass to `configure`")
584654

585-
subcommands.add_parser(
586-
"build-testbed", help="Build the testbed app")
587-
test = subcommands.add_parser(
588-
"test", help="Run the test suite")
655+
# Test arguments
589656
test.add_argument(
590657
"-v", "--verbose", action="count", default=0,
591658
help="Show Gradle output, and non-Python logcat messages. "
@@ -614,14 +681,17 @@ def main():
614681
stream.reconfigure(line_buffering=True)
615682

616683
context = parse_args()
617-
dispatch = {"configure-build": configure_build_python,
618-
"make-build": make_build_python,
619-
"configure-host": configure_host_python,
620-
"make-host": make_host_python,
621-
"build": build_all,
622-
"clean": clean_all,
623-
"build-testbed": build_testbed,
624-
"test": run_testbed}
684+
dispatch = {
685+
"configure-build": configure_build_python,
686+
"make-build": make_build_python,
687+
"configure-host": configure_host_python,
688+
"make-host": make_host_python,
689+
"build": build_all,
690+
"clean": clean_all,
691+
"build-testbed": build_testbed,
692+
"test": run_testbed,
693+
"package": package,
694+
}
625695

626696
try:
627697
result = dispatch[context.subcommand](context)

Android/testbed/app/build.gradle.kts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ plugins {
66
id("org.jetbrains.kotlin.android")
77
}
88

9-
val PYTHON_DIR = file("../../..").canonicalPath
10-
val ANDROID_DIR = "$PYTHON_DIR/Android"
9+
val ANDROID_DIR = file("../..").canonicalPath
1110
val PREFIX_DIR = "$ANDROID_DIR/prefix"
1211

1312
val ABIS = mapOf(
@@ -147,8 +146,14 @@ androidComponents.onVariants { variant ->
147146
}
148147

149148
into("lib/$pyPlusVer") {
150-
// To aid debugging, the source directory takes priority.
151-
from("$PYTHON_DIR/Lib")
149+
// To aid debugging, the source directory takes priority when
150+
// running inside a Python source tree.
151+
if (
152+
file(ANDROID_DIR).name == "Android"
153+
&& file("$ANDROID_DIR/../pyconfig.h.in").exists()
154+
) {
155+
from("$ANDROID_DIR/../Lib")
156+
}
152157

153158
// The predix directory provides ABI-specific files such as
154159
// sysconfigdata.

Doc/using/android.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,8 @@ details.
2727
Adding Python to an Android app
2828
-------------------------------
2929

30-
These instructions are only needed if you're planning to compile Python for
31-
Android yourself. Most users should *not* need to do this. Instead, use one of
32-
the following tools, which will provide a much easier experience:
30+
Most app developers should use one of the following tools, which will provide a
31+
much easier experience:
3332

3433
* `Briefcase <https://briefcase.readthedocs.io>`__, from the BeeWare project
3534
* `Buildozer <https://buildozer.readthedocs.io>`__, from the Kivy project
@@ -41,11 +40,12 @@ If you're sure you want to do all of this manually, read on. You can use the
4140
:source:`testbed app <Android/testbed>` as a guide; each step below contains a
4241
link to the relevant file.
4342

44-
* Build Python by following the instructions in :source:`Android/README.md`.
43+
* Build Python by following the instructions in :source:`Android/README.md`,
44+
or download a pre-built package from python.org.
4545

4646
* Add code to your :source:`build.gradle <Android/testbed/app/build.gradle.kts>`
4747
file to copy the following items into your project. All except your own Python
48-
code can be copied from ``cross-build/HOST/prefix/lib``:
48+
code can be copied from ``prefix/HOST/lib``:
4949

5050
* In your JNI libraries:
5151

0 commit comments

Comments
 (0)