import com.android.build.api.variant.* import kotlin.math.max plugins { id("com.android.application") id("org.jetbrains.kotlin.android") } val PYTHON_DIR = file("../../..").canonicalPath val PYTHON_CROSS_DIR = "$PYTHON_DIR/cross-build" val ABIS = mapOf( "arm64-v8a" to "aarch64-linux-android", "x86_64" to "x86_64-linux-android", ).filter { file("$PYTHON_CROSS_DIR/${it.value}").exists() } if (ABIS.isEmpty()) { throw GradleException( "No Android ABIs found in $PYTHON_CROSS_DIR: see Android/README.md " + "for building instructions." ) } val PYTHON_VERSION = file("$PYTHON_DIR/Include/patchlevel.h").useLines { for (line in it) { val match = """#define PY_VERSION\s+"(\d+\.\d+)""".toRegex().find(line) if (match != null) { return@useLines match.groupValues[1] } } throw GradleException("Failed to find Python version") } android { val androidEnvFile = file("../../android-env.sh").absoluteFile namespace = "org.python.testbed" compileSdk = 34 defaultConfig { applicationId = "org.python.testbed" minSdk = androidEnvFile.useLines { for (line in it) { """api_level:=(\d+)""".toRegex().find(line)?.let { return@useLines it.groupValues[1].toInt() } } throw GradleException("Failed to find API level in $androidEnvFile") } targetSdk = 34 versionCode = 1 versionName = "1.0" ndk.abiFilters.addAll(ABIS.keys) externalNativeBuild.cmake.arguments( "-DPYTHON_CROSS_DIR=$PYTHON_CROSS_DIR", "-DPYTHON_VERSION=$PYTHON_VERSION", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON", ) testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } ndkVersion = androidEnvFile.useLines { for (line in it) { """ndk_version=(\S+)""".toRegex().find(line)?.let { return@useLines it.groupValues[1] } } throw GradleException("Failed to find NDK version in $androidEnvFile") } externalNativeBuild.cmake { path("src/main/c/CMakeLists.txt") } // Set this property to something non-empty, otherwise it'll use the default // list, which ignores asset directories beginning with an underscore. aaptOptions.ignoreAssetsPattern = ".git" compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = "1.8" } testOptions { managedDevices { localDevices { create("minVersion") { device = "Small Phone" // Managed devices have a minimum API level of 27. apiLevel = max(27, defaultConfig.minSdk!!) // ATD devices are smaller and faster, but have a minimum // API level of 30. systemImageSource = if (apiLevel >= 30) "aosp-atd" else "aosp" } create("maxVersion") { device = "Small Phone" apiLevel = defaultConfig.targetSdk!! systemImageSource = "aosp-atd" } } // If the previous test run succeeded and nothing has changed, // Gradle thinks there's no need to run it again. Override that. afterEvaluate { (localDevices.names + listOf("connected")).forEach { tasks.named("${it}DebugAndroidTest") { outputs.upToDateWhen { false } } } } } } } dependencies { implementation("androidx.appcompat:appcompat:1.6.1") implementation("com.google.android.material:material:1.11.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test:rules:1.5.0") } // Create some custom tasks to copy Python and its standard library from // elsewhere in the repository. androidComponents.onVariants { variant -> val pyPlusVer = "python$PYTHON_VERSION" generateTask(variant, variant.sources.assets!!) { into("python") { into("include/$pyPlusVer") { for (triplet in ABIS.values) { from("$PYTHON_CROSS_DIR/$triplet/prefix/include/$pyPlusVer") } duplicatesStrategy = DuplicatesStrategy.EXCLUDE } into("lib/$pyPlusVer") { // To aid debugging, the source directory takes priority. from("$PYTHON_DIR/Lib") // The cross-build directory provides ABI-specific files such as // sysconfigdata. for (triplet in ABIS.values) { from("$PYTHON_CROSS_DIR/$triplet/prefix/lib/$pyPlusVer") } into("site-packages") { from("$projectDir/src/main/python") } duplicatesStrategy = DuplicatesStrategy.EXCLUDE exclude("**/__pycache__") } } } generateTask(variant, variant.sources.jniLibs!!) { for ((abi, triplet) in ABIS.entries) { into(abi) { from("$PYTHON_CROSS_DIR/$triplet/prefix/lib") include("libpython*.*.so") include("lib*_python.so") } } } } fun generateTask( variant: ApplicationVariant, directories: SourceDirectories, configure: GenerateTask.() -> Unit ) { val taskName = "generate" + listOf(variant.name, "Python", directories.name) .map { it.replaceFirstChar(Char::uppercase) } .joinToString("") directories.addGeneratedSourceDirectory( tasks.register(taskName) { into(outputDir) configure() }, GenerateTask::outputDir) } // addGeneratedSourceDirectory requires the task to have a DirectoryProperty. abstract class GenerateTask: Sync() { @get:OutputDirectory abstract val outputDir: DirectoryProperty }