Currently CI runs gradlew lint for android projects using build-fat-aar which is not needed now that we can run lint from root project. Current setup also doesn't update mozlint.json so phabricator doesn't recognize the lint failures. This bug adds gradlew lint to mach lint through the android-{fenix|focus|ac} linters in addition of ktlint and detekt which were added in D216999
This patch changes android lint configuration to generate JSON report of errors and then reads these errors in lints.py to report in the same format as other linters.
Creating new treeherder task for each of `android-{fenix|focus|ac}` linter as android-fenix can take up to 20 minutes ot run now with the addition of gradlew lint, which is almost the same as the existing mozlint-android-lints task takes, so doing this in parallel will be nice.
Follow up work would include removing all the original detekt, ktlint and lint tasks from treeherder which will make creating these new tasks more justified.
Differential Revision: https://phabricator.services.mozilla.com/D243832
547 lines
21 KiB
Groovy
547 lines
21 KiB
Groovy
import org.tomlj.Toml
|
|
import org.tomlj.TomlParseResult
|
|
import org.tomlj.TomlTable
|
|
|
|
buildscript {
|
|
repositories {
|
|
gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORIES.each { repository ->
|
|
maven {
|
|
url = repository
|
|
if (gradle.mozconfig.substs.ALLOW_INSECURE_GRADLE_REPOSITORIES) {
|
|
allowInsecureProtocol = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
dependencies {
|
|
classpath libs.apilint
|
|
classpath libs.tools.androidgradle
|
|
classpath libs.commons.exec
|
|
classpath libs.spotless
|
|
classpath libs.tomlj
|
|
|
|
// Used in mobile/android/fenix/app/build.gradle
|
|
classpath libs.androidx.navigation.safeargs
|
|
classpath libs.osslicenses.plugin
|
|
classpath libs.tools.benchmarkgradle
|
|
classpath libs.glean.gradle.plugin
|
|
classpath "${ApplicationServicesConfig.groupId}:tooling-nimbus-gradle:${ApplicationServicesConfig.version}"
|
|
}
|
|
}
|
|
|
|
plugins {
|
|
id 'ApkSizePlugin'
|
|
id "mozac.ConfigPlugin"
|
|
alias(libs.plugins.detekt)
|
|
alias(libs.plugins.kotlin.android) apply false
|
|
alias(libs.plugins.kotlin.compose) apply false
|
|
alias(libs.plugins.ksp)
|
|
}
|
|
|
|
def tryInt = { string ->
|
|
if (string == null) {
|
|
return string
|
|
}
|
|
if (string.isInteger()) {
|
|
return string as Integer
|
|
}
|
|
return string
|
|
}
|
|
|
|
abstract class VerifyGleanVersionTask extends DefaultTask {
|
|
@InputFile
|
|
final RegularFileProperty source = project.objects.fileProperty().convention(project.layout.projectDirectory.file("Cargo.lock"))
|
|
|
|
@Input
|
|
String expectedVersion = project.ext.gleanVersion
|
|
|
|
@OutputFiles
|
|
FileCollection outputFiles = project.objects.fileCollection()
|
|
|
|
@TaskAction
|
|
void verifyGleanVersion() {
|
|
def foundVersion = getRustVersionFor(source.get().asFile, "glean")
|
|
if (expectedVersion != foundVersion) {
|
|
throw new GradleException("Mismatched Glean version, expected: '${expectedVersion}'," +
|
|
" found '${foundVersion}'")
|
|
} else {
|
|
logger.lifecycle("verifyGleanVersion> expected version matches found version '${foundVersion}'")
|
|
}
|
|
}
|
|
|
|
// Parses the Cargo.lock and returns the version for the given package name.
|
|
static String getRustVersionFor(file, packageName) {
|
|
String version = null;
|
|
TomlParseResult result = Toml.parse(file.getText());
|
|
for (object in result.getArray("package").toList()) {
|
|
def table = (TomlTable) object
|
|
if (table.getString("name") == packageName) {
|
|
if (version != null) {
|
|
throw new GradleException("Multiple versions for '${packageName}' found." +
|
|
" Ensure '${packageName}' is only included once.")
|
|
}
|
|
version = table.getString("version")
|
|
}
|
|
}
|
|
return version
|
|
}
|
|
}
|
|
|
|
tasks.register("verifyGleanVersion", VerifyGleanVersionTask)
|
|
|
|
allprojects {
|
|
// Expose the per-object-directory configuration to all projects.
|
|
ext {
|
|
mozconfig = gradle.mozconfig
|
|
topsrcdir = gradle.mozconfig.topsrcdir
|
|
topobjdir = gradle.mozconfig.topobjdir
|
|
|
|
gleanVersion = libs.versions.mozilla.glean.get() // Verification done in verifyGleanVersion task
|
|
|
|
artifactSuffix = getArtifactSuffix()
|
|
versionName = getVersionName()
|
|
versionCode = computeVersionCode()
|
|
versionNumber = getVersionNumber()
|
|
buildId = getBuildId()
|
|
|
|
buildToolsVersion = mozconfig.substs.ANDROID_BUILD_TOOLS_VERSION
|
|
compileSdkVersion = tryInt(mozconfig.substs.ANDROID_COMPILE_SDK)
|
|
targetSdkVersion = tryInt(mozconfig.substs.ANDROID_TARGET_SDK)
|
|
minSdkVersion = tryInt(mozconfig.substs.MOZ_ANDROID_MIN_SDK_VERSION)
|
|
manifestPlaceholders = [
|
|
ANDROID_PACKAGE_NAME: mozconfig.substs.ANDROID_PACKAGE_NAME,
|
|
ANDROID_TARGET_SDK: mozconfig.substs.ANDROID_TARGET_SDK,
|
|
MOZ_ANDROID_MIN_SDK_VERSION: mozconfig.substs.MOZ_ANDROID_MIN_SDK_VERSION,
|
|
]
|
|
}
|
|
|
|
afterEvaluate {
|
|
if (it.hasProperty('android')) {
|
|
android {
|
|
buildToolsVersion gradle.mozconfig.substs.ANDROID_BUILD_TOOLS_VERSION
|
|
testOptions {
|
|
unitTests.includeAndroidResources = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
repositories {
|
|
gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORIES.each { repository ->
|
|
maven {
|
|
url = repository
|
|
if (gradle.mozconfig.substs.ALLOW_INSECURE_GRADLE_REPOSITORIES) {
|
|
allowInsecureProtocol = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Use the semanticdb-javac and semanticdb-kotlinc plugins to generate semanticdb files for Searchfox
|
|
if (mozconfig.substs.ENABLE_MOZSEARCH_PLUGIN || mozconfig.substs.DOWNLOAD_ALL_GRADLE_DEPENDENCIES) {
|
|
def targetRoot = new File(topobjdir, "mozsearch_java_index")
|
|
|
|
afterEvaluate {
|
|
def addDependencyToConfigurationIfExists = { configurationName, dependency ->
|
|
def configuration = configurations.findByName(configurationName)
|
|
if (configuration != null) {
|
|
dependencies.add(configurationName, dependency)
|
|
}
|
|
}
|
|
|
|
addDependencyToConfigurationIfExists("compileOnly", libs.semanticdb.java)
|
|
addDependencyToConfigurationIfExists("testCompileOnly", libs.semanticdb.java)
|
|
addDependencyToConfigurationIfExists("androidTestCompileOnly", libs.semanticdb.java)
|
|
addDependencyToConfigurationIfExists("kotlinCompilerPluginClasspath", libs.semanticdb.kotlin)
|
|
}
|
|
|
|
tasks.withType(JavaCompile) {
|
|
options.compilerArgs += [
|
|
"-Xplugin:semanticdb -sourceroot:${topsrcdir} -targetroot:${targetRoot}",
|
|
]
|
|
}
|
|
|
|
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) {
|
|
compilerOptions.freeCompilerArgs.addAll([
|
|
"-P", "plugin:semanticdb-kotlinc:sourceroot=${topsrcdir}".toString(),
|
|
"-P", "plugin:semanticdb-kotlinc:targetroot=${targetRoot}".toString(),
|
|
])
|
|
}
|
|
}
|
|
|
|
task downloadDependencies() {
|
|
description = 'Download all dependencies to the Gradle cache'
|
|
doLast {
|
|
configurations.each { configuration ->
|
|
if (configuration.canBeResolved) {
|
|
configuration.allDependencies.each { dependency ->
|
|
try {
|
|
configuration.files(dependency)
|
|
} catch(e) {
|
|
println("Could not resolve ${configuration.name} -> ${dependency.name}")
|
|
if (project.hasProperty('displayErrorDetails') && project.property('displayErrorDetails') == 'true') {
|
|
println(" > ${e.message}")
|
|
if (e.cause) {
|
|
println(" >> ${e.cause}")
|
|
if (e.cause.cause) {
|
|
println(" >> ${e.cause.cause}")
|
|
}
|
|
}
|
|
println("")
|
|
} else {
|
|
println("\t >> Re-run with -PdisplayErrorDetails=true to see more details")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
import org.gradle.api.services.BuildServiceParameters
|
|
abstract class MozconfigService implements BuildService<MozconfigService.Params>, AutoCloseable {
|
|
interface Params extends BuildServiceParameters {
|
|
MapProperty<String, Object> getMozconfigParam()
|
|
}
|
|
|
|
void close() {
|
|
}
|
|
|
|
Object getMozconfig() {
|
|
return parameters.mozconfigParam.get()
|
|
}
|
|
}
|
|
|
|
// Non-official versions are like "61.0a1" or "61.0b1", where "a1" and "b1"
|
|
// are the milestone.
|
|
// This simply strips that off, leaving "61.0" in this example.
|
|
def getAppVersionWithoutMilestone() {
|
|
return project.ext.mozconfig.substs.MOZ_APP_VERSION.replaceFirst(/[ab][0-9]/, "")
|
|
}
|
|
|
|
// This converts MOZ_APP_VERSION into an integer
|
|
// version code.
|
|
//
|
|
// We take something like 58.1.2a1 and come out with 5800102
|
|
// This gives us 3 digits for the major number, and 2 digits
|
|
// each for the minor and build number. Beta and Release
|
|
//
|
|
// This must be synchronized with _compute_gecko_version(...) in /taskcluster/gecko_taskgraph/transforms/task.py
|
|
def computeVersionCode() {
|
|
String appVersion = getAppVersionWithoutMilestone()
|
|
|
|
// Split on the dot delimiter, e.g. 58.1.1a1 -> ["58, "1", "1a1"]
|
|
String[] parts = appVersion.split('\\.')
|
|
|
|
assert parts.size() == 2 || parts.size() == 3
|
|
|
|
// Major
|
|
int code = Integer.parseInt(parts[0]) * 100000
|
|
|
|
// Minor
|
|
code += Integer.parseInt(parts[1]) * 100
|
|
|
|
// Build
|
|
if (parts.size() == 3) {
|
|
code += Integer.parseInt(parts[2])
|
|
}
|
|
|
|
return code;
|
|
}
|
|
|
|
def getVersionName() {
|
|
return "${mozconfig.substs.MOZ_APP_VERSION}-${mozconfig.substs.MOZ_UPDATE_CHANNEL}"
|
|
}
|
|
|
|
// Mimic Python: open(os.path.join(buildconfig.topobjdir, 'buildid.h')).readline().split()[2]
|
|
def getBuildId() {
|
|
return file("${topobjdir}/buildid.h").getText('utf-8').split()[2]
|
|
}
|
|
|
|
def getVersionNumber() {
|
|
def appVersion = getAppVersionWithoutMilestone()
|
|
def parts = appVersion.split('\\.')
|
|
def version = parts[0] + "." + parts[1] + "." + getBuildId()
|
|
def substs = project.ext.mozconfig.substs
|
|
if (!substs.MOZILLA_OFFICIAL && !substs.MOZ_ANDROID_FAT_AAR_ARCHITECTURES) {
|
|
// Use -SNAPSHOT versions locally to enable the local GeckoView substitution flow.
|
|
version += "-SNAPSHOT"
|
|
}
|
|
return version
|
|
}
|
|
|
|
def getArtifactSuffix() {
|
|
def substs = project.ext.mozconfig.substs
|
|
|
|
def suffix = ""
|
|
// Release artifacts don't specify the channel, for the sake of simplicity.
|
|
if (substs.MOZ_UPDATE_CHANNEL != 'release') {
|
|
suffix += "-${mozconfig.substs.MOZ_UPDATE_CHANNEL}"
|
|
}
|
|
|
|
return suffix
|
|
}
|
|
|
|
class MachExec extends Exec {
|
|
def MachExec() {
|
|
// Bug 1543982: When invoking `mach build` recursively, the outer `mach
|
|
// build` itself modifies the environment, causing configure to run
|
|
// again. This tries to restore the environment that the outer `mach
|
|
// build` was invoked in. See the comment in
|
|
// $topsrcdir/settings.gradle.
|
|
project.ext.mozconfig.mozconfig.env.unmodified.each { k, v -> environment.remove(k) }
|
|
environment project.ext.mozconfig.orig_mozconfig.env.unmodified
|
|
environment 'MOZCONFIG', project.ext.mozconfig.substs.MOZCONFIG
|
|
}
|
|
|
|
static def geckoBinariesOnlyIf(task, mozconfig) {
|
|
// Never when Gradle was invoked within `mach build`.
|
|
if ('1' == System.env.GRADLE_INVOKED_WITHIN_MACH_BUILD) {
|
|
task.logger.lifecycle("Skipping task ${task.path} because: within `mach build`")
|
|
return false
|
|
}
|
|
|
|
// Never for official builds.
|
|
if (mozconfig.substs.MOZILLA_OFFICIAL) {
|
|
task.logger.lifecycle("Skipping task ${task.path} because: MOZILLA_OFFICIAL")
|
|
return false
|
|
}
|
|
|
|
// Multi-l10n builds set `AB_CD=multi`, which isn't a valid locale, and
|
|
// `MOZ_CHROME_MULTILOCALE`. To avoid failures, if Gradle is invoked with
|
|
// either, we don't invoke Make at all; this allows a multi-locale omnijar
|
|
// to be consumed without modification.
|
|
if ('multi' == System.env.AB_CD || System.env.MOZ_CHROME_MULTILOCALE) {
|
|
task.logger.lifecycle("Skipping task ${task.path} because: AB_CD=multi")
|
|
return false
|
|
}
|
|
|
|
// Single-locale l10n repacks set `IS_LANGUAGE_REPACK=1` and handle resource
|
|
// and code generation themselves.
|
|
if ('1' == System.env.IS_LANGUAGE_REPACK) {
|
|
task.logger.lifecycle("Skipping task ${task.path} because: IS_LANGUAGE_REPACK")
|
|
return false
|
|
}
|
|
|
|
task.logger.lifecycle("Executing task ${task.path}")
|
|
return true
|
|
}
|
|
}
|
|
|
|
task machBuildFaster(type: MachExec) {
|
|
def mozconfigProvider = gradle.sharedServices.registerIfAbsent("mozconfig", MozconfigService) {
|
|
parameters.mozconfigParam.set(project.ext.mozconfig)
|
|
}
|
|
usesService(mozconfigProvider)
|
|
onlyIf { task -> MachExec.geckoBinariesOnlyIf(task, mozconfigProvider.get().getMozconfig()) }
|
|
|
|
workingDir "${topsrcdir}"
|
|
|
|
commandLine mozconfig.substs.PYTHON3
|
|
args "${topsrcdir}/mach"
|
|
args 'build'
|
|
args 'faster'
|
|
|
|
// Add `-v` if we're running under `--info` (or `--debug`).
|
|
if (project.logger.isEnabled(LogLevel.INFO)) {
|
|
args '-v'
|
|
}
|
|
|
|
standardOutput = System.out
|
|
errorOutput = System.err
|
|
}
|
|
|
|
task machStagePackage(type: MachExec) {
|
|
def mozconfigProvider = gradle.sharedServices.registerIfAbsent("mozconfig", MozconfigService) {
|
|
parameters.mozconfigParam.set(project.ext.mozconfig)
|
|
}
|
|
usesService(mozconfigProvider)
|
|
onlyIf { task -> MachExec.geckoBinariesOnlyIf(task, mozconfigProvider.get().getMozconfig()) }
|
|
dependsOn rootProject.machBuildFaster
|
|
|
|
workingDir "${topobjdir}"
|
|
|
|
// We'd prefer this to be a `mach` invocation, but `mach build
|
|
// mobile/android/installer/stage-package` doesn't work as expected.
|
|
commandLine mozconfig.substs.GMAKE
|
|
args '-C'
|
|
args "${topobjdir}/mobile/android/installer"
|
|
args 'stage-package'
|
|
|
|
outputs.file "${topobjdir}/dist/geckoview/assets/omni.ja"
|
|
|
|
outputs.file "${topobjdir}/dist/geckoview/assets/${mozconfig.substs.ANDROID_CPU_ARCH}/libxul.so"
|
|
outputs.file "${topobjdir}/dist/geckoview/lib/${mozconfig.substs.ANDROID_CPU_ARCH}/libmozglue.so"
|
|
|
|
// Force running `stage-package`.
|
|
outputs.upToDateWhen { false }
|
|
|
|
standardOutput = System.out
|
|
errorOutput = System.err
|
|
}
|
|
|
|
afterEvaluate {
|
|
subprojects { project ->
|
|
tasks.withType(JavaCompile) {
|
|
// Add compiler args for all code except third-party code.
|
|
options.compilerArgs += [
|
|
// Turn on all warnings, except...
|
|
"-Xlint:all",
|
|
// Deprecation, because we do use deprecated API for compatibility.
|
|
"-Xlint:-deprecation",
|
|
// Serial, because we don't use Java serialization.
|
|
"-Xlint:-serial",
|
|
// Classfile, because javac has a bug with MethodParameters attributes
|
|
// with Java 7. https://bugs.openjdk.java.net/browse/JDK-8190452
|
|
"-Xlint:-classfile"]
|
|
|
|
// In GeckoView java projects only, turn all remaining warnings
|
|
// into errors unless marked by @SuppressWarnings.
|
|
def projectName = project.getName()
|
|
if (projectName.startsWith('geckoview')
|
|
|| projectName == 'annotations'
|
|
|| projectName == 'exoplayer2'
|
|
|| projectName == 'messaging_example'
|
|
|| projectName == 'port_messaging_example'
|
|
|| projectName == 'test_runner'
|
|
) {
|
|
options.compilerArgs += [
|
|
"-Werror"
|
|
]
|
|
}
|
|
}
|
|
|
|
project.configurations.configureEach {
|
|
// Dependencies can't depend on a different major version of Glean than A-C itself.
|
|
resolutionStrategy.eachDependency { details ->
|
|
if (details.requested.group == 'org.mozilla.telemetry'
|
|
&& details.requested.name.contains('glean') ) {
|
|
def requested = details.requested.version.tokenize(".")
|
|
def defined = libs.versions.mozilla.glean.get().tokenize(".")
|
|
// Check the major version
|
|
if (requested[0] != defined[0]) {
|
|
throw new AssertionError("Cannot resolve to a single Glean version. Requested: ${details.requested.version}, A-C uses: ${libs.mozilla.glean}")
|
|
} else {
|
|
// Enforce that all (transitive) dependencies are using the defined Glean version
|
|
details.useVersion libs.versions.mozilla.glean.get()
|
|
}
|
|
}
|
|
}
|
|
|
|
resolutionStrategy.capabilitiesResolution.withCapability("org.mozilla.telemetry:glean-native") {
|
|
def toBeSelected = candidates.find { it.id instanceof ModuleComponentIdentifier && it.id.module.contains('geckoview') }
|
|
if (toBeSelected != null) {
|
|
select(toBeSelected)
|
|
}
|
|
because 'use GeckoView Glean instead of standalone Glean'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
apply plugin: 'idea'
|
|
|
|
idea {
|
|
project {
|
|
languageLevel = '1.8'
|
|
}
|
|
|
|
module {
|
|
// Object directories take a huge amount of time for IntelliJ to index.
|
|
// Exclude them. Convention is that object directories start with obj.
|
|
// IntelliJ is clever and will not exclude the parts of the object
|
|
// directory that are referenced, if there are any. In practice,
|
|
// indexing the entirety of the tree is taking too long, so exclude all
|
|
// but mobile/.
|
|
def topsrcdirURI = file(topsrcdir).toURI()
|
|
excludeDirs += files(file(topsrcdir)
|
|
.listFiles({it.isDirectory()} as FileFilter)
|
|
.collect({topsrcdirURI.relativize(it.toURI()).toString()}) // Relative paths.
|
|
.findAll({!it.equals('mobile/')}))
|
|
|
|
// If topobjdir is below topsrcdir, hide only some portions of that tree.
|
|
def topobjdirURI = file(topobjdir).toURI()
|
|
if (!topsrcdirURI.relativize(topobjdirURI).isAbsolute()) {
|
|
excludeDirs -= file(topobjdir)
|
|
excludeDirs += files(file(topobjdir).listFiles())
|
|
excludeDirs -= file("${topobjdir}/gradle")
|
|
}
|
|
}
|
|
}
|
|
|
|
subprojects { project ->
|
|
// Perform spotless lint in GeckoView projects only.
|
|
def projectName = project.getName()
|
|
if (projectName.startsWith('geckoview')
|
|
|| projectName == 'annotations'
|
|
|| projectName == 'exoplayer2'
|
|
|| projectName == 'messaging_example'
|
|
|| projectName == 'port_messaging_example'
|
|
|| projectName == 'test_runner'
|
|
) {
|
|
apply plugin: "com.diffplug.spotless"
|
|
|
|
spotless {
|
|
java {
|
|
target project.fileTree(project.projectDir) {
|
|
include '**/*.java'
|
|
exclude '**/thirdparty/**'
|
|
}
|
|
googleJavaFormat('1.17.0')
|
|
}
|
|
kotlin {
|
|
target project.fileTree(project.projectDir) {
|
|
include '**/*.kt'
|
|
exclude '**/thirdparty/**'
|
|
}
|
|
ktlint("${libs.versions.ktlint.get()}").editorConfigOverride([
|
|
// Disable some of the new ktlint rules for GV code.
|
|
'ktlint_standard_annotation' : 'disabled',
|
|
'ktlint_standard_argument-list-wrapping' : 'disabled',
|
|
'ktlint_standard_class-signature' : 'disabled',
|
|
'ktlint_standard_enum-wrapping' : 'disabled',
|
|
'ktlint_standard_function-expression-body' : 'disabled',
|
|
'ktlint_standard_function-signature' : 'disabled',
|
|
'ktlint_standard_max-line-length' : 'disabled',
|
|
'ktlint_standard_property-naming' : 'disabled',
|
|
'ktlint_standard_statement-wrapping' : 'disabled',
|
|
'ktlint_standard_type-parameter-list-spacing': 'disabled',
|
|
])
|
|
}
|
|
}
|
|
}
|
|
|
|
afterEvaluate {
|
|
// Our vendored copy of exoplayer2 hits build failures when targeting Java 17.
|
|
// Given our intent to remove it in the near future, just leave it alone here.
|
|
if (it.hasProperty('android') && projectName != 'exoplayer2') {
|
|
kotlin {
|
|
jvmToolchain(config.jvmTargetCompatibility)
|
|
}
|
|
}
|
|
}
|
|
|
|
project.configurations.configureEach {
|
|
resolutionStrategy.capabilitiesResolution.withCapability("org.mozilla.telemetry:glean-native") {
|
|
def toBeSelected = candidates.find {
|
|
it.id instanceof ProjectComponentIdentifier && it.id.projectName.contains('geckoview')
|
|
}
|
|
if (toBeSelected != null) {
|
|
select(toBeSelected)
|
|
}
|
|
because 'use GeckoView Glean instead of standalone Glean'
|
|
}
|
|
}
|
|
}
|
|
|
|
tasks.register("lint-a-c") {
|
|
subprojects.each{
|
|
if (it.tasks.findByName("lint") != null && it.projectDir.absolutePath.contains("android-components")) {
|
|
dependsOn it.tasks.named("lint")
|
|
println("Subproject ${it.name} has a lint task")
|
|
}
|
|
}
|
|
}
|