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

foundry.gradle.ApkVersioningPlugin.kt Maven / Gradle / Ivy

/*
 * Copyright (C) 2022 Slack Technologies, LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
@file:Suppress("UnstableApiUsage")

package foundry.gradle

import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.gradle.AppPlugin
import foundry.gradle.properties.localGradleProperty
import foundry.gradle.properties.setDisallowChanges
import java.util.Properties
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction

/**
 * Uses AGP 4.0's new APIs for manipulating variant properties in a more cache-friendly way.
 *
 * Normally we should use cacheable tasks and reuse their values, but manifest manipulation appears
 * to break their task dependencies in a way we don't understand. Since our computations are cheap,
 * we just compute them on the fly for now.
 *
 * Based on examples here: https://github.com/android/gradle-recipes
 */
@Suppress("unused")
internal class ApkVersioningPlugin : Plugin {

  @Suppress("LongMethod")
  override fun apply(project: Project) {
    project.plugins.withType(AppPlugin::class.java) {
      val versionMajor = project.localGradleProperty("versionMajor")
      val versionMinor = project.localGradleProperty("versionMinor")
      val versionPatch = project.localGradleProperty("versionPatch")

      val foundryProperties = FoundryProperties.getOrCreate(project)

      val debugVersionNameProvider =
        versionNameProvider(
          versionMajor,
          versionMinor,
          versionPatch,
          user = project.provider { foundryProperties.debugUserString },
        )

      val defaultVersionNameProvider =
        versionNameProvider(
          versionMajor,
          versionMinor,
          versionPatch,
          user =
            project.ciBuildNumber
              .map { "" }
              // Only provider the user if this is _not_ running on CI. Composition of properties
              // is still a little weird in Gradle.
              .orElse(project.providers.environmentVariable("USER")),
        )

      val debugVersionCodeProvider: Provider =
        project.provider { foundryProperties.debugVersionCode }

      val ciVersionFileProvider =
        project.rootProject.layout.projectDirectory.file("ci/release.version")
      val ciContent = project.providers.fileContents(ciVersionFileProvider)
      val defaultVersionCodeProvider: Provider =
        project.ciBuildNumber
          .flatMap { ciContent.asText.map { it.toInt() } }
          .orElse(debugVersionCodeProvider)

      configureVariants(
        project,
        debugVersionNameProvider,
        debugVersionCodeProvider,
        defaultVersionNameProvider,
        defaultVersionCodeProvider,
      )

      // Register a version properties task. This is run on ci via android_preflight.sh
      val shortGitShaProvider = project.gitShaProvider()
      val longGitShaProvider = project.fullGitShaProvider()
      project.tasks.register("generateVersionProperties") {
        outputFile.setDisallowChanges(
          project.layout.buildDirectory.file("intermediates/versioning/version.properties")
        )
        versionCode.setDisallowChanges(defaultVersionCodeProvider)
        versionName.setDisallowChanges(defaultVersionNameProvider)
        shortGitSha.setDisallowChanges(shortGitShaProvider)
        longGitSha.setDisallowChanges(longGitShaProvider)
        this.versionMajor.setDisallowChanges(versionMajor)
        this.versionMinor.setDisallowChanges(versionMinor)
      }
    }
  }

  private fun configureVariants(
    project: Project,
    debugVersionNameProvider: Provider,
    debugVersionCodeProvider: Provider,
    defaultVersionNameProvider: Provider,
    defaultVersionCodeProvider: Provider,
  ) {
    // Register version calculations for variants
    project.configure {
      onVariants { variant ->
        val flavorName = variant.flavorName

        val versionCodeProvider =
          if (variant.buildType == "debug") {
            debugVersionCodeProvider
          } else {
            defaultVersionCodeProvider
          }
        val versionNameProvider =
          if (variant.buildType == "debug") {
            debugVersionNameProvider
          } else {
            defaultVersionNameProvider
          }
        val mappedVersionNameProvider =
          if (flavorName != "external") {
            // Replacement for versionNameSuffix, which this overrides
            versionNameProvider.map { "$it-$flavorName" }
          } else {
            versionNameProvider
          }

        // Have to iterate outputs because of APK splits.
        variant.outputs.forEach { variantOutput ->
          variantOutput.versionName.setDisallowChanges(mappedVersionNameProvider)
          variantOutput.versionCode.setDisallowChanges(versionCodeProvider)
        }
      }
    }
  }

  private fun versionNameProvider(
    versionMajor: Provider,
    versionMinor: Provider,
    versionPatch: Provider,
    user: Provider,
  ): Provider {
    return versionMajor
      .zip(versionMinor) { major, minor -> "$major.$minor" }
      .zip(versionPatch) { prev, patch -> "$prev.$patch" }
      .zip(user) { prev, possibleUser ->
        val addOn =
          possibleUser.takeIf { it.isNotEmpty() }?.let { presentUser -> "-$presentUser" } ?: ""
        "$prev$addOn"
      }
  }
}

/**
 * This generates a `version.properties` file that CI uses (see
 * `ci/master/master.multibranch.jenkinsfile`).
 */
@CacheableTask
internal abstract class VersionPropertiesTask : DefaultTask() {

  @get:Input abstract val versionMajor: Property

  @get:Input abstract val versionMinor: Property

  @get:Input abstract val shortGitSha: Property

  @get:Input abstract val longGitSha: Property

  @get:Input abstract val versionName: Property

  @get:Input abstract val versionCode: Property

  @get:OutputFile abstract val outputFile: RegularFileProperty

  @TaskAction
  fun generate() {
    val releaseVersion = "${versionMajor.get()}.${versionMinor.get()}"
    val resolvedVersionCode = versionCode.get()
    val resolvedVersionName = versionName.get()
    val versionProps =
      Properties().apply {
        setProperty("tag", "$resolvedVersionName-$resolvedVersionCode")
        setProperty("git_short", shortGitSha.get())
        setProperty("version_code", "" + resolvedVersionCode)
        setProperty("full_git_sha", longGitSha.get())
        setProperty("release_version", releaseVersion)
      }

    outputFile.asFile.get().writer().use {
      versionProps.store(it, "Auto-generated by build. Do not check in")
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy