]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-144418: Increase Android testbed emulator RAM to 4 GB (#148054)
authorMalcolm Smith <smith@chaquo.com>
Mon, 6 Apr 2026 05:48:00 +0000 (06:48 +0100)
committerGitHub <noreply@github.com>
Mon, 6 Apr 2026 05:48:00 +0000 (13:48 +0800)
Pre-create the Android emulator image so that the the configuration can be
modified to use 4GB of RAM.

Android/README.md
Android/testbed/app/build.gradle.kts
Misc/NEWS.d/next/Tests/2026-04-03-21-37-18.gh-issue-144418.PusC0S.rst [new file with mode: 0644]

index 9f71aeb934f386d42cb1b6be368ab4b921e68364..0004f26e72b21c4c30f656207c9624849c149e52 100644 (file)
@@ -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
index 7529fdb8f7852f2e967d58d2697cfbcf74f74881..b58edc04a929d9f323e607810157d893ded2fcf9 100644 (file)
@@ -20,6 +20,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<File>()
 if (inSourceTree) {
@@ -151,6 +159,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"
 
@@ -159,13 +170,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"
                 }
             }
 
@@ -191,6 +202,138 @@ 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<CreateEmulatorTask>("${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<String>
+    @get:Input abstract val apiLevel: Property<Int>
+    @get:Input abstract val systemImageSource: Property<String>
+    @get:Input abstract val abi: Property<String>
+    @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 {
+        // 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")
+    }
+
+    @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 (file)
index 0000000..dd72996
--- /dev/null
@@ -0,0 +1 @@
+The Android testbed's emulator RAM has been increased from 2 GB to 4 GB.