diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /mobile/android/gradle | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | mobile/android/gradle.configure | 617 | ||||
-rw-r--r-- | mobile/android/gradle.py | 60 | ||||
-rw-r--r-- | mobile/android/gradle/debug_level.gradle | 17 | ||||
-rw-r--r-- | mobile/android/gradle/dotgradle-offline/gradle.properties | 3 | ||||
-rw-r--r-- | mobile/android/gradle/dotgradle-offline/init.gradle | 4 | ||||
-rw-r--r-- | mobile/android/gradle/dotgradle-online/gradle.properties | 3 | ||||
-rw-r--r-- | mobile/android/gradle/dotgradle-online/init.gradle | 4 | ||||
-rw-r--r-- | mobile/android/gradle/mach_env.gradle | 29 | ||||
-rw-r--r-- | mobile/android/gradle/product_flavors.gradle | 17 | ||||
-rw-r--r-- | mobile/android/gradle/with_gecko_binaries.gradle | 357 |
10 files changed, 1111 insertions, 0 deletions
diff --git a/mobile/android/gradle.configure b/mobile/android/gradle.configure new file mode 100644 index 0000000000..09c69e124a --- /dev/null +++ b/mobile/android/gradle.configure @@ -0,0 +1,617 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# If --with-gradle is specified, build mobile/android with Gradle. If no +# Gradle binary is specified use the in tree Gradle wrapper. The wrapper +# downloads and installs Gradle, which is good for local developers but not +# good in automation. +option( + "--without-gradle", + nargs="?", + help="Disable building mobile/android with Gradle " + "(argument: location of binary or wrapper (gradle/gradlew))", +) + + +@depends("--with-gradle") +def with_gradle(value): + if not value: + die( + "Building --without-gradle is no longer supported: " + "see https://bugzilla.mozilla.org/show_bug.cgi?id=1414415." + ) + + if value: + return True + + +@depends(host, "--with-gradle", build_environment) +@imports(_from="os.path", _import="isfile") +def gradle(host, value, build_env): + if len(value): + gradle = value[0] + else: + gradle = os.path.join(build_env.topsrcdir, "gradlew") + if host.os == "WINNT": + gradle = gradle + ".bat" + + # TODO: verify that $GRADLE is executable. + if not isfile(gradle): + die("GRADLE must be executable: %s", gradle) + + return gradle + + +set_config("GRADLE", gradle) + + +@dependable +@imports(_from="itertools", _import="chain") +def gradle_android_build_config(): + def capitalize(s): + # str.capitalize lower cases trailing letters. + if s: + return s[0].upper() + s[1:] + else: + return s + + def variant(productFlavors, buildType): + return namespace( + productFlavors=productFlavors, + buildType=buildType, + # Like 'WithoutGeckoBinariesDebug' + name="".join(capitalize(t) for t in chain(productFlavors, (buildType,))), + ) + + return namespace( + geckoview=namespace( + variant=variant(("withGeckoBinaries",), "debug"), + ), + geckoview_example=namespace( + variant=variant(("withGeckoBinaries",), "debug"), + ), + ) + + +@depends(gradle_android_build_config) +def gradle_android_intermediates_folder(build_config): + """Path to intermediates classes folder.""" + + def uncapitalize(s): + if s: + return s[0].lower() + s[1:] + else: + return s + + def capitalize(s): + # str.capitalize lower cases trailing letters. + if s: + return s[0].upper() + s[1:] + else: + return s + + productFlavor = uncapitalize( + "".join(capitalize(f) for f in build_config.geckoview.variant.productFlavors) + ) + buildType = uncapitalize(build_config.geckoview.variant.buildType) + + return ( + "gradle/build/mobile/android/geckoview/intermediates/javac/{}{}/classes".format( + productFlavor, + capitalize(buildType), + ) + ) + + +set_config( + "GRADLE_ANDROID_GECKOVIEW_APILINT_FOLDER", gradle_android_intermediates_folder +) + + +@depends(gradle_android_build_config) +def gradle_android_geckoview_test_runner_bundle(build_config): + """Path to intermediates classes folder.""" + + def uncapitalize(s): + if s: + return s[0].lower() + s[1:] + else: + return s + + def capitalize(s): + # str.capitalize lower cases trailing letters. + if s: + return s[0].upper() + s[1:] + else: + return s + + productFlavor = uncapitalize( + "".join(capitalize(f) for f in build_config.geckoview.variant.productFlavors) + ) + buildType = uncapitalize(build_config.geckoview.variant.buildType) + variant = uncapitalize(build_config.geckoview.variant.name) + + return "gradle/build/mobile/android/test_runner/outputs/bundle/{}/test_runner-{}-{}.aab".format( + variant, + productFlavor, + buildType, + ) + + +set_config( + "GRADLE_ANDROID_GECKOVIEW_TEST_RUNNER_BUNDLE", + gradle_android_geckoview_test_runner_bundle, +) + + +@depends(gradle_android_build_config) +def gradle_android_geckoview_example_bundle(build_config): + """Path to intermediates classes folder.""" + + def uncapitalize(s): + if s: + return s[0].lower() + s[1:] + else: + return s + + def capitalize(s): + # str.capitalize lower cases trailing letters. + if s: + return s[0].upper() + s[1:] + else: + return s + + productFlavor = uncapitalize( + "".join(capitalize(f) for f in build_config.geckoview.variant.productFlavors) + ) + buildType = uncapitalize(build_config.geckoview.variant.buildType) + variant = uncapitalize(build_config.geckoview.variant.name) + + return "gradle/build/mobile/android/geckoview_example/outputs/bundle/{}/geckoview_example-{}-{}.aab".format( + variant, + productFlavor, + buildType, + ) + + +set_config( + "GRADLE_ANDROID_GECKOVIEW_EXAMPLE_BUNDLE", gradle_android_geckoview_example_bundle +) + + +@depends(gradle_android_build_config) +def gradle_android_variant_name(build_config): + """Like "withoutGeckoBinariesDebug".""" + + def uncapitalize(s): + if s: + return s[0].lower() + s[1:] + else: + return s + + return namespace( + geckoview=uncapitalize(build_config.geckoview.variant.name), + ) + + +set_config( + "GRADLE_ANDROID_GECKOVIEW_VARIANT_NAME", gradle_android_variant_name.geckoview +) + + +@depends(gradle_android_build_config) +def gradle_android_app_tasks(build_config): + """Gradle tasks run by |mach android assemble-app|.""" + return [ + "geckoview:generateJNIWrappersForGenerated{geckoview.variant.name}".format( + geckoview=build_config.geckoview + ), + ] + + +set_config("GRADLE_ANDROID_APP_TASKS", gradle_android_app_tasks) + + +@dependable +def gradle_android_generate_sdk_bindings_tasks(): + """Gradle tasks run by |mach android generate-sdk-bindings|.""" + return [ + "geckoview:generateSDKBindings", + ] + + +set_config( + "GRADLE_ANDROID_GENERATE_SDK_BINDINGS_TASKS", + gradle_android_generate_sdk_bindings_tasks, +) + + +@depends(gradle_android_build_config) +def gradle_android_generate_generated_jni_wrappers_tasks(build_config): + """Gradle tasks run by |mach android generate-generated-jni-wrappers|.""" + return [ + "geckoview:generateJNIWrappersForGenerated{geckoview.variant.name}".format( + geckoview=build_config.geckoview + ), + ] + + +set_config( + "GRADLE_ANDROID_GENERATE_GENERATED_JNI_WRAPPERS_TASKS", + gradle_android_generate_generated_jni_wrappers_tasks, +) + + +@depends(gradle_android_build_config) +def gradle_android_test_tasks(build_config): + """Gradle tasks run by |mach android test|.""" + return [ + "geckoview:test{geckoview.variant.name}UnitTest".format( + geckoview=build_config.geckoview + ), + ] + + +set_config("GRADLE_ANDROID_TEST_TASKS", gradle_android_test_tasks) + + +@depends(gradle_android_build_config) +def gradle_android_lint_tasks(build_config): + """Gradle tasks run by |mach android lint|.""" + return [ + "geckoview:lint{geckoview.variant.name}".format( + geckoview=build_config.geckoview + ), + ] + + +set_config("GRADLE_ANDROID_LINT_TASKS", gradle_android_lint_tasks) + + +@depends(gradle_android_build_config) +def gradle_android_api_lint_tasks(build_config): + """Gradle tasks run by |mach android api-lint|.""" + return [ + "geckoview:apiLint{geckoview.variant.name}".format( + geckoview=build_config.geckoview + ), + ] + + +set_config("GRADLE_ANDROID_API_LINT_TASKS", gradle_android_api_lint_tasks) + + +set_config( + "GRADLE_ANDROID_FORMAT_LINT_FIX_TASKS", ["spotlessJavaApply", "spotlessKotlinApply"] +) + + +@dependable +def gradle_android_format_lint_check_tasks(): + return ["spotlessJavaCheck", "spotlessKotlinCheck"] + + +set_config( + "GRADLE_ANDROID_FORMAT_LINT_CHECK_TASKS", gradle_android_format_lint_check_tasks +) + +set_config( + "GRADLE_ANDROID_FORMAT_LINT_FOLDERS", + [ + "mobile/android/annotations", + "mobile/android/geckoview", + "mobile/android/geckoview_example", + "mobile/android/test_runner", + "mobile/android/examples/messaging_example", + "mobile/android/examples/port_messaging_example", + ], +) + + +@depends(gradle_android_build_config) +def gradle_android_checkstyle_tasks(build_config): + """Gradle tasks run by |mach android checkstyle|.""" + return [ + "geckoview:checkstyle{geckoview.variant.name}".format( + geckoview=build_config.geckoview + ), + ] + + +set_config("GRADLE_ANDROID_CHECKSTYLE_TASKS", gradle_android_checkstyle_tasks) + + +@depends(gradle_android_build_config) +def gradle_android_checkstyle_output_files(build_config): + def uncapitalize(s): + if s: + return s[0].lower() + s[1:] + else: + return s + + variant = uncapitalize(build_config.geckoview.variant.name) + + """Output folder for checkstyle""" + return [ + "gradle/build/mobile/android/geckoview/reports/checkstyle/{}.xml".format( + variant + ), + ] + + +set_config( + "GRADLE_ANDROID_CHECKSTYLE_OUTPUT_FILES", gradle_android_checkstyle_output_files +) + + +option( + "--disable-android-bundle", + help="{Enable|Disable} AAB build.", +) + +imply_option("--disable-android-bundle", False, when="--enable-address-sanitizer") + + +@depends(gradle_android_build_config, "--disable-android-bundle") +def gradle_android_archive_geckoview_tasks(build_config, aab_enabled): + """Gradle tasks run by |mach android archive-geckoview|.""" + tasks = [ + "geckoview:assemble{geckoview.variant.name}".format( + geckoview=build_config.geckoview + ), + "geckoview:assemble{geckoview.variant.name}AndroidTest".format( + geckoview=build_config.geckoview + ), + "test_runner:assemble{geckoview_example.variant.name}".format( + geckoview_example=build_config.geckoview_example + ), + "geckoview_example:assemble{geckoview_example.variant.name}".format( + geckoview_example=build_config.geckoview_example + ), + "messaging_example:assemble{geckoview_example.variant.name}".format( + geckoview_example=build_config.geckoview_example + ), + "port_messaging_example:assemble{geckoview_example.variant.name}".format( + geckoview_example=build_config.geckoview_example + ), + "geckoview:publish{geckoview.variant.name}PublicationToMavenRepository".format( + geckoview=build_config.geckoview + ), + "exoplayer2:publishDebugPublicationToMavenRepository", + ] + + if aab_enabled: + tasks += [ + "test_runner:bundle{geckoview_example.variant.name}".format( + geckoview_example=build_config.geckoview_example + ), + "geckoview_example:bundle{geckoview_example.variant.name}".format( + geckoview_example=build_config.geckoview_example + ), + ] + return tasks + + +set_config( + "GRADLE_ANDROID_ARCHIVE_GECKOVIEW_TASKS", gradle_android_archive_geckoview_tasks +) + + +@depends(gradle_android_build_config) +def gradle_android_geckoview_docs_tasks(build_config): + """Gradle tasks run by |mach android geckoview-docs|.""" + return [ + "geckoview:javadoc{geckoview.variant.name}".format( + geckoview=build_config.geckoview + ), + ] + + +set_config("GRADLE_ANDROID_GECKOVIEW_DOCS_TASKS", gradle_android_geckoview_docs_tasks) + + +@depends(gradle_android_build_config) +def gradle_android_geckoview_docs_archive_tasks(build_config): + """Gradle tasks run by |mach android geckoview-docs --archive| or |... --upload.""" + return [ + "geckoview:javadocCopyJar{geckoview.variant.name}".format( + geckoview=build_config.geckoview + ), + ] + + +set_config( + "GRADLE_ANDROID_GECKOVIEW_DOCS_ARCHIVE_TASKS", + gradle_android_geckoview_docs_archive_tasks, +) + + +@depends(gradle_android_build_config) +def gradle_android_geckoview_docs_output_files(build_config): + """Output files for GeckoView javadoc.""" + + def uncapitalize(s): + if s: + return s[0].lower() + s[1:] + else: + return s + + variant = uncapitalize(build_config.geckoview.variant.name) + + return [ + "gradle/build/mobile/android/geckoview/reports/javadoc-results-{}.json".format( + variant + ), + ] + + +set_config( + "GRADLE_ANDROID_GECKOVIEW_DOCS_OUTPUT_FILES", + gradle_android_geckoview_docs_output_files, +) + + +@depends(gradle_android_build_config) +def gradle_android_archive_coverage_artifacts_tasks(build_config): + """Gradle tasks run by |mach android archive-coverage-artifacts|.""" + return [ + "geckoview:archiveClassfiles{geckoview.variant.name}".format( + geckoview=build_config.geckoview + ), + "geckoview:copyCoverageDependencies", + ] + + +set_config( + "GRADLE_ANDROID_ARCHIVE_COVERAGE_ARTIFACTS_TASKS", + gradle_android_archive_coverage_artifacts_tasks, +) + + +@depends(gradle_android_build_config) +def gradle_android_build_geckoview_example_tasks(build_config): + """Gradle tasks run by |mach android build-geckoview_example|.""" + return [ + "geckoview_example:assemble{geckoview_example.variant.name}".format( + geckoview_example=build_config.geckoview_example + ), + "geckoview_example:bundle{geckoview_example.variant.name}".format( + geckoview_example=build_config.geckoview_example + ), + "geckoview:assemble{geckoview.variant.name}AndroidTest".format( + geckoview=build_config.geckoview + ), + "test_runner:assemble{geckoview.variant.name}".format( + geckoview=build_config.geckoview + ), + "test_runner:bundle{geckoview.variant.name}".format( + geckoview=build_config.geckoview + ), + ] + + +set_config( + "GRADLE_ANDROID_BUILD_GECKOVIEW_EXAMPLE_TASKS", + gradle_android_build_geckoview_example_tasks, +) + + +@depends(gradle_android_build_config) +def gradle_android_install_geckoview_test_runner_tasks(build_config): + """Gradle tasks run by |mach android install-geckoview-test_runner|.""" + return [ + "test_runner:install{geckoview.variant.name}".format( + geckoview=build_config.geckoview + ), + ] + + +set_config( + "GRADLE_ANDROID_INSTALL_GECKOVIEW_TEST_RUNNER_TASKS", + gradle_android_install_geckoview_test_runner_tasks, +) + + +@depends(gradle_android_build_config) +def gradle_android_install_geckoview_test_tasks(build_config): + """Gradle tasks run by |mach android install-geckoview-test|.""" + return [ + "geckoview:install{geckoview.variant.name}AndroidTest".format( + geckoview=build_config.geckoview + ), + ] + + +set_config( + "GRADLE_ANDROID_INSTALL_GECKOVIEW_TEST_TASKS", + gradle_android_install_geckoview_test_tasks, +) + + +@depends(gradle_android_build_config) +def gradle_android_install_geckoview_example_tasks(build_config): + """Gradle tasks run by |mach android install-geckoview_example|.""" + return [ + "geckoview_example:install{geckoview_example.variant.name}".format( + geckoview_example=build_config.geckoview_example + ), + ] + + +set_config( + "GRADLE_ANDROID_INSTALL_GECKOVIEW_EXAMPLE_TASKS", + gradle_android_install_geckoview_example_tasks, +) + + +@depends( + gradle_android_api_lint_tasks, + gradle_android_format_lint_check_tasks, + gradle_android_checkstyle_tasks, +) +@imports(_from="itertools", _import="chain") +def gradle_android_dependencies_tasks(*tasks): + """Gradle tasks run by |mach android dependencies|.""" + # The union, plus a bit more, of all of the Gradle tasks + # invoked by the android-* automation jobs. + def withoutGeckoBinaries(task): + return task.replace("withGeckoBinaries", "withoutGeckoBinaries") + + return list(withoutGeckoBinaries(t) for t in chain(*tasks)) + + +set_config("GRADLE_ANDROID_DEPENDENCIES_TASKS", gradle_android_dependencies_tasks) + + +# Automation uses this to change log levels, not use the daemon, and use +# offline mode. +option(env="GRADLE_FLAGS", default="", help="Flags to pass to Gradle.") + + +@depends("GRADLE_FLAGS") +def gradle_flags(value): + return value[0] if value else "" + + +set_config("GRADLE_FLAGS", gradle_flags) + +# Automation will set this to (file:///path/to/local, ...) via the mozconfig. +# Local developer default is (maven.google.com). +option( + env="GRADLE_MAVEN_REPOSITORIES", + nargs="+", + default=( + "https://maven.mozilla.org/maven2/", + "https://maven.google.com/", + "https://repo.maven.apache.org/maven2/", + "https://plugins.gradle.org/m2/", + ), + help="Comma-separated URLs of Maven repositories containing Gradle dependencies.", +) + +option( + "--allow-insecure-gradle-repositories", + help="Gradle is allowed to connect to insecure Maven repositories.", +) + +set_config( + "ALLOW_INSECURE_GRADLE_REPOSITORIES", + True, + when="--allow-insecure-gradle-repositories", +) + + +@depends("GRADLE_MAVEN_REPOSITORIES") +@imports(_from="os.path", _import="isdir") +def gradle_maven_repositories(values): + if not values: + die("GRADLE_MAVEN_REPOSITORIES must not be empty") + if not all(values): + die("GRADLE_MAVEN_REPOSITORIES entries must not be empty") + return values + + +set_config("GRADLE_MAVEN_REPOSITORIES", gradle_maven_repositories) diff --git a/mobile/android/gradle.py b/mobile/android/gradle.py new file mode 100644 index 0000000000..9f310ddb7b --- /dev/null +++ b/mobile/android/gradle.py @@ -0,0 +1,60 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import subprocess +import sys +from contextlib import contextmanager + +import mozpack.path as mozpath +from mozbuild.util import ensureParentDir, lock_file + + +@contextmanager +def gradle_lock(topobjdir, max_wait_seconds=600): + # Building the same Gradle root project with multiple concurrent processes + # is not well supported, so we use a simple lock file to serialize build + # steps. + lock_path = "{}/gradle/mach_android.lockfile".format(topobjdir) + ensureParentDir(lock_path) + lock_instance = lock_file(lock_path, max_wait=max_wait_seconds) + + try: + yield + finally: + del lock_instance + + +def android(verb, *args): + import buildconfig + + with gradle_lock(buildconfig.topobjdir): + cmd = [ + sys.executable, + mozpath.join(buildconfig.topsrcdir, "mach"), + "android", + verb, + ] + cmd.extend(args) + env = dict(os.environ) + # Confusingly, `MACH` is set only within `mach build`. + if env.get("MACH"): + env["GRADLE_INVOKED_WITHIN_MACH_BUILD"] = "1" + if env.get("LD_LIBRARY_PATH"): + del env["LD_LIBRARY_PATH"] + subprocess.check_call(cmd, env=env) + + return 0 + + +def assemble_app(dummy_output_file, *inputs): + return android("assemble-app") + + +def generate_sdk_bindings(dummy_output_file, *args): + return android("generate-sdk-bindings", *args) + + +def generate_generated_jni_wrappers(dummy_output_file, *args): + return android("generate-generated-jni-wrappers", *args) diff --git a/mobile/android/gradle/debug_level.gradle b/mobile/android/gradle/debug_level.gradle new file mode 100644 index 0000000000..a9537da327 --- /dev/null +++ b/mobile/android/gradle/debug_level.gradle @@ -0,0 +1,17 @@ +/* -*- Mode: Groovy; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Bug 1353055 - Strip 'vars' debugging information to agree with moz.build. +ext.configureVariantDebugLevel = { variant -> + // Like 'debug' or 'release'. + def buildType = variant.buildType.name + + // The default is 'lines,source,vars', which includes debugging information + // that is quite large: roughly 500kb for Fennec. Therefore we remove + // 'vars' unless we're producing a debug build, where it is useful. + if (!'debug'.equals(buildType) || mozconfig.substs.MOZILLA_OFFICIAL) { + variant.javaCompileProvider.get().options.debugOptions.debugLevel = 'lines,source' + } +} diff --git a/mobile/android/gradle/dotgradle-offline/gradle.properties b/mobile/android/gradle/dotgradle-offline/gradle.properties new file mode 100644 index 0000000000..3f77ec9a2f --- /dev/null +++ b/mobile/android/gradle/dotgradle-offline/gradle.properties @@ -0,0 +1,3 @@ +// Per https://docs.gradle.org/current/userguide/build_environment.html, this +// overrides the gradle.properties in topsrcdir. +org.gradle.daemon=false diff --git a/mobile/android/gradle/dotgradle-offline/init.gradle b/mobile/android/gradle/dotgradle-offline/init.gradle new file mode 100644 index 0000000000..8f06472aed --- /dev/null +++ b/mobile/android/gradle/dotgradle-offline/init.gradle @@ -0,0 +1,4 @@ +// From https://discuss.gradle.org/t/enable-offline-mode-using-gradle-properties/12134/2. +startParameter.offline = true +// Sadly, this doesn't work: see http://stackoverflow.com/a/19686585. +// startParameter.logLevel = org.gradle.api.logging.LogLevel.INFO diff --git a/mobile/android/gradle/dotgradle-online/gradle.properties b/mobile/android/gradle/dotgradle-online/gradle.properties new file mode 100644 index 0000000000..3f77ec9a2f --- /dev/null +++ b/mobile/android/gradle/dotgradle-online/gradle.properties @@ -0,0 +1,3 @@ +// Per https://docs.gradle.org/current/userguide/build_environment.html, this +// overrides the gradle.properties in topsrcdir. +org.gradle.daemon=false diff --git a/mobile/android/gradle/dotgradle-online/init.gradle b/mobile/android/gradle/dotgradle-online/init.gradle new file mode 100644 index 0000000000..dbba0e3da5 --- /dev/null +++ b/mobile/android/gradle/dotgradle-online/init.gradle @@ -0,0 +1,4 @@ +// From https://discuss.gradle.org/t/enable-offline-mode-using-gradle-properties/12134/2. +startParameter.offline = false +// Sadly, this doesn't work: see http://stackoverflow.com/a/19686585. +// startParameter.logLevel = org.gradle.api.logging.LogLevel.INFO diff --git a/mobile/android/gradle/mach_env.gradle b/mobile/android/gradle/mach_env.gradle new file mode 100644 index 0000000000..560d7dac22 --- /dev/null +++ b/mobile/android/gradle/mach_env.gradle @@ -0,0 +1,29 @@ +ext.machEnv = { topsrcdir -> + // Allow to specify mozconfig in `local.properties` via + // `mozilla-central.mozconfig=/path/to/mozconfig`. This can't be an environment + // variable because it's not feasible to specify environment variables under + // Android Studio on some platforms including macOS. + def localProperties = new Properties() + def localPropertiesFile = new File(topsrcdir, 'local.properties') + if (localPropertiesFile.canRead()) { + localPropertiesFile.withInputStream { + localProperties.load(it) + logger.lifecycle("settings.gradle> Read local.properties: ${localPropertiesFile}") + } + } + + def localMozconfig = localProperties.getProperty("mozilla-central.mozconfig") + + def env = System.env.collect { k, v -> "${k}=${v}" } + if (localMozconfig) { + def envMozconfig = System.env.get('FOUND_MOZCONFIG') + if (!envMozconfig || localMozconfig == envMozconfig) { + logger.lifecycle("settings.gradle> Setting mozconfig from local.properties: ${localMozconfig}") + env << "MOZCONFIG=${localMozconfig}" + } else { + logger.lifecycle("settings.gradle> Preferring mozconfig set in mach environment to mozconfig set in local.properties: ${envMozconfig}") + } + } + + return env +} diff --git a/mobile/android/gradle/product_flavors.gradle b/mobile/android/gradle/product_flavors.gradle new file mode 100644 index 0000000000..6278d9ff3f --- /dev/null +++ b/mobile/android/gradle/product_flavors.gradle @@ -0,0 +1,17 @@ +/* -*- Mode: Groovy; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +ext.configureProductFlavors = { + flavorDimensions "geckoBinaries" + productFlavors { + withGeckoBinaries { + dimension "geckoBinaries" + } + + withoutGeckoBinaries { + dimension "geckoBinaries" + } + } +} diff --git a/mobile/android/gradle/with_gecko_binaries.gradle b/mobile/android/gradle/with_gecko_binaries.gradle new file mode 100644 index 0000000000..5f0d4eeddd --- /dev/null +++ b/mobile/android/gradle/with_gecko_binaries.gradle @@ -0,0 +1,357 @@ +/* -*- Mode: Groovy; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// The JNI wrapper generation tasks depend on the JAR creation task of the :annotations project. +evaluationDependsOn(':annotations') + +import groovy.util.FileNameFinder +import groovy.transform.Memoized + +import java.nio.file.Path +import java.nio.file.Paths +import java.security.MessageDigest + +// To find the Android NDK directory, there are a few wrinkles. In a compiled +// build, we can use our own `ANDROID_NDK` configure option. But in an +// artifact build, that isn't defined, so we fall back to +// `android.ndkDirectory` -- but that's defined in `local.properties`, which +// may not define it. In that case, fall back to crawling the filesystem. +def getNDKDirectory() { + if (project.mozconfig.substs.ANDROID_NDK) { + return project.mozconfig.substs.ANDROID_NDK + } + try { + if (project.android.ndkDirectory) { + return project.android.ndkDirectory + } + } catch (InvalidUserDataException ex) { + // The NDK is not installed, that's ok. + } + def mozbuild = System.getenv('MOZBUILD_STATE_PATH') ?: "${System.getProperty('user.home')}/.mozbuild" + def files = new FileNameFinder().getFileNames(mozbuild, "android-ndk-*/source.properties") + if (files) { + // It would be nice to sort these by version, but that's too much effort right now. + return project.file(files.first()).parentFile.toString() + } + return null +} + +// Whether to include compiled artifacts: `libs/**/*.so` and `assets/omni.ja`. +// Multi-locale packaging wants to include compiled artifacts but *not* rebuild +// them: see also `rootProject.{machStagePackage,geckoBinariesOnlyIf}`. +def hasCompileArtifacts() { + return project.mozconfig.substs.COMPILE_ENVIRONMENT // Full builds. + || project.mozconfig.substs.MOZ_ARTIFACT_BUILDS // Artifact builds. + || System.getenv("MOZ_CHROME_MULTILOCALE") // Multi-locale packaging. +} + +// Get the LLVM bin folder, either from the currently used toolchain or, if +// this is an artifact build, from the Android NDK. +@Memoized +def getLlvmBinFolder() { + // If we have a clang path, just use that + if (project.mozconfig.substs.MOZ_CLANG_PATH) { + return project.file(project.mozconfig.substs.MOZ_CLANG_PATH) + .parentFile + } + + def ndkDirectory = getNDKDirectory() + if (!ndkDirectory) { + // Give up + return null; + } + + // The `**` in the pattern depends on the host architecture. We could compute them or bake them + // in, but this is easy enough. `FileNameFinder` won't return a directory, so we find a file + // and return its parent directory. + return project + .file(new FileNameFinder() + .getFileNames(ndkDirectory.toString(), "toolchains/llvm/prebuilt/**/bin/llvm-*") + .first()) + .parentFile +} + +ext.getLlvmBinFolder = { + // This is the easiest way to expose the memoized function to consumers. + getLlvmBinFolder() +} + +// Bug 1657190: This task works around a bug in the Android-Gradle plugin. When using SNAPSHOT +// builds (and possibly in other situations) the JNI library invalidation process isn't correct. If +// there are two JNI libs `a.so` and `b.so` and a new snapshot updates only `a.so`, then the +// resulting AAR will include both JNI libs but the consuming project's resulting APK will include +// only the modified `a.so`. For GeckoView, it's usually `libxul.so` that is updated. For a +// consumer (like Fenix), this leads to a hard startup crash because `libmozglue.so` will be missing +// when it is read (first, before `libxul.so`) +// +// For !MOZILLA_OFFICIAL builds, we work around this issue to ensure that when *any* library is +// updated then *every* library is updated. We use the ELF build ID baked into each library to +// determine whether any library is updated. We digest (SHA256, for now) all of the ELF build IDs +// and use this as our own Mozilla-specific "library generation ID". We then add our own +// Mozilla-specific ELF section to every library so that they will all be invalidated by the +// Android-Gradle plugin of a consuming project. +class SyncLibsAndUpdateGenerationID extends DefaultTask { + @InputDirectory + File source + + @InputFile + File extraSource + + @OutputDirectory + File destinationDir + + @InputDirectory + File llvmBin = project.ext.getLlvmBinFolder() + + // Sibling to `.note.gnu.build-id`. + @Input + String newElfSection = ".note.mozilla.build-id" + + @TaskAction + void execute() { + Path root = Paths.get(source.toString()) + + def libs = project.fileTree(source.toString()).files.collect { + Path lib = Paths.get(it.toString()) + root.relativize(lib).toString() + } + + def generationId = new ByteArrayOutputStream().withStream { os -> + // Start with the hash of any extra source. + def digest = MessageDigest.getInstance("SHA-256") + extraSource.eachByte(64 * 1024) { byte[] buf, int bytesRead -> + digest.update(buf, 0, bytesRead); + } + def extraSourceHex = new BigInteger(1, digest.digest()).toString(16).padLeft(64, '0') + os.write("${extraSource.toString()} ${extraSourceHex}\n".getBytes("utf-8")); + + // Follow with all the ordered build ID section dumps. + libs.each { lib -> + // This should never fail. If it does, there's a real problem, so an exception is + // reasonable. + project.exec { + commandLine "${llvmBin}/llvm-readobj" + args '--hex-dump=.note.gnu.build-id' + args "${source}/${lib}" + standardOutput = os + } + } + + def allBuildIDs = os.toString() + + // For detailed debugging. + new File("${destinationDir}/${newElfSection}-details").setText(allBuildIDs, 'utf-8') + + allBuildIDs.sha256() + } + + logger.info("Mozilla-specific library generation ID is ${generationId} (see ${destinationDir}/${newElfSection}-details)") + + def generationIdIsStale = libs.any { lib -> + new ByteArrayOutputStream().withStream { os -> + // This can fail, but there's little value letting stderr go to the console in + // general, since it's just noise after a clobber build. + def execResult = project.exec { + ignoreExitValue true + commandLine "${llvmBin}/llvm-readobj" + args "--hex-dump=${newElfSection}" + args "${destinationDir}/${lib}" + standardOutput = os + errorOutput = new ByteArrayOutputStream() + } + + if (execResult.exitValue != 0) { + logger.info("Mozilla-specific library generation ID is missing: ${lib}") + } else if (!os.toString().contains(generationId)) { + logger.info("Mozilla-specific library generation ID is stale: ${lib}") + } else { + logger.debug("Mozilla-specific library generation ID is fresh: ${lib}") + } + execResult.exitValue != 0 || !os.toString().contains(generationId) + } + } + + if (generationIdIsStale) { + project.mkdir destinationDir + new File("${destinationDir}/${newElfSection}").setText(generationId, 'utf-8') + + libs.each { lib -> + project.mkdir project.file("${destinationDir}/${lib}").parent + + new ByteArrayOutputStream().withStream { os -> + // This should never fail: if it does, there's a real problem (again). + project.exec { + commandLine "${llvmBin}/llvm-objcopy" + args "--add-section=${newElfSection}=${destinationDir}/${newElfSection}" + args "${source}/${lib}" + args "${destinationDir}/${lib}" + standardOutput = os + } + } + } + } else { + logger.info("Mozilla-specific library generation ID is fresh!") + } + } +} + +ext.configureVariantWithGeckoBinaries = { variant -> + def omnijarDir = "${topobjdir}/dist/geckoview" + def distDir = "${topobjdir}/dist/geckoview" + + def syncOmnijarFromDistDir + if (hasCompileArtifacts()) { + syncOmnijarFromDistDir = task("syncOmnijarFromDistDirFor${variant.name.capitalize()}", type: Sync) { + onlyIf { + if (source.empty) { + throw new StopExecutionException("Required omnijar not found in ${omnijarDir}/{omni.ja,assets/omni.ja}. Have you built and packaged?") + } + return true + } + + into("${project.buildDir}/moz.build/src/${variant.name}/omnijar") + from("${omnijarDir}/omni.ja", + "${omnijarDir}/assets/omni.ja") { + // Throw an exception if we find multiple, potentially conflicting omni.ja files. + duplicatesStrategy 'fail' + } + } + } + + // For !MOZILLA_OFFICIAL builds, work around an Android-Gradle plugin bug that causes startup + // crashes with local substitution. But -- we want to allow artifact builds that don't have the + // NDK installed. See class comment above. + def shouldUpdateGenerationID = { + if (mozconfig.substs.MOZILLA_OFFICIAL) { + return false + } else if (mozconfig.substs.MOZ_ANDROID_FAT_AAR_ARCHITECTURES) { + return false + } else if (ext.getLlvmBinFolder() == null) { + logger.warn("Could not determine LLVM bin directory.") + logger.warn("Set `ndk.dir` in `${project.topsrcdir}/local.properties` to avoid startup crashes when using `substitute-local-geckoview.gradle`.") + logger.warn("See https://bugzilla.mozilla.org/show_bug.cgi?id=1657190.") + return false + } + return true + }() + + def syncLibsFromDistDir = { if (shouldUpdateGenerationID) { + def jarTask = tasks["bundleLibRuntimeToJar${variant.name.capitalize()}"] + def bundleJar = jarTask.outputs.files.find({ it.name == 'classes.jar' }) + + task("syncLibsAndUpdateGenerationIDFromDistDirFor${variant.name.capitalize()}", type: SyncLibsAndUpdateGenerationID) { + source file("${distDir}/lib") + destinationDir file("${project.buildDir}/moz.build/src/${variant.name}/jniLibs") + // Include the hash of classes.jar as well, so that JVM-only changes don't leave every + // JNI library unchanged and therefore invalidate all of the JNI libraries in a consumer + // doing local substitution. + extraSource bundleJar + dependsOn jarTask + } + } else { + task("syncLibsFromDistDirFor${variant.name.capitalize()}", type: Sync) { + into("${project.buildDir}/moz.build/src/${variant.name}/jniLibs") + from("${distDir}/lib") + } + } }() + + syncLibsFromDistDir.onlyIf { task -> + if (!hasCompileArtifacts()) { + // We won't have JNI libraries if we're not compiling and we're not downloading + // artifacts. Such a configuration is used for running lints, generating docs, etc. + return true + } + if (files(task.source).empty) { + throw new StopExecutionException("Required JNI libraries not found in ${task.source}. Have you built and packaged?") + } + return true + } + + def syncAssetsFromDistDir = task("syncAssetsFromDistDirFor${variant.name.capitalize()}", type: Sync) { + into("${project.buildDir}/moz.build/src/${variant.name}/assets") + from("${distDir}/assets") { + exclude 'omni.ja' + } + } + + if (hasCompileArtifacts()) { + // Local (read, not 'official') builds want to reflect developer changes to + // the omnijar sources, and (when compiling) to reflect developer changes to + // the native binaries. To do this, the Gradle build calls out to the + // moz.build system, which can be re-entrant. Official builds are driven by + // the moz.build system and should never be re-entrant in this way. + if (!mozconfig.substs.MOZILLA_OFFICIAL) { + syncOmnijarFromDistDir.dependsOn rootProject.machStagePackage + syncLibsFromDistDir.dependsOn rootProject.machStagePackage + syncAssetsFromDistDir.dependsOn rootProject.machStagePackage + } + + def assetGenTask = tasks.findByName("generate${variant.name.capitalize()}Assets") + def jniLibFoldersTask = tasks.findByName("merge${variant.name.capitalize()}JniLibFolders") + if ((variant.productFlavors*.name).contains('withGeckoBinaries')) { + assetGenTask.dependsOn syncOmnijarFromDistDir + assetGenTask.dependsOn syncAssetsFromDistDir + jniLibFoldersTask.dependsOn syncLibsFromDistDir + + android.sourceSets."${variant.name}".assets.srcDir syncOmnijarFromDistDir.destinationDir + android.sourceSets."${variant.name}".assets.srcDir syncAssetsFromDistDir.destinationDir + + if (!mozconfig.substs.MOZ_ANDROID_FAT_AAR_ARCHITECTURES) { + android.sourceSets."${variant.name}".jniLibs.srcDir syncLibsFromDistDir.destinationDir + } else { + android.sourceSets."${variant.name}".jniLibs.srcDir "${topobjdir}/dist/fat-aar/output/jni" + } + } + } +} + +ext.configureLibraryVariantWithJNIWrappers = { variant, module -> + // BundleLibRuntime prepares the library for further processing to be + // incorporated in an app. We use this version to create the JNI wrappers. + def jarTask = tasks["bundleLibRuntimeToJar${variant.name.capitalize()}"] + def bundleJar = jarTask.outputs.files.find({ it.name == 'classes.jar' }) + + def annotationProcessorsJarTask = project(':annotations').jar + + def wrapperTask + if (System.env.IS_LANGUAGE_REPACK == '1') { + // Single-locale l10n repacks set `IS_LANGUAGE_REPACK=1` and don't + // really have a build environment. + wrapperTask = task("generateJNIWrappersFor${module}${variant.name.capitalize()}") + } else { + wrapperTask = task("generateJNIWrappersFor${module}${variant.name.capitalize()}", type: JavaExec) { + classpath annotationProcessorsJarTask.archivePath + + // Configure the classpath at evaluation-time, not at + // configuration-time: see above comment. + doFirst { + classpath variant.javaCompileProvider.get().classpath + } + + mainClass = 'org.mozilla.gecko.annotationProcessors.AnnotationProcessor' + args module + args bundleJar + + workingDir "${topobjdir}/widget/android" + + inputs.file(bundleJar) + inputs.file(annotationProcessorsJarTask.archivePath) + inputs.property("module", module) + + outputs.file("${topobjdir}/widget/android/GeneratedJNINatives.h") + outputs.file("${topobjdir}/widget/android/GeneratedJNIWrappers.cpp") + outputs.file("${topobjdir}/widget/android/GeneratedJNIWrappers.h") + + dependsOn jarTask + dependsOn annotationProcessorsJarTask + } + } + + if (module == 'Generated') { + tasks["bundle${variant.name.capitalize()}Aar"].dependsOn wrapperTask + } else { + tasks["assemble${variant.name.capitalize()}"].dependsOn wrapperTask + } +} |