From 92b913ca3740ea1aa799a69d65dc13d0c3612b87 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Sun, 15 May 2022 13:56:58 -0400 Subject: [PATCH 01/12] Add Sparkle Updater framework to macOS build --- CMakeLists.txt | 7 ++++++- launcher/CMakeLists.txt | 19 ++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e07d2aa6..6e0bfd4c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -179,6 +179,7 @@ if(UNIX AND APPLE) set(BINARY_DEST_DIR "${Launcher_Name}.app/Contents/MacOS") set(LIBRARY_DEST_DIR "${Launcher_Name}.app/Contents/MacOS") set(PLUGIN_DEST_DIR "${Launcher_Name}.app/Contents/MacOS") + set(FRAMEWORK_DEST_DIR "${Launcher_Name}.app/Contents/Frameworks") set(RESOURCES_DEST_DIR "${Launcher_Name}.app/Contents/Resources") set(JARS_DEST_DIR "${Launcher_Name}.app/Contents/MacOS/jars") @@ -195,8 +196,12 @@ if(UNIX AND APPLE) set(MACOSX_BUNDLE_ICON_FILE ${Launcher_Name}.icns) set(MACOSX_BUNDLE_COPYRIGHT "Copyright 2021-2022 ${Launcher_Copyright}") + set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.1.0/Sparkle-2.1.0.tar.xz" CACHE STRING "URL to Sparkle release archive") + set(MACOSX_SPARKLE_SHA256 "bf6ac1caa9f8d321d5784859c88da874f28412f37fb327bc21b7b14c5d61ef94" CACHE STRING "SHA256 checksum for Sparkle release archive") + set(MACOSX_SPARKLE_DIR "${CMAKE_BINARY_DIR}/frameworks/Sparkle") + # directories to look for dependencies - set(DIRS ${QT_LIBS_DIR} ${QT_LIBEXECS_DIR} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) + set(DIRS ${QT_LIBS_DIR} ${QT_LIBEXECS_DIR} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} ${MACOSX_SPARKLE_DIR}) # install as bundle set(INSTALL_BUNDLE "full") diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 8e75be20..87ce3b68 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -501,7 +501,7 @@ set(API_SOURCES modplatform/flame/FlameAPI.h modplatform/modrinth/ModrinthAPI.h - + modplatform/helpers/NetworkModAPI.h modplatform/helpers/NetworkModAPI.cpp ) @@ -984,6 +984,22 @@ target_link_libraries(Launcher_logic LocalPeer Launcher_rainbow ) +if(APPLE) + set(CMAKE_MACOSX_RPATH 1) + set(CMAKE_INSTALL_RPATH "@loader_path/../Frameworks/") + + file(DOWNLOAD ${MACOSX_SPARKLE_DOWNLOAD_URL} ${CMAKE_BINARY_DIR}/Sparkle.tar.xz EXPECTED_HASH SHA256=${MACOSX_SPARKLE_SHA256}) + file(ARCHIVE_EXTRACT INPUT ${CMAKE_BINARY_DIR}/Sparkle.tar.xz DESTINATION ${CMAKE_BINARY_DIR}/frameworks/Sparkle) + + find_library(SPARKLE_FRAMEWORK Sparkle "${CMAKE_BINARY_DIR}/frameworks/Sparkle") + target_link_libraries(Launcher_logic + "-framework AppKit" + "-framework Carbon" + "-framework Foundation" + "-framework ApplicationServices" + ) + target_link_libraries(Launcher_logic ${SPARKLE_FRAMEWORK}) +endif() target_link_libraries(Launcher_logic) @@ -1006,6 +1022,7 @@ install(TARGETS ${Launcher_Name} BUNDLE DESTINATION "." COMPONENT Runtime LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime + FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime ) #### The bundle mess! #### From 7eb61a28be3b66c1016eab434ae93b5d94eb11af Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Tue, 19 Apr 2022 02:09:16 -0400 Subject: [PATCH 02/12] Add build options for Sparkle updater Two new build options are added: `MAC_SPARKLE_PUB_KEY`: the public key used to verify the signatures of the appcast `MAC_SPARKLE_APPCAST_URL`: the URL where the `appcast.xml` is located If the updater should be disabled on macOS, set either of these to an empty string. --- CMakeLists.txt | 2 ++ buildconfig/BuildConfig.cpp.in | 8 ++++++++ buildconfig/BuildConfig.h | 6 ++++++ cmake/MacOSXBundleInfo.plist.in | 4 ++++ 4 files changed, 20 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6e0bfd4c..289a7ec4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -195,6 +195,8 @@ if(UNIX AND APPLE) set(MACOSX_BUNDLE_LONG_VERSION_STRING "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}") set(MACOSX_BUNDLE_ICON_FILE ${Launcher_Name}.icns) set(MACOSX_BUNDLE_COPYRIGHT "Copyright 2021-2022 ${Launcher_Copyright}") + set(MACOSX_SPARKLE_UPDATE_PUBLIC_KEY "") + set(MACOSX_SPARKLE_UPDATE_FEED_URL "") set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.1.0/Sparkle-2.1.0.tar.xz" CACHE STRING "URL to Sparkle release archive") set(MACOSX_SPARKLE_SHA256 "bf6ac1caa9f8d321d5784859c88da874f28412f37fb327bc21b7b14c5d61ef94" CACHE STRING "SHA256 checksum for Sparkle release archive") diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 70f8f7f0..2d07bc58 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -61,6 +61,14 @@ Config::Config() BUILD_PLATFORM = "@Launcher_BUILD_PLATFORM@"; UPDATER_BASE = "@Launcher_UPDATER_BASE@"; + MAC_SPARKLE_PUB_KEY = "@MACOSX_SPARKLE_UPDATE_PUBLIC_KEY@"; + MAC_SPARKLE_APPCAST_URL = "@MACOSX_SPARKLE_UPDATE_FEED_URL@"; + + if (BUILD_PLATFORM == "macOS" && !MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty()) + { + UPDATER_ENABLED = true; + } + GIT_COMMIT = "@Launcher_GIT_COMMIT@"; GIT_REFSPEC = "@Launcher_GIT_REFSPEC@"; if (GIT_REFSPEC == QStringLiteral("GITDIR-NOTFOUND")) diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 8594e46d..e41d4ba0 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -74,6 +74,12 @@ class Config { /// URL for the updater's channel QString UPDATER_BASE; + /// The public key used to sign releases for the Sparkle updater appcast + QString MAC_SPARKLE_PUB_KEY; + + /// URL for the Sparkle updater's appcast + QString MAC_SPARKLE_APPCAST_URL; + /// User-Agent to use. QString USER_AGENT; diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in index 9e663d31..1b22e21f 100644 --- a/cmake/MacOSXBundleInfo.plist.in +++ b/cmake/MacOSXBundleInfo.plist.in @@ -40,5 +40,9 @@ NSHumanReadableCopyright ${MACOSX_BUNDLE_COPYRIGHT} + SUPublicEDKey + ${MACOSX_SPARKLE_UPDATE_PUBLIC_KEY} + SUFeedURL + ${MACOSX_SPARKLE_UPDATE_FEED_URL} From ea4ef1655bdadf04c36768f0f641ca7579f754cf Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Wed, 20 Apr 2022 00:34:17 -0400 Subject: [PATCH 03/12] Create `SparkleUpdater` class for access from Qt/C++ To actually get automatic updates going, all that needs to happen is that `SparkleUpdater` needs to be initialized. The rest of the functions can be connected to elements in the UI. --- launcher/Application.cpp | 1 + launcher/CMakeLists.txt | 9 + launcher/updater/macsparkle/SparkleUpdater.h | 124 +++++++++++ launcher/updater/macsparkle/SparkleUpdater.mm | 206 ++++++++++++++++++ 4 files changed, 340 insertions(+) create mode 100644 launcher/updater/macsparkle/SparkleUpdater.h create mode 100644 launcher/updater/macsparkle/SparkleUpdater.mm diff --git a/launcher/Application.cpp b/launcher/Application.cpp index dc8a7b0d..456ea02c 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -544,6 +544,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) { m_settings.reset(new INISettingsObject(BuildConfig.LAUNCHER_CONFIGFILE, this)); // Updates + // Multiple channels are separated by spaces m_settings->registerSetting("UpdateChannel", BuildConfig.VERSION_CHANNEL); m_settings->registerSetting("AutoUpdate", true); diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 87ce3b68..dc10c38e 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -164,6 +164,11 @@ set(UPDATE_SOURCES updater/DownloadTask.cpp ) +set(MAC_UPDATE_SOURCES + updater/macsparkle/SparkleUpdater.h + updater/macsparkle/SparkleUpdater.mm +) + add_unit_test(UpdateChecker SOURCES updater/UpdateChecker_test.cpp LIBS Launcher_logic @@ -600,6 +605,10 @@ set(LOGIC_SOURCES ${ATLAUNCHER_SOURCES} ) +if(APPLE) + set (LOGIC_SOURCES ${LOGIC_SOURCES} ${MAC_UPDATE_SOURCES}) +endif() + SET(LAUNCHER_SOURCES # Application base Application.h diff --git a/launcher/updater/macsparkle/SparkleUpdater.h b/launcher/updater/macsparkle/SparkleUpdater.h new file mode 100644 index 00000000..9768d960 --- /dev/null +++ b/launcher/updater/macsparkle/SparkleUpdater.h @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Kenneth Chew + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef LAUNCHER_SPARKLEUPDATER_H +#define LAUNCHER_SPARKLEUPDATER_H + +#include +#include + +class SparkleUpdater : public QObject +{ + Q_OBJECT + +public: + /*! + * Start the Sparkle updater, which automatically checks for updates if necessary. + */ + SparkleUpdater(); + ~SparkleUpdater(); + + /*! + * Check for updates manually, showing the user a progress bar and an alert if no updates are found. + */ + void checkForUpdates(); + + /*! + * Indicates whether or not to check for updates automatically. + */ + bool getAutomaticallyChecksForUpdates(); + + /*! + * Indicates the current automatic update check interval in seconds. + */ + double getUpdateCheckInterval(); + + /*! + * Indicates the set of Sparkle channels the updater is allowed to find new updates from. + */ + QSet getAllowedChannels(); + + /*! + * Set whether or not to check for updates automatically. + * + * As per Sparkle documentation, "By default, Sparkle asks users on second launch for permission if they want + * automatic update checks enabled and sets this property based on their response. If SUEnableAutomaticChecks is + * set in the Info.plist, this permission request is not performed however. + * + * Setting this property will persist in the host bundle’s user defaults. Only set this property if you need + * dynamic behavior (e.g. user preferences). + * + * The update schedule cycle will be reset in a short delay after the property’s new value is set. This is to allow + * reverting this property without kicking off a schedule change immediately." + */ + void setAutomaticallyChecksForUpdates(bool check); + + /*! + * Set the current automatic update check interval in seconds. + * + * As per Sparkle documentation, "Setting this property will persist in the host bundle’s user defaults. For this + * reason, only set this property if you need dynamic behavior (eg user preferences). Otherwise prefer to set + * SUScheduledCheckInterval directly in your Info.plist. + * + * The update schedule cycle will be reset in a short delay after the property’s new value is set. This is to allow + * reverting this property without kicking off a schedule change immediately." + */ + void setUpdateCheckInterval(double seconds); + + /*! + * Clears all allowed Sparkle channels, returning to the default updater channel behavior. + */ + void clearAllowedChannels(); + + /*! + * Set a single Sparkle channel the updater is allowed to find new updates from. + * + * Items in the default channel can always be found, regardless of this setting. If an empty string is passed, + * return to the default behavior. + */ + void setAllowedChannel(const QString& channel); + + /*! + * Set a set of Sparkle channels the updater is allowed to find new updates from. + * + * Items in the default channel can always be found, regardless of this setting. If an empty set is passed, + * return to the default behavior. + */ + void setAllowedChannels(const QSet& channels); + +signals: + /*! + * Emits whenever the user's ability to check for updates changes. + * + * As per Sparkle documentation, "An update check can be made by the user when an update session isn’t in progress, + * or when an update or its progress is being shown to the user. A user cannot check for updates when data (such + * as the feed or an update) is still being downloaded automatically in the background. + * + * This property is suitable to use for menu item validation for seeing if checkForUpdates can be invoked." + */ + void canCheckForUpdatesChanged(bool canCheck); + +private: + class Private; + + Private* priv; + + void loadChannelsFromSettings(); +}; + +#endif //LAUNCHER_SPARKLEUPDATER_H diff --git a/launcher/updater/macsparkle/SparkleUpdater.mm b/launcher/updater/macsparkle/SparkleUpdater.mm new file mode 100644 index 00000000..0d4119a4 --- /dev/null +++ b/launcher/updater/macsparkle/SparkleUpdater.mm @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Kenneth Chew + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "SparkleUpdater.h" + +#include "Application.h" + +#include +#include + +@interface UpdaterObserver : NSObject + +@property(nonatomic, readonly) SPUUpdater* updater; + +/// A callback to run when the state of `canCheckForUpdates` for the `updater` changes. +@property(nonatomic, copy) void (^callback) (bool); + +- (id)initWithUpdater:(SPUUpdater*)updater; + +@end + +@implementation UpdaterObserver + +- (id)initWithUpdater:(SPUUpdater*)updater +{ + self = [super init]; + _updater = updater; + [self addObserver:self forKeyPath:@"updater.canCheckForUpdates" options:NSKeyValueObservingOptionNew context:nil]; + + return self; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context +{ + if ([keyPath isEqualToString:@"updater.canCheckForUpdates"]) + { + bool canCheck = [change[NSKeyValueChangeNewKey] boolValue]; + self.callback(canCheck); + } +} + +@end + + +@interface UpdaterDelegate : NSObject + +@property(nonatomic, copy) NSSet *allowedChannels; + +@end + +@implementation UpdaterDelegate + +- (NSSet *)allowedChannelsForUpdater:(SPUUpdater *)updater +{ + return _allowedChannels; +} + +@end + + +class SparkleUpdater::Private +{ +public: + SPUStandardUpdaterController *updaterController; + UpdaterObserver *updaterObserver; + UpdaterDelegate *updaterDelegate; + NSAutoreleasePool *autoReleasePool; +}; + +SparkleUpdater::SparkleUpdater() +{ + priv = new SparkleUpdater::Private(); + + // Enable Cocoa's memory management. + NSApplicationLoad(); + priv->autoReleasePool = [[NSAutoreleasePool alloc] init]; + + // Delegate is used for setting/getting allowed update channels. + priv->updaterDelegate = [[UpdaterDelegate alloc] init]; + + // Controller is the interface for actually doing the updates. + priv->updaterController = [[SPUStandardUpdaterController alloc] initWithStartingUpdater:true + updaterDelegate:priv->updaterDelegate + userDriverDelegate:nil]; + + priv->updaterObserver = [[UpdaterObserver alloc] initWithUpdater:priv->updaterController.updater]; + // Use KVO to run a callback that emits a Qt signal when `canCheckForUpdates` changes, so the UI can respond accordingly. + priv->updaterObserver.callback = ^(bool canCheck) { + emit canCheckForUpdatesChanged(canCheck); + }; + + loadChannelsFromSettings(); +} + +SparkleUpdater::~SparkleUpdater() +{ + [priv->updaterObserver removeObserver:priv->updaterObserver forKeyPath:@"updater.canCheckForUpdates"]; + + [priv->updaterController release]; + [priv->updaterObserver release]; + [priv->updaterDelegate release]; + [priv->autoReleasePool release]; + delete priv; +} + +void SparkleUpdater::checkForUpdates() +{ + [priv->updaterController checkForUpdates:nil]; +} + +bool SparkleUpdater::getAutomaticallyChecksForUpdates() +{ + return priv->updaterController.updater.automaticallyChecksForUpdates; +} + +double SparkleUpdater::getUpdateCheckInterval() +{ + return priv->updaterController.updater.updateCheckInterval; +} + +QSet SparkleUpdater::getAllowedChannels() +{ + // Convert NSSet -> QSet + __block QSet channels; + [priv->updaterDelegate.allowedChannels enumerateObjectsUsingBlock:^(NSString *channel, BOOL *stop) + { + channels.insert(QString::fromNSString(channel)); + }]; + return channels; +} + +void SparkleUpdater::setAutomaticallyChecksForUpdates(bool check) +{ + priv->updaterController.updater.automaticallyChecksForUpdates = check ? YES : NO; // make clang-tidy happy +} + +void SparkleUpdater::setUpdateCheckInterval(double seconds) +{ + priv->updaterController.updater.updateCheckInterval = seconds; +} + +void SparkleUpdater::clearAllowedChannels() +{ + priv->updaterDelegate.allowedChannels = [NSSet set]; + APPLICATION->settings()->set("UpdateChannel", ""); +} + +void SparkleUpdater::setAllowedChannel(const QString &channel) +{ + if (channel.isEmpty()) + { + clearAllowedChannels(); + return; + } + + NSSet *nsChannels = [NSSet setWithObject:channel.toNSString()]; + priv->updaterDelegate.allowedChannels = nsChannels; + qDebug() << channel; + APPLICATION->settings()->set("UpdateChannel", channel); +} + +void SparkleUpdater::setAllowedChannels(const QSet &channels) +{ + if (channels.isEmpty()) + { + clearAllowedChannels(); + return; + } + + QString channelsConfig = ""; + // Convert QSet -> NSSet + NSMutableSet *nsChannels = [NSMutableSet setWithCapacity:channels.count()]; + foreach (const QString channel, channels) + { + [nsChannels addObject:channel.toNSString()]; + channelsConfig += channel + " "; + } + + priv->updaterDelegate.allowedChannels = nsChannels; + APPLICATION->settings()->set("UpdateChannel", channelsConfig.trimmed()); +} + +void SparkleUpdater::loadChannelsFromSettings() +{ + QStringList channelList = APPLICATION->settings()->get("UpdateChannel").toString().split(" "); + auto channels = QSet::fromList(channelList); + setAllowedChannels(channels); +} From b5bdfa6c2e9a0eb62e476dd399b82bfa972e0320 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Wed, 20 Apr 2022 22:34:13 -0400 Subject: [PATCH 04/12] Implement automatic and manual updates on macOS --- launcher/ui/MainWindow.cpp | 9 ++++++++- launcher/updater/UpdateChecker.cpp | 21 +++++++++++++++++++++ launcher/updater/UpdateChecker.h | 15 +++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index f016dc76..12761da1 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -283,7 +283,7 @@ public: updateLaunchAction(); } - void createMainToolbarActions(QMainWindow *MainWindow) + void createMainToolbarActions(MainWindow *MainWindow) { actionAddInstance = TranslatedAction(MainWindow); actionAddInstance->setObjectName(QStringLiteral("actionAddInstance")); @@ -1027,6 +1027,13 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow { updater->checkForUpdate(APPLICATION->settings()->get("UpdateChannel").toString(), false); } + +#ifdef Q_OS_MAC + connect(APPLICATION->updateChecker()->getSparkleUpdater(), + &SparkleUpdater::canCheckForUpdatesChanged, + this, + &MainWindow::updatesAllowedChanged); +#endif } setSelectedInstanceById(APPLICATION->settings()->get("SelectedInstance").toString()); diff --git a/launcher/updater/UpdateChecker.cpp b/launcher/updater/UpdateChecker.cpp index efdb6093..ad159841 100644 --- a/launcher/updater/UpdateChecker.cpp +++ b/launcher/updater/UpdateChecker.cpp @@ -44,8 +44,28 @@ bool UpdateChecker::hasChannels() const return !m_channels.isEmpty(); } +#ifdef Q_OS_MAC +SparkleUpdater* UpdateChecker::getSparkleUpdater() +{ + return m_sparkleUpdater; +} +#endif + void UpdateChecker::checkForUpdate(QString updateChannel, bool notifyNoUpdate) { +#ifdef Q_OS_MAC + m_sparkleUpdater->setAllowedChannel(updateChannel); + if (notifyNoUpdate) + { + qDebug() << "Checking for updates."; + m_sparkleUpdater->checkForUpdates(); + } + else + { + // Sparkle already handles automatic update checks. + return; + } +#else qDebug() << "Checking for updates."; // If the channel list hasn't loaded yet, load it and defer checking for updates until @@ -109,6 +129,7 @@ void UpdateChecker::checkForUpdate(QString updateChannel, bool notifyNoUpdate) connect(indexJob.get(), &NetJob::succeeded, [this, notifyNoUpdate](){ updateCheckFinished(notifyNoUpdate); }); connect(indexJob.get(), &NetJob::failed, this, &UpdateChecker::updateCheckFailed); indexJob->start(); +#endif } void UpdateChecker::updateCheckFinished(bool notifyNoUpdate) diff --git a/launcher/updater/UpdateChecker.h b/launcher/updater/UpdateChecker.h index 13ee4efd..c569e49a 100644 --- a/launcher/updater/UpdateChecker.h +++ b/launcher/updater/UpdateChecker.h @@ -18,6 +18,10 @@ #include "net/NetJob.h" #include "GoUpdate.h" +#ifdef Q_OS_MAC +#include "updater/macsparkle/SparkleUpdater.h" +#endif + class UpdateChecker : public QObject { Q_OBJECT @@ -54,6 +58,13 @@ public: */ bool hasChannels() const; +#ifdef Q_OS_MAC + /*! + * Returns a pointer to the Sparkle updater. + */ + SparkleUpdater *getSparkleUpdater(); +#endif + signals: //! Signal emitted when an update is available. Passes the URL for the repo and the ID and name for the version. void updateAvailable(GoUpdate::Status status); @@ -117,5 +128,9 @@ private: QString m_currentRepoUrl; QString m_newRepoUrl; + +#ifdef Q_OS_MAC + SparkleUpdater *m_sparkleUpdater = new SparkleUpdater(); +#endif }; From f9f46609ee288d8df80dd978f8c619a7e02e4787 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Wed, 20 Apr 2022 22:36:23 -0400 Subject: [PATCH 05/12] Use my public key and appcast URL for testing purposes This is for testing the PR only. If merged, this must be removed. --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 289a7ec4..9401923c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -195,8 +195,8 @@ if(UNIX AND APPLE) set(MACOSX_BUNDLE_LONG_VERSION_STRING "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}") set(MACOSX_BUNDLE_ICON_FILE ${Launcher_Name}.icns) set(MACOSX_BUNDLE_COPYRIGHT "Copyright 2021-2022 ${Launcher_Copyright}") - set(MACOSX_SPARKLE_UPDATE_PUBLIC_KEY "") - set(MACOSX_SPARKLE_UPDATE_FEED_URL "") + set(MACOSX_SPARKLE_UPDATE_PUBLIC_KEY "qATG/94DDm31fqxgoAEl0wbfVB3fWiL3XQP1YH9LWTA=") + set(MACOSX_SPARKLE_UPDATE_FEED_URL "https://gist.githubusercontent.com/kthchew/a88603ad35aef8ada84d574c74bd5a4e/raw/test-appcast.xml") set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.1.0/Sparkle-2.1.0.tar.xz" CACHE STRING "URL to Sparkle release archive") set(MACOSX_SPARKLE_SHA256 "bf6ac1caa9f8d321d5784859c88da874f28412f37fb327bc21b7b14c5d61ef94" CACHE STRING "SHA256 checksum for Sparkle release archive") From 34adcec6165662d6245a55ee0a75c36753061df2 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Fri, 22 Apr 2022 22:29:00 -0400 Subject: [PATCH 06/12] Add functionality to (Sparkle) updater settings on macOS Also remove a debug line I accidentally left in --- launcher/ui/pages/global/LauncherPage.cpp | 8 ++++++++ launcher/ui/pages/global/LauncherPage.ui | 2 +- launcher/updater/macsparkle/SparkleUpdater.mm | 1 - 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index af2e2cd1..51284a8e 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -261,7 +261,11 @@ void LauncherPage::applySettings() auto s = APPLICATION->settings(); // Updates +#ifdef Q_OS_MAC + APPLICATION->updateChecker()->getSparkleUpdater()->setAutomaticallyChecksForUpdates(ui->autoUpdateCheckBox->isChecked()); +#else s->set("AutoUpdate", ui->autoUpdateCheckBox->isChecked()); +#endif s->set("UpdateChannel", m_currentUpdateChannel); auto original = s->get("IconTheme").toString(); //FIXME: make generic @@ -343,7 +347,11 @@ void LauncherPage::loadSettings() { auto s = APPLICATION->settings(); // Updates +#ifdef Q_OS_MAC + ui->autoUpdateCheckBox->setChecked(APPLICATION->updateChecker()->getSparkleUpdater()->getAutomaticallyChecksForUpdates()); +#else ui->autoUpdateCheckBox->setChecked(s->get("AutoUpdate").toBool()); +#endif m_currentUpdateChannel = s->get("UpdateChannel").toString(); //FIXME: make generic auto theme = s->get("IconTheme").toString(); diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index ae7eb73f..a306a91b 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -54,7 +54,7 @@ - Check for updates on start? + Check for updates automatically diff --git a/launcher/updater/macsparkle/SparkleUpdater.mm b/launcher/updater/macsparkle/SparkleUpdater.mm index 0d4119a4..ad7b83c8 100644 --- a/launcher/updater/macsparkle/SparkleUpdater.mm +++ b/launcher/updater/macsparkle/SparkleUpdater.mm @@ -173,7 +173,6 @@ void SparkleUpdater::setAllowedChannel(const QString &channel) NSSet *nsChannels = [NSSet setWithObject:channel.toNSString()]; priv->updaterDelegate.allowedChannels = nsChannels; - qDebug() << channel; APPLICATION->settings()->set("UpdateChannel", channel); } From 05cd30ac06b67ebc594773fc7e7ccf110fc336a3 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Mon, 25 Apr 2022 19:33:17 -0400 Subject: [PATCH 07/12] Refactor code, create abstract class `ExternalUpdater` (Hopefully) this makes implementing updaters using external libraries easier on other platforms. To implement an updater on a new platform, create a new class that implements the pure virtual methods from `ExternalUpdater` and add code in the `UpdateChecker` initializer to initialize the new class. --- launcher/CMakeLists.txt | 5 +- launcher/ui/MainWindow.cpp | 13 +- launcher/ui/pages/global/LauncherPage.cpp | 37 +++-- launcher/updater/ExternalUpdater.h | 87 ++++++++++ .../SparkleUpdater.h => MacSparkleUpdater.h} | 44 ++--- ...SparkleUpdater.mm => MacSparkleUpdater.mm} | 47 ++++-- launcher/updater/UpdateChecker.cpp | 156 +++++++++--------- launcher/updater/UpdateChecker.h | 22 ++- 8 files changed, 273 insertions(+), 138 deletions(-) create mode 100644 launcher/updater/ExternalUpdater.h rename launcher/updater/{macsparkle/SparkleUpdater.h => MacSparkleUpdater.h} (78%) rename launcher/updater/{macsparkle/SparkleUpdater.mm => MacSparkleUpdater.mm} (83%) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index dc10c38e..51f3dc36 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -162,11 +162,12 @@ set(UPDATE_SOURCES updater/UpdateChecker.cpp updater/DownloadTask.h updater/DownloadTask.cpp + updater/ExternalUpdater.h ) set(MAC_UPDATE_SOURCES - updater/macsparkle/SparkleUpdater.h - updater/macsparkle/SparkleUpdater.mm + updater/MacSparkleUpdater.h + updater/MacSparkleUpdater.mm ) add_unit_test(UpdateChecker diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 12761da1..951fcccf 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1028,12 +1028,13 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow updater->checkForUpdate(APPLICATION->settings()->get("UpdateChannel").toString(), false); } -#ifdef Q_OS_MAC - connect(APPLICATION->updateChecker()->getSparkleUpdater(), - &SparkleUpdater::canCheckForUpdatesChanged, - this, - &MainWindow::updatesAllowedChanged); -#endif + if (APPLICATION->updateChecker()->getExternalUpdater()) + { + connect(APPLICATION->updateChecker()->getExternalUpdater(), + &ExternalUpdater::canCheckForUpdatesChanged, + this, + &MainWindow::updatesAllowedChanged); + } } setSelectedInstanceById(APPLICATION->settings()->get("SelectedInstance").toString()); diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 51284a8e..b244b039 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -90,6 +90,13 @@ LauncherPage::LauncherPage(QWidget *parent) : QWidget(parent), ui(new Ui::Launch { APPLICATION->updateChecker()->updateChanList(false); } + + if (APPLICATION->updateChecker()->getExternalUpdater()) + { + ui->updateChannelComboBox->setVisible(false); + ui->updateChannelDescLabel->setVisible(false); + ui->updateChannelLabel->setVisible(false); + } } else { @@ -261,11 +268,16 @@ void LauncherPage::applySettings() auto s = APPLICATION->settings(); // Updates -#ifdef Q_OS_MAC - APPLICATION->updateChecker()->getSparkleUpdater()->setAutomaticallyChecksForUpdates(ui->autoUpdateCheckBox->isChecked()); -#else - s->set("AutoUpdate", ui->autoUpdateCheckBox->isChecked()); -#endif + if (BuildConfig.UPDATER_ENABLED && APPLICATION->updateChecker()->getExternalUpdater()) + { + APPLICATION->updateChecker()->getExternalUpdater()->setAutomaticallyChecksForUpdates( + ui->autoUpdateCheckBox->isChecked()); + } + else + { + s->set("AutoUpdate", ui->autoUpdateCheckBox->isChecked()); + } + s->set("UpdateChannel", m_currentUpdateChannel); auto original = s->get("IconTheme").toString(); //FIXME: make generic @@ -347,11 +359,16 @@ void LauncherPage::loadSettings() { auto s = APPLICATION->settings(); // Updates -#ifdef Q_OS_MAC - ui->autoUpdateCheckBox->setChecked(APPLICATION->updateChecker()->getSparkleUpdater()->getAutomaticallyChecksForUpdates()); -#else - ui->autoUpdateCheckBox->setChecked(s->get("AutoUpdate").toBool()); -#endif + if (BuildConfig.UPDATER_ENABLED && APPLICATION->updateChecker()->getExternalUpdater()) + { + ui->autoUpdateCheckBox->setChecked( + APPLICATION->updateChecker()->getExternalUpdater()->getAutomaticallyChecksForUpdates()); + } + else + { + ui->autoUpdateCheckBox->setChecked(s->get("AutoUpdate").toBool()); + } + m_currentUpdateChannel = s->get("UpdateChannel").toString(); //FIXME: make generic auto theme = s->get("IconTheme").toString(); diff --git a/launcher/updater/ExternalUpdater.h b/launcher/updater/ExternalUpdater.h new file mode 100644 index 00000000..a053e081 --- /dev/null +++ b/launcher/updater/ExternalUpdater.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Kenneth Chew + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef LAUNCHER_EXTERNALUPDATER_H +#define LAUNCHER_EXTERNALUPDATER_H + +#include + +/*! + * A base class for an updater that uses an external library. + * This class contains basic functions to control the updater. + * + * To implement the updater on a new platform, create a new class that inherits from this class and + * implement the pure virtual functions. + * + * The initializer of the new class should have the side effect of starting the automatic updater. That is, + * once the class is initialized, the program should automatically check for updates if necessary. + */ +class ExternalUpdater : public QObject +{ + Q_OBJECT + +public: + /*! + * Check for updates manually, showing the user a progress bar and an alert if no updates are found. + */ + virtual void checkForUpdates() = 0; + + /*! + * Indicates whether or not to check for updates automatically. + */ + virtual bool getAutomaticallyChecksForUpdates() = 0; + + /*! + * Indicates the current automatic update check interval in seconds. + */ + virtual double getUpdateCheckInterval() = 0; + + /*! + * Indicates whether or not beta updates should be checked for in addition to regular releases. + */ + virtual bool getBetaAllowed() = 0; + + /*! + * Set whether or not to check for updates automatically. + */ + virtual void setAutomaticallyChecksForUpdates(bool check) = 0; + + /*! + * Set the current automatic update check interval in seconds. + */ + virtual void setUpdateCheckInterval(double seconds) = 0; + + /*! + * Set whether or not beta updates should be checked for in addition to regular releases. + */ + virtual void setBetaAllowed(bool allowed) = 0; + +signals: + /*! + * Emits whenever the user's ability to check for updates changes. + * + * As per Sparkle documentation, "An update check can be made by the user when an update session isn’t in progress, + * or when an update or its progress is being shown to the user. A user cannot check for updates when data (such + * as the feed or an update) is still being downloaded automatically in the background. + * + * This property is suitable to use for menu item validation for seeing if checkForUpdates can be invoked." + */ + void canCheckForUpdatesChanged(bool canCheck); +}; + +#endif //LAUNCHER_EXTERNALUPDATER_H diff --git a/launcher/updater/macsparkle/SparkleUpdater.h b/launcher/updater/MacSparkleUpdater.h similarity index 78% rename from launcher/updater/macsparkle/SparkleUpdater.h rename to launcher/updater/MacSparkleUpdater.h index 9768d960..d50dbd68 100644 --- a/launcher/updater/macsparkle/SparkleUpdater.h +++ b/launcher/updater/MacSparkleUpdater.h @@ -16,13 +16,17 @@ * along with this program. If not, see . */ -#ifndef LAUNCHER_SPARKLEUPDATER_H -#define LAUNCHER_SPARKLEUPDATER_H +#ifndef LAUNCHER_MACSPARKLEUPDATER_H +#define LAUNCHER_MACSPARKLEUPDATER_H #include #include +#include "ExternalUpdater.h" -class SparkleUpdater : public QObject +/*! + * An implementation for the updater on macOS that uses the Sparkle framework. + */ +class MacSparkleUpdater : public ExternalUpdater { Q_OBJECT @@ -30,29 +34,34 @@ public: /*! * Start the Sparkle updater, which automatically checks for updates if necessary. */ - SparkleUpdater(); - ~SparkleUpdater(); + MacSparkleUpdater(); + ~MacSparkleUpdater() override; /*! * Check for updates manually, showing the user a progress bar and an alert if no updates are found. */ - void checkForUpdates(); + void checkForUpdates() override; /*! * Indicates whether or not to check for updates automatically. */ - bool getAutomaticallyChecksForUpdates(); + bool getAutomaticallyChecksForUpdates() override; /*! * Indicates the current automatic update check interval in seconds. */ - double getUpdateCheckInterval(); + double getUpdateCheckInterval() override; /*! * Indicates the set of Sparkle channels the updater is allowed to find new updates from. */ QSet getAllowedChannels(); + /*! + * Indicates whether or not beta updates should be checked for in addition to regular releases. + */ + bool getBetaAllowed() override; + /*! * Set whether or not to check for updates automatically. * @@ -66,7 +75,7 @@ public: * The update schedule cycle will be reset in a short delay after the property’s new value is set. This is to allow * reverting this property without kicking off a schedule change immediately." */ - void setAutomaticallyChecksForUpdates(bool check); + void setAutomaticallyChecksForUpdates(bool check) override; /*! * Set the current automatic update check interval in seconds. @@ -78,7 +87,7 @@ public: * The update schedule cycle will be reset in a short delay after the property’s new value is set. This is to allow * reverting this property without kicking off a schedule change immediately." */ - void setUpdateCheckInterval(double seconds); + void setUpdateCheckInterval(double seconds) override; /*! * Clears all allowed Sparkle channels, returning to the default updater channel behavior. @@ -101,24 +110,17 @@ public: */ void setAllowedChannels(const QSet& channels); -signals: /*! - * Emits whenever the user's ability to check for updates changes. - * - * As per Sparkle documentation, "An update check can be made by the user when an update session isn’t in progress, - * or when an update or its progress is being shown to the user. A user cannot check for updates when data (such - * as the feed or an update) is still being downloaded automatically in the background. - * - * This property is suitable to use for menu item validation for seeing if checkForUpdates can be invoked." + * Set whether or not beta updates should be checked for in addition to regular releases. */ - void canCheckForUpdatesChanged(bool canCheck); + void setBetaAllowed(bool allowed) override; private: class Private; - Private* priv; + Private *priv; void loadChannelsFromSettings(); }; -#endif //LAUNCHER_SPARKLEUPDATER_H +#endif //LAUNCHER_MACSPARKLEUPDATER_H diff --git a/launcher/updater/macsparkle/SparkleUpdater.mm b/launcher/updater/MacSparkleUpdater.mm similarity index 83% rename from launcher/updater/macsparkle/SparkleUpdater.mm rename to launcher/updater/MacSparkleUpdater.mm index ad7b83c8..63f5469b 100644 --- a/launcher/updater/macsparkle/SparkleUpdater.mm +++ b/launcher/updater/MacSparkleUpdater.mm @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -#include "SparkleUpdater.h" +#include "MacSparkleUpdater.h" #include "Application.h" @@ -76,7 +76,7 @@ @end -class SparkleUpdater::Private +class MacSparkleUpdater::Private { public: SPUStandardUpdaterController *updaterController; @@ -85,9 +85,9 @@ public: NSAutoreleasePool *autoReleasePool; }; -SparkleUpdater::SparkleUpdater() +MacSparkleUpdater::MacSparkleUpdater() { - priv = new SparkleUpdater::Private(); + priv = new MacSparkleUpdater::Private(); // Enable Cocoa's memory management. NSApplicationLoad(); @@ -110,7 +110,7 @@ SparkleUpdater::SparkleUpdater() loadChannelsFromSettings(); } -SparkleUpdater::~SparkleUpdater() +MacSparkleUpdater::~MacSparkleUpdater() { [priv->updaterObserver removeObserver:priv->updaterObserver forKeyPath:@"updater.canCheckForUpdates"]; @@ -121,22 +121,22 @@ SparkleUpdater::~SparkleUpdater() delete priv; } -void SparkleUpdater::checkForUpdates() +void MacSparkleUpdater::checkForUpdates() { [priv->updaterController checkForUpdates:nil]; } -bool SparkleUpdater::getAutomaticallyChecksForUpdates() +bool MacSparkleUpdater::getAutomaticallyChecksForUpdates() { return priv->updaterController.updater.automaticallyChecksForUpdates; } -double SparkleUpdater::getUpdateCheckInterval() +double MacSparkleUpdater::getUpdateCheckInterval() { return priv->updaterController.updater.updateCheckInterval; } -QSet SparkleUpdater::getAllowedChannels() +QSet MacSparkleUpdater::getAllowedChannels() { // Convert NSSet -> QSet __block QSet channels; @@ -147,23 +147,28 @@ QSet SparkleUpdater::getAllowedChannels() return channels; } -void SparkleUpdater::setAutomaticallyChecksForUpdates(bool check) +bool MacSparkleUpdater::getBetaAllowed() +{ + return getAllowedChannels().contains("beta"); +} + +void MacSparkleUpdater::setAutomaticallyChecksForUpdates(bool check) { priv->updaterController.updater.automaticallyChecksForUpdates = check ? YES : NO; // make clang-tidy happy } -void SparkleUpdater::setUpdateCheckInterval(double seconds) +void MacSparkleUpdater::setUpdateCheckInterval(double seconds) { priv->updaterController.updater.updateCheckInterval = seconds; } -void SparkleUpdater::clearAllowedChannels() +void MacSparkleUpdater::clearAllowedChannels() { priv->updaterDelegate.allowedChannels = [NSSet set]; APPLICATION->settings()->set("UpdateChannel", ""); } -void SparkleUpdater::setAllowedChannel(const QString &channel) +void MacSparkleUpdater::setAllowedChannel(const QString &channel) { if (channel.isEmpty()) { @@ -176,7 +181,7 @@ void SparkleUpdater::setAllowedChannel(const QString &channel) APPLICATION->settings()->set("UpdateChannel", channel); } -void SparkleUpdater::setAllowedChannels(const QSet &channels) +void MacSparkleUpdater::setAllowedChannels(const QSet &channels) { if (channels.isEmpty()) { @@ -197,7 +202,19 @@ void SparkleUpdater::setAllowedChannels(const QSet &channels) APPLICATION->settings()->set("UpdateChannel", channelsConfig.trimmed()); } -void SparkleUpdater::loadChannelsFromSettings() +void MacSparkleUpdater::setBetaAllowed(bool allowed) +{ + if (allowed) + { + setAllowedChannel("beta"); + } + else + { + clearAllowedChannels(); + } +} + +void MacSparkleUpdater::loadChannelsFromSettings() { QStringList channelList = APPLICATION->settings()->get("UpdateChannel").toString().split(" "); auto channels = QSet::fromList(channelList); diff --git a/launcher/updater/UpdateChecker.cpp b/launcher/updater/UpdateChecker.cpp index ad159841..fa6e5a97 100644 --- a/launcher/updater/UpdateChecker.cpp +++ b/launcher/updater/UpdateChecker.cpp @@ -24,7 +24,6 @@ #define CHANLIST_FORMAT 0 #include "BuildConfig.h" -#include "sys.h" UpdateChecker::UpdateChecker(shared_qobject_ptr nam, QString channelUrl, QString currentChannel, int currentBuild) { @@ -32,6 +31,10 @@ UpdateChecker::UpdateChecker(shared_qobject_ptr nam, QStr m_channelUrl = channelUrl; m_currentChannel = currentChannel; m_currentBuild = currentBuild; + +#ifdef Q_OS_MAC + m_externalUpdater = new MacSparkleUpdater(); +#endif } QList UpdateChecker::getChannelList() const @@ -44,92 +47,95 @@ bool UpdateChecker::hasChannels() const return !m_channels.isEmpty(); } -#ifdef Q_OS_MAC -SparkleUpdater* UpdateChecker::getSparkleUpdater() +ExternalUpdater* UpdateChecker::getExternalUpdater() { - return m_sparkleUpdater; + return m_externalUpdater; } -#endif -void UpdateChecker::checkForUpdate(QString updateChannel, bool notifyNoUpdate) +void UpdateChecker::checkForUpdate(const QString& updateChannel, bool notifyNoUpdate) { -#ifdef Q_OS_MAC - m_sparkleUpdater->setAllowedChannel(updateChannel); - if (notifyNoUpdate) + if (m_externalUpdater) { - qDebug() << "Checking for updates."; - m_sparkleUpdater->checkForUpdates(); + m_externalUpdater->setBetaAllowed(updateChannel == "beta"); + if (notifyNoUpdate) + { + qDebug() << "Checking for updates."; + m_externalUpdater->checkForUpdates(); + } else + { + // The updater library already handles automatic update checks. + return; + } } else { - // Sparkle already handles automatic update checks. - return; - } -#else - qDebug() << "Checking for updates."; - - // If the channel list hasn't loaded yet, load it and defer checking for updates until - // later. - if (!m_chanListLoaded) - { - qDebug() << "Channel list isn't loaded yet. Loading channel list and deferring update check."; - m_checkUpdateWaiting = true; - m_deferredUpdateChannel = updateChannel; - updateChanList(notifyNoUpdate); - return; - } - - if (m_updateChecking) - { - qDebug() << "Ignoring update check request. Already checking for updates."; - return; - } - - // Find the desired channel within the channel list and get its repo URL. If if cannot be - // found, error. - QString stableUrl; - m_newRepoUrl = ""; - for (ChannelListEntry entry : m_channels) - { - qDebug() << "channelEntry = " << entry.id; - if(entry.id == "stable") { - stableUrl = entry.url; + qDebug() << "Checking for updates."; + // If the channel list hasn't loaded yet, load it and defer checking for updates until + // later. + if (!m_chanListLoaded) + { + qDebug() << "Channel list isn't loaded yet. Loading channel list and deferring update check."; + m_checkUpdateWaiting = true; + m_deferredUpdateChannel = updateChannel; + updateChanList(notifyNoUpdate); + return; } - if (entry.id == updateChannel) { - m_newRepoUrl = entry.url; - qDebug() << "is intended update channel: " << entry.id; + + if (m_updateChecking) + { + qDebug() << "Ignoring update check request. Already checking for updates."; + return; } - if (entry.id == m_currentChannel) { - m_currentRepoUrl = entry.url; - qDebug() << "is current update channel: " << entry.id; + + // Find the desired channel within the channel list and get its repo URL. If if cannot be + // found, error. + QString stableUrl; + m_newRepoUrl = ""; + for (ChannelListEntry entry: m_channels) + { + qDebug() << "channelEntry = " << entry.id; + if (entry.id == "stable") + { + stableUrl = entry.url; + } + if (entry.id == updateChannel) + { + m_newRepoUrl = entry.url; + qDebug() << "is intended update channel: " << entry.id; + } + if (entry.id == m_currentChannel) + { + m_currentRepoUrl = entry.url; + qDebug() << "is current update channel: " << entry.id; + } } + + qDebug() << "m_repoUrl = " << m_newRepoUrl; + + if (m_newRepoUrl.isEmpty()) + { + qWarning() << "m_repoUrl was empty. defaulting to 'stable': " << stableUrl; + m_newRepoUrl = stableUrl; + } + + // If nothing applies, error + if (m_newRepoUrl.isEmpty()) + { + qCritical() << "failed to select any update repository for: " << updateChannel; + emit updateCheckFailed(); + return; + } + + m_updateChecking = true; + + QUrl indexUrl = QUrl(m_newRepoUrl).resolved(QUrl("index.json")); + + indexJob = new NetJob("GoUpdate Repository Index", m_network); + indexJob->addNetAction(Net::Download::makeByteArray(indexUrl, &indexData)); + connect(indexJob.get(), &NetJob::succeeded, [this, notifyNoUpdate]() { updateCheckFinished(notifyNoUpdate); }); + connect(indexJob.get(), &NetJob::failed, this, &UpdateChecker::updateCheckFailed); + indexJob->start(); } - - qDebug() << "m_repoUrl = " << m_newRepoUrl; - - if (m_newRepoUrl.isEmpty()) { - qWarning() << "m_repoUrl was empty. defaulting to 'stable': " << stableUrl; - m_newRepoUrl = stableUrl; - } - - // If nothing applies, error - if (m_newRepoUrl.isEmpty()) - { - qCritical() << "failed to select any update repository for: " << updateChannel; - emit updateCheckFailed(); - return; - } - - m_updateChecking = true; - - QUrl indexUrl = QUrl(m_newRepoUrl).resolved(QUrl("index.json")); - - indexJob = new NetJob("GoUpdate Repository Index", m_network); - indexJob->addNetAction(Net::Download::makeByteArray(indexUrl, &indexData)); - connect(indexJob.get(), &NetJob::succeeded, [this, notifyNoUpdate](){ updateCheckFinished(notifyNoUpdate); }); - connect(indexJob.get(), &NetJob::failed, this, &UpdateChecker::updateCheckFailed); - indexJob->start(); -#endif } void UpdateChecker::updateCheckFinished(bool notifyNoUpdate) diff --git a/launcher/updater/UpdateChecker.h b/launcher/updater/UpdateChecker.h index c569e49a..94e4312b 100644 --- a/launcher/updater/UpdateChecker.h +++ b/launcher/updater/UpdateChecker.h @@ -17,9 +17,10 @@ #include "net/NetJob.h" #include "GoUpdate.h" +#include "ExternalUpdater.h" #ifdef Q_OS_MAC -#include "updater/macsparkle/SparkleUpdater.h" +#include "MacSparkleUpdater.h" #endif class UpdateChecker : public QObject @@ -28,7 +29,7 @@ class UpdateChecker : public QObject public: UpdateChecker(shared_qobject_ptr nam, QString channelUrl, QString currentChannel, int currentBuild); - void checkForUpdate(QString updateChannel, bool notifyNoUpdate); + void checkForUpdate(const QString& updateChannel, bool notifyNoUpdate); /*! * Causes the update checker to download the channel list from the URL specified in config.h (generated by CMake). @@ -58,12 +59,10 @@ public: */ bool hasChannels() const; -#ifdef Q_OS_MAC /*! - * Returns a pointer to the Sparkle updater. + * Returns a pointer to an object that controls the external updater, or nullptr if an external updater is not used. */ - SparkleUpdater *getSparkleUpdater(); -#endif + ExternalUpdater *getExternalUpdater(); signals: //! Signal emitted when an update is available. Passes the URL for the repo and the ID and name for the version. @@ -129,8 +128,13 @@ private: QString m_newRepoUrl; -#ifdef Q_OS_MAC - SparkleUpdater *m_sparkleUpdater = new SparkleUpdater(); -#endif + /*! + * If not a nullptr, then the updater here will be used instead of the old updater that uses GoUpdate when + * checking for updates. + * + * As a result, signals from this class won't be emitted, and most of the functions in this class other + * than checkForUpdate are not useful. Call functions from this external updater object instead. + */ + ExternalUpdater *m_externalUpdater = nullptr; }; From f3c72f4f0888aa16793354890055e17df07084fc Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Thu, 19 May 2022 20:22:45 -0400 Subject: [PATCH 08/12] Actually install the full `Sparkle.framework` CMake's `fixup_bundle` did not copy everything in the framework, which caused the app to crash when updating. Oops. --- launcher/CMakeLists.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 51f3dc36..0be10682 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -1035,6 +1035,13 @@ install(TARGETS ${Launcher_Name} FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime ) +if (UNIX AND APPLE) + # Add Sparkle updater + # It has to be copied here instead of just allowing fixup_bundle to install it, otherwise essential parts of + # the framework aren't installed + install(DIRECTORY ${MACOSX_SPARKLE_DIR}/Sparkle.framework DESTINATION ${FRAMEWORK_DEST_DIR} USE_SOURCE_PERMISSIONS) +endif() + #### The bundle mess! #### # Bundle utilities are used to complete the portable packages - they add all the libraries that would otherwise be missing on the target system. # NOTE: it seems that this absolutely has to be here, and nowhere else. From b1f486518e3db19cca8ea9f33eb1d8d1afa247e7 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Fri, 20 May 2022 21:57:37 -0400 Subject: [PATCH 09/12] Use GitHub Actions to get signature for Sparkle Requires a secret called `SPARKLE_ED25519_KEY`, in the format of a private key file --- .github/workflows/build.yml | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0590b348..b6d6a9c3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -100,7 +100,7 @@ jobs: run: | brew update brew install qt@5 ninja - + - name: Update Qt (AppImage) if: runner.os == 'Linux' && matrix.appimage == true run: | @@ -170,6 +170,27 @@ jobs: sudo codesign --sign - --deep --force --entitlements "../program_info/App.entitlements" --options runtime "PolyMC.app/Contents/MacOS/polymc" tar -czf ../PolyMC.tar.gz * + - name: Make Sparkle signature (macOS) + if: runner.os == 'macOS' + run: | + if [ '${{ secrets.SPARKLE_ED25519_KEY }}' != '' ]; then + brew install openssl@3 + echo '${{ secrets.SPARKLE_ED25519_KEY }}' > ed25519-priv.pem + signature=$(/usr/local/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PolyMC.tar.gz -inkey ed25519-priv.pem | openssl base64 | tr -d \\n) + rm ed25519-priv.pem + cat >> $GITHUB_STEP_SUMMARY << EOF + ### Sparkle Signatures :memo: + + - macOS Archive: $signature + EOF + else + cat >> $GITHUB_STEP_SUMMARY << EOF + ### Sparkle Signatures :memo: + + - macOS Archive: No private key available (likely a pull request or fork) + EOF + fi + - name: Package (Windows) if: runner.os == 'Windows' shell: msys2 {0} From 3bc02b9662b84c2ab86b5de1b08b4537177fde90 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Sat, 21 May 2022 13:23:02 -0400 Subject: [PATCH 10/12] Fix Sparkle signing step --- .github/workflows/build.yml | 14 ++++++++------ .github/workflows/trigger_builds.yml | 2 ++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b6d6a9c3..abab0ed0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,6 +7,10 @@ on: description: Type of build (Debug, Release, RelWithDebInfo, MinSizeRel) type: string default: Debug + secrets: + SPARKLE_ED25519_KEY: + description: Private key for signing Sparkle updates + required: false jobs: build: @@ -179,15 +183,13 @@ jobs: signature=$(/usr/local/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PolyMC.tar.gz -inkey ed25519-priv.pem | openssl base64 | tr -d \\n) rm ed25519-priv.pem cat >> $GITHUB_STEP_SUMMARY << EOF - ### Sparkle Signatures :memo: - - - macOS Archive: $signature + ### Artifact Information :information_source: + - :memo: Sparkle Signature (ed25519): \`$signature\` EOF else cat >> $GITHUB_STEP_SUMMARY << EOF - ### Sparkle Signatures :memo: - - - macOS Archive: No private key available (likely a pull request or fork) + ### Artifact Information :information_source: + - :warning: Sparkle Signature (ed25519): No private key available (likely a pull request or fork) EOF fi diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml index 3ec6bb95..ee9eb4ea 100644 --- a/.github/workflows/trigger_builds.yml +++ b/.github/workflows/trigger_builds.yml @@ -28,3 +28,5 @@ jobs: uses: ./.github/workflows/build.yml with: build_type: Debug + secrets: + SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }} From 03429db5285db3d988e42665de3f718afb99d51c Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Sun, 10 Jul 2022 18:51:52 -0400 Subject: [PATCH 11/12] Switch to production Sparkle appcast DCO Remediation Commit for Kenneth Chew I, Kenneth Chew , hereby add my Signed-off-by to this commit: 92b913ca3740ea1aa799a69d65dc13d0c3612b87 I, Kenneth Chew , hereby add my Signed-off-by to this commit: 7eb61a28be3b66c1016eab434ae93b5d94eb11af I, Kenneth Chew , hereby add my Signed-off-by to this commit: ea4ef1655bdadf04c36768f0f641ca7579f754cf I, Kenneth Chew , hereby add my Signed-off-by to this commit: b5bdfa6c2e9a0eb62e476dd399b82bfa972e0320 I, Kenneth Chew , hereby add my Signed-off-by to this commit: f9f46609ee288d8df80dd978f8c619a7e02e4787 I, Kenneth Chew , hereby add my Signed-off-by to this commit: 34adcec6165662d6245a55ee0a75c36753061df2 I, Kenneth Chew , hereby add my Signed-off-by to this commit: 05cd30ac06b67ebc594773fc7e7ccf110fc336a3 I, Kenneth Chew , hereby add my Signed-off-by to this commit: f3c72f4f0888aa16793354890055e17df07084fc I, Kenneth Chew , hereby add my Signed-off-by to this commit: b1f486518e3db19cca8ea9f33eb1d8d1afa247e7 I, Kenneth Chew , hereby add my Signed-off-by to this commit: 3bc02b9662b84c2ab86b5de1b08b4537177fde90 Signed-off-by: Kenneth Chew --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cef40843..fc9cdd31 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -229,8 +229,8 @@ if(UNIX AND APPLE) set(MACOSX_BUNDLE_LONG_VERSION_STRING "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}") set(MACOSX_BUNDLE_ICON_FILE ${Launcher_Name}.icns) set(MACOSX_BUNDLE_COPYRIGHT "Copyright 2021-2022 ${Launcher_Copyright}") - set(MACOSX_SPARKLE_UPDATE_PUBLIC_KEY "qATG/94DDm31fqxgoAEl0wbfVB3fWiL3XQP1YH9LWTA=") - set(MACOSX_SPARKLE_UPDATE_FEED_URL "https://gist.githubusercontent.com/kthchew/a88603ad35aef8ada84d574c74bd5a4e/raw/test-appcast.xml") + set(MACOSX_SPARKLE_UPDATE_PUBLIC_KEY "idALcUIazingvKSSsEa9U7coDVxZVx/ORpOEE/QtJfg=") + set(MACOSX_SPARKLE_UPDATE_FEED_URL "https://polymc.org/feed/appcast.xml") set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.1.0/Sparkle-2.1.0.tar.xz" CACHE STRING "URL to Sparkle release archive") set(MACOSX_SPARKLE_SHA256 "bf6ac1caa9f8d321d5784859c88da874f28412f37fb327bc21b7b14c5d61ef94" CACHE STRING "SHA256 checksum for Sparkle release archive") From eae8a2914e99f6e9a6db042a0bc0f984fd890227 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Sun, 10 Jul 2022 19:07:16 -0400 Subject: [PATCH 12/12] Remove use of obsolete method Fixes Qt 6 build Signed-off-by: Kenneth Chew --- launcher/updater/MacSparkleUpdater.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/updater/MacSparkleUpdater.mm b/launcher/updater/MacSparkleUpdater.mm index 63f5469b..ca6da55a 100644 --- a/launcher/updater/MacSparkleUpdater.mm +++ b/launcher/updater/MacSparkleUpdater.mm @@ -217,6 +217,6 @@ void MacSparkleUpdater::setBetaAllowed(bool allowed) void MacSparkleUpdater::loadChannelsFromSettings() { QStringList channelList = APPLICATION->settings()->get("UpdateChannel").toString().split(" "); - auto channels = QSet::fromList(channelList); + QSet channels(channelList.begin(), channelList.end()); setAllowedChannels(channels); }