From f1e8c65aa1442f32cb0fdbc20b22465ef6f3ebf8 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Mon, 26 Aug 2024 20:06:21 +0000 Subject: [PATCH] android, desktop: using SemVer when checking for updates (#4768) * android, desktop: using SemVer when checking for updates * simplify * simplify * no comment * simplify * change --------- Co-authored-by: Evgeny Poberezkin --- .../common/views/helpers/AppUpdater.kt | 94 ++++++++++++++++--- .../kotlin/chat/simplex/app/SemVerTest.kt | 63 +++++++++++++ 2 files changed, 145 insertions(+), 12 deletions(-) create mode 100644 apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/SemVerTest.kt diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt index faef957705..ac69c41832 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt @@ -19,12 +19,76 @@ import chat.simplex.res.MR import kotlinx.coroutines.* import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient import okhttp3.OkHttpClient import okhttp3.Request import java.io.Closeable import java.io.File import java.net.InetSocketAddress import java.net.Proxy +import kotlin.math.min + +data class SemVer( + val major: Int, + val minor: Int, + val patch: Int, + val preRelease: String? = null, + val buildNumber: Int? = null, +): Comparable { + + val isNotStable: Boolean = preRelease != null + + override fun compareTo(other: SemVer?): Int { + if (other == null) return 1 + return when { + major != other.major -> major.compareTo(other.major) + minor != other.minor -> minor.compareTo(other.minor) + patch != other.patch -> patch.compareTo(other.patch) + preRelease != null && other.preRelease != null -> { + val pr = preRelease.compareTo(other.preRelease, ignoreCase = true) + when { + pr != 0 -> pr + buildNumber != null && other.buildNumber != null -> buildNumber.compareTo(other.buildNumber) + buildNumber != null -> -1 + other.buildNumber != null -> 1 + else -> 0 + } + } + preRelease != null -> -1 + other.preRelease != null -> 1 + else -> 0 + } + } + + companion object { + private val regex = Regex("^(\\d+)\\.(\\d+)\\.(\\d+)(?:-([A-Za-z]+)\\.(\\d+))?\$") + fun from(tagName: String): SemVer? { + val trimmed = tagName.trimStart { it == 'v' } + val redacted = when { + trimmed.contains('-') && trimmed.substringBefore('-').count { it == '.' } == 1 -> "${trimmed.substringBefore('-')}.0-${trimmed.substringAfter('-')}" + trimmed.substringBefore('-').count { it == '.' } == 1 -> "${trimmed}.0" + else -> trimmed + } + val group = regex.matchEntire(redacted)?.groups + return if (group != null) { + SemVer( + major = group[1]?.value?.toIntOrNull() ?: return null, + minor = group[2]?.value?.toIntOrNull() ?: return null, + patch = group[3]?.value?.toIntOrNull() ?: return null, + preRelease = group[4]?.value, + buildNumber = group[5]?.value?.toIntOrNull(), + ) + } else { + null + } + } + + fun fromCurrentVersionName(): SemVer? { + val currentVersionName = if (appPlatform.isAndroid) BuildConfigCommon.ANDROID_VERSION_NAME else BuildConfigCommon.DESKTOP_VERSION_NAME + return from(currentVersionName) + } + } +} @Serializable data class GitHubRelease( @@ -34,12 +98,18 @@ data class GitHubRelease( val htmlUrl: String, val name: String, val draft: Boolean, - val prerelease: Boolean, + @SerialName("prerelease") + private val preRelease: Boolean, val body: String, @SerialName("published_at") val publishedAt: String, val assets: List -) +) { + @Transient + val semVer: SemVer? = SemVer.from(tagName) + + val isConsideredBeta: Boolean = preRelease || semVer == null || semVer.isNotStable +} @Serializable data class GitHubAsset( @@ -105,25 +175,25 @@ private fun createUpdateJob() { fun checkForUpdate() { Log.d(TAG, "Checking for update") + val currentSemVer = SemVer.fromCurrentVersionName() + if (currentSemVer == null) { + Log.e(TAG, "Current SemVer cannot be parsed") + return + } val client = setupHttpClient() try { val request = Request.Builder().url("https://api.github.com/repos/simplex-chat/simplex-chat/releases").addHeader("User-agent", "curl").build() client.newCall(request).execute().use { response -> response.body?.use { val body = it.string() - val releases = json.decodeFromString>(body).filterNot { it.draft } + val releases = json.decodeFromString>(body) val release = when (appPrefs.appUpdateChannel.get()) { - AppUpdatesChannel.STABLE -> releases.firstOrNull { !it.prerelease } - AppUpdatesChannel.BETA -> releases.firstOrNull() + AppUpdatesChannel.STABLE -> releases.firstOrNull { r -> !r.draft && !r.isConsideredBeta && currentSemVer < r.semVer } + AppUpdatesChannel.BETA -> releases.firstOrNull { r -> !r.draft && currentSemVer < r.semVer } AppUpdatesChannel.DISABLED -> return - } ?: return - val currentVersionName = "v" + (if (appPlatform.isAndroid) BuildConfigCommon.ANDROID_VERSION_NAME else BuildConfigCommon.DESKTOP_VERSION_NAME) - val redactedCurrentVersionName = when { - currentVersionName.contains('-') && currentVersionName.substringBefore('-').count { it == '.' } == 1 -> "${currentVersionName.substringBefore('-')}.0-${currentVersionName.substringAfter('-')}" - currentVersionName.substringBefore('-').count { it == '.' } == 1 -> "${currentVersionName}.0" - else -> currentVersionName } - if (release.tagName == appPrefs.appSkippedUpdate.get() || release.tagName == currentVersionName || release.tagName == redactedCurrentVersionName) { + + if (release == null || release.tagName == appPrefs.appSkippedUpdate.get()) { Log.d(TAG, "Skipping update because of the same version or skipped version") return } diff --git a/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/SemVerTest.kt b/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/SemVerTest.kt new file mode 100644 index 0000000000..561911773f --- /dev/null +++ b/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/SemVerTest.kt @@ -0,0 +1,63 @@ +package chat.simplex.app + +import chat.simplex.common.views.helpers.SemVer +import kotlin.test.Test +import kotlin.test.assertEquals + +// use this command for testing: +// ./gradlew desktopTest +class SemVerTest { + @Test + fun testValidSemVer() { + assertEquals(SemVer.from("1.0.0"), SemVer(1, 0, 0)) + assertEquals(SemVer.from("1.0"), SemVer(1, 0, 0)) + assertEquals(SemVer.from("v1.0"), SemVer(1, 0, 0)) + assertEquals(SemVer.from("v1.0-beta.1"), SemVer(1, 0, 0, "beta", 1)) + val r = listOf>( + "0.0.4" to SemVer(0, 0, 4), + "1.2.3" to SemVer(1, 2, 3), + "10.20.30" to SemVer(10, 20, 30), + "1.0.0-alpha.1" to SemVer(1, 0, 0, "alpha", buildNumber = 1), + "1.0.0" to SemVer(1, 0, 0), + "2.0.0" to SemVer(2, 0, 0), + "1.1.7" to SemVer(1, 1, 7), + "2.0.1-alpha.1227" to SemVer(2, 0, 1, "alpha", 1227), + ) + r.forEach { (value, correct) -> + assertEquals(SemVer.from(value), correct) + } + } + + @Test + fun testComparisonSemVer() { + assert(SemVer(0, 1, 0) == SemVer.from("0.1.0")) + assert(SemVer(1, 1, 0) == SemVer.from("v1.1.0")) + assert(SemVer(0, 1, 0) > SemVer(0, 0, 1)) + assert(SemVer(1, 0, 0) > SemVer(0, 100, 100)) + assert(SemVer(0, 200, 0) > SemVer(0, 100, 100)) + assert(SemVer(0, 1, 0, "beta") > SemVer(0, 1, 0, "alpha")) + assert(SemVer(0, 1, 0) > SemVer(0, 1, 0, "alpha")) + assert(SemVer(0, 1, 0) > SemVer(0, 1, 0, "beta")) + assert(SemVer(0, 1, 0) > SemVer(0, 1, 0, "beta.0")) + assert(SemVer(0, 1, 0, "beta", 1) > SemVer(0, 1, 0, "beta", 0)) + assert(SemVer(0, 1, 0, "beta", 11) > SemVer(0, 1, 0, "beta", 10)) + assert(SemVer(0, 1, 0, "beta", 11) > SemVer(0, 1, 0, "beta", 9)) + assert(SemVer(0, 1, 0, "beta.1") > SemVer(0, 1, 0, "alpha.2")) + assert(SemVer(1, 1, 0, "beta.1") > SemVer(0, 1, 0, "beta.1")) + assert(SemVer(1, 0, 0) > SemVer(1, 0, 0, "beta.1")) + assert(SemVer(1, 0, 0) > null) + assert(SemVer.from("v6.0.0")!! > SemVer.from("v6.0.0-beta.3")) + assert(SemVer.from("v6.0.0-beta.3")!! > SemVer.from("v6.0.0-beta.2")) + assert(SemVer.from("0.1.0") == SemVer.from("0.1.0")) + assert(SemVer.from("0.1.1")!! > SemVer.from("0.1.0")) + assert(SemVer.from("0.2.1")!! > SemVer.from("0.1.1")) + assert(SemVer.from("2.0.1")!! > SemVer.from("0.1.1")) + assert(SemVer.from("0.1.1-beta.0")!! > SemVer.from("0.1.0-beta.0")) + assert(SemVer.from("0.1.1-beta.0")!! == SemVer.from("0.1.1-beta.0")) + assert(SemVer.from("0.1.1-beta.1")!! > SemVer.from("0.1.1-beta.0")) + assert(SemVer.from("10.0.0-beta.12")!! > SemVer.from("1.1.1")) + assert(SemVer.from("1.1.1-beta.120")!! > SemVer.from("1.1.1-alpha.9")) + assert(SemVer.from("1.1.1-beta.120")!! > SemVer.from("1.1.1-alpha.120")) + assert(SemVer.from("2.0.1")!! > SemVer.from("0.1.1")) + } +}