Skip to content

gh-137242: Add Android CI job #137186

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

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
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
56 changes: 56 additions & 0 deletions .github/actions/build-android/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# This is coded as an action rather than a workflow so the release-tools
# repository can load it from a dynamically-chosen commit. Cross-repository
# workflow calls must have a Git reference hard-coded in the calling workflow,
# but actions can be run dynamically from the runner's filesystem.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense; but it also raises an eyebrow because this is something that no other platform has needed. I presume this is because no other platform that is generating binary artefacts is doing so with the tooling in release-tools?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@webknjaz, do you have any thougts here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take a look, thanks for tagging.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mhsmith so I've tried to understand the context and this justtification didn't make sense to me.

It is perfectly possible to call reusable workflows from other repositories (in fact, this is what I'm building my reusable-tox.yml ecosystem on).

My understanding is that this is meant to be used in https://github.com/python/release-tools/blob/698deaf2ebff433a6ab9d4b5ded97a40fce109a1/.github/workflows/source-and-docs-release.yml, right?

In any case, I've been moving away from composite actions in favor of reusable workflows. This is because composite actions (or any actions for that matter) are represented as a single step in job runs. And it's rather difficult to inspect what actions are doing. So from the troubleshooting perspective, I'd strongly advise against composite actions.

It is important to make every step visible and transparent. And if you follow the practice I established with reusable workflows as modules in here, this is definitely possible.

I started with in-repo "modules" two years ago because I was solving the problem of making it possible to collect all the job statuses in a single check (for branch protection). This wasn't because it's somehow impossible to host them externally. This was just not something necessary for that purpose.

@encukou I've actually been meaning to ask if there's any workflows that are being duplicated in the python org. If yes, it'd make sense to host them in a shared repository. This could be a .github repo or even that release-tools one (although, I don't know if it makes semantic sense). This is a separate question, though.

That said, if you've faced any confusion or need help adapting this to be a reusable workflow, just let me know where you're stuck. I can help you navigate this or just rewrite it for you.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense; but it also raises an eyebrow because this is something that no other platform has needed. I presume this is because no other platform that is generating binary artefacts is doing so with the tooling in release-tools?

Right now, release-tools only creates source zips and docs artifacts. See https://github.com/python/release-tools/actions/runs/16450411678 for 3.14 RC2.

The Windows artifacts are built in Azure Pipelines, here's RC2. And Ned builds the macOS artifacts. (release-tools later takes these Windows and macOS artifacts and signs and uploads them.)

We're hoping to build the macOS artifacts using CI in the near future, so what we decide here may help inform how to do that to :)

My understanding is that this is meant to be used in python/release-tools@698deaf/.github/workflows/source-and-docs-release.yml, right?

Yes, see https://github.com/python/release-tools/pull/265/files#diff-4d14704b6b88fb06db888f96c03a8e9b3a5e07a4ee566d97d4111b2c05210e84R220.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I'd like to see is CI in this repo to check Android builds okay, so we don't get caught ought on release day because we only build in release-tools.

This happened in 3.14 RC1 with the plaintext docs, which had broken back in April or so, but RC1 is the first prerelease to build docs and we hadn't caught it here.

One deciding factor for whether we have stuff over here (via composite actions or something else) might be how much difference there'll be between different versions (3.14, 3.15, etc). If a lot, we might not want it all in release-tools and would benefit from versioning things in branches over here.

On the other hand, the docs build is also versioned in branches here, release-tools CI calls a make dist command in this repo.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the other hand, the docs build is also versioned in branches here, release-tools CI calls a make dist command in this repo.

That's more or less the way I've been trying to suggest.


name: Build and test (Android)
description: Build and test (Android)

inputs:
triplet:
description: Host triplet
required: true

runs:
using: composite

steps:
# Build Python, and package it into a release artifact.
- shell: bash
run: ./Android/android.py build ${{ inputs.triplet }}
- shell: bash
run: ./Android/android.py package ${{ inputs.triplet }}
- uses: actions/upload-artifact@v4
with:
name: ${{ inputs.triplet }}
path: cross-build/${{ inputs.triplet }}/dist/*
if-no-files-found: error

# Currently, GitHub Actions can only run the Android emulator on Linux, so
# all the remaining steps are conditional on that.

# (https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/).
- name: Enable KVM for Android emulator
if: runner.os == 'Linux'
shell: bash
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- name: Unpack release artifact
if: runner.os == 'Linux'
shell: bash
run: |
mkdir $RUNNER_TEMP/android
tar -C $RUNNER_TEMP/android -xf cross-build/${{ inputs.triplet }}/dist/*

- name: Tests
if: runner.os == 'Linux'
shell: bash
# Arguments are similar to --fast-ci, but in single-process mode.
run: |
$RUNNER_TEMP/android/android.py test --managed maxVersion -v -- \
--single-process --fail-env-changed --rerun --slowest --verbose3 \
-u "all,-cpu" --timeout=600
27 changes: 27 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,31 @@ jobs:
- name: SSL tests
run: ./python Lib/test/ssltests.py

build-android:
name: "Android"
needs: build-context
if: needs.build-context.outputs.run-tests == 'true'
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
arch: [aarch64, x86_64]
include:
# Use the same runs-on configuration as build-macos and build-ubuntu.
- arch: aarch64
runs-on: ${{ github.repository_owner == 'python' && 'ghcr.io/cirruslabs/macos-runner:sonoma' || 'macos-14' }}
- arch: x86_64
runs-on: ubuntu-24.04

runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: ./.github/actions/build-android
with:
triplet: ${{ matrix.arch }}-linux-android

build-wasi:
name: 'WASI'
needs: build-context
Expand Down Expand Up @@ -705,6 +730,7 @@ jobs:
- build-ubuntu
- build-ubuntu-ssltests-awslc
- build-ubuntu-ssltests-openssl
- build-android
- build-wasi
- test-hypothesis
- build-asan
Expand Down Expand Up @@ -740,6 +766,7 @@ jobs:
build-ubuntu,
build-ubuntu-ssltests-awslc,
build-ubuntu-ssltests-openssl,
build-android,
build-wasi,
test-hypothesis,
build-asan,
Expand Down
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ repos:
rev: v1.6.0
hooks:
- id: zizmor
# Action files are misidentified as workflows.
exclude: ^.github/actions/

- repo: https://github.com/sphinx-contrib/sphinx-lint
rev: v1.0.0
Expand Down
9 changes: 6 additions & 3 deletions Android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,13 @@ similar to the `Android` directory of the CPython source tree.

## Testing

The Python 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
a DISPLAY environment variable pointing at an X server. Xvfb is acceptable.
On Linux, the emulator needs access to the KVM virtualization interface. This may
require adding your user to a group, or [changing your udev
rules](https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/).
If the emulator fails to start, try running `$ANDROID_HOME/emulator/emulator
-accel-check`.

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
Expand Down
55 changes: 39 additions & 16 deletions Android/android.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,13 @@ def make_host_python(context):
# flags to be duplicated. So we don't use the `host` argument here.
os.chdir(host_dir)
run(["make", "-j", str(os.cpu_count())])
run(["make", "install", f"prefix={prefix_dir}"])

# The `make install` output is very verbose and rarely useful, so
# suppress it by default.
run(
["make", "install", f"prefix={prefix_dir}"],
capture_output=not context.verbose,
)


def build_all(context):
Expand Down Expand Up @@ -671,6 +677,14 @@ def package(context):
else:
shutil.copy2(src, dst, follow_symlinks=False)

# Strip debug information.
if not context.debug:
run(
[android_env(context.host)["STRIP"]]
+ glob(f"{temp_dir}/**/*.so", recursive=True),
log=False,
)

dist_dir = subdir(context.host, "dist", create=True)
package_path = shutil.make_archive(
f"{dist_dir}/python-{version}-{context.host}", "gztar", temp_dir
Expand All @@ -695,24 +709,31 @@ def parse_args():
parser = argparse.ArgumentParser()
subcommands = parser.add_subparsers(dest="subcommand", required=True)

def add_parser(*args, **kwargs):
parser = subcommands.add_parser(*args, **kwargs)
parser.add_argument(
"-v", "--verbose", action="count", default=0,
help="Show verbose output. Use twice to be even more verbose.")
return parser

# Subcommands
build = subcommands.add_parser(
build = add_parser(
"build", help="Run configure-build, make-build, configure-host and "
"make-host")
configure_build = subcommands.add_parser(
configure_build = add_parser(
"configure-build", help="Run `configure` for the build Python")
subcommands.add_parser(
add_parser(
"make-build", help="Run `make` for the build Python")
configure_host = subcommands.add_parser(
configure_host = add_parser(
"configure-host", help="Run `configure` for Android")
make_host = subcommands.add_parser(
make_host = 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")
add_parser("clean", help="Delete all build directories")
add_parser("build-testbed", help="Build the testbed app")
test = add_parser("test", help="Run the testbed app")
package = add_parser("package", help="Make a release package")
env = add_parser("env", help="Print environment variables")

# Common arguments
for subcommand in build, configure_build, configure_host:
Expand All @@ -733,11 +754,6 @@ def parse_args():
help="Extra arguments to pass to `configure`")

# Test arguments
test.add_argument(
"-v", "--verbose", action="count", default=0,
help="Show Gradle output, and non-Python logcat messages. "
"Use twice to include high-volume messages which are rarely useful.")

device_group = test.add_mutually_exclusive_group(required=True)
device_group.add_argument(
"--connected", metavar="SERIAL", help="Run on a connected device. "
Expand Down Expand Up @@ -765,6 +781,11 @@ def parse_args():
"args", nargs="*", help=f"Arguments to add to sys.argv. "
f"Separate them from {SCRIPT_NAME}'s own arguments with `--`.")

# Package arguments.
package.add_argument(
"-g", action="store_true", default=False, dest="debug",
help="Include debug information")

return parser.parse_args()


Expand Down Expand Up @@ -803,6 +824,8 @@ def main():
def print_called_process_error(e):
for stream_name in ["stdout", "stderr"]:
content = getattr(e, stream_name)
if isinstance(content, bytes):
content = content.decode(*DECODE_ARGS)
stream = getattr(sys, stream_name)
if content:
stream.write(content)
Expand Down
Loading