1 /*
<lambda>null2 * Copyright 2020 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.build.checkapi
18
19 import androidx.build.AndroidXExtension
20 import androidx.build.Release
21 import androidx.build.RunApiTasks
22 import androidx.build.binarycompatibilityvalidator.BinaryCompatibilityValidation
23 import androidx.build.getSupportRootFolder
24 import androidx.build.isWriteVersionedApiFilesEnabled
25 import androidx.build.metalava.MetalavaTasks
26 import androidx.build.multiplatformExtension
27 import androidx.build.resources.ResourceTasks
28 import androidx.build.stableaidl.setupWithStableAidlPlugin
29 import androidx.build.version
30 import com.android.build.api.artifact.SingleArtifact
31 import com.android.build.api.attributes.BuildTypeAttr
32 import com.android.build.api.variant.LibraryVariant
33 import java.io.File
34 import org.gradle.api.GradleException
35 import org.gradle.api.Project
36 import org.gradle.api.artifacts.Configuration
37 import org.gradle.api.artifacts.type.ArtifactTypeDefinition
38 import org.gradle.api.attributes.Attribute
39 import org.gradle.api.attributes.Usage
40 import org.gradle.api.file.RegularFile
41 import org.gradle.api.plugins.JavaPluginExtension
42 import org.gradle.api.provider.Provider
43 import org.gradle.kotlin.dsl.getByType
44 import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
45
46 sealed class ApiTaskConfig
47
48 data class LibraryApiTaskConfig(val variant: LibraryVariant) : ApiTaskConfig()
49
50 object JavaApiTaskConfig : ApiTaskConfig()
51
52 object KmpApiTaskConfig : ApiTaskConfig()
53
54 object AndroidMultiplatformApiTaskConfig : ApiTaskConfig()
55
56 fun AndroidXExtension.shouldConfigureApiTasks(): Boolean {
57 if (!project.state.executed) {
58 throw GradleException(
59 "Project ${project.name} has not been evaluated. Extension" +
60 "properties may only be accessed after the project has been evaluated."
61 )
62 }
63
64 return when (type.checkApi) {
65 is RunApiTasks.No -> {
66 project.logger.info("Projects of type ${type.name} do not track API.")
67 false
68 }
69 is RunApiTasks.Yes -> {
70 (type.checkApi as RunApiTasks.Yes).reason?.let { reason ->
71 project.logger.info(
72 "Project ${project.name} has explicitly enabled API tasks " +
73 "with reason: $reason"
74 )
75 }
76 true
77 }
78 }
79 }
80
81 /**
82 * Returns whether the project should write versioned API files, e.g. `1.1.0-alpha01.txt`.
83 *
84 * <p>
85 * When set to `true`, the `updateApi` task will write the current API surface to both `current.txt`
86 * and `<version>.txt`. When set to `false`, only `current.txt` will be written. The default value
87 * is `true`.
88 */
shouldWriteVersionedApiFilenull89 internal fun Project.shouldWriteVersionedApiFile(): Boolean {
90 // Is versioned file writing disabled globally, ex. we're on a downstream branch?
91 if (!project.isWriteVersionedApiFilesEnabled()) {
92 return false
93 }
94
95 // Policy: Don't write versioned files for non-final API surfaces, ex. dev or alpha, or for
96 // versions that should only exist in dead-end release branches, ex. rc or stable.
97 if (
98 !project.version().isFinalApi() || project.version().isRC() || project.version().isStable()
99 ) {
100 return false
101 }
102
103 return true
104 }
105
Projectnull106 fun Project.configureProjectForApiTasks(config: ApiTaskConfig, extension: AndroidXExtension) {
107 // afterEvaluate required to read extension properties
108 afterEvaluate {
109 if (!extension.shouldConfigureApiTasks()) {
110 return@afterEvaluate
111 }
112
113 val builtApiLocation = project.getBuiltApiLocation()
114 val versionedApiLocation = project.getVersionedApiLocation()
115 val currentApiLocation = project.getCurrentApiLocation()
116 val outputApiLocations =
117 if (project.shouldWriteVersionedApiFile()) {
118 listOf(versionedApiLocation, currentApiLocation)
119 } else {
120 listOf(currentApiLocation)
121 }
122
123 val (compilationInputs, androidManifest) =
124 configureCompilationInputsAndManifest(config) ?: return@afterEvaluate
125 val baselinesApiLocation = ApiBaselinesLocation.fromApiLocation(currentApiLocation)
126 val generateApiDependencies = createReleaseApiConfiguration()
127
128 MetalavaTasks.setupProject(
129 project,
130 compilationInputs,
131 generateApiDependencies,
132 extension,
133 androidManifest,
134 baselinesApiLocation,
135 builtApiLocation,
136 outputApiLocations
137 )
138
139 project.setupWithStableAidlPlugin()
140
141 if (config is LibraryApiTaskConfig) {
142 ResourceTasks.setupProject(
143 project,
144 config.variant.artifacts.get(SingleArtifact.PUBLIC_ANDROID_RESOURCES_LIST),
145 builtApiLocation,
146 outputApiLocations
147 )
148 } else if (config is AndroidMultiplatformApiTaskConfig) {
149 // Android Multiplatform does not currently support resources, so we generate a blank
150 // "api" file to make sure the check task breaks if there were tracked resources before
151 ResourceTasks.setupProject(
152 project,
153 project.provider { BlankApiRegularFile(project) },
154 builtApiLocation,
155 outputApiLocations
156 )
157 }
158 multiplatformExtension?.let { multiplatformExtension ->
159 BinaryCompatibilityValidation(project, multiplatformExtension)
160 .setupBinaryCompatibilityValidatorTasks()
161 }
162 }
163 }
164
configureCompilationInputsAndManifestnull165 internal fun Project.configureCompilationInputsAndManifest(
166 config: ApiTaskConfig
167 ): Pair<CompilationInputs, Provider<RegularFile>?>? {
168 return when (config) {
169 is LibraryApiTaskConfig -> {
170 if (config.variant.name != Release.DEFAULT_PUBLISH_CONFIG) {
171 return null
172 }
173 CompilationInputs.fromLibraryVariant(config.variant, project) to
174 config.variant.artifacts.get(SingleArtifact.MERGED_MANIFEST)
175 }
176 is AndroidMultiplatformApiTaskConfig -> {
177 CompilationInputs.fromKmpAndroidTarget(project) to null
178 }
179 is KmpApiTaskConfig -> {
180 CompilationInputs.fromKmpJvmTarget(project) to null
181 }
182 is JavaApiTaskConfig -> {
183 val javaExtension = extensions.getByType<JavaPluginExtension>()
184 val mainSourceSet = javaExtension.sourceSets.getByName("main")
185 CompilationInputs.fromSourceSet(mainSourceSet, this) to null
186 }
187 }
188 }
189
createReleaseApiConfigurationnull190 internal fun Project.createReleaseApiConfiguration(): Configuration {
191 return configurations.findByName("ReleaseApiDependencies")
192 ?: configurations
193 .create("ReleaseApiDependencies") {
194 it.isCanBeConsumed = false
195 it.isTransitive = false
196 it.attributes.attribute(
197 BuildTypeAttr.ATTRIBUTE,
198 project.objects.named(BuildTypeAttr::class.java, "release")
199 )
200 it.attributes.attribute(
201 Usage.USAGE_ATTRIBUTE,
202 objects.named(Usage::class.java, Usage.JAVA_API)
203 )
204 it.attributes.attribute(
205 ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE,
206 ArtifactTypeDefinition.JAR_TYPE
207 )
208 // If this is a KMP project targeting android, make sure to select the android
209 // compilation and not a different jvm target compilation
210 multiplatformExtension?.let { extension ->
211 if (
212 extension.targets.any { it.platformType == KotlinPlatformType.androidJvm }
213 ) {
214 it.attributes.attribute(
215 Attribute.of(
216 "org.jetbrains.kotlin.platform.type",
217 KotlinPlatformType::class.java
218 ),
219 KotlinPlatformType.androidJvm
220 )
221 }
222 }
223 }
224 .apply { project.dependencies.add(name, project.project(path)) }
225 }
226
227 internal class BlankApiRegularFile(project: Project) : RegularFile {
228 val file = File(project.getSupportRootFolder(), "buildSrc/blank-res-api/public.txt")
229
getAsFilenull230 override fun getAsFile(): File = file
231 }
232