All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.avito.plugin.SignServicePlugin.kt Maven / Gradle / Ivy

Go to download

Collection of infrastructure libraries and gradle plugins of Avito Android project

There is a newer version: 2022.1
Show newest version
package com.avito.plugin

import com.android.build.api.artifact.ArtifactType
import com.android.build.api.variant.Variant
import com.android.build.gradle.api.ApplicationVariant
import com.avito.android.androidCommonExtension
import com.avito.android.bundleTaskProvider
import com.avito.android.withAndroidApp
import com.avito.kotlin.dsl.getBooleanProperty
import com.avito.kotlin.dsl.hasTasks
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.execution.TaskExecutionGraph
import org.gradle.api.tasks.TaskContainer
import org.gradle.kotlin.dsl.create
import org.gradle.kotlin.dslx.closureOf
import org.gradle.util.Path
import java.util.Objects.requireNonNull
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract

/**
 * Подписываем apk при помощи собственного сервиса.
 *
 * Пример использования:
 *
 * ```
 * применяем к модулю приложения (порядок не важен)
 * plugins {
 *   id("com.android.application")
 *   id("com.avito.android.signer")
 * }
 *
 * регистрируем какие buildVariant'ы мы хотим подписывать при помощи сервиса
 * неуказанные варианты будут использовать стандартный механизм подписи android gradle plugin
 * https://developer.android.com/studio/publish/app-signing
 *
 * signService {
 *
 *          buildType                     String токен сервиса       sha1 от подписи для проверки перед отправкой
 *
 *   apk(android.buildTypes.release, project.properties.get("avitoSignToken"), "")
 *   bundle(android.buildTypes.release, project.properties.get("avitoSignBundleToken"), "")
 * }
 * ```
 * [SignExtension]
 *
 * Плагин генерирует каждому варианту следущие таски:
 * - signApkViaService
 * - signBundleViaService
 *
 * Если какой-то таске требуется получить подписанную apk/bundle, следует указать dependsOn(signXXX)

 * Локально таски молча скипнутся если не предоставить нужный для варианта token,
 * на CI есть механизм защиты от этого, [failOnMissingToken]
 *
 * Плагин соблюдает неявный контракт: apk и aab файлы заменяются после подписи по тому же пути где лежали неподписанными
 *
 * Есть возможность отключить плагин флагом билда: `disableSignService` = true
 */
@Suppress("UnstableApiUsage")
class SignServicePlugin : Plugin {

    private val taskGroup = "ci"

    override fun apply(target: Project) {
        val signExtension = target.extensions.create("signService")

        // todo rename to `avito.signer.disable`
        if (target.getBooleanProperty("disableSignService")) {
            return
        }

        // todo explain why do we have multiple options to skip signing
        //  disableSignService (avito.signer.disable) + avito.signer.allowSkip
        //  Is it feasible to have only one?
        val skipSigning: Boolean = target.getBooleanProperty("avito.signer.allowSkip")

        target.afterEvaluate {
            if (!skipSigning) {
                require(!signExtension.host.isNullOrBlank()) { "signService.host must be set" }
            }
        }

        val registeredBuildTypes = mutableMapOf()

        target.withAndroidApp { appExtension ->

            target.androidCommonExtension.onVariants {
                val variant = this

                registerTask(
                    tasks = target.tasks,
                    type = SignApkTask::class.java,
                    variant = this,
                    taskName = signApkTaskName(variant),
                    serviceUrl = signExtension.host.orEmpty(),
                    signTokensMap = signExtension.apkSignTokens
                )

                registerTask(
                    tasks = target.tasks,
                    type = SignBundleTask::class.java,
                    variant = this,
                    taskName = signBundleTaskName(variant),
                    serviceUrl = signExtension.host.orEmpty(),
                    signTokensMap = signExtension.bundleSignTokens
                )

                registeredBuildTypes[variant.name] = requireNotNull(variant.buildType)
            }

            target.androidCommonExtension.onVariantProperties {

                artifacts.use(target.tasks.signedApkTaskProvider(this))
                    .wiredWithDirectories(
                        taskInput = SignApkTask::unsignedDirProperty,
                        taskOutput = SignApkTask::signedDirProperty
                    )
                    .toTransform(ArtifactType.APK)

                artifacts.use(target.tasks.signedBundleTaskProvider(this))
                    .wiredWithFiles(
                        taskInput = SignBundleTask::unsignedFileProperty,
                        taskOutput = SignBundleTask::signedFileProperty
                    )
                    .toTransform(ArtifactType.BUNDLE)
            }

            appExtension.applicationVariants.all { variant: ApplicationVariant ->

                val buildTypeName = variant.buildType.name
                val apkToken: String? = signExtension.apkSignTokens[buildTypeName]
                val bundleToken: String? = signExtension.bundleSignTokens[buildTypeName]

                variant.outputsAreSigned = apkToken.hasContent() || bundleToken.hasContent()

                target.tasks.signedApkTaskProvider(variant.name).configure {
                    it.dependsOn(variant.packageApplicationProvider)
                }

                target.tasks.signedBundleTaskProvider(variant.name).configure {
                    it.dependsOn(target.tasks.bundleTaskProvider(variant))
                }
            }
        }

        if (!skipSigning) {
            target.gradle.taskGraph.whenReady(
                closureOf {
                    failOnMissingToken(
                        projectPath = target.path,
                        variantToBuildType = registeredBuildTypes,
                        taskExecutionGraph = this,
                        apkSignTokens = signExtension.apkSignTokens,
                        bundleSignTokens = signExtension.bundleSignTokens
                    )
                }
            )
        }
    }

    // TODO: extract to factory
    private fun registerTask(
        tasks: TaskContainer,
        type: Class,
        variant: Variant<*>,
        taskName: String,
        serviceUrl: String,
        signTokensMap: Map
    ) {
        val buildTypeName = requireNonNull(variant.buildType)
        val token: String? = signTokensMap[buildTypeName]

        val isSignNeeded: Boolean = token.hasContent()

        tasks.register(taskName, type) {
            it.group = taskGroup
            it.description = "Sign ${variant.name} with in-house service"

            it.serviceUrl.set(serviceUrl)
            it.tokenProperty.set(token)

            it.onlyIf { isSignNeeded }
        }
    }

    private fun failOnMissingToken(
        projectPath: String,
        variantToBuildType: Map,
        taskExecutionGraph: TaskExecutionGraph,
        apkSignTokens: Map,
        bundleSignTokens: Map
    ) {
        variantToBuildType.forEach { (variantName, buildTypeName) ->
            checkToken(
                buildTypeName = buildTypeName,
                signTokens = apkSignTokens,
                signTaskPath = Path.path("$projectPath:${signApkTaskName(variantName)}"),
                taskExecutionGraph = taskExecutionGraph
            )
            checkToken(
                buildTypeName = buildTypeName,
                signTokens = bundleSignTokens,
                signTaskPath = Path.path("$projectPath:${signBundleTaskName(variantName)}"),
                taskExecutionGraph = taskExecutionGraph
            )
        }
    }

    private fun checkToken(
        buildTypeName: String,
        signTokens: Map,
        signTaskPath: Path,
        taskExecutionGraph: TaskExecutionGraph
    ) {
        val isSignIntended = signTokens.containsKey(buildTypeName)

        if (isSignIntended && taskExecutionGraph.hasTasks(setOf(signTaskPath))) {
            requireNotNull(signTokens[buildTypeName]) {
                "[SignServicePlugin] can't sign $buildTypeName, token is not set"
            }
        }
    }
}

@OptIn(ExperimentalContracts::class)
private fun String?.hasContent(): Boolean {
    contract {
        returns(true) implies (this@hasContent != null)
    }

    if (isNullOrBlank()) return false
    if (this == "null") return false
    return true
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy