Skip to content

Commit f660ec3

Browse files
mhsmithfreakboy3742webknjaz
authored
gh-137242: Add Android CI job (#137186)
Co-authored-by: Russell Keith-Magee <[email protected]> Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) <[email protected]>
1 parent be56464 commit f660ec3

File tree

4 files changed

+134
-23
lines changed

4 files changed

+134
-23
lines changed

.github/workflows/build.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,29 @@ jobs:
397397
- name: SSL tests
398398
run: ./python Lib/test/ssltests.py
399399

400+
build-android:
401+
name: Android (${{ matrix.arch }})
402+
needs: build-context
403+
if: needs.build-context.outputs.run-tests == 'true'
404+
timeout-minutes: 60
405+
strategy:
406+
fail-fast: false
407+
matrix:
408+
include:
409+
# Use the same runs-on configuration as build-macos and build-ubuntu.
410+
- arch: aarch64
411+
runs-on: ${{ github.repository_owner == 'python' && 'ghcr.io/cirruslabs/macos-runner:sonoma' || 'macos-14' }}
412+
- arch: x86_64
413+
runs-on: ubuntu-24.04
414+
415+
runs-on: ${{ matrix.runs-on }}
416+
steps:
417+
- uses: actions/checkout@v4
418+
with:
419+
persist-credentials: false
420+
- name: Build and test
421+
run: ./Android/android.py ci ${{ matrix.arch }}-linux-android
422+
400423
build-wasi:
401424
name: 'WASI'
402425
needs: build-context
@@ -705,6 +728,7 @@ jobs:
705728
- build-ubuntu
706729
- build-ubuntu-ssltests-awslc
707730
- build-ubuntu-ssltests-openssl
731+
- build-android
708732
- build-wasi
709733
- test-hypothesis
710734
- build-asan
@@ -740,6 +764,7 @@ jobs:
740764
build-ubuntu,
741765
build-ubuntu-ssltests-awslc,
742766
build-ubuntu-ssltests-openssl,
767+
build-android,
743768
build-wasi,
744769
test-hypothesis,
745770
build-asan,

Android/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,12 @@ similar to the `Android` directory of the CPython source tree.
9696

9797
## Testing
9898

99-
The Python test suite can be run on Linux, macOS, or Windows:
99+
The Python test suite can be run on Linux, macOS, or Windows.
100100

101-
* On Linux, the emulator needs access to the KVM virtualization interface, and
102-
a DISPLAY environment variable pointing at an X server. Xvfb is acceptable.
101+
On Linux, the emulator needs access to the KVM virtualization interface. This may
102+
require adding your user to a group, or changing your udev rules. On GitHub
103+
Actions, the test script will do this automatically using the commands shown
104+
[here](https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/).
103105

104106
The test suite can usually be run on a device with 2 GB of RAM, but this is
105107
borderline, so you may need to increase it to 4 GB. As of Android

Android/android-env.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ fail() {
2424
# * https://android.googlesource.com/platform/ndk/+/ndk-rXX-release/docs/BuildSystemMaintainers.md
2525
# where XX is the NDK version. Do a diff against the version you're upgrading from, e.g.:
2626
# https://android.googlesource.com/platform/ndk/+/ndk-r25-release..ndk-r26-release/docs/BuildSystemMaintainers.md
27-
ndk_version=27.2.12479018
27+
ndk_version=27.3.13750724
2828

2929
ndk=$ANDROID_HOME/ndk/$ndk_version
3030
if ! [ -e "$ndk" ]; then

Android/android.py

Lines changed: 103 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import asyncio
44
import argparse
55
import os
6+
import platform
67
import re
78
import shlex
89
import shutil
@@ -247,7 +248,13 @@ def make_host_python(context):
247248
# flags to be duplicated. So we don't use the `host` argument here.
248249
os.chdir(host_dir)
249250
run(["make", "-j", str(os.cpu_count())])
250-
run(["make", "install", f"prefix={prefix_dir}"])
251+
252+
# The `make install` output is very verbose and rarely useful, so
253+
# suppress it by default.
254+
run(
255+
["make", "install", f"prefix={prefix_dir}"],
256+
capture_output=not context.verbose,
257+
)
251258

252259

253260
def build_all(context):
@@ -266,6 +273,18 @@ def clean_all(context):
266273
clean(host)
267274

268275

276+
def setup_ci():
277+
# https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/
278+
if "GITHUB_ACTIONS" in os.environ and platform.system() == "Linux":
279+
run(
280+
["sudo", "tee", "/etc/udev/rules.d/99-kvm4all.rules"],
281+
input='KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"\n',
282+
text=True,
283+
)
284+
run(["sudo", "udevadm", "control", "--reload-rules"])
285+
run(["sudo", "udevadm", "trigger", "--name-match=kvm"])
286+
287+
269288
def setup_sdk():
270289
sdkmanager = android_home / (
271290
"cmdline-tools/latest/bin/sdkmanager"
@@ -578,6 +597,7 @@ async def gradle_task(context):
578597

579598

580599
async def run_testbed(context):
600+
setup_ci()
581601
setup_sdk()
582602
setup_testbed()
583603

@@ -671,11 +691,63 @@ def package(context):
671691
else:
672692
shutil.copy2(src, dst, follow_symlinks=False)
673693

694+
# Strip debug information.
695+
if not context.debug:
696+
so_files = glob(f"{temp_dir}/**/*.so", recursive=True)
697+
run([android_env(context.host)["STRIP"], *so_files], log=False)
698+
674699
dist_dir = subdir(context.host, "dist", create=True)
675700
package_path = shutil.make_archive(
676701
f"{dist_dir}/python-{version}-{context.host}", "gztar", temp_dir
677702
)
678703
print(f"Wrote {package_path}")
704+
return package_path
705+
706+
707+
def ci(context):
708+
for step in [
709+
configure_build_python,
710+
make_build_python,
711+
configure_host_python,
712+
make_host_python,
713+
package,
714+
]:
715+
caption = (
716+
step.__name__.replace("_", " ")
717+
.capitalize()
718+
.replace("python", "Python")
719+
)
720+
print(f"::group::{caption}")
721+
result = step(context)
722+
if step is package:
723+
package_path = result
724+
print("::endgroup::")
725+
726+
if (
727+
"GITHUB_ACTIONS" in os.environ
728+
and (platform.system(), platform.machine()) != ("Linux", "x86_64")
729+
):
730+
print(
731+
"Skipping tests: GitHub Actions does not support the Android "
732+
"emulator on this platform."
733+
)
734+
else:
735+
with TemporaryDirectory(prefix=SCRIPT_NAME) as temp_dir:
736+
print("::group::Tests")
737+
# Prove the package is self-contained by using it to run the tests.
738+
shutil.unpack_archive(package_path, temp_dir)
739+
740+
# Arguments are similar to --fast-ci, but in single-process mode.
741+
launcher_args = ["--managed", "maxVersion", "-v"]
742+
test_args = [
743+
"--single-process", "--fail-env-changed", "--rerun", "--slowest",
744+
"--verbose3", "-u", "all,-cpu", "--timeout=600"
745+
]
746+
run(
747+
["./android.py", "test", *launcher_args, "--", *test_args],
748+
cwd=temp_dir
749+
)
750+
print("::endgroup::")
679751

680752

681753
def env(context):
@@ -695,49 +767,52 @@ def parse_args():
695767
parser = argparse.ArgumentParser()
696768
subcommands = parser.add_subparsers(dest="subcommand", required=True)
697769

770+
def add_parser(*args, **kwargs):
771+
parser = subcommands.add_parser(*args, **kwargs)
772+
parser.add_argument(
773+
"-v", "--verbose", action="count", default=0,
774+
help="Show verbose output. Use twice to be even more verbose.")
775+
return parser
776+
698777
# Subcommands
699-
build = subcommands.add_parser(
778+
build = add_parser(
700779
"build", help="Run configure-build, make-build, configure-host and "
701780
"make-host")
702-
configure_build = subcommands.add_parser(
781+
configure_build = add_parser(
703782
"configure-build", help="Run `configure` for the build Python")
704-
subcommands.add_parser(
783+
add_parser(
705784
"make-build", help="Run `make` for the build Python")
706-
configure_host = subcommands.add_parser(
785+
configure_host = add_parser(
707786
"configure-host", help="Run `configure` for Android")
708-
make_host = subcommands.add_parser(
787+
make_host = add_parser(
709788
"make-host", help="Run `make` for Android")
710789

711-
subcommands.add_parser("clean", help="Delete all build directories")
712-
subcommands.add_parser("build-testbed", help="Build the testbed app")
713-
test = subcommands.add_parser("test", help="Run the testbed app")
714-
package = subcommands.add_parser("package", help="Make a release package")
715-
env = subcommands.add_parser("env", help="Print environment variables")
790+
add_parser("clean", help="Delete all build directories")
791+
add_parser("build-testbed", help="Build the testbed app")
792+
test = add_parser("test", help="Run the testbed app")
793+
package = add_parser("package", help="Make a release package")
794+
ci = add_parser("ci", help="Run build, package and test")
795+
env = add_parser("env", help="Print environment variables")
716796

717797
# Common arguments
718-
for subcommand in build, configure_build, configure_host:
798+
for subcommand in [build, configure_build, configure_host, ci]:
719799
subcommand.add_argument(
720800
"--clean", action="store_true", default=False, dest="clean",
721801
help="Delete the relevant build directories first")
722802

723-
host_commands = [build, configure_host, make_host, package]
803+
host_commands = [build, configure_host, make_host, package, ci]
724804
if in_source_tree:
725805
host_commands.append(env)
726806
for subcommand in host_commands:
727807
subcommand.add_argument(
728808
"host", metavar="HOST", choices=HOSTS,
729809
help="Host triplet: choices=[%(choices)s]")
730810

731-
for subcommand in build, configure_build, configure_host:
811+
for subcommand in [build, configure_build, configure_host, ci]:
732812
subcommand.add_argument("args", nargs="*",
733813
help="Extra arguments to pass to `configure`")
734814

735815
# Test arguments
736-
test.add_argument(
737-
"-v", "--verbose", action="count", default=0,
738-
help="Show Gradle output, and non-Python logcat messages. "
739-
"Use twice to include high-volume messages which are rarely useful.")
740-
741816
device_group = test.add_mutually_exclusive_group(required=True)
742817
device_group.add_argument(
743818
"--connected", metavar="SERIAL", help="Run on a connected device. "
@@ -765,6 +840,12 @@ def parse_args():
765840
"args", nargs="*", help=f"Arguments to add to sys.argv. "
766841
f"Separate them from {SCRIPT_NAME}'s own arguments with `--`.")
767842

843+
# Package arguments.
844+
for subcommand in [package, ci]:
845+
subcommand.add_argument(
846+
"-g", action="store_true", default=False, dest="debug",
847+
help="Include debug information in package")
848+
768849
return parser.parse_args()
769850

770851

@@ -788,6 +869,7 @@ def main():
788869
"build-testbed": build_testbed,
789870
"test": run_testbed,
790871
"package": package,
872+
"ci": ci,
791873
"env": env,
792874
}
793875

@@ -803,6 +885,8 @@ def main():
803885
def print_called_process_error(e):
804886
for stream_name in ["stdout", "stderr"]:
805887
content = getattr(e, stream_name)
888+
if isinstance(content, bytes):
889+
content = content.decode(*DECODE_ARGS)
806890
stream = getattr(sys, stream_name)
807891
if content:
808892
stream.write(content)

0 commit comments

Comments
 (0)