import csv
import hashlib
import os
import platform
import re
import shlex
import shutil
import subprocess
import sysconfig
from collections.abc import Iterable, Iterator, MutableMapping
from dataclasses import dataclass
from os.path import relpath
from pathlib import Path
from pprint import pprint
from runpy import run_path
from textwrap import dedent
from typing import Any

from build import ProjectBuilder
from build.env import IsolatedEnv
from elftools.common.exceptions import ELFError
from elftools.elf.elffile import ELFFile
from filelock import FileLock

from cibuildwheel import errors, platforms  # pylint: disable=cyclic-import
from cibuildwheel.architecture import Architecture, arch_synonym
from cibuildwheel.frontend import get_build_frontend_extra_flags, parse_config_settings
from cibuildwheel.logger import log
from cibuildwheel.options import BuildOptions, Options
from cibuildwheel.selector import BuildSelector
from cibuildwheel.util import resources
from cibuildwheel.util.cmd import call, shell
from cibuildwheel.util.file import CIBW_CACHE_PATH, copy_test_sources, download, move_file
from cibuildwheel.util.helpers import prepare_command
from cibuildwheel.util.packaging import find_compatible_wheel
from cibuildwheel.util.python_build_standalone import create_python_build_standalone_environment
from cibuildwheel.venv import constraint_flags, find_uv, virtualenv

ANDROID_TRIPLET = {
    "arm64_v8a": "aarch64-linux-android",
    "x86_64": "x86_64-linux-android",
}


def parse_identifier(identifier: str) -> tuple[str, str]:
    match = re.fullmatch(r"cp(\d)(\d+)-android_(.+)", identifier)
    if not match:
        msg = f"invalid Android identifier: '{identifier}'"
        raise ValueError(msg)
    major, minor, arch = match.groups()
    return (f"{major}.{minor}", arch)


def android_triplet(identifier: str) -> str:
    return ANDROID_TRIPLET[parse_identifier(identifier)[1]]


@dataclass(frozen=True)
class PythonConfiguration:
    version: str
    identifier: str
    url: str

    @property
    def arch(self) -> str:
        return parse_identifier(self.identifier)[1]


def all_python_configurations() -> list[PythonConfiguration]:
    return [PythonConfiguration(**item) for item in resources.read_python_configs("android")]


def get_python_configurations(
    build_selector: BuildSelector, architectures: set[Architecture]
) -> list[PythonConfiguration]:
    return [
        c
        for c in all_python_configurations()
        if c.arch in architectures and build_selector(c.identifier)
    ]


def shell_prepared(command: str, *, build_options: BuildOptions, env: dict[str, str]) -> None:
    shell(
        prepare_command(command, project=".", package=build_options.package_dir),
        env=env,
    )


def before_all(options: Options, python_configurations: list[PythonConfiguration]) -> None:
    before_all_options = options.build_options(python_configurations[0].identifier)
    if before_all_options.before_all:
        log.step("Running before_all...")
        shell_prepared(
            before_all_options.before_all,
            build_options=before_all_options,
            env=before_all_options.environment.as_dictionary(os.environ),
        )


@dataclass(frozen=True)
class BuildState:
    config: PythonConfiguration
    options: BuildOptions
    build_path: Path
    python_dir: Path
    build_env: dict[str, str]
    android_env: dict[str, str]


def build(options: Options, tmp_path: Path) -> None:
    if "ANDROID_HOME" not in os.environ:
        msg = (
            "ANDROID_HOME environment variable is not set. For instructions, see "
            "https://cibuildwheel.pypa.io/en/stable/platforms/#android"
        )
        raise errors.FatalError(msg)

    configs = get_python_configurations(
        options.globals.build_selector, options.globals.architectures
    )
    if not configs:
        return

    try:
        before_all(options, configs)

        built_wheels: list[Path] = []
        for config in configs:
            log.build_start(config.identifier)
            build_options = options.build_options(config.identifier)
            build_path = tmp_path / config.identifier
            build_path.mkdir()
            python_dir = setup_target_python(config, build_path)
            build_env, android_env = setup_env(config, build_options, build_path, python_dir)
            state = BuildState(
                config, build_options, build_path, python_dir, build_env, android_env
            )

            compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
            if compatible_wheel:
                print(
                    f"\nFound previously built wheel {compatible_wheel.name} that is "
                    f"compatible with {config.identifier}. Skipping build step..."
                )
                repaired_wheel = compatible_wheel
            else:
                before_build(state)
                built_wheel = build_wheel(state)
                repaired_wheel = repair_wheel(state, built_wheel)

            test_wheel(state, repaired_wheel, build_frontend=build_options.build_frontend.name)

            output_wheel: Path | None = None
            if compatible_wheel is None:
                output_wheel = move_file(
                    repaired_wheel, build_options.output_dir / repaired_wheel.name
                )
                built_wheels.append(output_wheel)

            shutil.rmtree(build_path)
            log.build_end(output_wheel)

    except subprocess.CalledProcessError as error:
        msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
        raise errors.FatalError(msg) from error


def setup_target_python(config: PythonConfiguration, build_path: Path) -> Path:
    log.step("Installing target Python...")
    python_tgz = CIBW_CACHE_PATH / config.url.rpartition("/")[-1]
    with FileLock(f"{python_tgz}.lock"):
        if not python_tgz.exists():
            download(config.url, python_tgz)

    python_dir = build_path / "python"
    python_dir.mkdir()
    shutil.unpack_archive(python_tgz, python_dir)
    return python_dir


def setup_env(
    config: PythonConfiguration, build_options: BuildOptions, build_path: Path, python_dir: Path
) -> tuple[dict[str, str], dict[str, str]]:
    """
    Returns two environment dicts, both pointing at the same virtual environment:

    * build_env, which uses the environment normally.
    * android_env, which uses the environment while simulating running on Android.
    """
    log.step("Setting up build environment...")
    build_frontend = build_options.build_frontend.name
    use_uv = build_frontend in {"build[uv]", "uv"}
    uv_path = find_uv()
    if use_uv and uv_path is None:
        msg = "uv not found"
        raise AssertionError(msg)
    pip = ["pip"] if not use_uv else [str(uv_path), "pip"]

    # Create virtual environment
    python_exe = create_python_build_standalone_environment(
        config.version, build_path, CIBW_CACHE_PATH
    )
    venv_dir = build_path / "venv"
    dependency_constraint = build_options.dependency_constraints.get_for_python_version(
        version=config.version, tmp_dir=build_path
    )
    build_env = virtualenv(
        config.version, python_exe, venv_dir, dependency_constraint, use_uv=use_uv
    )
    create_cmake_toolchain(config, build_path, python_dir, build_env)

    # Apply custom environment variables, and check environment is still valid
    build_env = build_options.environment.as_dictionary(build_env)
    build_env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
    for command in ["python"] if use_uv else ["python", "pip"]:
        command_path = call("which", command, env=build_env, capture_stdout=True).strip()
        if command_path != f"{venv_dir}/bin/{command}":
            msg = (
                f"{command} available on PATH doesn't match our installed instance. If you "
                f"have modified PATH, ensure that you don't overwrite cibuildwheel's entry "
                f"or insert {command} above it."
            )
            raise errors.FatalError(msg)
        call(command, "--version", env=build_env)

    # Construct an altered environment which simulates running on Android.
    android_env = setup_android_env(config, python_dir, venv_dir, build_env)

    # Install build tools
    if build_frontend not in {"build", "build[uv]", "uv"}:
        msg = "Android requires the build frontend to be 'build' or 'uv'"
        raise errors.FatalError(msg)
    call(*pip, "install", "build", *constraint_flags(dependency_constraint), env=build_env)

    # Build-time requirements must be queried within android_env, because
    # `get_requires_for_build` can run arbitrary code in setup.py scripts, which may be
    # affected by the target platform. However, the requirements must be installed
    # within build_env, because they're going to run on the build machine.
    #
    # The `build` CLI doesn't support this combination, so we use its API to query the
    # requirements, and then install them ourselves with pip. We'll later run `build` in
    # the same environment, passing the `--no-isolation` option.
    class AndroidEnv(IsolatedEnv):
        @property
        def python_executable(self) -> str:
            return f"{venv_dir}/bin/python"

        def make_extra_environ(self) -> dict[str, str]:
            return android_env

    pb = ProjectBuilder.from_isolated_env(AndroidEnv(), build_options.package_dir)
    if pb.build_system_requires:
        call(*pip, "install", *pb.build_system_requires, env=build_env)

    requires_for_build = pb.get_requires_for_build(
        "wheel", parse_config_settings(build_options.config_settings)
    )
    if requires_for_build:
        call(*pip, "install", *requires_for_build, env=build_env)

    return build_env, android_env


def create_cmake_toolchain(
    config: PythonConfiguration, build_path: Path, python_dir: Path, build_env: dict[str, str]
) -> None:
    toolchain_path = build_path / "toolchain.cmake"
    build_env["CMAKE_TOOLCHAIN_FILE"] = str(toolchain_path)
    with open(toolchain_path, "w", encoding="UTF-8") as toolchain_file:
        print(
            dedent(
                f"""\
                # To support as many build systems as possible, we use environment
                # variables as the single source of truth for compiler flags and paths,
                # so they don't need to be specified here.

                set(CMAKE_SYSTEM_NAME Android)
                set(CMAKE_SYSTEM_PROCESSOR {android_triplet(config.identifier).split("-")[0]})

                # Inhibit all of CMake's own NDK handling code.
                set(CMAKE_SYSTEM_VERSION 1)

                # Tell CMake where to look for headers and libraries.
                set(CMAKE_FIND_ROOT_PATH "{python_dir}/prefix")
                set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
                set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
                set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
                set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH)

                # Allow CMake to run Python in the simulated Android environment when
                # policy CMP0190 is active.
                set(CMAKE_CROSSCOMPILING_EMULATOR /bin/sh -c [["$0" "$@"]])
                """
            ),
            file=toolchain_file,
        )


def localize_sysconfigdata(
    python_dir: Path, build_env: dict[str, str], sysconfigdata_path: Path
) -> dict[str, Any]:
    sysconfigdata: dict[str, Any] = run_path(str(sysconfigdata_path))["build_time_vars"]
    with sysconfigdata_path.open("w", encoding="UTF-8") as f:
        f.write("# Generated by cibuildwheel\n")
        f.write("build_time_vars = ")
        sysconfigdata = localized_vars(build_env, sysconfigdata, python_dir / "prefix")
        pprint(sysconfigdata, stream=f, compact=True)
        return sysconfigdata


def localized_vars(
    build_env: dict[str, str], orig_vars: dict[str, Any], prefix: Path
) -> dict[str, Any]:
    orig_prefix = orig_vars["prefix"]
    localized_vars_ = {}
    for key, value in orig_vars.items():
        # The host's sysconfigdata will include references to build-time paths.
        # Update these to refer to the current prefix.
        final = value
        if isinstance(final, str):
            final = final.replace(orig_prefix, str(prefix))

        if key == "ANDROID_API_LEVEL":
            if api_level := build_env.get(key):
                final = int(api_level)

        # Build systems vary in whether FLAGS variables are read from sysconfig, and if so,
        # whether they're replaced by environment variables or combined with them. Even
        # setuptools has changed its behavior here
        # (https://github.com/pypa/setuptools/issues/4836).
        #
        # Ensure consistency by clearing the sysconfig variables and letting the environment
        # variables take effect alone. This will also work for any non-Python build systems
        # which the build script may call.
        elif key in ["CFLAGS", "CXXFLAGS", "LDFLAGS"]:
            final = ""

        # These variables contain an embedded copy of LDFLAGS.
        elif key in ["LDSHARED", "LDCXXSHARED"]:
            final = final.removesuffix(" " + orig_vars["LDFLAGS"])

        localized_vars_[key] = final

    return localized_vars_


def setup_android_env(
    config: PythonConfiguration, python_dir: Path, venv_dir: Path, build_env: dict[str, str]
) -> dict[str, str]:
    site_packages = next(venv_dir.glob("lib/python*/site-packages"))
    for suffix in ["pth", "py"]:
        shutil.copy(resources.PATH / f"_cross_venv.{suffix}", site_packages)

    sysconfigdata_path = Path(
        shutil.copy(
            next(python_dir.glob("prefix/lib/python*/_sysconfigdata_*.py")),
            site_packages,
        )
    )
    sysconfigdata = localize_sysconfigdata(python_dir, build_env, sysconfigdata_path)

    # Activate the code in _cross_venv.py.
    android_env = build_env.copy()
    android_env["CIBW_HOST_TRIPLET"] = android_triplet(config.identifier)

    # Get the environment variables needed to build for Android (CC, CFLAGS, etc). These are
    # generated by https://github.com/python/cpython/blob/main/Android/android-env.sh.
    env_output = call(python_dir / "android.py", "env", env=build_env, capture_stdout=True)

    # shlex.split should produce a sequence alternating between:
    #   * the word "export"
    #   * a key=value string, without quotes
    for i, token in enumerate(shlex.split(env_output)):
        if i % 2 == 0:
            assert token == "export", token
        else:
            key, sep, value = token.partition("=")
            assert sep == "=", token
            android_env[key] = value

    # localized_vars cleared the CFLAGS and CXXFLAGS in the sysconfigdata, but most
    # packages take their optimization flags from these variables. Pass these flags via
    # environment variables instead.
    #
    # We don't enable debug information, because it significantly increases binary size,
    # and most Android app developers don't have the NDK installed, so they would have no
    # way to strip it.
    opt = " ".join(word for word in sysconfigdata["OPT"].split() if not word.startswith("-g"))
    for key in ["CFLAGS", "CXXFLAGS"]:
        android_env[key] += " " + opt

    # Cargo target linker needs to be specified after CC is set
    setup_rust(config, python_dir, android_env)

    # Format the environment so it can be pasted into a shell when debugging.
    for key, value in sorted(android_env.items()):
        if os.environ.get(key) != value:
            print(f"export {key}={shlex.quote(value)}")

    return android_env


def setup_rust(
    config: PythonConfiguration,
    python_dir: Path,
    env: MutableMapping[str, str],
) -> None:
    cargo_target = android_triplet(config.identifier)

    # CARGO_BUILD_TARGET is the variable used by Cargo and setuptools_rust
    env["CARGO_BUILD_TARGET"] = cargo_target

    # The linker needs to be specified after CC is set by android-env.sh
    cargo_target_linker_env_name = f"CARGO_TARGET_{cargo_target.upper().replace('-', '_')}_LINKER"
    # CC has already been set by calling android.py (it calls android-env.sh)
    env[cargo_target_linker_env_name] = env["CC"]

    # All Python extension modules must be explicitly linked against libpython3.x.so when building for Android.
    # See: https://peps.python.org/pep-0738/#linkage
    # For projects using PyO3, this requires setting PYO3_CROSS_LIB_DIR to the directory containing libpython3.x.so.
    # See: https://pyo3.rs/v0.27.1/building-and-distribution.html#cross-compiling
    env["PYO3_CROSS_LIB_DIR"] = str(python_dir / "prefix" / "lib")

    venv_bin = Path(env["VIRTUAL_ENV"]) / "bin"
    for tool in ["cargo", "rustup"]:
        shim_path = venv_bin / tool
        shutil.copy(resources.PATH / "_rust_shim.py", shim_path)
        shim_path.chmod(0o755)


def before_build(state: BuildState) -> None:
    if state.options.before_build:
        log.step("Running before_build...")
        shell_prepared(
            state.options.before_build,
            build_options=state.options,
            env=state.build_env,
        )


def build_wheel(state: BuildState) -> Path:
    log.step("Building wheel...")
    built_wheel_dir = state.build_path / "built_wheel"
    match state.options.build_frontend.name:
        case "build" | "build[uv]":
            call(
                "python",
                "-m",
                "build",
                state.options.package_dir,
                "--wheel",
                "--no-isolation",
                "--skip-dependency-check",
                f"--outdir={built_wheel_dir}",
                *get_build_frontend_extra_flags(
                    state.options.build_frontend,
                    state.options.build_verbosity,
                    state.options.config_settings,
                    py38=False,
                ),
                env=state.android_env,
            )
        case "uv":
            uv_path = find_uv()
            assert uv_path is not None
            call(
                uv_path,
                "build",
                state.options.package_dir,
                "--wheel",
                "--no-build-isolation",
                f"--out-dir={built_wheel_dir}",
                *get_build_frontend_extra_flags(
                    state.options.build_frontend,
                    state.options.build_verbosity,
                    state.options.config_settings,
                    py38=False,
                ),
                env=state.android_env,
            )
        case x:
            msg = f"Invalid build backend {x!r}"
            raise AssertionError(msg)

    built_wheels = list(built_wheel_dir.glob("*.whl"))
    if len(built_wheels) != 1:
        msg = f"{built_wheel_dir} contains {len(built_wheels)} wheels; expected 1"
        raise errors.FatalError(msg)
    built_wheel = built_wheels[0]

    if built_wheel.name.endswith("none-any.whl"):
        raise errors.NonPlatformWheelError()
    return built_wheel


def repair_wheel(state: BuildState, built_wheel: Path) -> Path:
    log.step("Repairing wheel...")
    repaired_wheel_dir = state.build_path / "repaired_wheel"
    repaired_wheel_dir.mkdir()

    if state.options.repair_command:
        shell(
            prepare_command(
                state.options.repair_command,
                wheel=built_wheel,
                dest_dir=repaired_wheel_dir,
                package=state.options.package_dir,
                project=".",
            ),
            env=state.build_env,
        )
    else:
        repair_default(state.android_env, built_wheel, repaired_wheel_dir)

    repaired_wheels = list(repaired_wheel_dir.glob("*.whl"))
    if len(repaired_wheels) == 0:
        raise errors.RepairStepProducedNoWheelError()
    if len(repaired_wheels) != 1:
        msg = f"{repaired_wheel_dir} contains {len(repaired_wheels)} wheels; expected 1"
        raise errors.FatalError(msg)
    repaired_wheel = repaired_wheels[0]

    if repaired_wheel.name.endswith("none-any.whl"):
        raise errors.NonPlatformWheelError()
    return repaired_wheel


def repair_default(
    android_env: dict[str, str], built_wheel: Path, repaired_wheel_dir: Path
) -> None:
    """
    Adds libc++ to the wheel if anything links against it. In the future this should be
    moved to auditwheel and generalized to support more libraries.
    """
    if (match := re.search(r"^(.+?)-", built_wheel.name)) is None:
        msg = f"Failed to parse wheel filename: {built_wheel.name}"
        raise errors.FatalError(msg)
    wheel_name = match[1]

    unpacked_dir = repaired_wheel_dir / "unpacked"
    unpacked_dir.mkdir()
    shutil.unpack_archive(built_wheel, unpacked_dir, format="zip")

    # Some build systems are inconsistent about name normalization, so don't assume the
    # dist-info name is identical to the wheel name.
    record_paths = list(unpacked_dir.glob("*.dist-info/RECORD"))
    if len(record_paths) != 1:
        msg = f"{built_wheel.name} contains {len(record_paths)} dist-info/RECORD files; expected 1"
        raise errors.FatalError(msg)

    old_soname = "libc++_shared.so"
    paths_to_patch = []
    for path, elffile in elf_file_filter(
        unpacked_dir / filename
        for filename, *_ in csv.reader(record_paths[0].read_text().splitlines())
    ):
        if (dynamic := elffile.get_section_by_name(".dynamic")) and any(  # type: ignore[no-untyped-call]
            tag.entry.d_tag == "DT_NEEDED" and tag.needed == old_soname
            for tag in dynamic.iter_tags()
        ):
            paths_to_patch.append(path)

    if not paths_to_patch:
        shutil.copyfile(built_wheel, repaired_wheel_dir / built_wheel.name)
    else:
        # Android doesn't support DT_RPATH, but supports DT_RUNPATH since API level 24
        # (https://github.com/aosp-mirror/platform_bionic/blob/master/android-changes-for-ndk-developers.md).
        if int(sysconfig_print('get_config_vars()["ANDROID_API_LEVEL"]', android_env)) < 24:
            msg = f"Adding {old_soname} requires ANDROID_API_LEVEL to be at least 24"
            raise errors.FatalError(msg)

        toolchain = Path(android_env["CC"]).parent.parent
        src_path = toolchain / f"sysroot/usr/lib/{android_env['CIBW_HOST_TRIPLET']}/{old_soname}"

        # Use the same library location as auditwheel would.
        libs_dir = unpacked_dir / (wheel_name + ".libs")
        libs_dir.mkdir()
        new_soname = soname_with_hash(src_path)
        dst_path = libs_dir / new_soname
        shutil.copyfile(src_path, dst_path)
        call(which("patchelf"), "--set-soname", new_soname, dst_path)

        for path in paths_to_patch:
            call(which("patchelf"), "--replace-needed", old_soname, new_soname, path)
            call(
                which("patchelf"),
                "--set-rpath",
                f"${{ORIGIN}}/{relpath(libs_dir, path.parent)}",
                path,
            )
        call(which("wheel"), "pack", unpacked_dir, "-d", repaired_wheel_dir)


# If cibuildwheel was called without activating its environment, its scripts directory
# will not be on the PATH.
def which(cmd: str) -> str:
    scripts_dir = sysconfig.get_path("scripts")
    result = shutil.which(cmd, path=scripts_dir + os.pathsep + os.environ["PATH"])
    if result is None:
        msg = f"Couldn't find {cmd!r} in {scripts_dir} or on the PATH"
        raise errors.FatalError(msg)
    return result


def elf_file_filter(paths: Iterable[Path]) -> Iterator[tuple[Path, ELFFile]]:
    """Filter through an iterator of filenames and load up only ELF files"""
    for path in paths:
        if not path.name.endswith(".py"):
            try:
                with open(path, "rb") as f:
                    candidate = ELFFile(f)  # type: ignore[no-untyped-call]
                    yield path, candidate
            except ELFError:
                pass  # Not an ELF file


def soname_with_hash(src_path: Path) -> str:
    """Return the same library filename as auditwheel would"""
    shorthash = hashlib.sha256(src_path.read_bytes()).hexdigest()[:8]
    src_name = src_path.name
    base, ext = src_name.split(".", 1)
    if not base.endswith(f"-{shorthash}"):
        return f"{base}-{shorthash}.{ext}"
    else:
        return src_name


def test_wheel(state: BuildState, wheel: Path, *, build_frontend: str) -> None:
    test_command = state.options.test_command
    if not (test_command and state.options.test_selector(state.config.identifier)):
        return

    log.step("Testing wheel...")
    use_uv = build_frontend == "build[uv]"
    uv_path = find_uv()
    if use_uv and uv_path is None:
        msg = "uv not found"
        raise AssertionError(msg)
    pip = ["pip"] if not use_uv else [str(uv_path), "pip"]

    native_arch = arch_synonym(platform.machine(), platforms.native_platform(), "android")
    if state.config.arch != native_arch:
        log.warning(
            f"Skipping tests for {state.config.arch}, as the build machine only "
            f"supports {native_arch}"
        )
        return

    if state.options.before_test:
        shell_prepared(
            state.options.before_test,
            build_options=state.options,
            env=state.build_env,
        )

    platform_args = (
        ["--python-platform", android_triplet(state.config.identifier)]
        if use_uv
        else [
            "--platform",
            sysconfig_print("get_platform()", state.android_env).replace("-", "_"),
        ]
    )

    # Install the wheel and test-requires.
    site_packages_dir = state.build_path / "site-packages"
    site_packages_dir.mkdir()
    call(
        *pip,
        "install",
        "--only-binary=:all:",
        *platform_args,
        "--target",
        site_packages_dir,
        f"{wheel}{state.options.test_extras}",
        *state.options.test_requires,
        env=state.android_env,
    )

    # Copy test-sources.
    cwd_dir = state.build_path / "cwd"
    cwd_dir.mkdir()
    if state.options.test_sources:
        copy_test_sources(state.options.test_sources, Path.cwd(), cwd_dir)
    else:
        (cwd_dir / "test_fail.py").write_text(
            resources.TEST_FAIL_CWD_FILE.read_text(),
        )

    # Android doesn't support placeholders in the test command.
    if any(("{" + placeholder + "}") in test_command for placeholder in ("project", "package")):
        msg = (
            f"Test command {test_command!r} with a "
            "'{project}' or '{package}' placeholder is not supported on Android, "
            "because the source directory is not visible on the emulator."
        )
        raise errors.FatalError(msg)

    # Parse test-command.
    test_args = shlex.split(test_command)
    if test_args[0] in {"python", "python3"} and any(arg in test_args for arg in ("-c", "-m")):
        # Forward the args to the CPython testbed script. We require '-c' or '-m'
        # to be in the command, because without those flags, the testbed script
        # will prepend '-m test', which will run Python's own test suite.
        del test_args[0]
    elif test_args[0] == "pytest":
        # We transform some commands into the `python -m` form, but this is deprecated.
        msg = (
            f"Test command {test_command!r} is not supported on Android. "
            "cibuildwheel will try to execute it as if it started with 'python -m'. "
            "If this works, all you need to do is add that to your test command."
        )
        log.warning(msg)
        test_args.insert(0, "-m")
    else:
        msg = (
            f"Test command {test_command!r} is not supported on Android. "
            f"Command must begin with 'python' or 'python3', and contain '-m' or '-c'."
        )
        raise errors.FatalError(msg)

    # By default, run on a testbed managed emulator running the newest supported
    # Android version. However, if the user specifies a --managed or --connected
    # test execution argument, that argument takes precedence.
    test_runtime_args = state.options.test_runtime.args

    if any(arg.startswith(("--managed", "--connected")) for arg in test_runtime_args):
        emulator_args = []
    else:
        emulator_args = ["--managed", "maxVersion"]

    # Run the test app.
    call(
        state.python_dir / "android.py",
        "test",
        "--site-packages",
        site_packages_dir,
        "--cwd",
        cwd_dir,
        *emulator_args,
        *(["-v"] if state.options.build_verbosity > 0 else []),
        *test_runtime_args,
        "--",
        *test_args,
        env=state.build_env,
    )


def sysconfig_print(method_call: str, env: dict[str, str]) -> str:
    return call(
        "python",
        "-c",
        f'import sysconfig; print(sysconfig.{method_call}, end="")',
        env=env,
        capture_stdout=True,
    )
