Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
25 changes: 25 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,29 @@ jobs:
- name: SSL tests
run: ./python Lib/test/ssltests.py

build-android:
name: Android (${{ matrix.arch }})
needs: build-context
if: needs.build-context.outputs.run-tests == 'true'
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
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
- name: Build and test
run: ./Android/android.py ci ${{ matrix.arch }}-linux-android

build-wasi:
name: 'WASI'
needs: build-context
Expand Down Expand Up @@ -705,6 +728,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 +764,7 @@ jobs:
build-ubuntu,
build-ubuntu-ssltests-awslc,
build-ubuntu-ssltests-openssl,
build-android,
build-wasi,
test-hypothesis,
build-asan,
Expand Down
8 changes: 5 additions & 3 deletions Android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,12 @@ 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. On GitHub
Actions, the test script will do this automatically using the commands shown
[here](https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/).

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
2 changes: 1 addition & 1 deletion Android/android-env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ fail() {
# * https://android.googlesource.com/platform/ndk/+/ndk-rXX-release/docs/BuildSystemMaintainers.md
# where XX is the NDK version. Do a diff against the version you're upgrading from, e.g.:
# https://android.googlesource.com/platform/ndk/+/ndk-r25-release..ndk-r26-release/docs/BuildSystemMaintainers.md
ndk_version=27.2.12479018
ndk_version=27.3.13750724

ndk=$ANDROID_HOME/ndk/$ndk_version
if ! [ -e "$ndk" ]; then
Expand Down
122 changes: 103 additions & 19 deletions Android/android.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import asyncio
import argparse
import os
import platform
import re
import shlex
import shutil
Expand Down Expand Up @@ -247,7 +248,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 All @@ -266,6 +273,18 @@ def clean_all(context):
clean(host)


def setup_ci():
# https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/
if "GITHUB_ACTIONS" in os.environ and platform.system() == "Linux":
run(
["sudo", "tee", "/etc/udev/rules.d/99-kvm4all.rules"],
input='KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"\n',
text=True,
)
run(["sudo", "udevadm", "control", "--reload-rules"])
run(["sudo", "udevadm", "trigger", "--name-match=kvm"])


def setup_sdk():
sdkmanager = android_home / (
"cmdline-tools/latest/bin/sdkmanager"
Expand Down Expand Up @@ -578,6 +597,7 @@ async def gradle_task(context):


async def run_testbed(context):
setup_ci()
setup_sdk()
setup_testbed()

Expand Down Expand Up @@ -671,11 +691,63 @@ def package(context):
else:
shutil.copy2(src, dst, follow_symlinks=False)

# Strip debug information.
if not context.debug:
so_files = glob(f"{temp_dir}/**/*.so", recursive=True)
run([android_env(context.host)["STRIP"], *so_files], 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
)
print(f"Wrote {package_path}")
return package_path


def ci(context):
for step in [
configure_build_python,
make_build_python,
configure_host_python,
make_host_python,
package,
]:
caption = (
step.__name__.replace("_", " ")
.capitalize()
.replace("python", "Python")
)
print(f"::group::{caption}")
result = step(context)
if step is package:
package_path = result
print("::endgroup::")

if (
"GITHUB_ACTIONS" in os.environ
and (platform.system(), platform.machine()) != ("Linux", "x86_64")
):
print(
"Skipping tests: GitHub Actions does not support the Android "
"emulator on this platform."
)
else:
with TemporaryDirectory(prefix=SCRIPT_NAME) as temp_dir:
print("::group::Tests")
# Prove the package is self-contained by using it to run the tests.
shutil.unpack_archive(package_path, temp_dir)

# Arguments are similar to --fast-ci, but in single-process mode.
launcher_args = ["--managed", "maxVersion", "-v"]
test_args = [
"--single-process", "--fail-env-changed", "--rerun", "--slowest",
"--verbose3", "-u", "all,-cpu", "--timeout=600"
]
run(
["./android.py", "test", *launcher_args, "--", *test_args],
cwd=temp_dir
)
print("::endgroup::")


def env(context):
Expand All @@ -695,49 +767,52 @@ 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")
ci = add_parser("ci", help="Run build, package and test")
env = add_parser("env", help="Print environment variables")

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

host_commands = [build, configure_host, make_host, package]
host_commands = [build, configure_host, make_host, package, ci]
if in_source_tree:
host_commands.append(env)
for subcommand in host_commands:
subcommand.add_argument(
"host", metavar="HOST", choices=HOSTS,
help="Host triplet: choices=[%(choices)s]")

for subcommand in build, configure_build, configure_host:
for subcommand in [build, configure_build, configure_host, ci]:
subcommand.add_argument("args", nargs="*",
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 +840,12 @@ 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.
for subcommand in [package, ci]:
subcommand.add_argument(
"-g", action="store_true", default=False, dest="debug",
help="Include debug information in package")

return parser.parse_args()


Expand All @@ -788,6 +869,7 @@ def main():
"build-testbed": build_testbed,
"test": run_testbed,
"package": package,
"ci": ci,
"env": env,
}

Expand All @@ -803,6 +885,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
4 changes: 2 additions & 2 deletions Doc/howto/enum.rst
Original file line number Diff line number Diff line change
Expand Up @@ -990,9 +990,9 @@ Supported ``_sunder_`` names
from the final class
- :meth:`~Enum._generate_next_value_` -- used to get an appropriate value for
an enum member; may be overridden
- :meth:`~EnumType._add_alias_` -- adds a new name as an alias to an existing
- :meth:`~Enum._add_alias_` -- adds a new name as an alias to an existing
member.
- :meth:`~EnumType._add_value_alias_` -- adds a new value as an alias to an
- :meth:`~Enum._add_value_alias_` -- adds a new value as an alias to an
existing member. See `MultiValueEnum`_ for an example.

.. note::
Expand Down
53 changes: 34 additions & 19 deletions Doc/library/enum.rst
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ Data Types
final *enum*, as well as creating the enum members, properly handling
duplicates, providing iteration over the enum class, etc.

.. versionadded:: 3.11

Before 3.11 ``EnumType`` was called ``EnumMeta``, which is still available as an alias.

.. method:: EnumType.__call__(cls, value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)

This method is called in two different ways:
Expand Down Expand Up @@ -206,7 +210,7 @@ Data Types
>>> Color.RED.value in Color
True

.. versionchanged:: 3.12
.. versionchanged:: 3.12

Before Python 3.12, a ``TypeError`` is raised if a
non-Enum-member is used in a containment check.
Expand Down Expand Up @@ -251,20 +255,6 @@ Data Types
>>> list(reversed(Color))
[<Color.BLUE: 3>, <Color.GREEN: 2>, <Color.RED: 1>]

.. method:: EnumType._add_alias_

Adds a new name as an alias to an existing member. Raises a
:exc:`NameError` if the name is already assigned to a different member.

.. method:: EnumType._add_value_alias_

Adds a new value as an alias to an existing member. Raises a
:exc:`ValueError` if the value is already linked with a different member.

.. versionadded:: 3.11

Before 3.11 ``EnumType`` was called ``EnumMeta``, which is still available as an alias.


.. class:: Enum

Expand Down Expand Up @@ -470,6 +460,30 @@ Data Types

.. versionchanged:: 3.12 Added :ref:`enum-dataclass-support`

.. method:: Enum._add_alias_

Adds a new name as an alias to an existing member::

>>> Color.RED._add_alias_("ERROR")
>>> Color.ERROR
<Color.RED: 1>

Raises a :exc:`NameError` if the name is already assigned to a different member.

.. versionadded:: 3.13

.. method:: Enum._add_value_alias_

Adds a new value as an alias to an existing member::

>>> Color.RED._add_value_alias_(42)
>>> Color(42)
<Color.RED: 1>

Raises a :exc:`ValueError` if the value is already linked with a different member.

.. versionadded:: 3.13


.. class:: IntEnum

Expand Down Expand Up @@ -879,10 +893,6 @@ Once all the members are created it is no longer used.
Supported ``_sunder_`` names
""""""""""""""""""""""""""""

- :meth:`~EnumType._add_alias_` -- adds a new name as an alias to an existing
member.
- :meth:`~EnumType._add_value_alias_` -- adds a new value as an alias to an
existing member.
- :attr:`~Enum._name_` -- name of the member
- :attr:`~Enum._value_` -- value of the member; can be set in ``__new__``
- :meth:`~Enum._missing_` -- a lookup function used when a value is not found;
Expand All @@ -903,6 +913,11 @@ Supported ``_sunder_`` names
For :class:`Flag` classes the next value chosen will be the next highest
power-of-two.

- :meth:`~Enum._add_alias_` -- adds a new name as an alias to an existing
member.
- :meth:`~Enum._add_value_alias_` -- adds a new value as an alias to an
existing member.

- While ``_sunder_`` names are generally reserved for the further development
of the :class:`Enum` class and can not be used, some are explicitly allowed:

Expand Down
Loading
Loading