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