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

com.squareup.hephaestus.plugin.HephaestusPlugin.kt Maven / Gradle / Ivy

package com.squareup.hephaestus.plugin

import com.android.build.gradle.AppExtension
import com.android.build.gradle.AppPlugin
import com.android.build.gradle.LibraryExtension
import com.android.build.gradle.LibraryPlugin
import com.android.build.gradle.TestExtension
import com.android.build.gradle.TestedExtension
import com.android.build.gradle.api.BaseVariant
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.AppliedPlugin
import org.gradle.api.plugins.PluginManager
import org.jetbrains.kotlin.gradle.internal.KaptGenerateStubsTask
import org.jetbrains.kotlin.gradle.plugin.KaptExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapper
import org.jetbrains.kotlin.gradle.plugin.PLUGIN_CLASSPATH_CONFIGURATION_NAME
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.util.Locale.US
import java.util.concurrent.atomic.AtomicBoolean

open class HephaestusPlugin : Plugin {
  override fun apply(project: Project) {
    val once = AtomicBoolean()

    fun PluginManager.withPluginOnce(
      id: String,
      action: (AppliedPlugin) -> Unit
    ) {
      withPlugin(id) {
        if (once.compareAndSet(false, true)) {
          action(it)
        }
      }
    }

    // Apply the Hephaestus plugin after the Kotlin plugin was applied. There could be a timing
    // issue. Also make sure to apply it only once. A module could accidentally apply the JVM and
    // Android Kotlin plugin.
    project.pluginManager.withPluginOnce("org.jetbrains.kotlin.android") {
      realApply(project, true)
    }
    project.pluginManager.withPluginOnce("org.jetbrains.kotlin.jvm") {
      realApply(project, false)
    }

    project.afterEvaluate {
      if (!once.get()) {
        throw GradleException(
            "No supported plugins for Hephaestus found on project " +
                "'${project.path}'. Only Android and Java modules are supported for now."
        )
      }
    }
  }

  private fun realApply(
    project: Project,
    isAndroidProject: Boolean
  ) {
    disableIncrementalKotlinCompilation(project, isAndroidProject)
    disablePreciseJavaTracking(project)

    project.pluginManager.withPlugin("org.jetbrains.kotlin.kapt") {
      // This needs to be disabled, otherwise compiler plugins fail in weird ways when generating stubs.
      project.extensions.findByType(KaptExtension::class.java)?.correctErrorTypes = false
    }

    project.dependencies.add("api", "$GROUP:annotations:$VERSION")
  }

  private fun disablePreciseJavaTracking(
    project: Project
  ) {
    project.tasks
        .withType(KotlinCompile::class.java)
        .configureEach { compileTask ->
          compileTask.doFirst {
            // Disable precise java tracking if needed. Note that the doFirst() action only runs
            // if the task is not up to date. That's ideal, because if nothing needs to be
            // compiled, then we don't need to disable the flag.
            CheckMixedSourceSet(project, compileTask).disablePreciseJavaTrackingIfNeeded()

            compileTask.logger.info(
                "Hephaestus: Use precise java tracking: ${compileTask.usePreciseJavaTracking}"
            )
          }
        }
  }

  @OptIn(ExperimentalStdlibApi::class)
  private fun disableIncrementalKotlinCompilation(
    project: Project,
    isAndroidProject: Boolean
  ) {
    project.tasks
        .withType(KaptGenerateStubsTask::class.java)
        .configureEach { stubsTask ->
          // Disable incremental compilation for the stub generating task. Trigger the compiler
          // plugin if any dependencies in the compile classpath have changed. This will make sure
          // that we pick up any change from a dependency when merging all the classes. Without
          // this workaround we could make changes in any library, but these changes wouldn't be
          // contributed to the Dagger graph, because incremental compilation tricked us.
          stubsTask.doFirst {
            stubsTask.incremental = false
            stubsTask.logger.info(
                "Hephaestus: Incremental compilation enabled: ${stubsTask.incremental} (stub)"
            )
          }
        }

    // Use this signal to share state between DisableIncrementalCompilationTask and the Kotlin
    // compile task. If the plugin classpath changed, then DisableIncrementalCompilationTask sets
    // the signal to false.
    @Suppress("UnstableApiUsage")
    val incrementalSignal = project.gradle.sharedServices
        .registerIfAbsent("incrementalSignal", IncrementalSignal::class.java) { }

    val disableIncrementalCompilationAction: (String) -> Unit = { compileTaskName ->
      // Disable incremental compilation, if the compiler plugin dependency isn't up-to-date.
      // This will trigger a full compilation of a module using Hephaestus even though its
      // source files might not have changed. This workaround is necessary, otherwise
      // incremental builds are broken. See https://youtrack.jetbrains.com/issue/KT-38570
      val disableIncrementalCompilationTaskProvider = project.tasks.register(
          compileTaskName + "CheckIncrementalCompilationHephaestus",
          DisableIncrementalCompilationTask::class.java
      ) { task ->
        task.pluginClasspath.from(
            project.configurations.getByName(PLUGIN_CLASSPATH_CONFIGURATION_NAME)
        )
        task.incrementalSignal.set(incrementalSignal)
      }

      project.tasks.named(compileTaskName, KotlinCompile::class.java) { compileTask ->
        compileTask.dependsOn(disableIncrementalCompilationTaskProvider)

        compileTask.doFirst {
          // If the signal is set, then the plugin classpath changed. Apply the setting that
          // DisableIncrementalCompilationTask requested.
          val incremental = incrementalSignal.get().incremental[project.path]
          if (incremental != null) {
            compileTask.incremental = incremental
          }

          compileTask.logger.info(
              "Hephaestus: Incremental compilation enabled: ${compileTask.incremental} (compile)"
          )
        }
      }
    }

    if (isAndroidProject) {
      project.androidVariantsConfigure { variant ->
        val compileTaskName = "compile${variant.name.capitalize(US)}Kotlin"
        disableIncrementalCompilationAction(compileTaskName)
      }
    } else {
      // The Java plugin has two Kotlin tasks we care about: compileKotlin and compileTestKotlin.
      disableIncrementalCompilationAction("compileKotlin")
      disableIncrementalCompilationAction("compileTestKotlin")
    }
  }
}

/**
 * Returns all variants including the androidTest and unit test variants.
 */
fun Project.androidVariants(): Set {
  return when (val androidExtension = project.extensions.findByName("android")) {
    is AppExtension -> androidExtension.applicationVariants + androidExtension.testVariants +
        androidExtension.unitTestVariants
    is LibraryExtension -> androidExtension.libraryVariants + androidExtension.testVariants +
        androidExtension.unitTestVariants
    else -> throw GradleException("Unknown Android module type for project ${project.path}")
  }
}

/**
 * Runs the given [action] for each Android variant including androidTest and unit test variants.
 */
fun Project.androidVariantsConfigure(action: (BaseVariant) -> Unit) {
  val androidExtension = project.extensions.findByName("android")

  if (androidExtension is AppExtension) {
    androidExtension.applicationVariants.configureEach(action)
  }
  if (androidExtension is LibraryExtension) {
    androidExtension.libraryVariants.configureEach(action)
  }
  if (androidExtension is TestExtension) {
    androidExtension.applicationVariants.configureEach(action)
  }
  if (androidExtension is TestedExtension) {
    androidExtension.unitTestVariants.configureEach(action)
    androidExtension.testVariants.configureEach(action)
  }
}

@OptIn(ExperimentalStdlibApi::class)
fun Collection.findVariantForCompileTask(
  compileTask: KotlinCompile
): BaseVariant = this
    .filter { variant ->
      compileTask.name.contains(variant.name.capitalize(US))
    }
    .maxBy {
      // The filter above still returns multiple variants, e.g. for the
      // "compileDebugUnitTestKotlin" task it returns the variants "debug" and "debugUnitTest".
      // In this case prefer the variant with the longest matching name, because that's the more
      // explicit variant that we want.
      it.name.length
    }!!

val Project.isKotlinJvmProject: Boolean
  get() = plugins.hasPlugin(KotlinPluginWrapper::class.java)

val Project.isAndroidProject: Boolean
  get() = AGP_ON_CLASSPATH &&
      (plugins.hasPlugin(AppPlugin::class.java) || plugins.hasPlugin(LibraryPlugin::class.java))

@Suppress("SENSELESS_COMPARISON")
private val AGP_ON_CLASSPATH = try {
  Class.forName("com.android.build.gradle.AppPlugin") != null
} catch (t: Throwable) {
  false
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy