1/* 2 * Copyright (C) 2016 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 17package android.support.checkapi; 18 19import org.gradle.api.DefaultTask 20import org.gradle.api.Nullable 21import org.gradle.api.GradleException 22import org.gradle.api.InvalidUserDataException 23import org.gradle.api.tasks.Input 24import org.gradle.api.tasks.InputFile 25import org.gradle.api.tasks.InputFiles 26import org.gradle.api.tasks.ParallelizableTask 27import org.gradle.api.tasks.TaskAction 28import org.gradle.api.tasks.Optional 29import org.gradle.api.tasks.OutputFile 30import org.gradle.process.ExecResult 31 32import java.security.MessageDigest 33 34/** 35 * Task used to verify changes between two API files. 36 * <p> 37 * This task may be configured to ignore, warn, or fail with a message for a specific set of 38 * Doclava-defined error codes. See {@link com.google.doclava.Errors} for a complete list of 39 * supported error codes. 40 * <p> 41 * Specific failures may be ignored by specifying a list of SHAs in {@link #whitelistErrors}. Each 42 * SHA is unique to a specific API change and is logged to the error output on failure. 43 */ 44@ParallelizableTask 45public class CheckApiTask extends DefaultTask { 46 /** Character that resets console output color. */ 47 private static final String ANSI_RESET = "\u001B[0m"; 48 49 /** Character that sets console output color to red. */ 50 private static final String ANSI_RED = "\u001B[31m"; 51 52 /** Character that sets console output color to yellow. */ 53 private static final String ANSI_YELLOW = "\u001B[33m"; 54 55 /** API file that represents the existing API surface. */ 56 @InputFile 57 File oldApiFile 58 59 /** API file that represents the existing API surface's removals. */ 60 @InputFile 61 File oldRemovedApiFile 62 63 /** API file that represents the candidate API surface. */ 64 @InputFile 65 File newApiFile 66 67 /** API file that represents the candidate API surface's removals. */ 68 @InputFile 69 File newRemovedApiFile 70 71 /** Optional file containing a newline-delimited list of error SHAs to ignore. */ 72 @Nullable 73 File whitelistErrorsFile 74 75 @Optional 76 @Nullable 77 @InputFile 78 File getWhiteListErrorsFileInput() { 79 // Gradle requires non-null InputFiles to exist -- even with Optional -- so work around that 80 // by returning null for this field if the file doesn't exist. 81 if (whitelistErrorsFile && whitelistErrorsFile.exists()) { 82 return whitelistErrorsFile; 83 } 84 return null; 85 } 86 87 /** 88 * Optional list of packages to ignore. 89 * <p> 90 * Packages names will be matched exactly; sub-packages are not automatically recognized. 91 */ 92 @Optional 93 @Nullable 94 @Input 95 Collection ignoredPackages = null 96 97 /** 98 * Optional list of classes to ignore. 99 * <p> 100 * Class names will be matched exactly by their fully-qualified names; inner classes are not 101 * automatically recognized. 102 */ 103 @Optional 104 @Nullable 105 @Input 106 Collection ignoredClasses = null 107 108 /** 109 * Optional set of error SHAs to ignore. 110 * <p> 111 * Each error SHA is unique to a specific API change. 112 */ 113 @Optional 114 @Input 115 Set whitelistErrors = [] 116 117 @InputFiles 118 Collection<File> doclavaClasspath 119 120 // A dummy output file meant only to tag when this check was last ran. 121 // Without any outputs, Gradle will run this task every time. 122 @Optional 123 @Nullable 124 private File mOutputFile = null; 125 126 @OutputFile 127 public File getOutputFile() { 128 return mOutputFile ?: new File(project.buildDir, "checkApi/${name}-completed") 129 } 130 131 @Optional 132 public void setOutputFile(File outputFile) { 133 mOutputFile = outputFile 134 } 135 136 /** 137 * List of Doclava error codes to treat as errors. 138 * <p> 139 * See {@link com.google.doclava.Errors} for a complete list of error codes. 140 */ 141 @Input 142 Collection checkApiErrors 143 144 /** 145 * List of Doclava error codes to treat as warnings. 146 * <p> 147 * See {@link com.google.doclava.Errors} for a complete list of error codes. 148 */ 149 @Input 150 Collection checkApiWarnings 151 152 /** 153 * List of Doclava error codes to ignore. 154 * <p> 155 * See {@link com.google.doclava.Errors} for a complete list of error codes. 156 */ 157 @Input 158 Collection checkApiHidden 159 160 /** Message to display on API check failure. */ 161 @Input 162 String onFailMessage 163 164 public CheckApiTask() { 165 group = 'Verification' 166 description = 'Invoke Doclava\'s ApiCheck tool to make sure current.txt is up to date.' 167 } 168 169 private Set<File> collectAndVerifyInputs() { 170 Set<File> apiFiles = [getOldApiFile(), getNewApiFile(), getOldRemovedApiFile(), 171 getNewRemovedApiFile()] as Set 172 if (apiFiles.size() != 4) { 173 throw new InvalidUserDataException("""Conflicting input files: 174 oldApiFile: ${getOldApiFile()} 175 newApiFile: ${getNewApiFile()} 176 oldRemovedApiFile: ${getOldRemovedApiFile()} 177 newRemovedApiFile: ${getNewRemovedApiFile()} 178All of these must be distinct files.""") 179 } 180 return apiFiles; 181 } 182 183 public void setCheckApiErrors(Collection errors) { 184 // Make it serializable. 185 checkApiErrors = errors as int[] 186 } 187 188 public void setCheckApiWarnings(Collection warnings) { 189 // Make it serializable. 190 checkApiWarnings = warnings as int[] 191 } 192 193 public void setCheckApiHidden(Collection hidden) { 194 // Make it serializable. 195 checkApiHidden = hidden as int[] 196 } 197 198 @TaskAction 199 public void exec() { 200 final def apiFiles = collectAndVerifyInputs() 201 202 OutputStream errStream = new ByteArrayOutputStream() 203 204 // If either of those gets tweaked, then this should be refactored to extend JavaExec. 205 project.javaexec { 206 // Put Doclava on the classpath so we can get the ApiCheck class. 207 classpath(getDoclavaClasspath()) 208 main = 'com.google.doclava.apicheck.ApiCheck' 209 210 minHeapSize = '128m' 211 maxHeapSize = '1024m' 212 213 // [other options] old_api.txt new_api.txt old_removed_api.txt new_removed_api.txt 214 215 // add -error LEVEL for every error level we want to fail the build on. 216 getCheckApiErrors().each { args('-error', it) } 217 getCheckApiWarnings().each { args('-warning', it) } 218 getCheckApiHidden().each { args('-hide', it) } 219 220 Collection ignoredPackages = getIgnoredPackages() 221 if (ignoredPackages) { 222 ignoredPackages.each { args('-ignorePackage', it) } 223 } 224 Collection ignoredClasses = getIgnoredClasses() 225 if (ignoredClasses) { 226 ignoredClasses.each { args('-ignoreClass', it) } 227 } 228 229 args(apiFiles.collect( { it.absolutePath } )) 230 231 // Redirect error output so that we can whitelist specific errors. 232 errorOutput = errStream 233 234 // We will be handling failures ourselves with a custom message. 235 ignoreExitValue = true 236 } 237 238 // Load the whitelist file, if present. 239 if (whitelistErrorsFile && whitelistErrorsFile.exists()) { 240 whitelistErrors += whitelistErrorsFile.readLines() 241 } 242 243 // Parse the error output. 244 def unparsedErrors = [] 245 def ignoredErrors = [] 246 def parsedErrors = [] 247 errStream.toString().split("\n").each { 248 if (it) { 249 def matcher = it =~ ~/^(.+):(.+): (\w+) (\d+): (.+)$/ 250 if (!matcher) { 251 unparsedErrors += [it] 252 } else if (matcher[0][3] == "error") { 253 def hash = getShortHash(matcher[0][5]); 254 def error = matcher[0][1..-1] + [hash] 255 if (hash in whitelistErrors) { 256 ignoredErrors += [error] 257 } else { 258 parsedErrors += [error] 259 } 260 } 261 } 262 } 263 264 unparsedErrors.each { error -> logger.error "$ANSI_RED$error$ANSI_RESET" } 265 parsedErrors.each { logger.error "$ANSI_RED${it[5]}$ANSI_RESET ${it[4]}"} 266 ignoredErrors.each { logger.warn "$ANSI_YELLOW${it[5]}$ANSI_RESET ${it[4]}"} 267 268 if (unparsedErrors || parsedErrors) { 269 throw new GradleException(onFailMessage) 270 } 271 272 // Just create a dummy file upon completion. Without any outputs, Gradle will run this task 273 // every time. 274 File outputFile = getOutputFile() 275 outputFile.parentFile.mkdirs() 276 outputFile.createNewFile() 277 } 278 279 def getShortHash(src) { 280 return MessageDigest.getInstance("SHA-1") 281 .digest(src.toString().bytes) 282 .encodeHex() 283 .toString()[-7..-1] 284 } 285}