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