Skip to content
Open
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
8 changes: 0 additions & 8 deletions Android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
147 changes: 145 additions & 2 deletions Android/testbed/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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<File>()
if (inSourceTree) {
Expand Down Expand Up @@ -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"

Expand All @@ -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"
}
}

Expand All @@ -189,6 +200,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 ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The Android testbed's emulator RAM has been increased from 2 GB to 4 GB.
Loading