1 /*
<lambda>null2 * Copyright 2017 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.doclava.ChecksConfig
20 import org.gradle.api.DefaultTask
21 import org.gradle.api.GradleException
22 import org.gradle.api.tasks.Input
23 import org.gradle.api.tasks.InputFile
24 import org.gradle.api.tasks.InputFiles
25 import org.gradle.api.tasks.Optional
26 import org.gradle.api.tasks.OutputFile
27 import org.gradle.api.tasks.TaskAction
28 import java.io.ByteArrayInputStream
29 import java.io.ByteArrayOutputStream
30 import java.io.File
31 import java.security.MessageDigest
32
33 /** Character that resets console output color. */
34 private const val ANSI_RESET = "\u001B[0m"
35
36 /** Character that sets console output color to red. */
37 private const val ANSI_RED = "\u001B[31m"
38
39 /** Character that sets console output color to yellow. */
40 private const val ANSI_YELLOW = "\u001B[33m"
41
42 private val ERROR_REGEX = Regex("^(.+):(.+): (\\w+) (\\d+): (.+)$")
43
44 private fun ByteArray.encodeHex() = fold(StringBuilder(), { builder, byte ->
45 val hexString = Integer.toHexString(byte.toInt() and 0xFF)
46 if (hexString.length < 2) {
47 builder.append("0")
48 }
49 builder.append(hexString)
50 }).toString()
51
getShortHashnull52 private fun getShortHash(src: String): String {
53 val str = MessageDigest.getInstance("SHA-1")
54 .digest(src.toByteArray()).encodeHex()
55 val len = str.length
56 return str.substring(len - 7, len)
57 }
58
59 /**
60 * Task used to verify changes between two API files.
61 * <p>
62 * This task may be configured to ignore, warn, or fail with a message for a specific set of
63 * Doclava-defined error codes. See {@link com.google.doclava.Errors} for a complete list of
64 * supported error codes.
65 * <p>
66 * Specific failures may be ignored by specifying a list of SHAs in {@link #whitelistErrors}. Each
67 * SHA is unique to a specific API change and is logged to the error output on failure.
68 */
69 open class CheckApiTask : DefaultTask() {
70
71 /** API file that represents the existing API surface. */
72 @Optional
73 @InputFile
74 var oldApiFile: File? = null
75
76 /** API file that represents the existing API surface's removals. */
77 @Optional
78 @InputFile
79 var oldRemovedApiFile: File? = null
80
81 /** API file that represents the candidate API surface. */
82 @InputFile
83 lateinit var newApiFile: File
84
85 /** API file that represents the candidate API surface's removals. */
86 @Optional
87 @InputFile
88 var newRemovedApiFile: File? = null
89
90 /** Optional file containing a newline-delimited list of error SHAs to ignore. */
91 var whitelistErrorsFile: File? = null
92
93 @Optional
94 @InputFile
getWhiteListErrorsFileInputnull95 fun getWhiteListErrorsFileInput(): File? {
96 // Gradle requires non-null InputFiles to exist -- even with Optional -- so work around that
97 // by returning null for this field if the file doesn't exist.
98 if (whitelistErrorsFile?.exists() == true) {
99 return whitelistErrorsFile
100 }
101 return null
102 }
103
104 /**
105 * Optional set of error SHAs to ignore.
106 * <p>
107 * Each error SHA is unique to a specific API change.
108 */
109 @Optional
110 @Input
111 var whitelistErrors = emptySet<String>()
112
113 var detectedWhitelistErrors = mutableSetOf<String>()
114
115 @InputFiles
116 var doclavaClasspath: Collection<File> = emptyList()
117
118 // A dummy output file meant only to tag when this check was last ran.
119 // Without any outputs, Gradle will run this task every time.
120 @Optional
121 private var mOutputFile: File? = null
122
123 @OutputFile
getOutputFilenull124 fun getOutputFile(): File {
125 return if (mOutputFile != null) {
126 mOutputFile!!
127 } else {
128 File(project.buildDir, "checkApi/$name-completed")
129 }
130 }
131
132 @Optional
setOutputFilenull133 fun setOutputFile(outputFile: File) {
134 mOutputFile = outputFile
135 }
136
137 @Input
138 lateinit var checksConfig: ChecksConfig
139
140 init {
141 group = "Verification"
142 description = "Invoke Doclava\'s ApiCheck tool to make sure current.txt is up to date."
143 }
144
collectAndVerifyInputsnull145 private fun collectAndVerifyInputs(): Set<File> {
146 if (oldRemovedApiFile != null && newRemovedApiFile != null) {
147 return setOf(oldApiFile!!, newApiFile, oldRemovedApiFile!!, newRemovedApiFile!!)
148 } else {
149 return setOf(oldApiFile!!, newApiFile)
150 }
151 }
152
153 @TaskAction
execnull154 fun exec() {
155 if (oldApiFile == null) {
156 // Nothing to do.
157 return
158 }
159
160 val apiFiles = collectAndVerifyInputs()
161
162 val errStream = ByteArrayOutputStream()
163
164 // If either of those gets tweaked, then this should be refactored to extend JavaExec.
165 project.javaexec { spec ->
166 spec.apply {
167 // Put Doclava on the classpath so we can get the ApiCheck class.
168 classpath(doclavaClasspath)
169 main = "com.google.doclava.apicheck.ApiCheck"
170
171 minHeapSize = "128m"
172 maxHeapSize = "1024m"
173
174 // add -error LEVEL for every error level we want to fail the build on.
175 checksConfig.errors.forEach { args("-error", it) }
176 checksConfig.warnings.forEach { args("-warning", it) }
177 checksConfig.hidden.forEach { args("-hide", it) }
178
179 spec.args(apiFiles.map { it.absolutePath })
180
181 // Redirect error output so that we can whitelist specific errors.
182 errorOutput = errStream
183 // We will be handling failures ourselves with a custom message.
184 setIgnoreExitValue(true)
185 }
186 }
187
188 // Load the whitelist file, if present.
189 val whitelistFile = whitelistErrorsFile
190 if (whitelistFile?.exists() == true) {
191 whitelistErrors += whitelistFile.readLines()
192 }
193
194 // Parse the error output.
195 val unparsedErrors = mutableSetOf<String>()
196 val detectedErrors = mutableSetOf<List<String>>()
197 val parsedErrors = mutableSetOf<List<String>>()
198 ByteArrayInputStream(errStream.toByteArray()).bufferedReader().lines().forEach {
199 val match = ERROR_REGEX.matchEntire(it)
200
201 if (match == null) {
202 unparsedErrors.add(it)
203 } else if (match.groups[3]?.value == "error") {
204 val hash = getShortHash(match.groups[5]?.value!!)
205 val error = match.groupValues.subList(1, match.groupValues.size) + listOf(hash)
206 if (hash in whitelistErrors) {
207 detectedErrors.add(error)
208 detectedWhitelistErrors.add(error[5])
209 } else {
210 parsedErrors.add(error)
211 }
212 }
213 }
214
215 unparsedErrors.forEach { error -> logger.error("$ANSI_RED$error$ANSI_RESET") }
216 parsedErrors.forEach { logger.error("$ANSI_RED${it[5]}$ANSI_RESET ${it[4]}") }
217 detectedErrors.forEach { logger.warn("$ANSI_YELLOW${it[5]}$ANSI_RESET ${it[4]}") }
218
219 if (unparsedErrors.isNotEmpty() || parsedErrors.isNotEmpty()) {
220 throw GradleException(checksConfig.onFailMessage ?: "")
221 }
222
223 // Just create a dummy file upon completion. Without any outputs, Gradle will run this task
224 // every time.
225 val outputFile = getOutputFile()
226 outputFile.parentFile.mkdirs()
227 outputFile.createNewFile()
228 }
229 }