Skip to content

Commit

Permalink
Use runtime classpath at root to workaround Dagger/Hilt API vs Impl i…
Browse files Browse the repository at this point in the history
…ssue.

This CL adds a new Hilt option called 'enableClasspathAggregation' that will configure the compile classpath of the project (for app modules and tests) to use the runtime classpath. This means that transitive dependencies will be available during compilation which in turn will allow Dagger to traverse the classes along the dependency tree and will allow for Hilt's aggregating classes to be discovered.

The classpath configuration is done by resolving the runtime configuration with an artifact view and adding it to the 'CompileOnly' config. This solution is inefficient and will cause build performance impact, but it is a starting point that can be further optimized by using a smarter transform that can extract the necessary classes required by Dagger and Hilt.

Fixes: #1991
RELNOTES=Use runtime classpath at root to workaround Dagger/Hilt API vs Impl issue.
PiperOrigin-RevId: 350239023
  • Loading branch information
java-team-github-bot authored and Dagger Team committed Jan 6, 2021
1 parent 5959306 commit 239768b
Show file tree
Hide file tree
Showing 44 changed files with 1,399 additions and 296 deletions.
7 changes: 7 additions & 0 deletions java/dagger/hilt/android/plugin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ configurations {
dependencies {
implementation gradleApi()
compileOnly 'com.android.tools.build:gradle:4.2.0-beta01'
// TODO(user): Make compileOnly to avoid dep for non-Kotlin projects.
implementation 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.20'
implementation 'org.javassist:javassist:3.26.0-GA'
implementation 'org.ow2.asm:asm:9.0'
Expand All @@ -61,6 +62,12 @@ tasks.withType(PluginUnderTestMetadata.class).named("pluginUnderTestMetadata").c
it.pluginClasspath.from(configurations.additionalTestPlugin)
}

compileKotlin {
kotlinOptions {
jvmTarget = "1.8"
}
}

// Create sources Jar from main kotlin sources
tasks.register("sourcesJar", Jar).configure {
group = JavaBasePlugin.DOCUMENTATION_GROUP
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ package dagger.hilt.android.plugin
* Configuration options for the Hilt Gradle Plugin
*/
interface HiltExtension {

/**
* If set to `true`, Hilt will adjust the compile classpath such that it includes transitive
* dependencies, ignoring `api` or `implementation` boundaries during compilation. You should
* enable this option if your project has multiple level of transitive dependencies that contain
* injected classes or entry points.
*
* Enabling this option also requires android.lintOptions.checkReleaseBuilds to be set to 'false'
* if the Android Gradle Plugin version being used is less than 7.0.
*
* See https://github.com/google/dagger/issues/1991 for more context.
*/
var enableExperimentalClasspathAggregation: Boolean

/**
* If set to `true`, Hilt will register a transform task that will rewrite `@AndroidEntryPoint`
* annotated classes before the host-side JVM tests run. You should enable this option if you are
Expand All @@ -31,5 +45,6 @@ interface HiltExtension {
}

internal open class HiltExtensionImpl : HiltExtension {
override var enableExperimentalClasspathAggregation: Boolean = false
override var enableTransformForLocalTests: Boolean = false
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,21 @@ import com.android.build.api.component.Component
import com.android.build.api.extension.AndroidComponentsExtension
import com.android.build.api.instrumentation.FramesComputationMode
import com.android.build.api.instrumentation.InstrumentationScope
import com.android.build.gradle.AppExtension
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.LibraryExtension
import com.android.build.gradle.TestExtension
import com.android.build.gradle.TestedExtension
import com.android.build.gradle.api.AndroidBasePlugin
import com.android.build.gradle.api.BaseVariant
import com.android.build.gradle.api.TestVariant
import com.android.build.gradle.api.UnitTestVariant
import dagger.hilt.android.plugin.util.CopyTransform
import dagger.hilt.android.plugin.util.SimpleAGPVersion
import java.io.File
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.attributes.Attribute

/**
* A Gradle plugin that checks if the project is an Android project and if so, registers a
Expand Down Expand Up @@ -57,6 +65,7 @@ class HiltGradlePlugin : Plugin<Project> {
val hiltExtension = project.extensions.create(
HiltExtension::class.java, "hilt", HiltExtensionImpl::class.java
)
configureCompileClasspath(project, hiltExtension)
if (SimpleAGPVersion.ANDROID_GRADLE_PLUGIN_VERSION < SimpleAGPVersion(4, 2)) {
// Configures bytecode transform using older APIs pre AGP 4.2
configureTransform(project, hiltExtension)
Expand All @@ -67,6 +76,125 @@ class HiltGradlePlugin : Plugin<Project> {
configureProcessorFlags(project)
}

private fun configureCompileClasspath(project: Project, hiltExtension: HiltExtension) {
val androidExtension = project.extensions.findByType(BaseExtension::class.java)
?: throw error("Android BaseExtension not found.")
when (androidExtension) {
is AppExtension -> {
// For an app project we configure the app variant and both androidTest and test variants,
// Hilt components are generated in all of them.
androidExtension.applicationVariants.all {
configureVariantCompileClasspath(project, hiltExtension, androidExtension, it)
}
androidExtension.testVariants.all {
configureVariantCompileClasspath(project, hiltExtension, androidExtension, it)
}
androidExtension.unitTestVariants.all {
configureVariantCompileClasspath(project, hiltExtension, androidExtension, it)
}
}
is LibraryExtension -> {
// For a library project, only the androidTest and test variant are configured since
// Hilt components are not generated in a library.
androidExtension.testVariants.all {
configureVariantCompileClasspath(project, hiltExtension, androidExtension, it)
}
androidExtension.unitTestVariants.all {
configureVariantCompileClasspath(project, hiltExtension, androidExtension, it)
}
}
is TestExtension -> {
androidExtension.applicationVariants.all {
configureVariantCompileClasspath(project, hiltExtension, androidExtension, it)
}
}
else -> error(
"Hilt plugin is unable to configure the compile classpath for project with extension " +
"'$androidExtension'"
)
}

project.dependencies.apply {
registerTransform(CopyTransform::class.java) { spec ->
// Java/Kotlin library projects offer an artifact of type 'jar'.
spec.from.attribute(ARTIFACT_TYPE_ATTRIBUTE, "jar")
// Android library projects (with or without Kotlin) offer an artifact of type
// 'android-classes', which AGP can offer as a jar.
spec.from.attribute(ARTIFACT_TYPE_ATTRIBUTE, "android-classes-jar")
spec.to.attribute(ARTIFACT_TYPE_ATTRIBUTE, DAGGER_ARTIFACT_TYPE_VALUE)
}
}
}

private fun configureVariantCompileClasspath(
project: Project,
hiltExtension: HiltExtension,
androidExtension: BaseExtension,
variant: BaseVariant
) {
if (!hiltExtension.enableExperimentalClasspathAggregation) {
// Option is not enabled, don't configure compile classpath. Note that the option can't be
// checked earlier (before iterating over the variants) since it would have been too early for
// the value to be populated from the build file.
return
}

if (androidExtension.lintOptions.isCheckReleaseBuilds &&
SimpleAGPVersion.ANDROID_GRADLE_PLUGIN_VERSION < SimpleAGPVersion(7, 0)
) {
// Sadly we have to ask users to disable lint when enableExperimentalClasspathAggregation is
// set to true and they are not in AGP 7.0+ since Lint will cause issues during the
// configuration phase. See b/158753935 and b/160392650
error(
"Invalid Hilt plugin configuration: When 'enableExperimentalClasspathAggregation' is " +
"enabled 'android.lintOptions.checkReleaseBuilds' has to be set to false unless " +
"com.android.tools.build:gradle:7.0.0+ is used."
)
}

if (
listOf(
"android.injected.build.model.only", // Sent by AS 1.0 only
"android.injected.build.model.only.advanced", // Sent by AS 1.1+
"android.injected.build.model.only.versioned", // Sent by AS 2.4+
"android.injected.build.model.feature.full.dependencies", // Sent by AS 2.4+
"android.injected.build.model.v2", // Sent by AS 4.2+
).any { project.properties.containsKey(it) }
) {
// Do not configure compile classpath when AndroidStudio is building the model (syncing)
// otherwise it will cause a freeze.
return
}

val runtimeConfiguration = if (variant is TestVariant) {
// For Android test variants, the tested runtime classpath is used since the test app has
// tested dependencies removed.
variant.testedVariant.runtimeConfiguration
} else {
variant.runtimeConfiguration
}
val artifactView = runtimeConfiguration.incoming.artifactView { view ->
view.attributes.attribute(ARTIFACT_TYPE_ATTRIBUTE, DAGGER_ARTIFACT_TYPE_VALUE)
}

// CompileOnly config names don't follow the usual convention:
// <Variant Name> -> <Config Name>
// debug -> debugCompileOnly
// debugAndroidTest -> androidTestDebugCompileOnly
// debugUnitTest -> testDebugCompileOnly
// release -> releaseCompileOnly
// releaseUnitTest -> testReleaseCompileOnly
val compileOnlyConfigName = when (variant) {
is TestVariant ->
"androidTest${variant.name.substringBeforeLast("AndroidTest").capitalize()}CompileOnly"
is UnitTestVariant ->
"test${variant.name.substringBeforeLast("UnitTest").capitalize()}CompileOnly"
else ->
"${variant.name}CompileOnly"
}
project.dependencies.add(compileOnlyConfigName, artifactView.files)
}

@Suppress("UnstableApiUsage")
private fun configureTransformASM(project: Project, hiltExtension: HiltExtension) {
var warnAboutLocalTestsFlag = false
Expand Down Expand Up @@ -145,6 +273,9 @@ class HiltGradlePlugin : Plugin<Project> {
}

companion object {
val ARTIFACT_TYPE_ATTRIBUTE = Attribute.of("artifactType", String::class.java)
const val DAGGER_ARTIFACT_TYPE_VALUE = "jar-for-dagger"

const val LIBRARY_GROUP = "com.google.dagger"
val PROCESSOR_OPTIONS = listOf(
"dagger.fastInit" to "enabled",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package dagger.hilt.android.plugin.util

import org.gradle.api.artifacts.transform.CacheableTransform
import org.gradle.api.artifacts.transform.InputArtifact
import org.gradle.api.artifacts.transform.TransformAction
import org.gradle.api.artifacts.transform.TransformOutputs
import org.gradle.api.artifacts.transform.TransformParameters
import org.gradle.api.file.FileSystemLocation
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Classpath

// A transform that registers the input jar file as an output and thus changing from one artifact
// type to another.
// TODO: Improve to only copy classes that need to be visible by Hilt & Dagger.
@CacheableTransform
abstract class CopyTransform : TransformAction<TransformParameters.None> {
@get:Classpath
@get:InputArtifact
abstract val inputArtifactProvider: Provider<FileSystemLocation>

override fun transform(outputs: TransformOutputs) {
val input = inputArtifactProvider.get().asFile
when {
input.isDirectory -> outputs.dir(input)
input.isFile -> outputs.file(input)
else -> error("File/directory does not exist: ${input.absolutePath}")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
plugins {
id 'com.android.library'
id 'dagger.hilt.android.plugin'
}

android {
compileSdkVersion 30
buildToolsVersion "30.0.2"

defaultConfig {
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"
}
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
}

dependencies {
implementation 'com.google.dagger:hilt-android:LOCAL-SNAPSHOT'
annotationProcessor 'com.google.dagger:hilt-compiler:LOCAL-SNAPSHOT'

implementation project(':libraryB')
implementation project(':libraryC')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="liba">
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2020 The Dagger Authors.
*
* 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
*
* http://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.
*/

package liba;

import javax.inject.Inject;
import libb.LibraryB;
import libc.LibraryC;

/** Test LibA */
public class LibraryA {
@Inject
public LibraryA(LibraryB b, LibraryC c) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
plugins {
id 'com.android.library'
id 'dagger.hilt.android.plugin'
}

android {
compileSdkVersion 30
buildToolsVersion "30.0.2"

defaultConfig {
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"
}
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
}

dependencies {
implementation 'com.google.dagger:hilt-android:LOCAL-SNAPSHOT'
annotationProcessor 'com.google.dagger:hilt-compiler:LOCAL-SNAPSHOT'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="libc">
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (C) 2020 The Dagger Authors.
*
* 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
*
* http://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.
*/

package libc;

import javax.inject.Inject;

/** Test LibC */
public class LibraryC {
@Inject
public LibraryC() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
plugins {
id 'java-library'
}

java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

dependencies {
implementation 'com.google.dagger:hilt-core:LOCAL-SNAPSHOT'
annotationProcessor 'com.google.dagger:hilt-compiler:LOCAL-SNAPSHOT'

implementation project(':libraryB')
}
Loading

0 comments on commit 239768b

Please sign in to comment.