From 315879fe6b3143e3828259284e8e332be163411f Mon Sep 17 00:00:00 2001 From: tooomm Date: Tue, 7 Jul 2026 22:30:20 +0200 Subject: [PATCH 1/7] first version --- .../network/update/client/release_channel.cpp | 133 ++++++++++-------- .../network/update/client/release_channel.h | 4 +- 2 files changed, 76 insertions(+), 61 deletions(-) diff --git a/cockatrice/src/client/network/update/client/release_channel.cpp b/cockatrice/src/client/network/update/client/release_channel.cpp index 260167bc8..5f438e26d 100644 --- a/cockatrice/src/client/network/update/client/release_channel.cpp +++ b/cockatrice/src/client/network/update/client/release_channel.cpp @@ -2,6 +2,7 @@ #include "version_string.h" +#include #include #include #include @@ -44,54 +45,84 @@ void ReleaseChannel::checkForUpdates() } // Different release channel checking functions for different operating systems -bool ReleaseChannel::downloadMatchesCurrentOS(const QString &fileName) +std::optional ReleaseChannel::getTargetVersionForCurrentOS(const QString &fileName) { #if defined(Q_OS_MACOS) - static QRegularExpression version_regex("macOS(\\d+)"); - auto match = version_regex.match(fileName); - if (!match.hasMatch()) { - return false; - } - - auto getSystemVersion = [] { - // QSysInfo does not go through translation layers - // We need to use sysctl to reliably detect the underlying architecture + const bool isIntel = [] { char arch[255]; size_t len = sizeof(arch); if (sysctlbyname("machdep.cpu.brand_string", arch, &len, nullptr, 0) == 0) { - // Intel mac is only supported on macOS 13 versions - if (QString::fromUtf8(arch).contains("Intel")) { - return 13; - } + return QString::fromUtf8(arch).contains("Intel"); } - - return QSysInfo::productVersion().split(".")[0].toInt(); - }; - - // older(smaller) releases are compatible with a newer or the same system version - int sys_maj = getSystemVersion(); - int rel_maj = match.captured(1).toInt(); - return rel_maj == sys_maj; + return false; + }(); + const int systemVersion = QSysInfo::productVersion().split(".")[0].toInt(); + if (isIntel) { + static QRegularExpression regex(R"(macOS(\d+)_Intel)"); + auto match = regex.match(fileName); + if (!match.hasMatch()) { + return std::nullopt; + } + int version = match.captured(1).toInt(); + if (version <= systemVersion) { + return version; + } + return std::nullopt; + } + static QRegularExpression regex(R"(macOS(\d+)(?!_Intel))"); + auto match = regex.match(fileName); + if (!match.hasMatch()) { + return std::nullopt; + } + int version = match.captured(1).toInt(); + if (version <= systemVersion) { + return version; + } + return std::nullopt; #elif defined(Q_OS_WIN) -#if Q_PROCESSOR_WORDSIZE == 4 - return fileName.contains("32bit"); -#elif Q_PROCESSOR_WORDSIZE == 8 - const QString &version = QSysInfo::productVersion(); - if (version.startsWith("7") || version.startsWith("8")) { - return fileName.contains("Win7"); - } else { - return fileName.contains("Win10"); +#if Q_PROCESSOR_WORDSIZE == 8 + const int systemVersion = QSysInfo::productVersion().split(".")[0].toInt(); + static QRegularExpression regex(R"(Windows(\d+))"); + auto match = regex.match(fileName); + if (!match.hasMatch()) { + return std::nullopt; } + int version = match.captured(1).toInt(); + if (version <= systemVersion) { + return version; + } +#endif + return std::nullopt; #else Q_UNUSED(fileName); - return false; + return std::nullopt; #endif +} -#else // If the OS doesn't fit one of the above #defines, then it will never match - Q_UNUSED(fileName); - return false; -#endif +QString ReleaseChannel::findBestDownloadUrl(const QVariantList &assets) +{ + QString bestUrl; + int bestVersion = -1; + for (const auto &rawAsset : assets) { + QVariantMap asset = rawAsset.toMap(); + QString name = asset["name"].toString(); + QString url = asset["browser_download_url"].toString(); + auto version = getTargetVersionForCurrentOS(name); + if (!version) { + continue; + } + if (*version > bestVersion) { + bestVersion = *version; + bestUrl = url; + } + } + if (!bestUrl.isEmpty()) { + qCInfo(ReleaseChannelLog) + << "Selected compatible asset version=" << bestVersion + << "url=" << bestUrl; + } + return bestUrl; } QString StableReleaseChannel::getManualDownloadUrl() const @@ -138,16 +169,9 @@ void StableReleaseChannel::releaseListFinished() lastRelease->setPublishDate(resultMap["published_at"].toDate()); if (resultMap.contains("assets")) { - auto rawAssets = resultMap["assets"].toList(); - for (const auto &rawAsset : rawAssets) { - QVariantMap asset = rawAsset.toMap(); - QString name = asset["name"].toString(); - QString url = asset["browser_download_url"].toString(); - - if (downloadMatchesCurrentOS(name)) { - lastRelease->setDownloadUrl(url); - break; - } + auto url = findBestDownloadUrl(resultMap["assets"].toList()); + if (!url.isEmpty()) { + lastRelease->setDownloadUrl(url); } } @@ -289,21 +313,10 @@ void BetaReleaseChannel::fileListFinished() bool needToUpdate = (QString::compare(shortHash, myHash, Qt::CaseInsensitive) != 0); bool compatibleVersion = false; - QStringList resultUrlList{}; - for (QVariant file : resultList) { - QVariantMap map = file.toMap(); - resultUrlList << map["browser_download_url"].toString(); - } - - resultUrlList.sort(); - // iterate in reverse so the first item is the latest os version - for (auto url = resultUrlList.rbegin(); url < resultUrlList.rend(); ++url) { - if (downloadMatchesCurrentOS(*url)) { - compatibleVersion = true; - lastRelease->setDownloadUrl(*url); - qCInfo(ReleaseChannelLog) << "Found compatible version url=" << *url; - break; - } + QString downloadUrl = findBestDownloadUrl(resultList); + if (!downloadUrl.isEmpty()) { + compatibleVersion = true; + lastRelease->setDownloadUrl(downloadUrl); } emit finishedCheck(needToUpdate, compatibleVersion, lastRelease); diff --git a/cockatrice/src/client/network/update/client/release_channel.h b/cockatrice/src/client/network/update/client/release_channel.h index c56d0cfce..79a23f8cc 100644 --- a/cockatrice/src/client/network/update/client/release_channel.h +++ b/cockatrice/src/client/network/update/client/release_channel.h @@ -7,6 +7,7 @@ #ifndef RELEASECHANNEL_H #define RELEASECHANNEL_H +#include #include #include #include @@ -96,7 +97,8 @@ protected: Release *lastRelease; protected: - static bool downloadMatchesCurrentOS(const QString &fileName); + std::optional getTargetVersionForCurrentOS(const QString &fileName); + QString findBestDownloadUrl(const QVariantList &assets); [[nodiscard]] virtual QString getReleaseChannelUrl() const = 0; public: From 3ad7bae16db442c2eb15a50194b7358bd2325e83 Mon Sep 17 00:00:00 2001 From: tooomm Date: Tue, 7 Jul 2026 23:03:18 +0200 Subject: [PATCH 2/7] combined logic + better regex --- .../network/update/client/release_channel.cpp | 63 +++++++------------ 1 file changed, 24 insertions(+), 39 deletions(-) diff --git a/cockatrice/src/client/network/update/client/release_channel.cpp b/cockatrice/src/client/network/update/client/release_channel.cpp index 5f438e26d..42ded9076 100644 --- a/cockatrice/src/client/network/update/client/release_channel.cpp +++ b/cockatrice/src/client/network/update/client/release_channel.cpp @@ -49,6 +49,8 @@ std::optional ReleaseChannel::getTargetVersionForCurrentOS(const QString &f { #if defined(Q_OS_MACOS) const bool isIntel = [] { + // QSysInfo does not go through translation layers + // We need to use sysctl to reliably detect the underlying architecture char arch[255]; size_t len = sizeof(arch); if (sysctlbyname("machdep.cpu.brand_string", arch, &len, nullptr, 0) == 0) { @@ -56,48 +58,32 @@ std::optional ReleaseChannel::getTargetVersionForCurrentOS(const QString &f } return false; }(); - const int systemVersion = QSysInfo::productVersion().split(".")[0].toInt(); - if (isIntel) { - static QRegularExpression regex(R"(macOS(\d+)_Intel)"); - auto match = regex.match(fileName); - if (!match.hasMatch()) { - return std::nullopt; - } - int version = match.captured(1).toInt(); - if (version <= systemVersion) { - return version; - } - return std::nullopt; - } - static QRegularExpression regex(R"(macOS(\d+)(?!_Intel))"); - auto match = regex.match(fileName); - if (!match.hasMatch()) { - return std::nullopt; - } - int version = match.captured(1).toInt(); - if (version <= systemVersion) { - return version; - } - return std::nullopt; + static const QRegularExpression macIntelRegex(R"(macOS(\d+)_Intel\.[^.]+$)", QRegularExpression::CaseInsensitiveOption); + static const QRegularExpression macArmRegex(R"(macOS(\d+)\.[^.]+$)", QRegularExpression::CaseInsensitiveOption); + const QRegularExpression ®ex = isIntel ? macIntelRegex : macArmRegex; #elif defined(Q_OS_WIN) -#if Q_PROCESSOR_WORDSIZE == 8 - const int systemVersion = QSysInfo::productVersion().split(".")[0].toInt(); - static QRegularExpression regex(R"(Windows(\d+))"); - auto match = regex.match(fileName); - if (!match.hasMatch()) { - return std::nullopt; - } - int version = match.captured(1).toInt(); - if (version <= systemVersion) { - return version; - } -#endif - return std::nullopt; -#else +#if Q_PROCESSOR_WORDSIZE != 8 // non 64-bit host Q_UNUSED(fileName); return std::nullopt; #endif + static const QRegularExpression regex(R"(Win(?:dows)?(\d+)\.[^.]+$)", QRegularExpression::CaseInsensitiveOption); + +#else // if the OS doesn't fit one of the above #defines, then it will never match + Q_UNUSED(fileName); + return std::nullopt; +#endif + + const int systemVersion = QSysInfo::productVersion().split('.').first().toInt(); + auto match = regex.match(fileName); + if (!match.hasMatch()) { + return std::nullopt; + } + const int targetVersion = match.captured(1).toInt(); + if (targetVersion > systemVersion) { + return std::nullopt; + } + return targetVersion; } QString ReleaseChannel::findBestDownloadUrl(const QVariantList &assets) @@ -119,8 +105,7 @@ QString ReleaseChannel::findBestDownloadUrl(const QVariantList &assets) } if (!bestUrl.isEmpty()) { qCInfo(ReleaseChannelLog) - << "Selected compatible asset version=" << bestVersion - << "url=" << bestUrl; + << "Best compatible asset=" << bestUrl; } return bestUrl; } From 1675e318cc068b5567adde03dbf88e17651c47d2 Mon Sep 17 00:00:00 2001 From: tooomm Date: Wed, 8 Jul 2026 15:54:08 +0200 Subject: [PATCH 3/7] Simplify, we only support 64bit, installer prevents wrong installs --- .../src/client/network/update/client/release_channel.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cockatrice/src/client/network/update/client/release_channel.cpp b/cockatrice/src/client/network/update/client/release_channel.cpp index 42ded9076..5ed276765 100644 --- a/cockatrice/src/client/network/update/client/release_channel.cpp +++ b/cockatrice/src/client/network/update/client/release_channel.cpp @@ -63,10 +63,6 @@ std::optional ReleaseChannel::getTargetVersionForCurrentOS(const QString &f const QRegularExpression ®ex = isIntel ? macIntelRegex : macArmRegex; #elif defined(Q_OS_WIN) -#if Q_PROCESSOR_WORDSIZE != 8 // non 64-bit host - Q_UNUSED(fileName); - return std::nullopt; -#endif static const QRegularExpression regex(R"(Win(?:dows)?(\d+)\.[^.]+$)", QRegularExpression::CaseInsensitiveOption); #else // if the OS doesn't fit one of the above #defines, then it will never match From 42d001ee6ada86d9cf93c6c42f0e8b142c938d4c Mon Sep 17 00:00:00 2001 From: tooomm Date: Wed, 8 Jul 2026 16:21:06 +0200 Subject: [PATCH 4/7] lint + fix Linux versions (missing declaration) --- .../network/update/client/release_channel.cpp | 19 +++++++++++-------- .../network/update/client/release_channel.h | 2 +- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/cockatrice/src/client/network/update/client/release_channel.cpp b/cockatrice/src/client/network/update/client/release_channel.cpp index 5ed276765..beba05c7d 100644 --- a/cockatrice/src/client/network/update/client/release_channel.cpp +++ b/cockatrice/src/client/network/update/client/release_channel.cpp @@ -2,7 +2,6 @@ #include "version_string.h" -#include #include #include #include @@ -11,6 +10,7 @@ #include #include #include +#include #if defined(Q_OS_MACOS) #include @@ -47,6 +47,8 @@ void ReleaseChannel::checkForUpdates() // Different release channel checking functions for different operating systems std::optional ReleaseChannel::getTargetVersionForCurrentOS(const QString &fileName) { + const QRegularExpression *regex = nullptr; + #if defined(Q_OS_MACOS) const bool isIntel = [] { // QSysInfo does not go through translation layers @@ -58,12 +60,14 @@ std::optional ReleaseChannel::getTargetVersionForCurrentOS(const QString &f } return false; }(); - static const QRegularExpression macIntelRegex(R"(macOS(\d+)_Intel\.[^.]+$)", QRegularExpression::CaseInsensitiveOption); + static const QRegularExpression macIntelRegex(R"(macOS(\d+)_Intel\.[^.]+$)", + QRegularExpression::CaseInsensitiveOption); static const QRegularExpression macArmRegex(R"(macOS(\d+)\.[^.]+$)", QRegularExpression::CaseInsensitiveOption); - const QRegularExpression ®ex = isIntel ? macIntelRegex : macArmRegex; + regex = isIntel ? &macIntelRegex : &macArmRegex; #elif defined(Q_OS_WIN) - static const QRegularExpression regex(R"(Win(?:dows)?(\d+)\.[^.]+$)", QRegularExpression::CaseInsensitiveOption); + static const QRegularExpression winRegex(R"(Win(?:dows)?(\d+)\.[^.]+$)", QRegularExpression::CaseInsensitiveOption); + regex = &winRegex; #else // if the OS doesn't fit one of the above #defines, then it will never match Q_UNUSED(fileName); @@ -71,7 +75,7 @@ std::optional ReleaseChannel::getTargetVersionForCurrentOS(const QString &f #endif const int systemVersion = QSysInfo::productVersion().split('.').first().toInt(); - auto match = regex.match(fileName); + auto match = regex->match(fileName); if (!match.hasMatch()) { return std::nullopt; } @@ -100,8 +104,7 @@ QString ReleaseChannel::findBestDownloadUrl(const QVariantList &assets) } } if (!bestUrl.isEmpty()) { - qCInfo(ReleaseChannelLog) - << "Best compatible asset=" << bestUrl; + qCInfo(ReleaseChannelLog) << "Best compatible asset=" << bestUrl; } return bestUrl; } @@ -152,7 +155,7 @@ void StableReleaseChannel::releaseListFinished() if (resultMap.contains("assets")) { auto url = findBestDownloadUrl(resultMap["assets"].toList()); if (!url.isEmpty()) { - lastRelease->setDownloadUrl(url); + lastRelease->setDownloadUrl(url); } } diff --git a/cockatrice/src/client/network/update/client/release_channel.h b/cockatrice/src/client/network/update/client/release_channel.h index 79a23f8cc..c7a4ebc48 100644 --- a/cockatrice/src/client/network/update/client/release_channel.h +++ b/cockatrice/src/client/network/update/client/release_channel.h @@ -7,12 +7,12 @@ #ifndef RELEASECHANNEL_H #define RELEASECHANNEL_H -#include #include #include #include #include #include +#include #include inline Q_LOGGING_CATEGORY(ReleaseChannelLog, "release_channel"); From 8c126cdf7e5cda1937017b2ea3545d09d7978fbc Mon Sep 17 00:00:00 2001 From: tooomm Date: Fri, 17 Jul 2026 17:08:26 +0200 Subject: [PATCH 5/7] update regex --- .../src/client/network/update/client/release_channel.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cockatrice/src/client/network/update/client/release_channel.cpp b/cockatrice/src/client/network/update/client/release_channel.cpp index beba05c7d..af802a3d2 100644 --- a/cockatrice/src/client/network/update/client/release_channel.cpp +++ b/cockatrice/src/client/network/update/client/release_channel.cpp @@ -60,13 +60,14 @@ std::optional ReleaseChannel::getTargetVersionForCurrentOS(const QString &f } return false; }(); - static const QRegularExpression macIntelRegex(R"(macOS(\d+)_Intel\.[^.]+$)", + static const QRegularExpression macIntelRegex(R"(-macOS(\d+)_Intel\.[^.]+$)", QRegularExpression::CaseInsensitiveOption); - static const QRegularExpression macArmRegex(R"(macOS(\d+)\.[^.]+$)", QRegularExpression::CaseInsensitiveOption); + static const QRegularExpression macArmRegex(R"(-macOS(\d+)\.[^.]+$)", QRegularExpression::CaseInsensitiveOption); regex = isIntel ? &macIntelRegex : &macArmRegex; #elif defined(Q_OS_WIN) - static const QRegularExpression winRegex(R"(Win(?:dows)?(\d+)\.[^.]+$)", QRegularExpression::CaseInsensitiveOption); + static const QRegularExpression winRegex(R"(-(?:Win|Windows)(\d+)\.[^.]+$)", + QRegularExpression::CaseInsensitiveOption); regex = &winRegex; #else // if the OS doesn't fit one of the above #defines, then it will never match From 66d81e6f14364e854b808045d9cdd81009dd4c07 Mon Sep 17 00:00:00 2001 From: tooomm Date: Fri, 17 Jul 2026 18:08:01 +0200 Subject: [PATCH 6/7] improve/add comments --- .../src/client/network/update/client/release_channel.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cockatrice/src/client/network/update/client/release_channel.cpp b/cockatrice/src/client/network/update/client/release_channel.cpp index af802a3d2..35fe85fff 100644 --- a/cockatrice/src/client/network/update/client/release_channel.cpp +++ b/cockatrice/src/client/network/update/client/release_channel.cpp @@ -44,7 +44,7 @@ void ReleaseChannel::checkForUpdates() connect(response, &QNetworkReply::finished, this, &ReleaseChannel::releaseListFinished); } -// Different release channel checking functions for different operating systems +// Find compatible assets for host platform (Linux is not supported by in-client updater) std::optional ReleaseChannel::getTargetVersionForCurrentOS(const QString &fileName) { const QRegularExpression *regex = nullptr; @@ -75,6 +75,7 @@ std::optional ReleaseChannel::getTargetVersionForCurrentOS(const QString &f return std::nullopt; #endif + // Any asset targeting an OS version smaller or equal to the current host works const int systemVersion = QSysInfo::productVersion().split('.').first().toInt(); auto match = regex->match(fileName); if (!match.hasMatch()) { @@ -87,6 +88,7 @@ std::optional ReleaseChannel::getTargetVersionForCurrentOS(const QString &f return targetVersion; } +// Pick newest targeted version (highest number) amongst all compatible ones QString ReleaseChannel::findBestDownloadUrl(const QVariantList &assets) { QString bestUrl; From cc9cf9ba63bd491ff6a195c63070d53bbe0da044 Mon Sep 17 00:00:00 2001 From: tooomm Date: Fri, 24 Jul 2026 00:26:52 +0200 Subject: [PATCH 7/7] comments --- .../network/update/client/release_channel.cpp | 32 +++++++++++++++---- .../network/update/client/release_channel.h | 4 +-- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/cockatrice/src/client/network/update/client/release_channel.cpp b/cockatrice/src/client/network/update/client/release_channel.cpp index 35fe85fff..b760c021c 100644 --- a/cockatrice/src/client/network/update/client/release_channel.cpp +++ b/cockatrice/src/client/network/update/client/release_channel.cpp @@ -44,13 +44,28 @@ void ReleaseChannel::checkForUpdates() connect(response, &QNetworkReply::finished, this, &ReleaseChannel::releaseListFinished); } -// Find compatible assets for host platform (Linux is not supported by in-client updater) +// Find assets compatible with host platform (Linux is not supported by in-client updater) std::optional ReleaseChannel::getTargetVersionForCurrentOS(const QString &fileName) { const QRegularExpression *regex = nullptr; + static const std::optional systemVersion = [] { + bool ok = false; + const QString versionString = QSysInfo::productVersion(); + const int version = versionString.split('.').first().toInt(&ok); + if (!ok) { + qCWarning(ReleaseChannelLog) << "Unable to determine OS version from" << versionString; + return std::optional(); + } + return std::optional{version}; + }(); + + if (!systemVersion) { + return std::nullopt; + } + #if defined(Q_OS_MACOS) - const bool isIntel = [] { + static const bool isIntel = [] { // QSysInfo does not go through translation layers // We need to use sysctl to reliably detect the underlying architecture char arch[255]; @@ -60,12 +75,16 @@ std::optional ReleaseChannel::getTargetVersionForCurrentOS(const QString &f } return false; }(); + + // Apple (ARM) --> Cockatrice-3.0.0-macOS15.dmg + // Apple (x86) --> Cockatrice-3.0.0-macOS15_Intel.dmg static const QRegularExpression macIntelRegex(R"(-macOS(\d+)_Intel\.[^.]+$)", QRegularExpression::CaseInsensitiveOption); - static const QRegularExpression macArmRegex(R"(-macOS(\d+)\.[^.]+$)", QRegularExpression::CaseInsensitiveOption); - regex = isIntel ? &macIntelRegex : &macArmRegex; + static const QRegularExpression macRegex(R"(-macOS(\d+)\.[^.]+$)", QRegularExpression::CaseInsensitiveOption); + regex = isIntel ? &macIntelRegex : &macRegex; #elif defined(Q_OS_WIN) + // Windows (x86) --> Cockatrice-3.0.0-Windows10.exe static const QRegularExpression winRegex(R"(-(?:Win|Windows)(\d+)\.[^.]+$)", QRegularExpression::CaseInsensitiveOption); regex = &winRegex; @@ -75,14 +94,13 @@ std::optional ReleaseChannel::getTargetVersionForCurrentOS(const QString &f return std::nullopt; #endif - // Any asset targeting an OS version smaller or equal to the current host works - const int systemVersion = QSysInfo::productVersion().split('.').first().toInt(); + // Any asset targeting an OS version up to the current host version works auto match = regex->match(fileName); if (!match.hasMatch()) { return std::nullopt; } const int targetVersion = match.captured(1).toInt(); - if (targetVersion > systemVersion) { + if (targetVersion > *systemVersion) { return std::nullopt; } return targetVersion; diff --git a/cockatrice/src/client/network/update/client/release_channel.h b/cockatrice/src/client/network/update/client/release_channel.h index c7a4ebc48..e038cb2f4 100644 --- a/cockatrice/src/client/network/update/client/release_channel.h +++ b/cockatrice/src/client/network/update/client/release_channel.h @@ -97,8 +97,8 @@ protected: Release *lastRelease; protected: - std::optional getTargetVersionForCurrentOS(const QString &fileName); - QString findBestDownloadUrl(const QVariantList &assets); + static std::optional getTargetVersionForCurrentOS(const QString &fileName); + static QString findBestDownloadUrl(const QVariantList &assets); [[nodiscard]] virtual QString getReleaseChannelUrl() const = 0; public: