blob: 992d109462efb73e8c2b2b2ba901d66323984a08 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* 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 com.android.build.gradle.internal.dependency
import com.google.common.collect.ImmutableSet
import org.gradle.api.artifacts.ArtifactCollection
import org.gradle.api.artifacts.Dependency
import org.gradle.api.artifacts.ExternalDependency
import org.gradle.api.artifacts.FileCollectionDependency
import org.gradle.api.artifacts.ProjectDependency
import org.gradle.api.artifacts.ResolvableDependencies
import org.gradle.api.artifacts.component.ComponentIdentifier
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
import org.gradle.api.artifacts.component.ProjectComponentIdentifier
import org.gradle.api.artifacts.result.ResolvedArtifactResult
import org.gradle.api.artifacts.result.ResolvedDependencyResult
import org.gradle.api.file.FileCollection
import java.io.File
import java.util.HashSet
import java.util.function.Consumer
/**
* Implementation of a [ArtifactCollection] which filters out dependencies not explicitly requested.
*
* The main use case for this is building an ArtifactCollection that represents the runtime resource
* dependencies of a test app.
*
* Only resources from dependencies explicitly requested are packaged in
* the test APK.
*
* For example, as in the test AppWithAndroidTestDependencyOnLibTest, given
* the following.
*
* ### :app
* ```
* dependencies {
* implementation project('library2')
* implementation 'com.android.support.constraint:constraint-layout:1.0.2'
* androidTestImplementation project('library')
* }
* ```
* ### :library
* ```
* dependencies {
* implementation 'com.android.support.constraint:constraint-layout:1.0.2'
* }
* ```
* ```
* |-------------------|---------------------------------------------|
* | Resources from | Will be included in |
* |-------------------|---------------------------------------------|
* | :app main | Main (by definition) |
* | :app test | Test (by definition) |
* | :library main | Test (directly) |
* | :library2 main | Main (directly) (2) |
* | constraint layout | Main (directly) and test (via :library) (1) |
* |-----------------------------------------------------------------|
* ```
*
* 1. In 3.0.0-x before beta7, each androidTest configuration now extends
* the corresponding main configuration, and all the resource
* containing artifacts also present in the application were filtered
* out for tests. This was the right thing to do for classes, but not
* for resources, and lead to https://issuetracker.google.com/65175674.
*
* 2. The initial fix for the issue in 3.0.0-beta7,
* ag/I971a3894b3272a189fa0f54e27d6d3995db88378 was to include all
* resources in the test, which bloats the test APK significantly and
* broke the corner case of https://issuetracker.google.com/68275433
*
* @param dependenciesToFilter The ArtifactCollection of test resources.
* As androidTest extends main, this contains extra resources that do not need to be
* packaged in the separate test APK, which will be filtered by this artifact collection.
* @oaram dependencyRootsToKeep The unresolved dependencies that were explicitly part of the
* android test runtime classpath.
* @param dependencyGraph The dependency graph of the resources to filter configuration,
* when combined with the dependency roots to keep forms a whitelist of Component
* Identifiers, which is then used to
* Used to map the roots to keep to collections of
* component identifiers to keep.
*/
class AndroidTestResourceArtifactCollection(
private val dependenciesToFilter: ArtifactCollection,
private val dependencyRootsToKeep: Collection<Dependency>,
private val dependencyGraph: ResolvableDependencies) : ArtifactCollection {
/** Should be Set not MutableSet, but the iterator() override below requires it for some reason */
private val _artifacts: MutableSet<ResolvedArtifactResult> by lazy {
val requests = dependencyRootsToKeep.mapNotNull(this::toRequest).toSet()
// Traverse the dependency graph, flatten keeping the sub-graphs that
// were explicitly requested for the test side.
val keptComponents = HashSet<ComponentIdentifier>()
for (dependencyResult in dependencyGraph.resolutionResult.root.dependencies) {
dependencyResult as ResolvedDependencyResult
if (requests.contains(toRequest(dependencyResult.selected.id))) {
collect(keptComponents, dependencyResult)
}
}
val builder = ImmutableSet.builder<ResolvedArtifactResult>()
dependenciesToFilter.artifacts
.filter { keptComponents.contains(it.id.componentIdentifier) }
.forEach { builder.add(it) }
builder.build()
}
private val artifactFilesSet: Set<File> by lazy {
artifacts.map(ResolvedArtifactResult::getFile).toSet()
}
private val _artifactFiles: FileCollection by lazy {
dependenciesToFilter.artifactFiles.filter { artifactFilesSet.contains(it) }
}
override fun getArtifactFiles() = _artifactFiles
override fun getArtifacts() = _artifacts
override fun getFailures(): Collection<Throwable> = dependenciesToFilter.failures
override fun iterator() = _artifacts.iterator()
override fun forEach(action: Consumer<in ResolvedArtifactResult>) = _artifacts.forEach(action)
override fun spliterator() = _artifacts.spliterator()
private fun collect(
keptComponents: MutableSet<ComponentIdentifier>,
item: ResolvedDependencyResult) {
if (!keptComponents.add(item.selected.id)) {
// Avoid repeatedly traversing the same sub-graphs
// (Caused the scalability issue in https://issuetracker.google.com/124437190)
return
}
for (dependency in item.selected.dependencies) {
collect(keptComponents, dependency as ResolvedDependencyResult)
}
}
private fun toRequest(dependency: Dependency): Request? = when (dependency) {
is ProjectDependency -> Request.RequestProject(dependency.dependencyProject.path)
is ExternalDependency -> Request.RequestExternal(dependency.group, dependency.name)
// Local AARs are not supported, so local dependencies can't contain resources.
is FileCollectionDependency -> null
else -> throw IllegalArgumentException("unexpected dependency $dependency")
}
private fun toRequest(id: ComponentIdentifier): Request? = when (id) {
is ProjectComponentIdentifier -> Request.RequestProject(id.projectPath)
is ModuleComponentIdentifier -> Request.RequestExternal(id.group, id.module)
else -> null
}
sealed class Request {
data class RequestProject(val projectPath: String) : Request()
data class RequestExternal(val group: String?, val module: String) : Request()
}
}