From 171560bebf13fefbf9763df29611ae9fc5000fa0 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Fri, 3 Apr 2026 21:31:50 +0100 Subject: [PATCH 1/6] Increase Android emulator RAM to 4 GB --- Android/README.md | 8 - Android/testbed/app/build.gradle.kts | 145 +++++++++++++++++- ...-04-03-21-37-18.gh-issue-144418.PusC0S.rst | 1 + 3 files changed, 144 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Tests/2026-04-03-21-37-18.gh-issue-144418.PusC0S.rst diff --git a/Android/README.md b/Android/README.md index 9f71aeb934f386..0004f26e72b21c 100644 --- a/Android/README.md +++ b/Android/README.md @@ -103,14 +103,6 @@ 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 -Studio Koala, 2 GB is the default for all emulators, although the user interface -may indicate otherwise. Locate the emulator's directory under `~/.android/avd`, -and find `hw.ramSize` in both config.ini and hardware-qemu.ini. Either set these -manually to the same value, or use the Android Studio Device Manager, which will -update both files. - You can run the test suite either: * Within the CPython repository, after doing a build as described above. On diff --git a/Android/testbed/app/build.gradle.kts b/Android/testbed/app/build.gradle.kts index 53cdc591fa35fd..55f1b869762453 100644 --- a/Android/testbed/app/build.gradle.kts +++ b/Android/testbed/app/build.gradle.kts @@ -18,6 +18,14 @@ val KNOWN_ABIS = mapOf( "x86_64-linux-android" to "x86_64", ) +val osArch = System.getProperty("os.arch") +val NATIVE_ABI = mapOf( + "aarch64" to "arm64-v8a", + "amd64" to "x86_64", + "arm64" to "arm64-v8a", + "x86_64" to "x86_64", +)[osArch] ?: throw GradleException("Unknown os.arch '$osArch'") + // Discover prefixes. val prefixes = ArrayList() if (inSourceTree) { @@ -149,6 +157,9 @@ android { testOptions { managedDevices { localDevices { + // systemImageSource should use what its documentation calls an + // "explicit source", i.e. the sdkmanager package name format, because + // that will be required in CreateEmulatorTask below. create("minVersion") { device = "Small Phone" @@ -157,13 +168,13 @@ android { // ATD devices are smaller and faster, but have a minimum // API level of 30. - systemImageSource = if (apiLevel >= 30) "aosp-atd" else "aosp" + systemImageSource = if (apiLevel >= 30) "aosp_atd" else "default" } create("maxVersion") { device = "Small Phone" apiLevel = defaultConfig.targetSdk!! - systemImageSource = "aosp-atd" + systemImageSource = "aosp_atd" } } @@ -189,6 +200,136 @@ dependencies { } +afterEvaluate { + // Every new emulator has a maximum of 2 GB RAM, regardless of its hardware profile + // (https://cs.android.com/android-studio/platform/tools/base/+/refs/tags/studio-2025.3.2:sdklib/src/main/java/com/android/sdklib/internal/avd/EmulatedProperties.java;l=68). + // This is barely enough to test Python, and not enough to test Pandas + // (https://github.com/python/cpython/pull/137186#issuecomment-3136301023, + // https://github.com/pandas-dev/pandas/pull/63405#issuecomment-3667846159). + // So we'll increase it by editing the emulator configuration files. + // + // If the emulator doesn't exist yet, we want to edit it after it's created, but + // before it starts for the first time. Otherwise it'll need to be cold-booted + // again, which would slow down the first run, which is likely the only run in CI + // environments. But the Setup task both creates and starts the emulator if it + // doesn't already exist. So we create it ourselves before the Setup task runs. + for (device in android.testOptions.managedDevices.localDevices) { + val createTask = tasks.register("${device.name}Create") { + this.device = device.device + apiLevel = device.apiLevel + systemImageSource = device.systemImageSource + abi = NATIVE_ABI + } + tasks.named("${device.name}Setup") { + dependsOn(createTask) + } + } +} + +abstract class CreateEmulatorTask : DefaultTask() { + @get:Input abstract val device: Property + @get:Input abstract val apiLevel: Property + @get:Input abstract val systemImageSource: Property + @get:Input abstract val abi: Property + @get:Inject abstract val execOps: ExecOperations + + private val avdName by lazy { + listOf( + "dev${apiLevel.get()}", + systemImageSource.get(), + abi.get(), + device.get().replace(' ', '_'), + ).joinToString("_") + } + + private val avdDir by lazy { + val userHome = + System.getenv("ANDROID_USER_HOME") ?: + (System.getProperty("user.home")!! + "/.android") + File("$userHome/avd/gradle-managed", "$avdName.avd") + } + + @TaskAction + fun run() { + if (!avdDir.exists()) { + createAvd() + } + updateAvd() + } + + fun createAvd() { + val systemImage = listOf( + "system-images", + "android-${apiLevel.get()}", + systemImageSource.get(), + abi.get(), + ).joinToString(";") + + runCmdlineTool("sdkmanager", systemImage) + runCmdlineTool( + "avdmanager", "create", "avd", + "--name", avdName, + "--path", avdDir, + "--device", device.get().lowercase().replace(" ", "_"), + "--package", systemImage, + ) + + val iniName = "$avdName.ini" + if (!File(avdDir.parentFile.parentFile, iniName).renameTo( + File(avdDir.parentFile, iniName) + )) { + throw GradleException("Failed to rename $iniName") + } + } + + fun updateAvd() { + for (filename in listOf( + "config.ini", // Created by avdmanager; always exists + "hardware-qemu.ini", // Created on first run; might not exist + )) { + val iniFile = File(avdDir, filename) + if (!iniFile.exists()) { + if (filename == "config.ini") { + throw GradleException("$iniFile does not exist") + } + continue + } + + val iniText = iniFile.readText() + val pattern = Regex( + """^\s*hw.ramSize\s*=\s*(.+?)\s*$""", RegexOption.MULTILINE + ) + val matches = pattern.findAll(iniText).toList() + if (matches.size != 1) { + throw GradleException( + "Found ${matches.size} instances of $pattern in $iniFile; expected 1" + ) + } + + val expectedRam = "4096" + if (matches[0].groupValues[1] != expectedRam) { + iniFile.writeText( + iniText.replace(pattern, "hw.ramSize = $expectedRam") + ) + } + } + } + + fun runCmdlineTool(tool: String, vararg args: Any) { + val androidHome = System.getenv("ANDROID_HOME")!! + val exeSuffix = + if (System.getProperty("os.name").lowercase().startsWith("win")) ".exe" + else "" + val command = + listOf("$androidHome/cmdline-tools/latest/bin/$tool$exeSuffix", *args) + println(command.joinToString(" ")) + execOps.exec { + commandLine(command) + } + } +} + + // Create some custom tasks to copy Python and its standard library from // elsewhere in the repository. androidComponents.onVariants { variant -> diff --git a/Misc/NEWS.d/next/Tests/2026-04-03-21-37-18.gh-issue-144418.PusC0S.rst b/Misc/NEWS.d/next/Tests/2026-04-03-21-37-18.gh-issue-144418.PusC0S.rst new file mode 100644 index 00000000000000..dd72996d51aa88 --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2026-04-03-21-37-18.gh-issue-144418.PusC0S.rst @@ -0,0 +1 @@ +The Android testbed's emulator RAM has been increased from 2 GB to 4 GB. From e41718922787700466ec6a6d3555fdf22f2c9676 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Fri, 3 Apr 2026 21:59:57 +0100 Subject: [PATCH 2/6] Debug CI failure --- Android/testbed/app/build.gradle.kts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Android/testbed/app/build.gradle.kts b/Android/testbed/app/build.gradle.kts index 55f1b869762453..d1daac321368e5 100644 --- a/Android/testbed/app/build.gradle.kts +++ b/Android/testbed/app/build.gradle.kts @@ -278,6 +278,10 @@ abstract class CreateEmulatorTask : DefaultTask() { if (!File(avdDir.parentFile.parentFile, iniName).renameTo( File(avdDir.parentFile, iniName) )) { + // FIXME + execOps.exec { + commandLine("ls", "-lR", avdDir.parentFile.parentFile) + } throw GradleException("Failed to rename $iniName") } } From 201ac9f06e3adb186528f76d989ccc1017def830 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Fri, 3 Apr 2026 23:27:25 +0100 Subject: [PATCH 3/6] Add tmate --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 025032a3ae68c4..428c440451ecfa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -350,6 +350,7 @@ jobs: runs-on: ${{ matrix.runs-on }} steps: + - uses: mxschmitt/action-tmate@v3 # FIXME - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false From b6c79855a9290f331af5e10063522b1e5485434b Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Sat, 4 Apr 2026 00:17:13 +0100 Subject: [PATCH 4/6] Respect XDG_CONFIG_HOME environment variable --- .github/workflows/build.yml | 5 ++++- Android/testbed/app/build.gradle.kts | 12 +++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 428c440451ecfa..d69227a5913846 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -350,13 +350,16 @@ jobs: runs-on: ${{ matrix.runs-on }} steps: - - uses: mxschmitt/action-tmate@v3 # FIXME - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Build and test run: ./Android/android.py ci --fast-ci ${{ matrix.arch }}-linux-android + # FIXME + - if: ${{ failure() }} + uses: mxschmitt/action-tmate@v3 + build-ios: name: iOS needs: build-context diff --git a/Android/testbed/app/build.gradle.kts b/Android/testbed/app/build.gradle.kts index d1daac321368e5..2b64a715a6b16e 100644 --- a/Android/testbed/app/build.gradle.kts +++ b/Android/testbed/app/build.gradle.kts @@ -243,9 +243,11 @@ abstract class CreateEmulatorTask : DefaultTask() { } private val avdDir by lazy { - val userHome = - System.getenv("ANDROID_USER_HOME") ?: - (System.getProperty("user.home")!! + "/.android") + // XDG_CONFIG_HOME is respected by both avdmanager and Gradle. + val userHome = System.getenv("ANDROID_USER_HOME") ?: ( + (System.getenv("XDG_CONFIG_HOME") ?: System.getProperty("user.home")!!) + + "/.android" + ) File("$userHome/avd/gradle-managed", "$avdName.avd") } @@ -278,10 +280,6 @@ abstract class CreateEmulatorTask : DefaultTask() { if (!File(avdDir.parentFile.parentFile, iniName).renameTo( File(avdDir.parentFile, iniName) )) { - // FIXME - execOps.exec { - commandLine("ls", "-lR", avdDir.parentFile.parentFile) - } throw GradleException("Failed to rename $iniName") } } From 83c09857ae6dd9f609d6555549093da0e624c8f5 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Sat, 4 Apr 2026 00:40:45 +0100 Subject: [PATCH 5/6] Run tmate unconditionally --- .github/workflows/build.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d69227a5913846..8a444275df8f82 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -357,8 +357,7 @@ jobs: run: ./Android/android.py ci --fast-ci ${{ matrix.arch }}-linux-android # FIXME - - if: ${{ failure() }} - uses: mxschmitt/action-tmate@v3 + - uses: mxschmitt/action-tmate@v3 build-ios: name: iOS From 9cc17171a344b3fa0d720aeb06374f980e5bc5a9 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Sat, 4 Apr 2026 01:29:05 +0100 Subject: [PATCH 6/6] Remove tmate --- .github/workflows/build.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b8bb9eb104e209..a80262e3c0243c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -356,9 +356,6 @@ jobs: - name: Build and test run: ./Android/android.py ci --fast-ci ${{ matrix.arch }}-linux-android - # FIXME - - uses: mxschmitt/action-tmate@v3 - build-ios: name: iOS needs: build-context