<lambda>null1 import com.android.build.api.variant.*
2 import kotlin.math.max
3
4 plugins {
5 id("com.android.application")
6 id("org.jetbrains.kotlin.android")
7 }
8
9 val PYTHON_DIR = file("../../..").canonicalPath
10 val PYTHON_CROSS_DIR = "$PYTHON_DIR/cross-build"
11
12 val ABIS = mapOf(
13 "arm64-v8a" to "aarch64-linux-android",
14 "x86_64" to "x86_64-linux-android",
<lambda>null15 ).filter { file("$PYTHON_CROSS_DIR/${it.value}").exists() }
16 if (ABIS.isEmpty()) {
17 throw GradleException(
18 "No Android ABIs found in $PYTHON_CROSS_DIR: see Android/README.md " +
19 "for building instructions."
20 )
21 }
22
<lambda>null23 val PYTHON_VERSION = file("$PYTHON_DIR/Include/patchlevel.h").useLines {
24 for (line in it) {
25 val match = """#define PY_VERSION\s+"(\d+\.\d+)""".toRegex().find(line)
26 if (match != null) {
27 return@useLines match.groupValues[1]
28 }
29 }
30 throw GradleException("Failed to find Python version")
31 }
32
33
<lambda>null34 android {
35 namespace = "org.python.testbed"
36 compileSdk = 34
37
38 defaultConfig {
39 applicationId = "org.python.testbed"
40 minSdk = 21
41 targetSdk = 34
42 versionCode = 1
43 versionName = "1.0"
44
45 ndk.abiFilters.addAll(ABIS.keys)
46 externalNativeBuild.cmake.arguments(
47 "-DPYTHON_CROSS_DIR=$PYTHON_CROSS_DIR",
48 "-DPYTHON_VERSION=$PYTHON_VERSION",
49 "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON",
50 )
51
52 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
53 }
54
55 val androidEnvFile = file("../../android-env.sh").absoluteFile
56 ndkVersion = androidEnvFile.useLines {
57 for (line in it) {
58 """ndk_version=(\S+)""".toRegex().find(line)?.let {
59 return@useLines it.groupValues[1]
60 }
61 }
62 throw GradleException("Failed to find NDK version in $androidEnvFile")
63 }
64 externalNativeBuild.cmake {
65 path("src/main/c/CMakeLists.txt")
66 }
67
68 // Set this property to something non-empty, otherwise it'll use the default
69 // list, which ignores asset directories beginning with an underscore.
70 aaptOptions.ignoreAssetsPattern = ".git"
71
72 compileOptions {
73 sourceCompatibility = JavaVersion.VERSION_1_8
74 targetCompatibility = JavaVersion.VERSION_1_8
75 }
76 kotlinOptions {
77 jvmTarget = "1.8"
78 }
79
80 testOptions {
81 managedDevices {
82 localDevices {
83 create("minVersion") {
84 device = "Small Phone"
85
86 // Managed devices have a minimum API level of 27.
87 apiLevel = max(27, defaultConfig.minSdk!!)
88
89 // ATD devices are smaller and faster, but have a minimum
90 // API level of 30.
91 systemImageSource = if (apiLevel >= 30) "aosp-atd" else "aosp"
92 }
93
94 create("maxVersion") {
95 device = "Small Phone"
96 apiLevel = defaultConfig.targetSdk!!
97 systemImageSource = "aosp-atd"
98 }
99 }
100
101 // If the previous test run succeeded and nothing has changed,
102 // Gradle thinks there's no need to run it again. Override that.
103 afterEvaluate {
104 (localDevices.names + listOf("connected")).forEach {
105 tasks.named("${it}DebugAndroidTest") {
106 outputs.upToDateWhen { false }
107 }
108 }
109 }
110 }
111 }
112 }
113
<lambda>null114 dependencies {
115 implementation("androidx.appcompat:appcompat:1.6.1")
116 implementation("com.google.android.material:material:1.11.0")
117 implementation("androidx.constraintlayout:constraintlayout:2.1.4")
118 androidTestImplementation("androidx.test.ext:junit:1.1.5")
119 androidTestImplementation("androidx.test:rules:1.5.0")
120 }
121
122
123 // Create some custom tasks to copy Python and its standard library from
124 // elsewhere in the repository.
variantnull125 androidComponents.onVariants { variant ->
126 val pyPlusVer = "python$PYTHON_VERSION"
127 generateTask(variant, variant.sources.assets!!) {
128 into("python") {
129 into("include/$pyPlusVer") {
130 for (triplet in ABIS.values) {
131 from("$PYTHON_CROSS_DIR/$triplet/prefix/include/$pyPlusVer")
132 }
133 duplicatesStrategy = DuplicatesStrategy.EXCLUDE
134 }
135
136 into("lib/$pyPlusVer") {
137 // To aid debugging, the source directory takes priority.
138 from("$PYTHON_DIR/Lib")
139
140 // The cross-build directory provides ABI-specific files such as
141 // sysconfigdata.
142 for (triplet in ABIS.values) {
143 from("$PYTHON_CROSS_DIR/$triplet/prefix/lib/$pyPlusVer")
144 }
145
146 into("site-packages") {
147 from("$projectDir/src/main/python")
148 }
149
150 duplicatesStrategy = DuplicatesStrategy.EXCLUDE
151 exclude("**/__pycache__")
152 }
153 }
154 }
155
156 generateTask(variant, variant.sources.jniLibs!!) {
157 for ((abi, triplet) in ABIS.entries) {
158 into(abi) {
159 from("$PYTHON_CROSS_DIR/$triplet/prefix/lib")
160 include("libpython*.*.so")
161 include("lib*_python.so")
162 }
163 }
164 }
165 }
166
167
generateTasknull168 fun generateTask(
169 variant: ApplicationVariant, directories: SourceDirectories,
170 configure: GenerateTask.() -> Unit
171 ) {
172 val taskName = "generate" +
173 listOf(variant.name, "Python", directories.name)
174 .map { it.replaceFirstChar(Char::uppercase) }
175 .joinToString("")
176
177 directories.addGeneratedSourceDirectory(
178 tasks.register<GenerateTask>(taskName) {
179 into(outputDir)
180 configure()
181 },
182 GenerateTask::outputDir)
183 }
184
185
186 // addGeneratedSourceDirectory requires the task to have a DirectoryProperty.
187 abstract class GenerateTask: Sync() {
188 @get:OutputDirectory
189 abstract val outputDir: DirectoryProperty
190 }
191