1 /*
2  * Copyright 2018 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.metalava
18 
19 import androidx.build.Version
20 import androidx.build.checkapi.ApiBaselinesLocation
21 import androidx.build.checkapi.ApiLocation
22 import androidx.build.logging.TERMINAL_RED
23 import androidx.build.logging.TERMINAL_RESET
24 import java.io.File
25 import javax.inject.Inject
26 import org.gradle.api.provider.Property
27 import org.gradle.api.tasks.CacheableTask
28 import org.gradle.api.tasks.Input
29 import org.gradle.api.tasks.InputFiles
30 import org.gradle.api.tasks.Internal
31 import org.gradle.api.tasks.PathSensitive
32 import org.gradle.api.tasks.PathSensitivity
33 import org.gradle.api.tasks.TaskAction
34 import org.gradle.workers.WorkerExecutor
35 
36 /**
37  * This task validates that the API described in one signature txt file is compatible with the API
38  * in another.
39  */
40 @CacheableTask
41 abstract class CheckApiCompatibilityTask @Inject constructor(workerExecutor: WorkerExecutor) :
42     MetalavaTask(workerExecutor) {
43     // Text file from which the API signatures will be obtained.
44     @get:Internal // already expressed by getTaskInputs()
45     abstract val referenceApi: Property<ApiLocation>
46 
47     // Text file representing the current API surface to check.
48     @get:Internal // already expressed by getTaskInputs()
49     abstract val api: Property<ApiLocation>
50 
51     // Text file listing violations that should be ignored.
52     @get:Internal // already expressed by getTaskInputs()
53     abstract val baselines: Property<ApiBaselinesLocation>
54 
55     // Version for the current API surface.
56     @get:Input abstract val version: Property<Version>
57 
58     @PathSensitive(PathSensitivity.RELATIVE)
59     @InputFiles
getTaskInputsnull60     fun getTaskInputs(): List<File> {
61         val apiLocation = api.get()
62         val referenceApiLocation = referenceApi.get()
63         val baselineApiLocation = baselines.get()
64         return listOf(
65             apiLocation.publicApiFile,
66             apiLocation.restrictedApiFile,
67             referenceApiLocation.publicApiFile,
68             referenceApiLocation.restrictedApiFile,
69             baselineApiLocation.publicApiFile,
70             baselineApiLocation.restrictedApiFile
71         )
72     }
73 
74     @TaskAction
execnull75     fun exec() {
76         check(bootClasspath.files.isNotEmpty()) { "Android boot classpath not set." }
77 
78         val apiLocation = api.get()
79         val referenceApiLocation = referenceApi.get()
80         val baselineApiLocation = baselines.get()
81 
82         // Don't allow *any* API changes if we're comparing against a finalized API surface within
83         // the same major and minor version, e.g. between 1.1.0-beta01 and 1.1.0-beta02 or 1.1.0 and
84         // 1.1.1. We'll still allow changes between 1.1.0-alpha05 and 1.1.0-beta01.
85         val currentVersion = version.get()
86         val referenceVersion = referenceApiLocation.version()
87         val freezeApis = shouldFreezeApis(referenceVersion, currentVersion)
88 
89         checkApiFile(
90             apiLocation.publicApiFile,
91             referenceApiLocation.publicApiFile,
92             baselineApiLocation.publicApiFile,
93             referenceVersion,
94             freezeApis,
95         )
96 
97         if (referenceApiLocation.restrictedApiFile.exists()) {
98             checkApiFile(
99                 apiLocation.restrictedApiFile,
100                 referenceApiLocation.restrictedApiFile,
101                 baselineApiLocation.restrictedApiFile,
102                 referenceVersion,
103                 freezeApis,
104             )
105         }
106     }
107 
108     // Confirms that <api> <oldApi> except for any baselines listed in <baselineFile>
checkApiFilenull109     private fun checkApiFile(
110         api: File,
111         oldApi: File,
112         baselineFile: File,
113         referenceVersion: Version?,
114         freezeApis: Boolean,
115     ) {
116         var args =
117             listOf(
118                 "--classpath",
119                 (bootClasspath + dependencyClasspath.files).joinToString(File.pathSeparator),
120                 "--source-files",
121                 api.toString(),
122                 "--check-compatibility:api:released",
123                 oldApi.toString(),
124                 "--error-message:compatibility:released",
125                 if (freezeApis && referenceVersion != null) {
126                     createFrozenCompatibilityCheckError(referenceVersion.toString())
127                 } else {
128                     CompatibilityCheckError
129                 },
130                 "--warnings-as-errors",
131             )
132         if (baselineFile.exists()) {
133             args = args + listOf("--baseline", baselineFile.toString())
134         }
135         if (freezeApis) {
136             args = args + listOf("--error-category", "Compatibility")
137         }
138         runWithArgs(args)
139     }
140 }
141 
shouldFreezeApisnull142 fun shouldFreezeApis(referenceVersion: Version?, currentVersion: Version) =
143     referenceVersion != null &&
144         currentVersion.major == referenceVersion.major &&
145         currentVersion.minor == referenceVersion.minor &&
146         referenceVersion.isFinalApi()
147 
148 private const val CompatibilityCheckError =
149     """
150     ${TERMINAL_RED}Your change has API compatibility issues. Fix the code according to the messages above.$TERMINAL_RESET
151 
152     If you *intentionally* want to break compatibility, you can suppress it with
153     ./gradlew ignoreApiChanges && ./gradlew updateApi
154 """
155 
156 private fun createFrozenCompatibilityCheckError(referenceVersion: String) =
157     """
158     ${TERMINAL_RED}The API surface was finalized in $referenceVersion. Revert the changes noted in the errors above.$TERMINAL_RESET
159 
160     If you have obtained permission from Android API Council or Jetpack Working Group to bypass this policy, you can suppress this check with:
161     ./gradlew ignoreApiChanges && ./gradlew updateApi
162 """
163