Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add android.py test --site-packages and --cwd options
  • Loading branch information
mhsmith committed May 3, 2025
commit b273bc743dacb24128ed4bb11b6c4c0b4d498222
4 changes: 4 additions & 0 deletions Android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ repository's `Lib` directory will be picked up immediately. Changes in C files,
and architecture-specific files such as sysconfigdata, will not take effect
until you re-run `android.py make-host` or `build`.

The testbed app can also be used to test third-party packages. For more details,
run `android.py test --help`, paying attention to the options `--site-packages`,
`--cwd`, `-c` and `-m`.


## Using in your own app

Expand Down
54 changes: 33 additions & 21 deletions Android/android.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from glob import glob
from os.path import basename, relpath
from os.path import abspath, basename, relpath
from pathlib import Path
from subprocess import CalledProcessError
from tempfile import TemporaryDirectory
Expand Down Expand Up @@ -541,12 +541,17 @@ def log(line):
args = [
gradlew, "--console", "plain", f"{task_prefix}DebugAndroidTest",
] + [
# Build-time properties
f"-Ppython.{name}={value}"
for name, value in [
("sitePackages", context.site_packages), ("cwd", context.cwd)
] if value
] + [
# Runtime properties
f"-Pandroid.testInstrumentationRunnerArguments.python{name}={value}"
for name, value in [
("Mode", mode),
("Module", module),
("Args", join_command(context.args)),
]
("Mode", mode), ("Module", module), ("Args", join_command(context.args))
] if value
]
log("> " + join_command(args))

Expand Down Expand Up @@ -684,32 +689,32 @@ def signal_handler(*args):

def parse_args():
parser = argparse.ArgumentParser()
subcommands = parser.add_subparsers(dest="subcommand")
subcommands = parser.add_subparsers(dest="subcommand", required=True)

# Subcommands
build = subcommands.add_parser("build", help="Build everything")
configure_build = subcommands.add_parser("configure-build",
help="Run `configure` for the "
"build Python")
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")
subcommands.add_parser(
"clean", help="Delete all build and prefix directories")
build = subcommands.add_parser(
"build", help="Run configure-build, make-build, configure-host and "
"make-host")
configure_build = subcommands.add_parser(
"configure-build", help="Run `configure` for the build Python")
subcommands.add_parser(
"build-testbed", help="Build the testbed app")
test = subcommands.add_parser(
"test", help="Run the test suite")
"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")

subcommands.add_parser("clean", help="Delete all build directories")
subcommands.add_parser("build-testbed", help="Build the testbed app")
test = subcommands.add_parser("test", help="Run the testbed app")
package = subcommands.add_parser("package", help="Make a release package")
env = subcommands.add_parser("env", help="Print environment variables")

# Common arguments
for subcommand in build, configure_build, configure_host:
subcommand.add_argument(
"--clean", action="store_true", default=False, dest="clean",
help="Delete the relevant build and prefix directories first")
help="Delete the relevant build directories first")

host_commands = [build, configure_host, make_host, package]
if in_source_tree:
Expand Down Expand Up @@ -737,6 +742,13 @@ def parse_args():
"--managed", metavar="NAME", help="Run on a Gradle-managed device. "
"These are defined in `managedDevices` in testbed/app/build.gradle.kts.")

test.add_argument(
"--site-packages", metavar="DIR", type=abspath,
help="Directory to copy as the app's site-packages.")
test.add_argument(
"--cwd", metavar="DIR", type=abspath,
help="Directory to copy as the app's working directory.")

mode_group = test.add_mutually_exclusive_group()
mode_group.add_argument(
"-c", dest="command", help="Execute the given Python code.")
Expand Down
18 changes: 18 additions & 0 deletions Android/testbed/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,29 @@ androidComponents.onVariants { variant ->

into("site-packages") {
from("$projectDir/src/main/python")

val sitePackages = findProperty("python.sitePackages") as String?
if (!sitePackages.isNullOrEmpty()) {
if (!file(sitePackages).exists()) {
throw GradleException("$sitePackages does not exist")
}
from(sitePackages)
}
}

duplicatesStrategy = DuplicatesStrategy.EXCLUDE
exclude("**/__pycache__")
}

into("cwd") {
val cwd = findProperty("python.cwd") as String?
if (!cwd.isNullOrEmpty()) {
if (!file(cwd).exists()) {
throw GradleException("$cwd does not exist")
}
from(cwd)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class PythonTestRunner(val context: Context) {
fun run(instrumentationArgs: Bundle) = run(
instrumentationArgs.getString("pythonMode")!!,
instrumentationArgs.getString("pythonModule")!!,
instrumentationArgs.getString("pythonArgs")!!,
instrumentationArgs.getString("pythonArgs") ?: "",
)

/** Run Python.
Expand All @@ -35,7 +35,7 @@ class PythonTestRunner(val context: Context) {
* "-m" mode.
* @param args Arguments to add to sys.argv. Will be parsed by `shlex.split`.
* @return The Python exit status: zero on success, nonzero on failure. */
fun run(mode: String, module: String, args: String = "") : Int {
fun run(mode: String, module: String, args: String) : Int {
Os.setenv("PYTHON_MODE", mode, true)
Os.setenv("PYTHON_MODULE", module, true)
Os.setenv("PYTHON_ARGS", args, true)
Expand Down
10 changes: 10 additions & 0 deletions Android/testbed/app/src/main/python/android_testbed_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,19 @@
module = os.environ["PYTHON_MODULE"]
sys.argv[1:] = shlex.split(os.environ["PYTHON_ARGS"])

cwd = f"{sys.prefix}/cwd"
if not os.path.exists(cwd):
# Empty directories are lost in the asset packing/unpacking process.
os.mkdir(cwd)
os.chdir(cwd)

if mode == "-c":
# In -c mode, sys.path starts with an empty string, which means whatever the current
# working directory is at the moment of each import.
sys.path.insert(0, "")
exec(module, {})
elif mode == "-m":
sys.path.insert(0, os.getcwd())
runpy.run_module(module, run_name="__main__", alter_sys=True)
else:
raise ValueError(f"unknown mode: {mode}")
9 changes: 9 additions & 0 deletions Doc/using/android.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,12 @@ link to the relevant file.
* Add code to your app to :source:`start Python in embedded mode
<Android/testbed/app/src/main/c/main_activity.c>`. This will need to be C code
called via JNI.

Building a Python package for Android
-------------------------------------

Python packages can be built for Android as wheels and released on PyPI. The
recommended tool for doing this is `cibuildwheel
<https://cibuildwheel.pypa.io/en/stable/platforms/#android>`__, which automates
all the details of setting up a cross-compilation environment, building the
wheel, and testing it on an emulator.
Loading