瀏覽代碼

UI: Add update channels (macOS)

derrod 3 年之前
父節點
當前提交
12a27d8b99

+ 13 - 10
UI/CMakeLists.txt

@@ -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
UI/obs-app.cpp

@@ -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))

+ 0 - 141
UI/sparkle-updater.mm

@@ -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 - 0
UI/update/mac-update.cpp

@@ -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 - 0
UI/update/mac-update.hpp

@@ -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 - 0
UI/update/sparkle-updater.mm

@@ -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];
+		}
+	}
+}

+ 37 - 12
UI/window-basic-main.cpp

@@ -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,20 +3757,46 @@ 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();
+#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()

+ 1 - 0
UI/window-basic-main.hpp

@@ -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
UI/window-basic-settings.cpp

@@ -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",