• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }