Cleaned up and fixed version parsing. added graceful fallback if version parsing fails. improved performance

This commit is contained in:
Robinson 2021-04-16 16:48:04 +02:00
parent 1e6088c7bb
commit 333732e1a4

View File

@ -21,7 +21,6 @@ import org.gradle.api.Project
import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskAction
import java.io.InputStreamReader import java.io.InputStreamReader
import java.net.URL import java.net.URL
import java.util.*
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.Future import java.util.concurrent.Future
import java.util.concurrent.locks.ReentrantReadWriteLock import java.util.concurrent.locks.ReentrantReadWriteLock
@ -30,11 +29,13 @@ import kotlin.concurrent.write
open class open class
GetVersionInfoTask : DefaultTask() { GetVersionInfoTask : DefaultTask() {
private data class VersionHolder(var release: String?, val versions: MutableSet<String>) { private data class VersionHolder(var release: String?, val versions: MutableSet<String>) {
var dirtyVersions = false
fun updateReleaseVersion(version: String) { fun updateReleaseVersion(version: String) {
if (release == null) { if (release == null) {
release = version release = version
} else { } else {
// there can be errors when parsing version info, since not versions follow loose-semantic versioning // there can be errors when parsing version info, since not all version strings follow semantic versioning
try { try {
val currentVersion = Version.from(release) val currentVersion = Version.from(release)
val releaseVer = Version.from(version) val releaseVer = Version.from(version)
@ -49,45 +50,77 @@ GetVersionInfoTask : DefaultTask() {
} }
fun addVersion(ver: String) { fun addVersion(ver: String) {
versions.add(ver) if (!versions.contains(ver)) {
versions.add(ver)
} else {
dirtyVersions = true
}
} }
fun getVersionOptions(currentVersion: String): List<String> { fun getVersionOptions(currentVersion: String): List<String> {
// there can be errors when parsing version info, since not versions follow loose-semantic versioning // there can be errors when parsing version info, since not all version strings follow semantic versioning
val myVersionIndex = versions.indexOfFirst { it == currentVersion } // first! try version sorting. This may fail!
return if (myVersionIndex >= 0) { // there are no duplicates in this list
return versions.filterIndexed { index, _ -> index <= myVersionIndex } try {
} else { // this creates a LOT of version objects. Probably better to store these in a list, however we want all backing data
versions.toList() // structures to be strings.
val curVersion = Version.from(currentVersion)
return versions.sortedWith { o1, o2 ->
Version.from(o1).compareTo(Version.from(o2))
}.filter {
Version.from(it).greaterThan(curVersion)
}.toList()
} catch (e: Exception) {
// WHOOPS! There was an invalid version number! Instead of just crashing, try a different way...
if (dirtyVersions) {
// no idea, honestly... the list might not even be in order! Just return the entire thing and let the user sort it out
return versions.toMutableList().apply { add(0, "Error parsing!") }.toList()
} else {
// fortunately for us, the usually the maven order of version data is IN-ORDER, so we can "cheat" the system and look at
// indexing instead
val myVersionIndex = versions.indexOfFirst { it == currentVersion }
// println("INDEX: ${myVersionIndex}" )
return if (myVersionIndex >= 0) {
return versions.filterIndexed { index, _ -> index > myVersionIndex }
} else {
versions.toMutableList().apply { add(0, "Error parsing!") }.toList()
}
}
} }
} }
} }
companion object { companion object {
private val releaseMatcher = """^.*(<release>)(.*)(<\/release>)""".toRegex() private val releaseMatcher = """(<release>)(.*)(<\/release>)""".toRegex()
private val versionMatcher = """^.*(<version>)(.*)(<\/version>)""".toRegex()
private val httpDispatcher = Executors.newFixedThreadPool(8) private val httpDispatcher = Executors.newFixedThreadPool(8)
private fun getLatestVersionInfo( private fun getLatestVersionInfo(
repositories: List<String>, repositories: List<String>,
mergedDeps: MutableMap<DependencyScanner.Maven, MutableSet<DependencyScanner.Maven>>, mergedDeps: MutableMap<DependencyScanner.Maven, MutableSet<DependencyScanner.Maven>>,
): MutableMap<DependencyScanner.Maven, VersionHolder> { mergedVersionInfo: MutableMap<DependencyScanner.Maven, VersionHolder> = mutableMapOf(),
forGradleScripts: Boolean = false,
): Pair<MutableList<Future<*>>, MutableMap<DependencyScanner.Maven, VersionHolder>> {
// first get all version information across ALL projects. // first get all version information across ALL projects.
// do this in parallel with coroutines! // do this in parallel with coroutines!
val futures = mutableListOf<Future<*>>() val futures = mutableListOf<Future<*>>()
val mergedVersionInfo = mutableMapOf<DependencyScanner.Maven, VersionHolder>()
val downloadLock = ReentrantReadWriteLock() val downloadLock = ReentrantReadWriteLock()
// mergedDeps now has all deps for all projects. now we want to resolve (but only if we don't already have it) // mergedDeps now has all deps for all projects. now we want to resolve (but only if we don't already have it)
mergedDeps.forEach { (mergedDep, _) -> mergedDeps.forEach { (mergedDep, _) ->
val metadataUrl = "${mergedDep.group.replace(".", "/")}/${mergedDep.name}/maven-metadata.xml" val metadataUrl = if (forGradleScripts)
// we also have to ADD a prefix to the group ID, because a gradle plugin is **SLIGHTLY** different in how it works.
"gradle/plugin/${mergedDep.group.replace(".", "/")}/maven-metadata.xml"
else
"${mergedDep.group.replace(".", "/")}/${mergedDep.name}/maven-metadata.xml"
val mavenIdKey = DependencyScanner.Maven(mergedDep.group, mergedDep.name)
// version info is per dependency // version info is per dependency
val depVersionInfo = downloadLock.write { downloadLock.write {
mergedVersionInfo.getOrPut(DependencyScanner.Maven(mergedDep.group, mergedDep.name)) { mergedVersionInfo.getOrPut(mavenIdKey) {
VersionHolder(null, mutableSetOf()) VersionHolder(null, mutableSetOf())
} }
} }
@ -97,21 +130,46 @@ GetVersionInfoTask : DefaultTask() {
try { try {
val url = URL(repoUrl + metadataUrl) val url = URL(repoUrl + metadataUrl)
// println("Trying: $url") // println("Trying: $url")
with(url.openConnection() as java.net.HttpURLConnection) {
InputStreamReader(inputStream).readLines().forEach { line ->
var matchResult = releaseMatcher.find(line)
if (matchResult != null) {
val (_, ver, _) = matchResult.destructured
downloadLock.write {
depVersionInfo.updateReleaseVersion(ver)
}
}
matchResult = versionMatcher.find(line) with(url.openConnection() as java.net.HttpURLConnection) {
if (matchResult != null) { var inVersioningSection = false
val (_, ver, _) = matchResult.destructured InputStreamReader(inputStream).readLines().forEach { line ->
downloadLock.write { val trimmed = line.trim()
depVersionInfo.addVersion(ver)
if (!inVersioningSection) {
if (trimmed == "<versioning>") {
inVersioningSection = true
}
} else {
if (trimmed == "</versioning>") {
inVersioningSection = false
}
// only match version info when we are in the "<versioning>" section
val matchResult = releaseMatcher.find(trimmed)
if (matchResult != null) {
val (_, ver, _) = matchResult.destructured
// println("Release: ${mergedDep.group}:${mergedDep.name} $ver")
downloadLock.write {
mergedVersionInfo[mavenIdKey]!!.updateReleaseVersion(ver)
}
}
// not using regex, because this becomes complex.
// There can be a SINGLE version per line, or MULTIPLE versions per line.
// This handles both cases.
if (trimmed.startsWith("<version>")) {
// list out 1 or more versions
val versions = trimmed.split("<version>").filter {it.isNotEmpty()}
.map { it.substring(0, it.indexOf('<')) }
downloadLock.write {
versions.forEach { ver ->
// println("Version: ${mergedDep.group}:${mergedDep.name} $ver")
mergedVersionInfo[mavenIdKey]!!.addVersion(ver)
}
}
} }
} }
} }
@ -124,16 +182,12 @@ GetVersionInfoTask : DefaultTask() {
futures.add(future) futures.add(future)
} }
if (mergedDeps.isNotEmpty()) {
println("\tGetting version data for ${mergedDeps.size} dependencies...")
futures.forEach {
it.get()
}
}
downloadLock.write { downloadLock.write {
// println("SIZE: " + mergedVersionInfo) // mergedVersionInfo.forEach { t, u ->
return mergedVersionInfo // println("$t :: ${u.versions}")
// }
return Pair(futures, mergedVersionInfo)
} }
} }
@ -153,25 +207,36 @@ GetVersionInfoTask : DefaultTask() {
deps.add(dep) deps.add(dep)
} }
// for script dependencies, ALWAYS add the gradle plugin repo! // for script dependencies, ALWAYS add the gradle plugin repo!
// (we hardcode the value, this is not likely to change, but easy enough to fix if it does...) // (we hardcode the value, this is not likely to change, but easy enough to fix if it does...)
val newRepos = mutableSetOf<String>() val (futures, versionHolders) = getLatestVersionInfo(listOf("https://plugins.gradle.org/m2/"), mergedDeps, forGradleScripts = true)
newRepos.add("https://plugins.gradle.org/m2/") val (futures2, _) = getLatestVersionInfo(repositories, mergedDeps, versionHolders)
newRepos.addAll(repositories)
if (mergedDeps.isNotEmpty()) {
// suppress duplicate messages when initially parsing gradle scripts (since it's a redundant message )
println("\tGetting version data for ${mergedDeps.size} dependencies...")
}
(futures + futures2).forEach {
// wait for all of them to finish
it.get()
}
// versionHolders.forEach { (t, u) ->
// println("$t :: ${u.versions}")
// }
val mergedVersionInfo = getLatestVersionInfo(newRepos.toList(), mergedDeps)
mergedDeps.forEach { (mergedDep, existingVersions) -> mergedDeps.forEach { (mergedDep, existingVersions) ->
val latestInfo: VersionHolder = mergedVersionInfo[mergedDep]!! val versionHolder: VersionHolder = versionHolders[mergedDep]!!
existingVersions.forEach { dep -> existingVersions.forEach { dep ->
if (latestInfo.release == null) { if (versionHolder.release == null) {
unknownVersionInfo.add(dep) unknownVersionInfo.add(dep)
} else { } else {
if (dep.version == latestInfo.release) { if (dep.version == versionHolder.release) {
latestVersionInfo.add(dep) latestVersionInfo.add(dep)
} else { } else {
oldVersionInfo.add(Pair(dep, latestInfo)) oldVersionInfo.add(Pair(dep, versionHolder))
} }
} }
} }
@ -192,20 +257,29 @@ GetVersionInfoTask : DefaultTask() {
} }
if (hasOld) { if (hasOld) {
println() if (hasLatest) {
println()
}
println("\tThe following build script dependencies need updates:") println("\tThe following build script dependencies need updates:")
oldVersionInfo.forEach { (dep, versionHolder) -> oldVersionInfo.forEach { (dep, versionHolder) ->
// list release version AND all other versions greater than my version // list release version AND all other versions greater than my version
val versionOptions = versionHolder.getVersionOptions(dep.version) val possibleVersionChoices = versionHolder.getVersionOptions(dep.version)
if (possibleVersionChoices.size > 1) {
// BUILD SCRIPT DEPS HAVE FUNNY NOTATION! // BUILD SCRIPT DEPS HAVE FUNNY NOTATION!
println("\t - ${dep.group}:${dep.version} -> $versionOptions") println("\t - ${dep.group}:${dep.version} -> ${versionHolder.release} $possibleVersionChoices")
} else {
// BUILD SCRIPT DEPS HAVE FUNNY NOTATION!
println("\t - ${dep.group}:${dep.version} -> ${versionHolder.release}")
}
// println("\t - ${dep.group}:${dep.version} -> ${versionHolder.versions}")
} }
} }
if (hasUnknown) { if (hasUnknown) {
println() if (hasLatest || hasOld) {
println()
}
println("\tThe following build script dependencies have unknown updates:") println("\tThe following build script dependencies have unknown updates:")
unknownVersionInfo.forEach { dep -> unknownVersionInfo.forEach { dep ->
// BUILD SCRIPT DEPS HAVE FUNNY NOTATION! // BUILD SCRIPT DEPS HAVE FUNNY NOTATION!
@ -230,8 +304,19 @@ GetVersionInfoTask : DefaultTask() {
mergedRepos.addAll(staticMethodsAndTools.getProjectRepositoryUrls(subProject)) mergedRepos.addAll(staticMethodsAndTools.getProjectRepositoryUrls(subProject))
} }
// first get all version information across ALL projects. // first get all version information across ALL projects.
val mergedVersionInfo = getLatestVersionInfo(mergedRepos.toList(), mergedDeps) val (futures, versionHolders) = getLatestVersionInfo(mergedRepos.toList(), mergedDeps)
if (mergedDeps.isNotEmpty()) {
// suppress duplicate messages when initially parsing gradle scripts (since it's a redundant message )
println("\tGetting version data for ${mergedDeps.size} dependencies...")
}
futures.forEach {
// wait for all of them to finish
it.get()
}
val latestVersionInfo = mutableListOf<DependencyScanner.Maven>() val latestVersionInfo = mutableListOf<DependencyScanner.Maven>()
@ -255,13 +340,14 @@ GetVersionInfoTask : DefaultTask() {
} }
mergedDeps.forEach { (mergedDep, existingVersions) -> mergedDeps.forEach { (mergedDep, existingVersions) ->
val latestInfo: VersionHolder = mergedVersionInfo[mergedDep]!! val versionHolder: VersionHolder = versionHolders[mergedDep]!!
existingVersions.forEach { dep -> existingVersions.forEach { dep ->
if (latestInfo.release != null) { if (versionHolder.release != null) {
if (dep.version == latestInfo.release) { if (dep.version == versionHolder.release) {
latestVersionInfo.add(dep) latestVersionInfo.add(dep)
} else { } else {
oldVersionInfo.add(Pair(dep, latestInfo)) oldVersionInfo.add(Pair(dep, versionHolder))
} }
} else { } else {
unknownVersionInfo.add(dep) unknownVersionInfo.add(dep)
@ -293,21 +379,26 @@ GetVersionInfoTask : DefaultTask() {
if (hasOld) { if (hasOld) {
println() if (hasLatest) {
println()
}
println("\tThe following project$projectName dependencies need updates:") println("\tThe following project$projectName dependencies need updates:")
oldVersionInfo.forEach { (dep, versionHolder) -> oldVersionInfo.forEach { (dep, versionHolder) ->
// list release version AND all other versions greater than my version // list release version AND all other versions greater than my version
val possibleVersionChoices = versionHolder.getVersionOptions(dep.version) val possibleVersionChoices = versionHolder.getVersionOptions(dep.version)
if (possibleVersionChoices.size > 1) {
println("\t - ${dep.group}:${dep.name}:${dep.version} -> ${versionHolder.release}") println("\t - ${dep.group}:${dep.name}:${dep.version} -> ${versionHolder.release} $possibleVersionChoices")
println("\t - ${dep.group}:${dep.name}:${dep.version} -> ${versionHolder.versions}") } else {
println("\t - ${dep.group}:${dep.name}:${dep.version} -> $possibleVersionChoices") println("\t - ${dep.group}:${dep.name}:${dep.version} -> ${versionHolder.release}")
}
} }
} }
if (hasUnknown) { if (hasUnknown) {
println() if (hasLatest || hasOld) {
println()
}
println("\tThe following project$projectName dependencies have unknown updates:") println("\tThe following project$projectName dependencies have unknown updates:")
unknownVersionInfo.forEach { dep -> unknownVersionInfo.forEach { dep ->
println("\t - ${dep.group}:${dep.name}:${dep.version}") println("\t - ${dep.group}:${dep.name}:${dep.version}")