[automerger] Merge "Rewrite CheckApiTask to Kotlin" into oc-mr1-support-27.0-dev am: 3dd239634a am: 1cca89bbe3
Change-Id: I39ad16bace330d41331a3d3b322ff36115fee0dc
diff --git a/buildSrc/src/main/groovy/android/support/checkapi/CheckApiTask.groovy b/buildSrc/src/main/groovy/android/support/checkapi/CheckApiTask.groovy
deleted file mode 100644
index 4f829f6..0000000
--- a/buildSrc/src/main/groovy/android/support/checkapi/CheckApiTask.groovy
+++ /dev/null
@@ -1,280 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.checkapi
-
-import org.gradle.api.DefaultTask
-import org.gradle.api.GradleException
-import org.gradle.api.tasks.Input
-import org.gradle.api.tasks.InputFile
-import org.gradle.api.tasks.InputFiles
-import org.gradle.api.tasks.TaskAction
-import org.gradle.api.tasks.Optional
-import org.gradle.api.tasks.OutputFile
-
-import java.security.MessageDigest
-
-/**
- * Task used to verify changes between two API files.
- * <p>
- * This task may be configured to ignore, warn, or fail with a message for a specific set of
- * Doclava-defined error codes. See {@link com.google.doclava.Errors} for a complete list of
- * supported error codes.
- * <p>
- * Specific failures may be ignored by specifying a list of SHAs in {@link #whitelistErrors}. Each
- * SHA is unique to a specific API change and is logged to the error output on failure.
- */
-public class CheckApiTask extends DefaultTask {
- /** Character that resets console output color. */
- private static final String ANSI_RESET = "\u001B[0m";
-
- /** Character that sets console output color to red. */
- private static final String ANSI_RED = "\u001B[31m";
-
- /** Character that sets console output color to yellow. */
- private static final String ANSI_YELLOW = "\u001B[33m";
-
- /** API file that represents the existing API surface. */
- @Optional
- @InputFile
- File oldApiFile
-
- /** API file that represents the existing API surface's removals. */
- @Optional
- @InputFile
- File oldRemovedApiFile
-
- /** API file that represents the candidate API surface. */
- @InputFile
- File newApiFile
-
- /** API file that represents the candidate API surface's removals. */
- @Optional
- @InputFile
- File newRemovedApiFile
-
- /** Optional file containing a newline-delimited list of error SHAs to ignore. */
- File whitelistErrorsFile
-
- @Optional
- @InputFile
- File getWhiteListErrorsFileInput() {
- // Gradle requires non-null InputFiles to exist -- even with Optional -- so work around that
- // by returning null for this field if the file doesn't exist.
- if (whitelistErrorsFile && whitelistErrorsFile.exists()) {
- return whitelistErrorsFile;
- }
- return null;
- }
-
- /**
- * Optional list of packages to ignore.
- * <p>
- * Packages names will be matched exactly; sub-packages are not automatically recognized.
- */
- @Optional
- @Input
- Collection ignoredPackages
-
- /**
- * Optional list of classes to ignore.
- * <p>
- * Class names will be matched exactly by their fully-qualified names; inner classes are not
- * automatically recognized.
- */
- @Optional
- @Input
- Collection ignoredClasses
-
- /**
- * Optional set of error SHAs to ignore.
- * <p>
- * Each error SHA is unique to a specific API change.
- */
- @Optional
- @Input
- Set whitelistErrors = []
-
- // Errors that were both detected and whitelisted.
- Set detectedWhitelistErrors = []
-
- @InputFiles
- Collection<File> doclavaClasspath
-
- // A dummy output file meant only to tag when this check was last ran.
- // Without any outputs, Gradle will run this task every time.
- @Optional
- private File mOutputFile
-
- @OutputFile
- public File getOutputFile() {
- return mOutputFile ?: new File(project.buildDir, "checkApi/${name}-completed")
- }
-
- @Optional
- public void setOutputFile(File outputFile) {
- mOutputFile = outputFile
- }
-
- /**
- * List of Doclava error codes to treat as errors.
- * <p>
- * See {@link com.google.doclava.Errors} for a complete list of error codes.
- */
- @Input
- Collection checkApiErrors
-
- /**
- * List of Doclava error codes to treat as warnings.
- * <p>
- * See {@link com.google.doclava.Errors} for a complete list of error codes.
- */
- @Input
- Collection checkApiWarnings
-
- /**
- * List of Doclava error codes to ignore.
- * <p>
- * See {@link com.google.doclava.Errors} for a complete list of error codes.
- */
- @Input
- Collection checkApiHidden
-
- /** Message to display on API check failure. */
- @Input
- String onFailMessage
-
- public CheckApiTask() {
- group = 'Verification'
- description = 'Invoke Doclava\'s ApiCheck tool to make sure current.txt is up to date.'
- }
-
- private Set<File> collectAndVerifyInputs() {
- if (getOldRemovedApiFile() != null && getNewRemovedApiFile() != null) {
- return [getOldApiFile(), getNewApiFile(), getOldRemovedApiFile(),
- getNewRemovedApiFile()] as Set
- } else {
- return [getOldApiFile(), getNewApiFile()] as Set
- }
- }
-
- public void setCheckApiErrors(Collection errors) {
- // Make it serializable.
- checkApiErrors = errors as int[]
- }
-
- public void setCheckApiWarnings(Collection warnings) {
- // Make it serializable.
- checkApiWarnings = warnings as int[]
- }
-
- public void setCheckApiHidden(Collection hidden) {
- // Make it serializable.
- checkApiHidden = hidden as int[]
- }
-
- @TaskAction
- public void exec() {
- if (getOldApiFile() == null) {
- // Nothing to do.
- return
- }
-
- final def apiFiles = collectAndVerifyInputs()
-
- OutputStream errStream = new ByteArrayOutputStream()
-
- // If either of those gets tweaked, then this should be refactored to extend JavaExec.
- project.javaexec {
- // Put Doclava on the classpath so we can get the ApiCheck class.
- classpath(getDoclavaClasspath())
- main = 'com.google.doclava.apicheck.ApiCheck'
-
- minHeapSize = '128m'
- maxHeapSize = '1024m'
-
- // add -error LEVEL for every error level we want to fail the build on.
- getCheckApiErrors().each { args('-error', it) }
- getCheckApiWarnings().each { args('-warning', it) }
- getCheckApiHidden().each { args('-hide', it) }
-
- Collection ignoredPackages = getIgnoredPackages()
- if (ignoredPackages) {
- ignoredPackages.each { args('-ignorePackage', it) }
- }
- Collection ignoredClasses = getIgnoredClasses()
- if (ignoredClasses) {
- ignoredClasses.each { args('-ignoreClass', it) }
- }
-
- args(apiFiles.collect( { it.absolutePath } ))
-
- // Redirect error output so that we can whitelist specific errors.
- errorOutput = errStream
-
- // We will be handling failures ourselves with a custom message.
- ignoreExitValue = true
- }
-
- // Load the whitelist file, if present.
- if (whitelistErrorsFile && whitelistErrorsFile.exists()) {
- whitelistErrors += whitelistErrorsFile.readLines()
- }
-
- // Parse the error output.
- Set unparsedErrors = []
- Set detectedErrors = []
- Set parsedErrors = []
- errStream.toString().split("\n").each {
- if (it) {
- def matcher = it =~ ~/^(.+):(.+): (\w+) (\d+): (.+)$/
- if (!matcher) {
- unparsedErrors += [it]
- } else if (matcher[0][3] == "error") {
- def hash = getShortHash(matcher[0][5]);
- def error = matcher[0][1..-1] + [hash]
- if (hash in whitelistErrors) {
- detectedErrors += [error]
- detectedWhitelistErrors += error[5]
- } else {
- parsedErrors += [error]
- }
- }
- }
- }
-
- unparsedErrors.each { error -> logger.error "$ANSI_RED$error$ANSI_RESET" }
- parsedErrors.each { logger.error "$ANSI_RED${it[5]}$ANSI_RESET ${it[4]}"}
- detectedErrors.each { logger.warn "$ANSI_YELLOW${it[5]}$ANSI_RESET ${it[4]}"}
-
- if (unparsedErrors || parsedErrors) {
- throw new GradleException(onFailMessage)
- }
-
- // Just create a dummy file upon completion. Without any outputs, Gradle will run this task
- // every time.
- File outputFile = getOutputFile()
- outputFile.parentFile.mkdirs()
- outputFile.createNewFile()
- }
-
- def getShortHash(src) {
- return MessageDigest.getInstance("SHA-1")
- .digest(src.toString().bytes)
- .encodeHex()
- .toString()[-7..-1]
- }
-}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/android/support/checkapi/CheckApiTask.kt b/buildSrc/src/main/kotlin/android/support/checkapi/CheckApiTask.kt
new file mode 100644
index 0000000..8f96b2e
--- /dev/null
+++ b/buildSrc/src/main/kotlin/android/support/checkapi/CheckApiTask.kt
@@ -0,0 +1,251 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.checkapi
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.Optional
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.TaskAction
+import java.io.*
+import java.security.MessageDigest
+
+
+/** Character that resets console output color. */
+private const val ANSI_RESET = "\u001B[0m"
+
+/** Character that sets console output color to red. */
+private const val ANSI_RED = "\u001B[31m"
+
+/** Character that sets console output color to yellow. */
+private const val ANSI_YELLOW = "\u001B[33m"
+
+private val ERROR_REGEX = Regex("^(.+):(.+): (\\w+) (\\d+): (.+)$")
+
+private fun ByteArray.encodeHex() = fold(StringBuilder(), { builder, byte ->
+ val hexString = Integer.toHexString(byte.toInt() and 0xFF)
+ if (hexString.length < 2) {
+ builder.append("0")
+ }
+ builder.append(hexString)
+}).toString()
+
+private fun getShortHash(src: String): String {
+ val str = MessageDigest.getInstance("SHA-1")
+ .digest(src.toByteArray()).encodeHex()
+ val len = str.length
+ return str.substring(len - 7, len)
+}
+
+/**
+ * Task used to verify changes between two API files.
+ * <p>
+ * This task may be configured to ignore, warn, or fail with a message for a specific set of
+ * Doclava-defined error codes. See {@link com.google.doclava.Errors} for a complete list of
+ * supported error codes.
+ * <p>
+ * Specific failures may be ignored by specifying a list of SHAs in {@link #whitelistErrors}. Each
+ * SHA is unique to a specific API change and is logged to the error output on failure.
+ */
+open class CheckApiTask : DefaultTask() {
+
+ /** API file that represents the existing API surface. */
+ @Optional
+ @InputFile
+ var oldApiFile: File? = null
+
+ /** API file that represents the existing API surface's removals. */
+ @Optional
+ @InputFile
+ var oldRemovedApiFile: File? = null
+
+ /** API file that represents the candidate API surface. */
+ @InputFile
+ var newApiFile: File? = null
+
+ /** API file that represents the candidate API surface's removals. */
+ @Optional
+ @InputFile
+ var newRemovedApiFile: File? = null
+
+ /** Optional file containing a newline-delimited list of error SHAs to ignore. */
+ var whitelistErrorsFile: File? = null
+
+ @Optional
+ @InputFile
+ fun getWhiteListErrorsFileInput(): File? {
+ // Gradle requires non-null InputFiles to exist -- even with Optional -- so work around that
+ // by returning null for this field if the file doesn't exist.
+ if (whitelistErrorsFile?.exists() == true) {
+ return whitelistErrorsFile
+ }
+ return null
+ }
+
+ /**
+ * Optional set of error SHAs to ignore.
+ * <p>
+ * Each error SHA is unique to a specific API change.
+ */
+ @Optional
+ @Input
+ var whitelistErrors = emptySet<String>()
+
+ var detectedWhitelistErrors = mutableSetOf<String>()
+
+ @InputFiles
+ var doclavaClasspath: Collection<File> = emptyList()
+
+ // A dummy output file meant only to tag when this check was last ran.
+ // Without any outputs, Gradle will run this task every time.
+ @Optional
+ private var mOutputFile: File? = null
+
+ @OutputFile
+ fun getOutputFile(): File {
+ return if (mOutputFile != null)
+ mOutputFile!!
+ else File(project.buildDir, "checkApi/${name}-completed")
+ }
+
+ @Optional
+ fun setOutputFile(outputFile: File) {
+ mOutputFile = outputFile
+ }
+
+ /**
+ * List of Doclava error codes to treat as errors.
+ * <p>
+ * See {@link com.google.doclava.Errors} for a complete list of error codes.
+ */
+ @Input
+ var checkApiErrors: Collection<Int> = emptyList()
+
+ /**
+ * List of Doclava error codes to treat as warnings.
+ * <p>
+ * See {@link com.google.doclava.Errors} for a complete list of error codes.
+ */
+ @Input
+ var checkApiWarnings: Collection<Int> = emptyList()
+
+ /**
+ * List of Doclava error codes to ignore.
+ * <p>
+ * See {@link com.google.doclava.Errors} for a complete list of error codes.
+ */
+ @Input
+ var checkApiHidden: Collection<Int> = emptyList()
+
+ /** Message to display on API check failure. */
+ @Input
+ var onFailMessage: String = "Failed."
+
+ init {
+ group = "Verification"
+ description = "Invoke Doclava\'s ApiCheck tool to make sure current.txt is up to date."
+ }
+
+ private fun collectAndVerifyInputs(): Set<File> {
+ if (oldRemovedApiFile != null && newRemovedApiFile != null) {
+ return setOf(oldApiFile!!, newApiFile!!, oldRemovedApiFile!!, newRemovedApiFile!!)
+ } else {
+ return setOf(oldApiFile!!, newApiFile!!)
+ }
+ }
+
+ @TaskAction
+ fun exec() {
+ if (oldApiFile == null) {
+ // Nothing to do.
+ return
+ }
+
+ val apiFiles = collectAndVerifyInputs()
+
+ val errStream = ByteArrayOutputStream()
+
+ // If either of those gets tweaked, then this should be refactored to extend JavaExec.
+ project.javaexec { spec ->
+ spec.apply {
+ // Put Doclava on the classpath so we can get the ApiCheck class.
+ classpath(doclavaClasspath)
+ main = "com.google.doclava.apicheck.ApiCheck"
+
+ minHeapSize = "128m"
+ maxHeapSize = "1024m"
+
+ // add -error LEVEL for every error level we want to fail the build on.
+ checkApiErrors.forEach { args("-error", it) }
+ checkApiWarnings.forEach { args("-warning", it) }
+ checkApiHidden.forEach { args("-hide", it) }
+
+ spec.args(apiFiles.map { it.absolutePath })
+
+ // Redirect error output so that we can whitelist specific errors.
+ errorOutput = errStream
+ // We will be handling failures ourselves with a custom message.
+ setIgnoreExitValue(true)
+ }
+ }
+
+
+ // Load the whitelist file, if present.
+ val whitelistFile = whitelistErrorsFile
+ if (whitelistFile?.exists() == true) {
+ whitelistErrors += whitelistFile.readLines()
+ }
+
+ // Parse the error output.
+ val unparsedErrors = mutableSetOf<String>()
+ val detectedErrors = mutableSetOf<List<String>>()
+ val parsedErrors = mutableSetOf<List<String>>()
+ ByteArrayInputStream(errStream.toByteArray()).bufferedReader().lines().forEach {
+ val match = ERROR_REGEX.matchEntire(it)
+
+ if (match == null) {
+ unparsedErrors.add(it)
+ } else if (match.groups[3]?.value == "error") {
+ val hash = getShortHash(match.groups[5]?.value!!)
+ val error = match.groupValues.subList(1, match.groupValues.size) + listOf(hash)
+ if (hash in whitelistErrors) {
+ detectedErrors.add(error)
+ detectedWhitelistErrors.add(error[5])
+ } else {
+ parsedErrors.add(error)
+ }
+ }
+ }
+
+ unparsedErrors.forEach { error -> logger.error("$ANSI_RED$error$ANSI_RESET") }
+ parsedErrors.forEach { logger.error("$ANSI_RED${it[5]}$ANSI_RESET ${it[4]}") }
+ detectedErrors.forEach { logger.warn("$ANSI_YELLOW${it[5]}$ANSI_RESET ${it[4]}") }
+
+ if (unparsedErrors.isNotEmpty() || parsedErrors.isNotEmpty()) {
+ throw GradleException(onFailMessage)
+ }
+
+ // Just create a dummy file upon completion. Without any outputs, Gradle will run this task
+ // every time.
+ val outputFile = getOutputFile()
+ outputFile.parentFile.mkdirs()
+ outputFile.createNewFile()
+ }
+}
\ No newline at end of file