diff --git a/obs/CMakeLists.txt b/obs/CMakeLists.txt index 9cac34198e82bf79f882b3a0eaafd6b49c618142..ce7382935faf7d669abb1f4ce3fd069871c70e3c 100644 --- a/obs/CMakeLists.txt +++ b/obs/CMakeLists.txt @@ -50,6 +50,20 @@ elseif(APPLE) set(obs_PLATFORM_LIBRARIES ${APPKIT_LIBRARIES}) add_definitions(-fobjc-arc) + + option(ENABLE_SPARKLE_UPDATER "Enables updates via the Sparkle framework (don't forget to update the Info.plist for your .app)" OFF) + if(ENABLE_SPARKLE_UPDATER) + find_library(SPARKLE Sparkle) + include_directories(${SPARKLE}) + set(obs_PLATFORM_SOURCES + ${obs_PLATFORM_SOURCES} + sparkle-updater.mm) + set(obs_PLATFORM_LIBRARIES + ${obs_PLATFORM_LIBRARIES} + ${SPARKLE}) + add_definitions(-DUPDATE_SPARKLE=1) + endif() + elseif(UNIX) find_package(Qt5X11Extras REQUIRED) diff --git a/obs/sparkle-updater.mm b/obs/sparkle-updater.mm new file mode 100644 index 0000000000000000000000000000000000000000..69f9cbb392878037a9de46855cb47adc25be66a9 --- /dev/null +++ b/obs/sparkle-updater.mm @@ -0,0 +1,128 @@ +#import +#import + +static inline bool equali(NSString *a, NSString *b) +{ + return a && b && [a caseInsensitiveCompare:b] == NSOrderedSame; +} + +@interface OBSSparkleUpdateDelegate : + NSObject +{ +} +@property (nonatomic) bool updateToUndeployed; +@end + +@implementation OBSSparkleUpdateDelegate +{ +} + +@synthesize updateToUndeployed; + +- (SUAppcastItem *)bestValidUpdateInAppcast:(SUAppcast *)appcast + forUpdater:(SUUpdater *)updater +{ + static SUAppcastItem *selected; + 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 selected = [[SUAppcastItem alloc] initWithDictionary:dict]; +} + +- (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 ) + 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]; +} + diff --git a/obs/window-basic-main.cpp b/obs/window-basic-main.cpp index 73d8997e745c907faefa1e6fa673fa624a09eea2..425e7e1a5ded5d4cce4d44b67b45634eef2e802c 100644 --- a/obs/window-basic-main.cpp +++ b/obs/window-basic-main.cpp @@ -890,8 +890,17 @@ bool OBSBasic::QueryRemoveSource(obs_source_t *source) #define UPDATE_CHECK_INTERVAL (60*60*24*4) /* 4 days */ +#ifdef UPDATE_SPARKLE +void init_sparkle_updater(bool update_to_undeployed); +void trigger_sparkle_update(); +#endif + void OBSBasic::TimedCheckForUpdates() { +#ifdef UPDATE_SPARKLE + init_sparkle_updater(config_get_bool(App()->GlobalConfig(), "General", + "UpdateToUndeployed")); +#else long long lastUpdate = config_get_int(App()->GlobalConfig(), "General", "LastUpdateCheck"); uint32_t lastVersion = config_get_int(App()->GlobalConfig(), "General", @@ -908,10 +917,14 @@ void OBSBasic::TimedCheckForUpdates() if (secs > UPDATE_CHECK_INTERVAL) CheckForUpdates(); +#endif } void OBSBasic::CheckForUpdates() { +#ifdef UPDATE_SPARKLE + trigger_sparkle_update(); +#else ui->actionCheckForUpdates->setEnabled(false); string versionString("obs-basic "); @@ -926,6 +939,7 @@ void OBSBasic::CheckForUpdates() this, SLOT(updateFileFinished())); connect(updateReply, SIGNAL(readyRead()), this, SLOT(updateFileRead())); +#endif } void OBSBasic::updateFileRead()