From 9f4a85e7ecc612d290fd6e4615b3472c05aece51 Mon Sep 17 00:00:00 2001 From: Louis CAD Date: Sun, 13 Oct 2019 05:38:00 +0200 Subject: [PATCH] WIP dependencies updates proof of concept --- build.gradle.kts | 185 +++++++++++++++++- .../main/kotlin/dependencies/Dependencies.kt | 42 ++-- gradle.properties | 25 +++ 3 files changed, 227 insertions(+), 25 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 255189f34..2b8c4adcd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,14 +4,191 @@ @file:Suppress("SpellCheckingInspection") +import Build_gradle.ArtifactGroupNaming.* + // Top-level build file where you can add configuration options common to all sub-projects/modules. +task("clean") { + delete(rootProject.buildDir) +} + +val versionPlaceholder = "_" + allprojects { - repositories { - setupForProject() + repositories { setupForProject() } + configurations.all { setupVersionPlaceholdersResolving() } + //TODO: Replace code below by pluginManagement or something else? + // (it is incorrect, because it runs once classpath configuration is already resolved) + //buildscript.configurations.all { setupVersionPlaceholdersResolving() } +} + +tasks.register("checkDependenciesUpdates") { + doFirst { + val allConfigurations: Set = allprojects.flatMap { + it.configurations + it.buildscript.configurations + }.toSet() + val allDependencies = allConfigurations.asSequence() + .flatMap { it.allDependencies.asSequence() } + .distinctBy { it.group + it.name } + //TODO: Filter using known grouping strategies to only use the main artifact to resolve latest version, this + // will improve performance. + val dependenciesToUpdate: Sequence> = allDependencies.mapNotNull { dependency -> + val latestVersion = getLatestDependencyVersion(dependency) ?: return@mapNotNull null + val usedVersion = dependency.version + if (usedVersion == latestVersion) null else dependency to latestVersion + } + //TODO: Write updates to gradle.properties without overwriting unrelated properties, comments and structure. + dependenciesToUpdate.forEach { (dependency: Dependency, latestVersion: String) -> + println("Dependency ${dependency.group}:${dependency.name}:${dependency.version} -> $latestVersion") + } } } -task("clean") { - delete(rootProject.buildDir) +//TODO: Allow to get if release stability level between Stable, RC, M(ilestone), eap, beta, alpha, dev and unknown, +// then allow setting default level and per group/artifact exceptions. Maybe through comments in gradle.properties? +fun isVersionStable(version: String): Boolean { + //TODO: cache list and regex for improved efficiency. + val hasStableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.toUpperCase().contains(it) } + val regex = "^[0-9,.v-]+$".toRegex() + return hasStableKeyword || regex.matches(version) +} + +fun Project.getLatestDependencyVersion(dependency: Dependency): String? { + val tmpDependencyUpdateConfiguration = configurations.create("getLatestVersion") { + dependencies.add(dependency) + resolutionStrategy.componentSelection.all { + if (isVersionStable(candidate.version).not()) reject("Unstable version") + } + resolutionStrategy.eachDependency { + if (requested.version != null) useVersion("+") + } + } + try { + val lenientConfiguration = tmpDependencyUpdateConfiguration.resolvedConfiguration.lenientConfiguration + return lenientConfiguration.getFirstLevelModuleDependencies(Specs.SATISFIES_ALL).singleOrNull()?.moduleVersion + } finally { + configurations.remove(tmpDependencyUpdateConfiguration) + } +} + + +fun Configuration.setupVersionPlaceholdersResolving() { + resolutionStrategy.eachDependency { + if (requested.version != versionPlaceholder) return@eachDependency + useVersion(requested.getVersionFromProperties()) + } +} + +fun ModuleVersionSelector.getVersionFromProperties(): String { + val moduleIdentifier: ModuleIdentifier = try { + @Suppress("UnstableApiUsage") + module + } catch (e: Throwable) { // Guard against possible API changes. + println(e) + object : ModuleIdentifier { + override fun getGroup(): String = this@getVersionFromProperties.group + override fun getName(): String = this@getVersionFromProperties.name + } + } + val propertyName = getVersionPropertyName(moduleIdentifier) + val version = property(propertyName = propertyName) + return version as String +} + +fun getVersionPropertyName(moduleIdentifier: ModuleIdentifier): String { + val group = moduleIdentifier.group + val name = moduleIdentifier.name + val versionKey: String = when (moduleIdentifier.findArtifactGroupingRule()?.groupNaming) { + GroupOnly -> group + GroupLastPart -> group.substringAfterLast('.') + GroupFirstTwoParts -> { + val groupFirstPart = group.substringBefore('.') + val groupSecondPart = group.substringAfter('.').substringBefore('.') + "$groupFirstPart.$groupSecondPart" + } + GroupFirstThreeParts -> { + val groupFirstPart = group.substringBefore('.') + val groupSecondPart = group.substringAfter('.').substringBefore('.') + val groupThirdPart = group.substringAfter('.').substringAfter('.').substringBefore('.') + "$groupFirstPart.$groupSecondPart.$groupThirdPart" + } + GroupAndNameFirstPart -> "$group.${name.substringBefore('-')}" + GroupLastPartAndNameSecondPart -> { + val groupLastPart = group.substringAfterLast('.') + val nameSecondPart = name.substringAfter('-').substringBefore('-') + "$groupLastPart.$nameSecondPart" + } + GroupFirstPartAndNameTwoFirstParts -> { + val groupFirstPart = group.substringBefore('.') + val nameFirstPart = name.substringBefore('-') + val nameSecondPart = name.substringAfter('-').substringBefore('-') + "$groupFirstPart.$nameFirstPart-$nameSecondPart" + } + null -> "$group..$name" + } + return "version.$versionKey" +} + +fun ModuleIdentifier.findArtifactGroupingRule(): ArtifactGroupingRule? { + if (forceFullyQualifiedName(this)) return null + val fullArtifactName = "$group:$name" + return artifactsGroupingRules.find { fullArtifactName.startsWith(it.artifactNamesStartingWith) } +} + +fun forceFullyQualifiedName(moduleIdentifier: ModuleIdentifier): Boolean { + val group = moduleIdentifier.group + val name = moduleIdentifier.name + if (group.startsWith("androidx.") && group != "androidx.legacy") { + val indexOfV = name.indexOf("-v") + if (indexOfV != -1 && + indexOfV < name.lastIndex && + name.substring(indexOfV + 1, name.lastIndex).all { it.isDigit() } + ) return true // AndroidX artifacts ending in "v18" or other "v${someApiLevel}" have standalone version number. + } + return false +} + +val artifactsGroupingRules: List = sequenceOf( + "org.jetbrains.kotlin:kotlin" to GroupLastPart, + "org.jetbrains.kotlinx:kotlinx" to GroupLastPartAndNameSecondPart, + "androidx." to GroupOnly, + "androidx.media:media-widget" to GroupFirstPartAndNameTwoFirstParts, + "androidx.test:core" to GroupAndNameFirstPart, // Rest of androidx.test share the same version. + "androidx.test.ext:junit" to GroupAndNameFirstPart, + "androidx.test.ext:truth" to GroupFirstTwoParts, // Same version as the rest of androidx.test. + "androidx.test.services" to GroupFirstTwoParts, // Same version as the rest of androidx.test. + "androidx.test.espresso.idling" to GroupFirstThreeParts, // Same version as other androidx.test.espresso artifacts. + "com.louiscad.splitties:splitties" to GroupLastPart, + "com.squareup.retrofit2" to GroupLastPart, + "com.squareup.okhttp3" to GroupLastPart, + "com.squareup.moshi" to GroupLastPart, + "com.squareup.sqldelight" to GroupLastPart, + "org.robolectric" to GroupLastPart +).map { (artifactNamesStartingWith, groupNaming) -> + ArtifactGroupingRule( + artifactNamesStartingWith = artifactNamesStartingWith, + groupNaming = groupNaming + ) +}.sortedByDescending { it.artifactNamesStartingWith.length }.toList() + +/** + * We assume each part of the "group" is dot separated (`.`), and each part of the name is dash separated (`-`). + */ +sealed class ArtifactGroupNaming { + object GroupOnly : ArtifactGroupNaming() + object GroupLastPart : ArtifactGroupNaming() + object GroupFirstTwoParts : ArtifactGroupNaming() + object GroupFirstThreeParts : ArtifactGroupNaming() + object GroupAndNameFirstPart : ArtifactGroupNaming() + object GroupLastPartAndNameSecondPart : ArtifactGroupNaming() + object GroupFirstPartAndNameTwoFirstParts : ArtifactGroupNaming() +} + +class ArtifactGroupingRule( + val artifactNamesStartingWith: String, + val groupNaming: ArtifactGroupNaming +) { + init { + require(artifactNamesStartingWith.count { it == ':' } <= 1) + } } diff --git a/buildSrc/src/main/kotlin/dependencies/Dependencies.kt b/buildSrc/src/main/kotlin/dependencies/Dependencies.kt index ffdd767b3..c60426643 100644 --- a/buildSrc/src/main/kotlin/dependencies/Dependencies.kt +++ b/buildSrc/src/main/kotlin/dependencies/Dependencies.kt @@ -15,9 +15,9 @@ object Versions { @Suppress("unused") object Libs { const val junit = "junit:junit:4.12" - const val roboElectric = "org.robolectric:robolectric:4.3" + const val roboElectric = "org.robolectric:robolectric:_" const val timber = "com.jakewharton.timber:timber:4.7.1" - const val stetho = "com.facebook.stetho:stetho:1.5.0" + const val stetho = "com.facebook.stetho:stetho:_" val kotlin = Kotlin val kotlinX = KotlinX @@ -25,15 +25,15 @@ object Libs { val google = Google object Kotlin { - const val stdlibJdk7 = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.kotlin}" - const val testJunit = "org.jetbrains.kotlin:kotlin-test-junit:${Versions.kotlin}" + const val stdlibJdk7 = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:_" + const val testJunit = "org.jetbrains.kotlin:kotlin-test-junit:_" } object KotlinX { val coroutines = Coroutines object Coroutines { - private const val version = "1.3.1" + private const val version = "_" private const val artifactPrefix = "org.jetbrains.kotlinx:kotlinx-coroutines" const val core = "$artifactPrefix-core:$version" const val coreCommon = "$artifactPrefix-core-common:$version" @@ -47,25 +47,25 @@ object Libs { object AndroidX { private object Versions { - const val core = "1.0.1" + const val core = "_" const val multidex = "2.0.0" const val palette = "1.0.0" const val preference = "1.0.0" - const val recyclerView = "1.0.0" + const val recyclerView = "_" const val sqlite = "2.0.0" const val vectorDrawable = "1.0.0" const val leanback = "1.0.0" const val emoji = "1.0.0" - const val constraintLayout = "1.1.3" + const val constraintLayout = "_" const val collection = "1.0.0" } private val versions = Versions - const val annotation = "androidx.annotation:annotation:1.0.0" - const val appCompat = "androidx.appcompat:appcompat:1.0.2" + const val annotation = "androidx.annotation:annotation:_" + const val appCompat = "androidx.appcompat:appcompat:_" const val asyncLayoutInflater = "androidx.asynclayoutinflater:asynclayoutinflater:1.0.0" const val browser = "androidx.browser:browser:1.0.0" const val car = "androidx.car:car:1.0.0-alpha5" - const val cardView = "androidx.cardview:cardview:1.0.0" + const val cardView = "androidx.cardview:cardview:_" const val collection = "androidx.collection:collection:${versions.collection}" const val collectionKtx = "androidx.collection:collection-ktx:${versions.collection}" const val constraintLayout = @@ -73,7 +73,7 @@ object Libs { const val constraintLayoutSolver = "androidx.constraintlayout:constraintlayout-solver:${versions.constraintLayout}" const val contentPager = "androidx.contentpager:contentpager:1.0.0" - const val coordinatorLayout = "androidx.coordinatorlayout:coordinatorlayout:1.0.0" + const val coordinatorLayout = "androidx.coordinatorlayout:coordinatorlayout:_" const val core = "androidx.core:core:${versions.core}" const val coreKtx = "androidx.core:core-ktx:${versions.core}" const val cursorAdapter = "androidx.cursoradapter:cursoradapter:1.0.0" @@ -85,8 +85,8 @@ object Libs { const val emojiAppCompat = "androidx.emoji:emoji-appcompat:${versions.emoji}" const val emojiBundler = "androidx.emoji:emoji-bundled:${versions.emoji}" const val exifInterface = "androidx.exifinterface:exifinterface:1.0.0" - const val fragment = "androidx.fragment:fragment:1.0.0" - const val fragmentKtx = "androidx.fragment:fragment-ktx:1.0.0" + const val fragment = "androidx.fragment:fragment:_" + const val fragmentKtx = "androidx.fragment:fragment-ktx:_" const val gridLayout = "androidx.gridlayout:gridlayout:1.0.0" const val heifWriter = "androidx.heifwriter:heifwriter:1.0.0" const val interpolator = "androidx.interpolator:interpolator:1.0.0" @@ -140,7 +140,7 @@ object Libs { val legacy = Legacy object Lifecycle { - private const val version = "2.0.0" + private const val version = "_" const val common = "androidx.lifecycle:lifecycle-common:$version" const val commonJava8 = "androidx.lifecycle:lifecycle-common-java8:$version" const val compiler = "androidx.lifecycle:lifecycle-compiler:$version" @@ -158,7 +158,7 @@ object Libs { } object Room { - private const val version = "2.0.0" + private const val version = "_" const val common = "androidx.room:room-common:$version" const val compiler = "androidx.room:room-compiler:$version" const val guava = "androidx.room:room-guava:$version" @@ -214,11 +214,11 @@ object Libs { object Test { val espresso = Espresso - private const val runnerVersion = "1.2.0" + private const val runnerVersion = "_" private const val rulesVersion = runnerVersion private const val monitorVersion = runnerVersion private const val orchestratorVersion = runnerVersion - private const val coreVersion = "1.2.0" + private const val coreVersion = "_" const val core = "androidx.test:core:$coreVersion" const val coreKtx = "androidx.test:core-ktx:$coreVersion" const val monitor = "androidx.test:monitor:$monitorVersion" @@ -229,7 +229,7 @@ object Libs { val ext = Ext object Ext { - private const val extJunitVersion = "1.1.1" + private const val extJunitVersion = "_" const val junit = "androidx.test.ext:junit:$extJunitVersion" const val junitKtx = "androidx.test.ext:junit-ktx:$extJunitVersion" const val truth = "androidx.test.ext:truth:$coreVersion" @@ -245,7 +245,7 @@ object Libs { object Espresso { val idling = Idling - private const val version = "3.1.1" + private const val version = "_" const val core = "androidx.test.espresso:espresso-core:$version" const val contrib = "androidx.test.espresso:espresso-contrib:$version" const val idlingResource = @@ -275,7 +275,7 @@ object Libs { } object Google { - const val material = "com.google.android.material:material:1.0.0" + const val material = "com.google.android.material:material:_" private const val wearOsVersion = "2.4.0" const val wearable = "com.google.android.wearable:wearable:$wearOsVersion" const val supportWearable = "com.google.android.support:wearable:$wearOsVersion" diff --git a/gradle.properties b/gradle.properties index 6a4e73592..612bbc380 100644 --- a/gradle.properties +++ b/gradle.properties @@ -28,3 +28,28 @@ kotlin.native.ignoreDisabledTargets=true android.namespacedRClass=true splitties.version=3.0.0-dev-038 + +# Dependencies versions +version.androidx.annotation=1.1.0 +version.androidx.appcompat=1.0.2 +version.androidx.cardview=1.0.0 +version.androidx.coordinatorlayout=1.0.0 +version.androidx.core=1.0.1 +version.androidx.constraintlayout=1.1.3 +version.androidx.fragment=1.1.0 +version.androidx.lifecycle=2.0.0 +version.androidx.recyclerview=1.0.0 +version.androidx.room=2.0.0 +version.androidx.test=1.2.0 +version.androidx.test.core=1.2.0 +version.androidx.test.ext.junit=1.1.1 +version.androidx.test.espresso=3.1.1 + +version.kotlin=1.3.50 + +version.kotlinx.coroutines=1.3.1 + +version.robolectric=4.3 + +version.com.facebook.stetho..stetho=1.5.0 +version.com.google.android.material..material=1.0.0