buildSrc: remove

This commit is contained in:
Harsh Shandilya 2021-11-29 02:38:06 +05:30
parent a80020477f
commit 81e9926b72
No known key found for this signature in database
GPG key ID: 366D7BBAD1031E80
13 changed files with 7 additions and 708 deletions

1
buildSrc/.gitignore vendored
View file

@ -1 +0,0 @@
/build

View file

@ -1,44 +0,0 @@
/*
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
plugins { `kotlin-dsl` }
repositories {
mavenCentral()
gradlePluginPortal()
google()
}
gradlePlugin {
plugins {
register("aps") {
id = "aps-plugin"
implementationClass = "PasswordStorePlugin"
}
register("crowdin") {
id = "crowdin-plugin"
implementationClass = "CrowdinDownloadPlugin"
}
register("versioning") {
id = "versioning-plugin"
implementationClass = "VersioningPlugin"
}
register("psl") {
id = "psl-plugin"
implementationClass = "PublicSuffixListPlugin"
}
}
}
dependencies {
implementation("com.android.tools.build:gradle:7.0.3")
implementation("com.google.dagger:hilt-android-gradle-plugin:2.40.3")
implementation("com.squareup.okhttp3:okhttp:4.9.0")
implementation("com.vanniktech:gradle-maven-publish-plugin:0.18.0")
implementation("com.vdurmont:semver4j:3.1.0")
implementation("de.undercouch:gradle-download-task:4.1.2")
implementation("org.jetbrains.dokka:dokka-gradle-plugin:1.6.0")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.0")
}

View file

@ -1,97 +0,0 @@
/*
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
import com.android.build.gradle.TestedExtension
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.kotlin.dsl.register
import org.gradle.language.nativeplatform.internal.BuildType
/** Checks if we're building a snapshot */
@Suppress("UnstableApiUsage")
fun Project.isSnapshot(): Boolean {
with(project.providers) {
val workflow = environmentVariable("GITHUB_WORKFLOW").forUseAtConfigurationTime()
val snapshot = environmentVariable("SNAPSHOT").forUseAtConfigurationTime()
return workflow.isPresent && snapshot.isPresent
}
}
/** Apply configurations for app module */
@Suppress("UnstableApiUsage")
internal fun BaseAppModuleExtension.configureAndroidApplicationOptions(project: Project) {
val minifySwitch =
project.providers.environmentVariable("DISABLE_MINIFY").forUseAtConfigurationTime()
adbOptions.installOptions("--user 0")
buildFeatures {
viewBinding = true
buildConfig = true
}
flavorDimensions.add(FlavorDimensions.FREE)
productFlavors {
register(ProductFlavors.FREE) {}
register(ProductFlavors.NON_FREE) {}
}
buildTypes {
named(BuildType.RELEASE.name) {
isMinifyEnabled = !minifySwitch.isPresent
setProguardFiles(
listOf(
"proguard-android-optimize.txt",
"proguard-rules.pro",
"proguard-rules-missing-classes.pro",
)
)
buildConfigField("boolean", "ENABLE_DEBUG_FEATURES", "${project.isSnapshot()}")
}
named(BuildType.DEBUG.name) {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
isMinifyEnabled = false
buildConfigField("boolean", "ENABLE_DEBUG_FEATURES", "true")
}
}
}
/** Apply baseline configurations for all Android projects (Application and Library). */
@Suppress("UnstableApiUsage")
internal fun TestedExtension.configureCommonAndroidOptions() {
setCompileSdkVersion(31)
defaultConfig {
minSdk = 23
targetSdk = 29
}
sourceSets {
named("main") { java.srcDirs("src/main/kotlin") }
named("test") { java.srcDirs("src/test/kotlin") }
named("androidTest") { java.srcDirs("src/androidTest/kotlin") }
}
packagingOptions {
resources.excludes.add("**/*.version")
resources.excludes.add("**/*.txt")
resources.excludes.add("**/*.kotlin_module")
resources.excludes.add("**/plugin.properties")
resources.excludes.add("**/META-INF/AL2.0")
resources.excludes.add("**/META-INF/LGPL2.1")
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
testOptions {
animationsDisabled = true
unitTests.isReturnDefaultValues = true
}
}

View file

@ -1,121 +0,0 @@
/*
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
import de.undercouch.gradle.tasks.download.Download
import java.io.File
import javax.xml.parsers.DocumentBuilderFactory
import okhttp3.OkHttpClient
import okhttp3.Request
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.Copy
import org.gradle.kotlin.dsl.create
import org.gradle.kotlin.dsl.register
import org.w3c.dom.Document
private const val EXCEPTION_MESSAGE =
"""Applying `crowdin-plugin` requires a projectName to be configured via the "crowdin" extension."""
private const val CROWDIN_BUILD_API_URL =
"https://api.crowdin.com/api/project/%s/export?login=%s&account-key=%s"
class CrowdinDownloadPlugin : Plugin<Project> {
override fun apply(project: Project) {
with(project) {
val extension = extensions.create<CrowdinExtension>("crowdin")
afterEvaluate {
val projectName = extension.projectName
if (projectName.isEmpty()) {
throw GradleException(EXCEPTION_MESSAGE)
}
tasks.register("buildOnApi") {
doLast {
val login = providers.environmentVariable("CROWDIN_LOGIN").forUseAtConfigurationTime()
val key =
providers.environmentVariable("CROWDIN_PROJECT_KEY").forUseAtConfigurationTime()
if (!login.isPresent) {
throw GradleException("CROWDIN_LOGIN environment variable must be set")
}
if (!key.isPresent) {
throw GradleException("CROWDIN_PROJECT_KEY environment variable must be set")
}
val client = OkHttpClient()
val url = CROWDIN_BUILD_API_URL.format(projectName, login.get(), key.get())
val request = Request.Builder().url(url).get().build()
client.newCall(request).execute()
}
}
tasks.register<Download>("downloadCrowdin") {
dependsOn("buildOnApi")
src("https://crowdin.com/backend/download/project/$projectName.zip")
dest("$buildDir/translations.zip")
overwrite(true)
}
tasks.register<Copy>("extractCrowdin") {
dependsOn("downloadCrowdin")
doFirst { File(buildDir, "translations").deleteRecursively() }
from(zipTree("$buildDir/translations.zip"))
into("$buildDir/translations")
}
tasks.register<Copy>("extractStrings") {
dependsOn("extractCrowdin")
from("$buildDir/translations/")
into("${projectDir}/src/")
setFinalizedBy(setOf("removeIncompleteStrings"))
}
tasks.register("removeIncompleteStrings") {
doLast {
val sourceSets = arrayOf("main", "nonFree")
for (sourceSet in sourceSets) {
val stringFiles =
File("${projectDir}/src/$sourceSet").walkTopDown().filter {
it.name == "strings.xml"
}
val sourceFile =
stringFiles.firstOrNull { it.path.endsWith("values/strings.xml") }
?: throw GradleException("No root strings.xml found in '$sourceSet' sourceSet")
val sourceDoc = parseDocument(sourceFile)
val baselineStringCount = countStrings(sourceDoc)
val threshold = 0.80 * baselineStringCount
stringFiles.forEach { file ->
if (file != sourceFile) {
val doc = parseDocument(file)
val stringCount = countStrings(doc)
if (stringCount < threshold) {
file.delete()
}
}
}
}
}
}
tasks.register("crowdin") {
dependsOn("extractStrings")
if (!extension.skipCleanup) {
doLast {
File("$buildDir/translations").deleteRecursively()
File("$buildDir/nonFree-translations").deleteRecursively()
File("$buildDir/translations.zip").delete()
}
}
}
}
}
}
private fun parseDocument(file: File): Document {
val dbFactory = DocumentBuilderFactory.newInstance()
val documentBuilder = dbFactory.newDocumentBuilder()
return documentBuilder.parse(file)
}
private fun countStrings(document: Document): Int {
// Normalization is beneficial for us
// https://stackoverflow.com/questions/13786607/normalization-in-dom-parsing-with-java-how-does-it-work
document.documentElement.normalize()
return document.getElementsByTagName("string").length
}
}

View file

@ -1,17 +0,0 @@
/*
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
open class CrowdinExtension {
/** Configure the project name on Crowdin */
open var projectName = ""
/**
* Don't delete downloaded and extracted translation archives from build directory.
*
* Useful for debugging.
*/
open var skipCleanup = false
}

View file

@ -1,34 +0,0 @@
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.jetbrains.kotlin.gradle.plugin.KaptExtension
/** Apply default kapt configs to the [Project]. */
internal fun Project.configureKapt() {
extensions.configure<KaptExtension> {
javacOptions {
if (hasDaggerCompilerDependency) {
// https://dagger.dev/dev-guide/compiler-options#fastinit-mode
option("-Adagger.fastInit=enabled")
// Enable the better, experimental error messages
// https://github.com/google/dagger/commit/0d2505a727b54f47b8677f42dd4fc5c1924e37f5
option("-Adagger.experimentalDaggerErrorMessages=enabled")
// Share test components for when we start leveraging Hilt for tests
// https://github.com/google/dagger/releases/tag/dagger-2.34
option("-Adagger.hilt.shareTestComponents=true")
// KAPT nests errors causing real issues to be suppressed in CI logs
option("-Xmaxerrs", 500)
// Enables per-module validation for faster error detection
// https://github.com/google/dagger/commit/325b516ac6a53d3fc973d247b5231fafda9870a2
option("-Adagger.moduleBindingValidation=ERROR")
}
}
}
// disable kapt tasks for unit tests
tasks
.matching { it.name.startsWith("kapt") && it.name.endsWith("UnitTestKotlin") }
.configureEach { enabled = false }
}
private val Project.hasDaggerCompilerDependency: Boolean
get() =
configurations.any { it.dependencies.any { dependency -> dependency.name == "hilt-compiler" } }

View file

@ -1,73 +0,0 @@
/*
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
import com.android.build.gradle.TestedExtension
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
import com.android.build.gradle.internal.plugins.AppPlugin
import com.android.build.gradle.internal.plugins.LibraryPlugin
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.JavaLibraryPlugin
import org.gradle.api.plugins.JavaPlugin
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.withType
import org.gradle.plugins.signing.SigningExtension
import org.gradle.plugins.signing.SigningPlugin
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
import org.jetbrains.kotlin.gradle.internal.Kapt3GradleSubplugin
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapper
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
class PasswordStorePlugin : Plugin<Project> {
override fun apply(project: Project) {
project.plugins.all {
when (this) {
is JavaPlugin, is JavaLibraryPlugin -> {
project.tasks.withType<JavaCompile> {
options.compilerArgs.add("-Xlint:unchecked")
options.isDeprecation = true
options.isWarnings = true
}
}
is LibraryPlugin -> {
project.extensions.getByType<TestedExtension>().configureCommonAndroidOptions()
project.configureExplicitApi()
project.configureSlimTests()
}
is AppPlugin -> {
project
.extensions
.getByType<BaseAppModuleExtension>()
.configureAndroidApplicationOptions(project)
project.extensions.getByType<BaseAppModuleExtension>().configureBuildSigning(project)
project.extensions.getByType<TestedExtension>().configureCommonAndroidOptions()
project.configureSlimTests()
}
is SigningPlugin -> {
project.extensions.getByType<SigningExtension>().configureBuildSigning()
}
is KotlinPluginWrapper -> {
project.configureExplicitApi()
}
is Kapt3GradleSubplugin -> {
project.configureKapt()
}
}
}
}
private fun Project.configureExplicitApi() {
val project = this
tasks.withType<KotlinCompile> {
if (!name.contains("test", ignoreCase = true)) {
project.configure<KotlinProjectExtension> { explicitApi() }
kotlinOptions { freeCompilerArgs += listOf("-Xexplicit-api=strict") }
}
}
}
}

View file

@ -1,13 +0,0 @@
/*
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
object FlavorDimensions {
const val FREE = "free"
}
object ProductFlavors {
const val FREE = "free"
const val NON_FREE = "nonFree"
}

View file

@ -1,120 +0,0 @@
/* 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 java.io.File
import java.util.TreeSet
import okhttp3.OkHttpClient
import okhttp3.Request
import okio.ByteString
import okio.ByteString.Companion.encodeUtf8
import okio.buffer
import okio.sink
import org.gradle.api.Plugin
import org.gradle.api.Project
/**
* Gradle plugin to update the public suffix list used by the `lib-publicsuffixlist` component.
*
* Base on PublicSuffixListGenerator from OkHttp:
* https://github.com/square/okhttp/blob/master/okhttp/src/test/java/okhttp3/internal/publicsuffix/PublicSuffixListGenerator.java
*/
class PublicSuffixListPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.tasks.register("updatePSL") {
doLast {
val filename = project.projectDir.absolutePath + "/src/main/assets/publicsuffixes"
updatePublicSuffixList(filename)
}
}
}
private fun updatePublicSuffixList(destination: String) {
val list = fetchPublicSuffixList()
writeListToDisk(destination, list)
}
private fun writeListToDisk(destination: String, data: PublicSuffixListData) {
val fileSink = File(destination).sink()
fileSink.buffer().use { sink ->
sink.writeInt(data.totalRuleBytes)
for (domain in data.sortedRules) {
sink.write(domain).writeByte('\n'.toInt())
}
sink.writeInt(data.totalExceptionRuleBytes)
for (domain in data.sortedExceptionRules) {
sink.write(domain).writeByte('\n'.toInt())
}
}
}
private fun fetchPublicSuffixList(): PublicSuffixListData {
val client = OkHttpClient.Builder().build()
val request =
Request.Builder().url("https://publicsuffix.org/list/public_suffix_list.dat").build()
client.newCall(request).execute().use { response ->
val source = requireNotNull(response.body).source()
val data = PublicSuffixListData()
while (!source.exhausted()) {
val line = source.readUtf8LineStrict()
if (line.trim { it <= ' ' }.isEmpty() || line.startsWith("//")) {
continue
}
if (line.contains(WILDCARD_CHAR)) {
assertWildcardRule(line)
}
var rule = line.encodeUtf8()
if (rule.startsWith(EXCEPTION_RULE_MARKER)) {
rule = rule.substring(1)
// We use '\n' for end of value.
data.totalExceptionRuleBytes += rule.size + 1
data.sortedExceptionRules.add(rule)
} else {
data.totalRuleBytes += rule.size + 1 // We use '\n' for end of value.
data.sortedRules.add(rule)
}
}
return data
}
}
@Suppress("TooGenericExceptionThrown", "ThrowsCount")
private fun assertWildcardRule(rule: String) {
if (rule.indexOf(WILDCARD_CHAR) != 0) {
throw RuntimeException("Wildcard is not not in leftmost position")
}
if (rule.indexOf(WILDCARD_CHAR, 1) != -1) {
throw RuntimeException("Rule contains multiple wildcards")
}
if (rule.length == 1) {
throw RuntimeException("Rule wildcards the first level")
}
}
companion object {
private const val WILDCARD_CHAR = "*"
private val EXCEPTION_RULE_MARKER = "!".encodeUtf8()
}
}
data class PublicSuffixListData(
var totalRuleBytes: Int = 0,
var totalExceptionRuleBytes: Int = 0,
val sortedRules: TreeSet<ByteString> = TreeSet(),
val sortedExceptionRules: TreeSet<ByteString> = TreeSet()
)

View file

@ -1,40 +0,0 @@
/*
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
import java.util.Properties
import org.gradle.api.Project
import org.gradle.kotlin.dsl.provideDelegate
import org.gradle.plugins.signing.SigningExtension
private const val KEYSTORE_CONFIG_PATH = "keystore.properties"
/** Configure signing for all build types. */
@Suppress("UnstableApiUsage")
internal fun BaseAppModuleExtension.configureBuildSigning(project: Project) {
with(project) {
val keystoreConfigFile = rootProject.layout.projectDirectory.file(KEYSTORE_CONFIG_PATH)
if (!keystoreConfigFile.asFile.exists()) return
val contents = providers.fileContents(keystoreConfigFile).asText.forUseAtConfigurationTime()
val keystoreProperties = Properties()
keystoreProperties.load(contents.get().byteInputStream())
signingConfigs {
register("release") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = rootProject.file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["storePassword"] as String
}
}
val signingConfig = signingConfigs.getByName("release")
buildTypes.all { setSigningConfig(signingConfig) }
}
}
internal fun SigningExtension.configureBuildSigning() {
val signingKey: String? by project
val signingPassword: String? by project
useInMemoryPgpKeys(signingKey, signingPassword)
}

View file

@ -1,42 +0,0 @@
/*
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.LibraryAndroidComponentsExtension
import org.gradle.api.Project
import org.gradle.kotlin.dsl.findByType
import org.gradle.language.nativeplatform.internal.BuildType
/**
* When the "slimTests" project property is provided, disable the unit test tasks on `release` build
* type and `nonFree` product flavor to avoid running the same tests repeatedly in different build
* variants.
*
* Examples: `./gradlew test -PslimTests` will run unit tests for `nonFreeDebug` and `debug` build
* variants in Android App and Library projects, and all tests in JVM projects.
*/
internal fun Project.configureSlimTests() {
if (providers.gradleProperty(SLIM_TESTS_PROPERTY).forUseAtConfigurationTime().isPresent) {
// disable unit test tasks on the release build type for Android Library projects
extensions.findByType<LibraryAndroidComponentsExtension>()?.run {
beforeVariants(selector().withBuildType(BuildType.RELEASE.name)) {
it.enableUnitTest = false
it.enableAndroidTest = false
}
}
// disable unit test tasks on the release build type and free flavor for Android Application
// projects.
extensions.findByType<ApplicationAndroidComponentsExtension>()?.run {
beforeVariants(selector().withBuildType(BuildType.RELEASE.name)) { it.enableUnitTest = false }
beforeVariants(selector().withFlavor(FlavorDimensions.FREE to ProductFlavors.NON_FREE)) {
it.enableUnitTest = false
it.enableAndroidTest = false
}
}
}
}
private const val SLIM_TESTS_PROPERTY = "slimTests"

View file

@ -1,106 +0,0 @@
/*
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
import com.android.build.gradle.internal.plugins.AppPlugin
import com.vdurmont.semver4j.Semver
import java.io.OutputStream
import java.util.Properties
import org.gradle.api.Plugin
import org.gradle.api.Project
private const val VERSIONING_PROP_FILE = "version.properties"
private const val VERSIONING_PROP_VERSION_NAME = "versioning-plugin.versionName"
private const val VERSIONING_PROP_VERSION_CODE = "versioning-plugin.versionCode"
private const val VERSIONING_PROP_COMMENT =
"""
This file was automatically generated by 'versioning-plugin'. DO NOT EDIT MANUALLY.
"""
/**
* A Gradle [Plugin] that takes a [Project] with the [AppPlugin] applied and dynamically sets the
* versionCode and versionName properties based on values read from a [VERSIONING_PROP_FILE] file in
* the [Project.getBuildDir] directory. It also adds Gradle tasks to bump the major, minor, and
* patch versions along with one to prepare the next snapshot.
*/
@Suppress("UnstableApiUsage", "NAME_SHADOWING")
class VersioningPlugin : Plugin<Project> {
/** Generate the Android 'versionCode' property */
private fun Semver.androidCode(): Int {
return major * 1_00_00 + minor * 1_00 + patch
}
/** Write an Android-specific variant of [this] to [stream] */
private fun Semver.writeForAndroid(stream: OutputStream) {
val newVersionCode = androidCode()
val props = Properties()
props.setProperty(VERSIONING_PROP_VERSION_CODE, "$newVersionCode")
props.setProperty(VERSIONING_PROP_VERSION_NAME, toString())
props.store(stream, VERSIONING_PROP_COMMENT)
}
override fun apply(project: Project) {
with(project) {
val appPlugin =
requireNotNull(plugins.findPlugin(AppPlugin::class.java)) {
"Plugin 'com.android.application' must be applied to use this plugin"
}
val propFile = layout.projectDirectory.file(VERSIONING_PROP_FILE)
require(propFile.asFile.exists()) {
"A 'version.properties' file must exist in the project subdirectory to use this plugin"
}
val contents = providers.fileContents(propFile).asText.forUseAtConfigurationTime()
val versionProps = Properties().also { it.load(contents.get().byteInputStream()) }
val versionName =
requireNotNull(versionProps.getProperty(VERSIONING_PROP_VERSION_NAME)) {
"version.properties must contain a '$VERSIONING_PROP_VERSION_NAME' property"
}
val versionCode =
requireNotNull(versionProps.getProperty(VERSIONING_PROP_VERSION_CODE).toInt()) {
"version.properties must contain a '$VERSIONING_PROP_VERSION_CODE' property"
}
appPlugin.extension.defaultConfig.versionName = versionName
appPlugin.extension.defaultConfig.versionCode = versionCode
afterEvaluate {
val version = Semver(versionName)
tasks.register("clearPreRelease") {
doLast { version.withClearedSuffix().writeForAndroid(propFile.asFile.outputStream()) }
}
tasks.register("bumpMajor") {
doLast {
version
.withIncMajor()
.withClearedSuffix()
.writeForAndroid(propFile.asFile.outputStream())
}
}
tasks.register("bumpMinor") {
doLast {
version
.withIncMinor()
.withClearedSuffix()
.writeForAndroid(propFile.asFile.outputStream())
}
}
tasks.register("bumpPatch") {
doLast {
version
.withIncPatch()
.withClearedSuffix()
.writeForAndroid(propFile.asFile.outputStream())
}
}
tasks.register("bumpSnapshot") {
doLast {
version
.withIncMinor()
.withSuffix("SNAPSHOT")
.writeForAndroid(propFile.asFile.outputStream())
}
}
}
}
}
}

View file

@ -11,6 +11,13 @@ pluginManagement {
mavenCentral()
gradlePluginPortal()
}
resolutionStrategy {
eachPlugin {
if (requested.id.id == "dagger.hilt.android.plugin") {
useModule("com.google.dagger:hilt-android-gradle-plugin:2.40.3")
}
}
}
plugins {
id("com.vanniktech.maven.publish") version "0.18.0" apply false
id("org.jetbrains.dokka") version "1.6.0" apply false