@@ -20,6 +20,14 @@ val KNOWN_ABIS = mapOf(
2020 " x86_64-linux-android" to " x86_64" ,
2121)
2222
23+ val osArch = System .getProperty(" os.arch" )
24+ val NATIVE_ABI = mapOf (
25+ " aarch64" to " arm64-v8a" ,
26+ " amd64" to " x86_64" ,
27+ " arm64" to " arm64-v8a" ,
28+ " x86_64" to " x86_64" ,
29+ )[osArch] ? : throw GradleException (" Unknown os.arch '$osArch '" )
30+
2331// Discover prefixes.
2432val prefixes = ArrayList <File >()
2533if (inSourceTree) {
@@ -151,6 +159,9 @@ android {
151159 testOptions {
152160 managedDevices {
153161 localDevices {
162+ // systemImageSource should use what its documentation calls an
163+ // "explicit source", i.e. the sdkmanager package name format, because
164+ // that will be required in CreateEmulatorTask below.
154165 create(" minVersion" ) {
155166 device = " Small Phone"
156167
@@ -159,13 +170,13 @@ android {
159170
160171 // ATD devices are smaller and faster, but have a minimum
161172 // API level of 30.
162- systemImageSource = if (apiLevel >= 30 ) " aosp-atd " else " aosp "
173+ systemImageSource = if (apiLevel >= 30 ) " aosp_atd " else " default "
163174 }
164175
165176 create(" maxVersion" ) {
166177 device = " Small Phone"
167178 apiLevel = defaultConfig.targetSdk!!
168- systemImageSource = " aosp-atd "
179+ systemImageSource = " aosp_atd "
169180 }
170181 }
171182
@@ -191,6 +202,138 @@ dependencies {
191202}
192203
193204
205+ afterEvaluate {
206+ // Every new emulator has a maximum of 2 GB RAM, regardless of its hardware profile
207+ // (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).
208+ // This is barely enough to test Python, and not enough to test Pandas
209+ // (https://github.com/python/cpython/pull/137186#issuecomment-3136301023,
210+ // https://github.com/pandas-dev/pandas/pull/63405#issuecomment-3667846159).
211+ // So we'll increase it by editing the emulator configuration files.
212+ //
213+ // If the emulator doesn't exist yet, we want to edit it after it's created, but
214+ // before it starts for the first time. Otherwise it'll need to be cold-booted
215+ // again, which would slow down the first run, which is likely the only run in CI
216+ // environments. But the Setup task both creates and starts the emulator if it
217+ // doesn't already exist. So we create it ourselves before the Setup task runs.
218+ for (device in android.testOptions.managedDevices.localDevices) {
219+ val createTask = tasks.register<CreateEmulatorTask >(" ${device.name} Create" ) {
220+ this .device = device.device
221+ apiLevel = device.apiLevel
222+ systemImageSource = device.systemImageSource
223+ abi = NATIVE_ABI
224+ }
225+ tasks.named(" ${device.name} Setup" ) {
226+ dependsOn(createTask)
227+ }
228+ }
229+ }
230+
231+ abstract class CreateEmulatorTask : DefaultTask () {
232+ @get:Input abstract val device: Property <String >
233+ @get:Input abstract val apiLevel: Property <Int >
234+ @get:Input abstract val systemImageSource: Property <String >
235+ @get:Input abstract val abi: Property <String >
236+ @get:Inject abstract val execOps: ExecOperations
237+
238+ private val avdName by lazy {
239+ listOf (
240+ " dev${apiLevel.get()} " ,
241+ systemImageSource.get(),
242+ abi.get(),
243+ device.get().replace(' ' , ' _' ),
244+ ).joinToString(" _" )
245+ }
246+
247+ private val avdDir by lazy {
248+ // XDG_CONFIG_HOME is respected by both avdmanager and Gradle.
249+ val userHome = System .getenv(" ANDROID_USER_HOME" ) ? : (
250+ (System .getenv(" XDG_CONFIG_HOME" ) ? : System .getProperty(" user.home" )!! )
251+ + " /.android"
252+ )
253+ File (" $userHome /avd/gradle-managed" , " $avdName .avd" )
254+ }
255+
256+ @TaskAction
257+ fun run () {
258+ if (! avdDir.exists()) {
259+ createAvd()
260+ }
261+ updateAvd()
262+ }
263+
264+ fun createAvd () {
265+ val systemImage = listOf (
266+ " system-images" ,
267+ " android-${apiLevel.get()} " ,
268+ systemImageSource.get(),
269+ abi.get(),
270+ ).joinToString(" ;" )
271+
272+ runCmdlineTool(" sdkmanager" , systemImage)
273+ runCmdlineTool(
274+ " avdmanager" , " create" , " avd" ,
275+ " --name" , avdName,
276+ " --path" , avdDir,
277+ " --device" , device.get().lowercase().replace(" " , " _" ),
278+ " --package" , systemImage,
279+ )
280+
281+ val iniName = " $avdName .ini"
282+ if (! File (avdDir.parentFile.parentFile, iniName).renameTo(
283+ File (avdDir.parentFile, iniName)
284+ )) {
285+ throw GradleException (" Failed to rename $iniName " )
286+ }
287+ }
288+
289+ fun updateAvd () {
290+ for (filename in listOf (
291+ " config.ini" , // Created by avdmanager; always exists
292+ " hardware-qemu.ini" , // Created on first run; might not exist
293+ )) {
294+ val iniFile = File (avdDir, filename)
295+ if (! iniFile.exists()) {
296+ if (filename == " config.ini" ) {
297+ throw GradleException (" $iniFile does not exist" )
298+ }
299+ continue
300+ }
301+
302+ val iniText = iniFile.readText()
303+ val pattern = Regex (
304+ """ ^\s*hw.ramSize\s*=\s*(.+?)\s*$""" , RegexOption .MULTILINE
305+ )
306+ val matches = pattern.findAll(iniText).toList()
307+ if (matches.size != 1 ) {
308+ throw GradleException (
309+ " Found ${matches.size} instances of $pattern in $iniFile ; expected 1"
310+ )
311+ }
312+
313+ val expectedRam = " 4096"
314+ if (matches[0 ].groupValues[1 ] != expectedRam) {
315+ iniFile.writeText(
316+ iniText.replace(pattern, " hw.ramSize = $expectedRam " )
317+ )
318+ }
319+ }
320+ }
321+
322+ fun runCmdlineTool (tool : String , vararg args : Any ) {
323+ val androidHome = System .getenv(" ANDROID_HOME" )!!
324+ val exeSuffix =
325+ if (System .getProperty(" os.name" ).lowercase().startsWith(" win" )) " .exe"
326+ else " "
327+ val command =
328+ listOf (" $androidHome /cmdline-tools/latest/bin/$tool$exeSuffix " , * args)
329+ println (command.joinToString(" " ))
330+ execOps.exec {
331+ commandLine(command)
332+ }
333+ }
334+ }
335+
336+
194337// Create some custom tasks to copy Python and its standard library from
195338// elsewhere in the repository.
196339androidComponents.onVariants { variant ->
0 commit comments