UI: Add update channels (macOS)

This commit is contained in:
derrod
2023-02-19 01:35:56 +01:00
committed by Patrick Heyer
parent 634fd3231c
commit 12a27d8b99
9 changed files with 301 additions and 176 deletions
+13 -10
View File
@@ -430,16 +430,6 @@ elseif(OS_MACOS)
target_link_libraries(obs PRIVATE ${APPKIT} ${AVFOUNDATION}
${APPLICATIONSERVICES})
if(ENABLE_SPARKLE_UPDATER)
find_library(SPARKLE Sparkle)
mark_as_advanced(SPARKLE)
target_sources(obs PRIVATE sparkle-updater.mm)
target_compile_definitions(obs PRIVATE ENABLE_SPARKLE_UPDATER)
target_link_libraries(obs PRIVATE ${SPARKLE})
endif()
target_sources(obs PRIVATE platform-osx.mm)
target_sources(obs PRIVATE forms/OBSPermissions.ui window-permissions.cpp
window-permissions.hpp)
@@ -454,6 +444,19 @@ elseif(OS_MACOS)
PRIVATE update/crypto-helpers.hpp update/crypto-helpers-mac.mm
update/shared-update.cpp update/shared-update.hpp
update/update-helpers.cpp update/update-helpers.hpp)
if(ENABLE_SPARKLE_UPDATER)
find_library(SPARKLE Sparkle)
mark_as_advanced(SPARKLE)
target_sources(obs PRIVATE update/mac-update.cpp update/mac-update.hpp
update/sparkle-updater.mm)
target_compile_definitions(obs PRIVATE ENABLE_SPARKLE_UPDATER)
target_link_libraries(obs PRIVATE ${SPARKLE})
# Enable Automatic Reference Counting for Sparkle wrapper
set_source_files_properties(update/sparkle-updater.mm
PROPERTIES COMPILE_FLAGS -fobjc-arc)
endif()
endif()
set_source_files_properties(platform-osx.mm PROPERTIES COMPILE_FLAGS
+10 -4
View File
@@ -54,7 +54,6 @@
#include <curl/curl.h>
#ifdef _WIN32
#include <json11.hpp>
#include <windows.h>
#include <filesystem>
#else
@@ -62,6 +61,10 @@
#include <pthread.h>
#endif
#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER)
#include <json11.hpp>
#endif
#if !defined(_WIN32) && !defined(__APPLE__)
#include <obs-nix-platform.h>
#include <qpa/qplatformnativeinterface.h>
@@ -1268,7 +1271,7 @@ bool OBSApp::InitTheme()
return SetTheme("System");
}
#ifdef _WIN32
#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER)
void ParseBranchesJson(const std::string &jsonString, vector<UpdateBranch> &out,
std::string &error)
{
@@ -1281,6 +1284,9 @@ void ParseBranchesJson(const std::string &jsonString, vector<UpdateBranch> &out,
#ifdef _WIN32
if (!item["windows"].bool_value())
continue;
#elif defined(__APPLE__)
if (!item["macos"].bool_value())
continue;
#endif
UpdateBranch branch = {
@@ -1329,7 +1335,7 @@ fail:
void OBSApp::SetBranchData(const string &data)
{
#ifdef _WIN32
#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER)
string error;
vector<UpdateBranch> result;
@@ -1356,7 +1362,7 @@ std::vector<UpdateBranch> OBSApp::GetBranches()
/* Always ensure the default branch exists */
out.push_back(UpdateBranch{"stable", "", "", true, true});
#ifdef _WIN32
#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER)
if (!branches_loaded) {
vector<UpdateBranch> result;
if (LoadBranchesFile(result))
-141
View File
@@ -1,141 +0,0 @@
#import <Cocoa/Cocoa.h>
#import <Sparkle/Sparkle.h>
static inline bool equali(NSString *a, NSString *b)
{
return a && b && [a caseInsensitiveCompare:b] == NSOrderedSame;
}
@interface OBSSparkleUpdateDelegate
: NSObject <SUUpdaterDelegate, SUVersionComparison> {
}
@property (nonatomic) bool updateToUndeployed;
@end
@implementation OBSSparkleUpdateDelegate {
}
@synthesize updateToUndeployed;
- (SUAppcastItem *)bestValidUpdateWithDeltasInAppcast:(SUAppcast *)appcast
forUpdater:(SUUpdater *)updater
{
SUAppcastItem *item = appcast.items.firstObject;
if (!appcast.items.firstObject)
return nil;
SUAppcastItem *app = nil, *mpkg = nil;
for (SUAppcastItem *item in appcast.items) {
NSString *deployed = item.propertiesDictionary[@"ce:deployed"];
if (deployed && !(deployed.boolValue || updateToUndeployed))
continue;
NSString *type = item.propertiesDictionary[@"ce:packageType"];
if (!mpkg && (!type || equali(type, @"mpkg")))
mpkg = item;
else if (!app && type && equali(type, @"app"))
app = item;
if (app && mpkg)
break;
}
if (app)
item = app;
NSBundle *host = updater.hostBundle;
if (mpkg && (!app || equali(host.bundlePath, @"/Applications/OBS.app")))
item = mpkg;
NSMutableDictionary *dict = [NSMutableDictionary
dictionaryWithDictionary:item.propertiesDictionary];
NSString *build = [host objectForInfoDictionaryKey:@"CFBundleVersion"];
NSString *url = dict[@"sparkle:releaseNotesLink"];
dict[@"sparkle:releaseNotesLink"] =
[url stringByAppendingFormat:@"#%@", build];
return [[SUAppcastItem alloc] initWithDictionary:dict];
}
- (SUAppcastItem *)bestValidUpdateInAppcast:(SUAppcast *)appcast
forUpdater:(SUUpdater *)updater
{
SUAppcastItem *selected = [self
bestValidUpdateWithDeltasInAppcast:appcast
forUpdater:updater];
NSBundle *host = updater.hostBundle;
NSString *build = [host objectForInfoDictionaryKey:@"CFBundleVersion"];
SUAppcastItem *deltaUpdate = [selected deltaUpdates][build];
if (deltaUpdate)
return deltaUpdate;
return selected;
}
- (NSString *)feedURLStringForUpdater:(SUUpdater *)updater
{
//URL from Info.plist takes precedence because there may be bundles with
//differing feed URLs on the system
NSBundle *bundle = updater.hostBundle;
return [bundle objectForInfoDictionaryKey:@"SUFeedURL"];
}
- (NSComparisonResult)compareVersion:(NSString *)versionA
toVersion:(NSString *)versionB
{
if (![versionA isEqual:versionB])
return NSOrderedAscending;
return NSOrderedSame;
}
- (id<SUVersionComparison>)versionComparatorForUpdater:(SUUpdater *)__unused
updater
{
return self;
}
@end
static inline bool bundle_matches(NSBundle *bundle)
{
if (!bundle.executablePath)
return false;
NSRange r = [bundle.executablePath rangeOfString:@"Contents/MacOS/"];
return [bundle.bundleIdentifier isEqual:@"com.obsproject.obs-studio"] &&
r.location != NSNotFound;
}
static inline NSBundle *find_bundle()
{
NSFileManager *fm = [NSFileManager defaultManager];
NSString *path = [fm currentDirectoryPath];
NSString *prev = path;
do {
NSBundle *bundle = [NSBundle bundleWithPath:path];
if (bundle_matches(bundle))
return bundle;
prev = path;
path = [path stringByDeletingLastPathComponent];
} while (![prev isEqual:path]);
return nil;
}
static SUUpdater *updater;
static OBSSparkleUpdateDelegate *delegate;
void init_sparkle_updater(bool update_to_undeployed)
{
updater = [SUUpdater updaterForBundle:find_bundle()];
delegate = [[OBSSparkleUpdateDelegate alloc] init];
delegate.updateToUndeployed = update_to_undeployed;
updater.delegate = delegate;
}
void trigger_sparkle_update()
{
[updater checkForUpdates:nil];
}
+87
View File
@@ -0,0 +1,87 @@
#include "update-helpers.hpp"
#include "shared-update.hpp"
#include "qt-wrappers.hpp"
#include "mac-update.hpp"
#include "obs-app.hpp"
#include <string>
#include <QMessageBox>
/* ------------------------------------------------------------------------ */
#ifndef MAC_BRANCHES_URL
#define MAC_BRANCHES_URL "https://obsproject.com/update_studio/branches.json"
#endif
#ifndef MAC_DEFAULT_BRANCH
#define MAC_DEFAULT_BRANCH "stable"
#endif
/* ------------------------------------------------------------------------ */
bool GetBranch(std::string &selectedBranch)
{
const char *config_branch =
config_get_string(GetGlobalConfig(), "General", "UpdateBranch");
if (!config_branch)
return true;
bool found = false;
for (const UpdateBranch &branch : App()->GetBranches()) {
if (branch.name != config_branch)
continue;
/* A branch that is found but disabled will just silently fall back to
* the default. But if the branch was removed entirely, the user should
* be warned, so leave this false *only* if the branch was removed. */
found = true;
if (branch.is_enabled) {
selectedBranch = branch.name.toStdString();
}
break;
}
return found;
}
/* ------------------------------------------------------------------------ */
void MacUpdateThread::infoMsg(const QString &title, const QString &text)
{
OBSMessageBox::information(App()->GetMainWindow(), title, text);
}
void MacUpdateThread::info(const QString &title, const QString &text)
{
QMetaObject::invokeMethod(this, "infoMsg", Qt::BlockingQueuedConnection,
Q_ARG(QString, title), Q_ARG(QString, text));
}
void MacUpdateThread::run()
try {
std::string text;
std::string branch = MAC_DEFAULT_BRANCH;
/* ----------------------------------- *
* get branches from server */
if (FetchAndVerifyFile("branches", "obs-studio/updates/branches.json",
MAC_BRANCHES_URL, &text))
App()->SetBranchData(text);
/* ----------------------------------- *
* Validate branch selection */
if (!GetBranch(branch)) {
config_set_string(GetGlobalConfig(), "General", "UpdateBranch",
MAC_DEFAULT_BRANCH);
info(QTStr("Updater.BranchNotFound.Title"),
QTStr("Updater.BranchNotFound.Text"));
}
emit Result(QString::fromStdString(branch), manualUpdate);
} catch (std::string &text) {
blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str());
}
+51
View File
@@ -0,0 +1,51 @@
#ifndef MAC_UPDATER_H
#define MAC_UPDATER_H
#include <string>
#include <QThread>
#include <QString>
#include <QObject>
class QAction;
class MacUpdateThread : public QThread {
Q_OBJECT
bool manualUpdate;
virtual void run() override;
void info(const QString &title, const QString &text);
signals:
void Result(const QString &branch, bool manual);
private slots:
void infoMsg(const QString &title, const QString &text);
public:
MacUpdateThread(bool manual) : manualUpdate(manual) {}
};
#ifdef __OBJC__
@class OBSUpdateDelegate;
#endif
class OBSSparkle : public QObject {
Q_OBJECT
public:
OBSSparkle(const char *branch, QAction *checkForUpdatesAction);
void setBranch(const char *branch);
void checkForUpdates(bool manualCheck);
private:
#ifdef __OBJC__
OBSUpdateDelegate *updaterDelegate;
#else
void *updaterDelegate;
#endif
};
#endif
+95
View File
@@ -0,0 +1,95 @@
#include "mac-update.hpp"
#include <qaction.h>
#import <Cocoa/Cocoa.h>
#import <Sparkle/Sparkle.h>
@interface OBSUpdateDelegate : NSObject <SPUUpdaterDelegate> {
}
@property (copy) NSString *branch;
@property (nonatomic) SPUStandardUpdaterController *updaterController;
@end
@implementation OBSUpdateDelegate {
}
@synthesize branch;
- (nonnull NSSet<NSString *> *)allowedChannelsForUpdater:
(nonnull SPUUpdater *)updater
{
return [NSSet setWithObject:branch];
}
- (void)observeCanCheckForUpdatesWithAction:(QAction *)action
{
[_updaterController.updater
addObserver:self
forKeyPath:NSStringFromSelector(@selector(canCheckForUpdates))
options:(NSKeyValueObservingOptionInitial |
NSKeyValueObservingOptionNew)
context:(void *)action];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
context:(void *)context
{
if ([keyPath isEqualToString:NSStringFromSelector(
@selector(canCheckForUpdates))]) {
QAction *menuAction = (QAction *)context;
menuAction->setEnabled(
_updaterController.updater.canCheckForUpdates);
} else {
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
- (void)dealloc
{
@autoreleasepool {
[_updaterController.updater
removeObserver:self
forKeyPath:NSStringFromSelector(
@selector(canCheckForUpdates))];
}
}
@end
OBSSparkle::OBSSparkle(const char *branch, QAction *checkForUpdatesAction)
{
@autoreleasepool {
updaterDelegate = [[OBSUpdateDelegate alloc] init];
updaterDelegate.branch = [NSString stringWithUTF8String:branch];
updaterDelegate.updaterController =
[[SPUStandardUpdaterController alloc]
initWithStartingUpdater:YES
updaterDelegate:updaterDelegate
userDriverDelegate:nil];
[updaterDelegate observeCanCheckForUpdatesWithAction:
checkForUpdatesAction];
}
}
void OBSSparkle::setBranch(const char *branch)
{
updaterDelegate.branch = [NSString stringWithUTF8String:branch];
}
void OBSSparkle::checkForUpdates(bool manualCheck)
{
@autoreleasepool {
if (manualCheck) {
[updaterDelegate.updaterController checkForUpdates:nil];
} else {
[updaterDelegate.updaterController
.updater checkForUpdatesInBackground];
}
}
}
+38 -13
View File
@@ -86,6 +86,10 @@
#include "update/shared-update.hpp"
#endif
#ifdef ENABLE_SPARKLE_UPDATER
#include "update/mac-update.hpp"
#endif
#include "ui_OBSBasic.h"
#include "ui_ColorSelect.h"
@@ -2041,7 +2045,8 @@ void OBSBasic::OBSInit()
QMetaObject::invokeMethod(this, "on_autoConfigure_triggered",
Qt::QueuedConnection);
#if defined(_WIN32) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0)
#if (defined(_WIN32) || defined(__APPLE__)) && \
(OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0)
/* Automatically set branch to "beta" the first time a pre-release build is run. */
if (!config_get_bool(App()->GlobalConfig(), "General",
"AutoBetaOptIn")) {
@@ -3720,11 +3725,6 @@ bool OBSBasic::QueryRemoveSource(obs_source_t *source)
#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */
#if defined(ENABLE_SPARKLE_UPDATER)
void init_sparkle_updater(bool update_to_undeployed);
void trigger_sparkle_update();
#endif
void OBSBasic::TimedCheckForUpdates()
{
if (App()->IsUpdaterDisabled())
@@ -3734,8 +3734,7 @@ void OBSBasic::TimedCheckForUpdates()
return;
#if defined(ENABLE_SPARKLE_UPDATER)
init_sparkle_updater(config_get_bool(App()->GlobalConfig(), "General",
"UpdateToUndeployed"));
CheckForUpdates(false);
#elif _WIN32
long long lastUpdate = config_get_int(App()->GlobalConfig(), "General",
"LastUpdateCheck");
@@ -3758,22 +3757,48 @@ void OBSBasic::TimedCheckForUpdates()
void OBSBasic::CheckForUpdates(bool manualUpdate)
{
#if defined(ENABLE_SPARKLE_UPDATER)
trigger_sparkle_update();
#elif _WIN32
#if _WIN32
ui->actionCheckForUpdates->setEnabled(false);
ui->actionRepair->setEnabled(false);
if (updateCheckThread && updateCheckThread->isRunning())
return;
updateCheckThread.reset(new AutoUpdateThread(manualUpdate));
updateCheckThread->start();
#endif
#elif defined(ENABLE_SPARKLE_UPDATER)
ui->actionCheckForUpdates->setEnabled(false);
if (updateCheckThread && updateCheckThread->isRunning())
return;
MacUpdateThread *mut = new MacUpdateThread(manualUpdate);
connect(mut, &MacUpdateThread::Result, this,
&OBSBasic::MacBranchesFetched, Qt::QueuedConnection);
updateCheckThread.reset(mut);
updateCheckThread->start();
#endif
UNUSED_PARAMETER(manualUpdate);
}
void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate)
{
#ifdef ENABLE_SPARKLE_UPDATER
static OBSSparkle *updater;
if (!updater) {
updater = new OBSSparkle(QT_TO_UTF8(branch),
ui->actionCheckForUpdates);
return;
}
updater->setBranch(QT_TO_UTF8(branch));
updater->checkForUpdates(manualUpdate);
#else
UNUSED_PARAMETER(branch);
UNUSED_PARAMETER(manualUpdate);
#endif
}
void OBSBasic::updateCheckFinished()
{
ui->actionCheckForUpdates->setEnabled(true);
+1
View File
@@ -541,6 +541,7 @@ private:
obs_data_array_t *SaveProjectors();
void LoadSavedProjectors(obs_data_array_t *savedProjectors);
void MacBranchesFetched(const QString &branch, bool manualUpdate);
void ReceivedIntroJson(const QString &text);
void ShowWhatsNew(const QString &url);
+6 -8
View File
@@ -583,9 +583,6 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
ui->updateChannelBox = nullptr;
delete ui->updateSettingsGroupBox;
ui->updateSettingsGroupBox = nullptr;
#elif defined(__APPLE__)
delete ui->updateChannelBox;
ui->updateChannelBox = nullptr;
delete ui->updateChannelLabel;
ui->updateChannelLabel = nullptr;
#else
@@ -1207,7 +1204,7 @@ void OBSBasicSettings::LoadThemeList()
ui->theme->setCurrentIndex(idx);
}
#ifdef _WIN32
#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER)
void TranslateBranchInfo(const QString &name, QString &displayName,
QString &description)
{
@@ -1225,7 +1222,7 @@ void TranslateBranchInfo(const QString &name, QString &displayName,
void OBSBasicSettings::LoadBranchesList()
{
#ifdef _WIN32
#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER)
bool configBranchRemoved = true;
QString configBranch =
config_get_string(GetGlobalConfig(), "General", "UpdateBranch");
@@ -1286,7 +1283,7 @@ void OBSBasicSettings::LoadGeneralSettings()
"EnableAutoUpdates");
ui->enableAutoUpdates->setChecked(enableAutoUpdates);
#ifdef _WIN32
#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER)
LoadBranchesList();
#endif
#endif
@@ -3126,7 +3123,7 @@ void OBSBasicSettings::SaveGeneralSettings()
"EnableAutoUpdates",
ui->enableAutoUpdates->isChecked());
#endif
#ifdef _WIN32
#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER)
int branchIdx = ui->updateChannelBox->currentIndex();
QString branchName =
ui->updateChannelBox->itemData(branchIdx).toString();
@@ -3136,7 +3133,8 @@ void OBSBasicSettings::SaveGeneralSettings()
QT_TO_UTF8(branchName));
forceUpdateCheck = true;
}
#endif
#ifdef _WIN32
if (ui->hideOBSFromCapture && WidgetChanged(ui->hideOBSFromCapture)) {
bool hide_window = ui->hideOBSFromCapture->isChecked();
config_set_bool(GetGlobalConfig(), "BasicWindow",