blob: 36e9f4146cf162155d4d4404cd0b9d96a0bd08a3 [file] [log] [blame]
package androidx.build
import androidx.build.checkapi.ApiLocation
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputFiles
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.util.SortedSet
/**
* Task for updating the public resource surface
*/
abstract class UpdateResourceApiTask : DefaultTask() {
/** Optional text file from which the previously-released resource signatures will be read. */
@get:InputFile
@get:Optional
abstract val referenceResourceApiFile: Property<File>
/**
* Text file from which resource signatures will be read. A file path must be specified at
* configuration time even if the file may not exist at build time.
*/
@get:Internal
abstract val inputApiFile: Property<File>
@InputFile
@Optional
fun getInputApiFileIfExists(): File? {
val file = inputApiFile.get()
return if (file.exists()) {
file
} else {
null
}
}
/** Text files to which resource signatures will be written. */
@get:Internal
abstract val outputApiLocations: ListProperty<ApiLocation>
@OutputFiles
fun getTaskOutputs(): List<File> {
return outputApiLocations.get().flatMap { outputApiLocation ->
listOf(
outputApiLocation.resourceFile
)
}
}
@TaskAction
fun verifyAndUpdateResourceApi() {
val newApiFile = inputApiFile.get()
val referenceApiFile = referenceResourceApiFile.orNull
// Read the current API surface, if any, into memory.
val newApiSet = if (newApiFile.exists()) {
HashSet(newApiFile.readLines())
} else {
emptySet<String>()
}
// If a reference API file was specified, verify the current API surface.
if (referenceApiFile != null && referenceApiFile.exists()) {
// Read the reference API surface into memory.
val oldVersion = Version(
referenceApiFile.name.removePrefix("res-").removeSuffix(".txt")
)
val oldApiSet: HashSet<String> = HashSet(referenceApiFile.readLines())
checkApiCompatibility(oldVersion, oldApiSet, project.version(), newApiSet)
}
// Sort the resources for the sake of source control diffs.
val newApiSortedSet: SortedSet<String> = newApiSet.toSortedSet()
// Write current API surface to output locations.
for (outputApiLocation in outputApiLocations.get()) {
val outputApiFile = outputApiLocation.resourceFile
outputApiFile.bufferedWriter().use { out ->
newApiSortedSet.forEach {
out.write(it)
out.newLine()
}
}
}
}
private fun checkApiCompatibility(
referenceVersion: Version,
referenceApiSet: Set<String>,
newVersion: Version,
newApiSet: Set<String>
) {
// Compute the diff.
val removedApi = HashSet<String>()
val addedApi = HashSet<String>(newApiSet)
for (e in referenceApiSet) {
if (newApiSet.contains(e)) {
addedApi.remove(e)
} else {
removedApi.add(e)
}
}
// POLICY: Ensure that no resources are removed within the span of a major version.
if (referenceVersion.major == newVersion.major && removedApi.isNotEmpty()) {
var errorMessage = "Cannot remove public resources within the same major version, " +
"the following were removed since version $referenceVersion:\n"
for (e in removedApi) {
errorMessage += "$e\n"
}
throw GradleException(errorMessage)
}
// POLICY: Ensure that no resources are added to a finalized version.
if (newVersion.isFinalApi() && addedApi.isNotEmpty()) {
var errorMessage = "Cannot add public resources when api becomes final, " +
"the following resources were added since version $referenceVersion:\n"
for (e in addedApi) {
errorMessage += "$e\n"
}
throw GradleException(errorMessage)
}
}
}