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