1/* -*- Mode: Groovy; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 2 * This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6// The JNI wrapper generation tasks depend on the JAR creation task of the :annotations project. 7evaluationDependsOn(':annotations') 8 9import groovy.util.FileNameFinder 10import groovy.transform.Memoized 11 12import java.nio.file.Path 13import java.nio.file.Paths 14import java.security.MessageDigest 15 16// To find the Android NDK directory, there are a few wrinkles. In a compiled 17// build, we can use our own `ANDROID_NDK` configure option. But in an 18// artifact build, that isn't defined, so we fall back to 19// `android.ndkDirectory` -- but that's defined in `local.properties`, which 20// may not define it. In that case, fall back to crawling the filesystem. 21def getNDKDirectory() { 22 if (project.mozconfig.substs.ANDROID_NDK) { 23 return project.mozconfig.substs.ANDROID_NDK 24 } 25 try { 26 if (project.android.ndkDirectory) { 27 return project.android.ndkDirectory 28 } 29 } catch (InvalidUserDataException ex) { 30 // The NDK is not installed, that's ok. 31 } 32 def mozbuild = System.getenv('MOZBUILD_STATE_PATH') ?: "${System.getProperty('user.home')}/.mozbuild" 33 def files = new FileNameFinder().getFileNames(mozbuild, "android-ndk-*/source.properties") 34 if (files) { 35 // It would be nice to sort these by version, but that's too much effort right now. 36 return project.file(files.first()).parentFile.toString() 37 } 38 return null 39} 40 41def hasCompileArtifacts() { 42 return project.mozconfig.substs.COMPILE_ENVIRONMENT 43 || project.mozconfig.substs.MOZ_ARTIFACT_BUILDS 44} 45 46// Get the LLVM bin folder, either from the currently used toolchain or, if 47// this is an artifact build, from the Android NDK. 48@Memoized 49def getLlvmBinFolder() { 50 // If we have a clang path, just use that 51 if (project.mozconfig.substs.MOZ_CLANG_PATH) { 52 return project.file(project.mozconfig.substs.MOZ_CLANG_PATH) 53 .parentFile 54 } 55 56 def ndkDirectory = getNDKDirectory() 57 if (!ndkDirectory) { 58 // Give up 59 return null; 60 } 61 62 // The `**` in the pattern depends on the host architecture. We could compute them or bake them 63 // in, but this is easy enough. `FileNameFinder` won't return a directory, so we find a file 64 // and return its parent directory. 65 return project 66 .file(new FileNameFinder() 67 .getFileNames(ndkDirectory, "toolchains/llvm/prebuilt/**/bin/llvm-*") 68 .first()) 69 .parentFile 70} 71 72ext.getLlvmBinFolder = { 73 // This is the easiest way to expose the memoized function to consumers. 74 getLlvmBinFolder() 75} 76 77// Bug 1657190: This task works around a bug in the Android-Gradle plugin. When using SNAPSHOT 78// builds (and possibly in other situations) the JNI library invalidation process isn't correct. If 79// there are two JNI libs `a.so` and `b.so` and a new snapshot updates only `a.so`, then the 80// resulting AAR will include both JNI libs but the consuming project's resulting APK will include 81// only the modified `a.so`. For GeckoView, it's usually `libxul.so` that is updated. For a 82// consumer (like Fenix), this leads to a hard startup crash because `libmozglue.so` will be missing 83// when it is read (first, before `libxul.so`) 84// 85// For !MOZILLA_OFFICIAL builds, we work around this issue to ensure that when *any* library is 86// updated then *every* library is updated. We use the ELF build ID baked into each library to 87// determine whether any library is updated. We digest (SHA256, for now) all of the ELF build IDs 88// and use this as our own Mozilla-specific "library generation ID". We then add our own 89// Mozilla-specific ELF section to every library so that they will all be invalidated by the 90// Android-Gradle plugin of a consuming project. 91class SyncLibsAndUpdateGenerationID extends DefaultTask { 92 @InputDirectory 93 File source 94 95 @InputFile 96 File extraSource 97 98 @OutputDirectory 99 File destinationDir 100 101 @InputDirectory 102 File llvmBin = project.ext.getLlvmBinFolder() 103 104 // Sibling to `.note.gnu.build-id`. 105 @Input 106 String newElfSection = ".note.mozilla.build-id" 107 108 @TaskAction 109 void execute() { 110 Path root = Paths.get(source.toString()) 111 112 def libs = project.fileTree(source.toString()).files.collect { 113 Path lib = Paths.get(it.toString()) 114 root.relativize(lib).toString() 115 } 116 117 def generationId = new ByteArrayOutputStream().withStream { os -> 118 // Start with the hash of any extra source. 119 def digest = MessageDigest.getInstance("SHA-256") 120 extraSource.eachByte(64 * 1024) { byte[] buf, int bytesRead -> 121 digest.update(buf, 0, bytesRead); 122 } 123 def extraSourceHex = new BigInteger(1, digest.digest()).toString(16).padLeft(64, '0') 124 os.write("${extraSource.toString()} ${extraSourceHex}\n".getBytes("utf-8")); 125 126 // Follow with all the ordered build ID section dumps. 127 libs.each { lib -> 128 // This should never fail. If it does, there's a real problem, so an exception is 129 // reasonable. 130 project.exec { 131 commandLine "${llvmBin}/llvm-readobj" 132 args '--hex-dump=.note.gnu.build-id' 133 args "${source}/${lib}" 134 standardOutput = os 135 } 136 } 137 138 def allBuildIDs = os.toString() 139 140 // For detailed debugging. 141 new File("${destinationDir}/${newElfSection}-details").setText(allBuildIDs, 'utf-8') 142 143 allBuildIDs.sha256() 144 } 145 146 logger.info("Mozilla-specific library generation ID is ${generationId} (see ${destinationDir}/${newElfSection}-details)") 147 148 def generationIdIsStale = libs.any { lib -> 149 new ByteArrayOutputStream().withStream { os -> 150 // This can fail, but there's little value letting stderr go to the console in 151 // general, since it's just noise after a clobber build. 152 def execResult = project.exec { 153 ignoreExitValue true 154 commandLine "${llvmBin}/llvm-readobj" 155 args "--hex-dump=${newElfSection}" 156 args "${destinationDir}/${lib}" 157 standardOutput = os 158 errorOutput = new ByteArrayOutputStream() 159 } 160 161 if (execResult.exitValue != 0) { 162 logger.info("Mozilla-specific library generation ID is missing: ${lib}") 163 } else if (!os.toString().contains(generationId)) { 164 logger.info("Mozilla-specific library generation ID is stale: ${lib}") 165 } else { 166 logger.debug("Mozilla-specific library generation ID is fresh: ${lib}") 167 } 168 execResult.exitValue != 0 || !os.toString().contains(generationId) 169 } 170 } 171 172 if (generationIdIsStale) { 173 project.mkdir destinationDir 174 new File("${destinationDir}/${newElfSection}").setText(generationId, 'utf-8') 175 176 libs.each { lib -> 177 project.mkdir project.file("${destinationDir}/${lib}").parent 178 179 new ByteArrayOutputStream().withStream { os -> 180 // This should never fail: if it does, there's a real problem (again). 181 project.exec { 182 commandLine "${llvmBin}/llvm-objcopy" 183 args "--add-section=${newElfSection}=${destinationDir}/${newElfSection}" 184 args "${source}/${lib}" 185 args "${destinationDir}/${lib}" 186 standardOutput = os 187 } 188 } 189 } 190 } else { 191 logger.info("Mozilla-specific library generation ID is fresh!") 192 } 193 } 194} 195 196ext.configureVariantWithGeckoBinaries = { variant -> 197 def omnijarDir = "${topobjdir}/dist/geckoview" 198 def distDir = "${topobjdir}/dist/geckoview" 199 200 def syncOmnijarFromDistDir 201 if (hasCompileArtifacts()) { 202 syncOmnijarFromDistDir = task("syncOmnijarFromDistDirFor${variant.name.capitalize()}", type: Sync) { 203 onlyIf { 204 if (source.empty) { 205 throw new StopExecutionException("Required omnijar not found in ${omnijarDir}/{omni.ja,assets/omni.ja}. Have you built and packaged?") 206 } 207 return true 208 } 209 210 into("${project.buildDir}/moz.build/src/${variant.name}/omnijar") 211 from("${omnijarDir}/omni.ja", 212 "${omnijarDir}/assets/omni.ja") { 213 // Throw an exception if we find multiple, potentially conflicting omni.ja files. 214 duplicatesStrategy 'fail' 215 } 216 } 217 } 218 219 // For !MOZILLA_OFFICIAL builds, work around an Android-Gradle plugin bug that causes startup 220 // crashes with local substitution. But -- we want to allow artifact builds that don't have the 221 // NDK installed. See class comment above. 222 def shouldUpdateGenerationID = { 223 if (mozconfig.substs.MOZILLA_OFFICIAL) { 224 return false 225 } else if (mozconfig.substs.MOZ_ANDROID_FAT_AAR_ARCHITECTURES) { 226 return false 227 } else if (ext.getLlvmBinFolder() == null) { 228 logger.warn("Could not determine LLVM bin directory.") 229 logger.warn("Set `ndk.dir` in `${project.topsrcdir}/local.properties` to avoid startup crashes when using `substitute-local-geckoview.gradle`.") 230 logger.warn("See https://bugzilla.mozilla.org/show_bug.cgi?id=1657190.") 231 return false 232 } 233 return true 234 }() 235 236 def syncLibsFromDistDir = { if (shouldUpdateGenerationID) { 237 def jarTask = tasks["bundleLibRuntimeToJar${variant.name.capitalize()}"] 238 def bundleJar = jarTask.outputs.files.find({ it.name == 'classes.jar' }) 239 240 task("syncLibsAndUpdateGenerationIDFromDistDirFor${variant.name.capitalize()}", type: SyncLibsAndUpdateGenerationID) { 241 source file("${distDir}/lib") 242 destinationDir file("${project.buildDir}/moz.build/src/${variant.name}/jniLibs") 243 // Include the hash of classes.jar as well, so that JVM-only changes don't leave every 244 // JNI library unchanged and therefore invalidate all of the JNI libraries in a consumer 245 // doing local substitution. 246 extraSource bundleJar 247 dependsOn jarTask 248 } 249 } else { 250 task("syncLibsFromDistDirFor${variant.name.capitalize()}", type: Sync) { 251 into("${project.buildDir}/moz.build/src/${variant.name}/jniLibs") 252 from("${distDir}/lib") 253 } 254 } }() 255 256 syncLibsFromDistDir.onlyIf { task -> 257 if (!hasCompileArtifacts()) { 258 // We won't have JNI libraries if we're not compiling and we're not downloading 259 // artifacts. Such a configuration is used for running lints, generating docs, etc. 260 return true 261 } 262 if (files(task.source).empty) { 263 throw new StopExecutionException("Required JNI libraries not found in ${task.source}. Have you built and packaged?") 264 } 265 return true 266 } 267 268 def syncAssetsFromDistDir = task("syncAssetsFromDistDirFor${variant.name.capitalize()}", type: Sync) { 269 into("${project.buildDir}/moz.build/src/${variant.name}/assets") 270 from("${distDir}/assets") { 271 exclude 'omni.ja' 272 } 273 } 274 275 if (hasCompileArtifacts()) { 276 // Local (read, not 'official') builds want to reflect developer changes to 277 // the omnijar sources, and (when compiling) to reflect developer changes to 278 // the native binaries. To do this, the Gradle build calls out to the 279 // moz.build system, which can be re-entrant. Official builds are driven by 280 // the moz.build system and should never be re-entrant in this way. 281 if (!mozconfig.substs.MOZILLA_OFFICIAL) { 282 syncOmnijarFromDistDir.dependsOn rootProject.machStagePackage 283 syncLibsFromDistDir.dependsOn rootProject.machStagePackage 284 syncAssetsFromDistDir.dependsOn rootProject.machStagePackage 285 } 286 287 def assetGenTask = tasks.findByName("generate${variant.name.capitalize()}Assets") 288 def jniLibFoldersTask = tasks.findByName("merge${variant.name.capitalize()}JniLibFolders") 289 if ((variant.productFlavors*.name).contains('withGeckoBinaries')) { 290 assetGenTask.dependsOn syncOmnijarFromDistDir 291 assetGenTask.dependsOn syncAssetsFromDistDir 292 jniLibFoldersTask.dependsOn syncLibsFromDistDir 293 294 android.sourceSets."${variant.name}".assets.srcDir syncOmnijarFromDistDir.destinationDir 295 android.sourceSets."${variant.name}".assets.srcDir syncAssetsFromDistDir.destinationDir 296 297 if (!mozconfig.substs.MOZ_ANDROID_FAT_AAR_ARCHITECTURES) { 298 android.sourceSets."${variant.name}".jniLibs.srcDir syncLibsFromDistDir.destinationDir 299 } else { 300 android.sourceSets."${variant.name}".jniLibs.srcDir "${topobjdir}/dist/fat-aar/output/jni" 301 } 302 } 303 } 304} 305 306ext.configureLibraryVariantWithJNIWrappers = { variant, module -> 307 // BundleLibRuntime prepares the library for further processing to be 308 // incorporated in an app. We use this version to create the JNI wrappers. 309 def jarTask = tasks["bundleLibRuntimeToJar${variant.name.capitalize()}"] 310 def bundleJar = jarTask.outputs.files.find({ it.name == 'classes.jar' }) 311 312 def annotationProcessorsJarTask = project(':annotations').jar 313 314 def wrapperTask 315 if (System.env.IS_LANGUAGE_REPACK == '1') { 316 // Single-locale l10n repacks set `IS_LANGUAGE_REPACK=1` and don't 317 // really have a build environment. 318 wrapperTask = task("generateJNIWrappersFor${module}${variant.name.capitalize()}") 319 } else { 320 wrapperTask = task("generateJNIWrappersFor${module}${variant.name.capitalize()}", type: JavaExec) { 321 classpath annotationProcessorsJarTask.archivePath 322 323 // Configure the classpath at evaluation-time, not at 324 // configuration-time: see above comment. 325 doFirst { 326 classpath variant.javaCompileProvider.get().classpath 327 } 328 329 mainClass = 'org.mozilla.gecko.annotationProcessors.AnnotationProcessor' 330 args module 331 args bundleJar 332 333 workingDir "${topobjdir}/widget/android" 334 335 inputs.file(bundleJar) 336 inputs.file(annotationProcessorsJarTask.archivePath) 337 inputs.property("module", module) 338 339 outputs.file("${topobjdir}/widget/android/GeneratedJNINatives.h") 340 outputs.file("${topobjdir}/widget/android/GeneratedJNIWrappers.cpp") 341 outputs.file("${topobjdir}/widget/android/GeneratedJNIWrappers.h") 342 343 dependsOn jarTask 344 dependsOn annotationProcessorsJarTask 345 } 346 } 347 348 if (module == 'Generated') { 349 tasks["bundle${variant.name.capitalize()}Aar"].dependsOn wrapperTask 350 } else { 351 tasks["assemble${variant.name.capitalize()}"].dependsOn wrapperTask 352 } 353} 354