diff --git a/Sparkle.xcodeproj/project.pbxproj b/Sparkle.xcodeproj/project.pbxproj index 91729ac5..c14ae90d 100644 --- a/Sparkle.xcodeproj/project.pbxproj +++ b/Sparkle.xcodeproj/project.pbxproj @@ -81,7 +81,7 @@ 5D1AF5900FD767AD0065DB48 /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D1AF58F0FD767AD0065DB48 /* libxml2.dylib */; }; 5D1AF59A0FD767E50065DB48 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D1AF5990FD767E50065DB48 /* libz.dylib */; }; 5F1510A21C96E591006E1629 /* testnamespaces.xml in Resources */ = {isa = PBXBuildFile; fileRef = 5F1510A11C96E591006E1629 /* testnamespaces.xml */; }; - 61299A5C09CA6D4500B7442F /* SUConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 61299A5B09CA6D4500B7442F /* SUConstants.h */; settings = {ATTRIBUTES = (); }; }; + 61299A5C09CA6D4500B7442F /* SUConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 61299A5B09CA6D4500B7442F /* SUConstants.h */; }; 61299A6009CA6EB100B7442F /* SUConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 61299A5F09CA6EB100B7442F /* SUConstants.m */; }; 61299B3609CB04E000B7442F /* Sparkle.h in Headers */ = {isa = PBXBuildFile; fileRef = 61299B3509CB04E000B7442F /* Sparkle.h */; settings = {ATTRIBUTES = (Public, ); }; }; 612DCBB00D488BC60015DBEA /* SUUpdatePermissionPrompt.m in Sources */ = {isa = PBXBuildFile; fileRef = 612DCBAE0D488BC60015DBEA /* SUUpdatePermissionPrompt.m */; }; @@ -188,6 +188,7 @@ 723C8A541E2D60DB00C14942 /* SUTouchBarButtonGroup.h in Headers */ = {isa = PBXBuildFile; fileRef = 723C8A521E2D60DB00C14942 /* SUTouchBarButtonGroup.h */; }; 723C8A551E2D60DB00C14942 /* SUTouchBarButtonGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = 723C8A531E2D60DB00C14942 /* SUTouchBarButtonGroup.m */; }; 723C8A561E2D60DB00C14942 /* SUTouchBarButtonGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = 723C8A531E2D60DB00C14942 /* SUTouchBarButtonGroup.m */; }; + 723EDC3F26885A8E000BCBA4 /* testappcast_channels.xml in Resources */ = {isa = PBXBuildFile; fileRef = 723EDC3E26885A8E000BCBA4 /* testappcast_channels.xml */; }; 72464F701E1F31E000FB341C /* SUOperatingSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = 726F2CE41BC9C33D001971A4 /* SUOperatingSystem.m */; }; 72464F711E1F326A00FB341C /* SUOperatingSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = 726F2CE41BC9C33D001971A4 /* SUOperatingSystem.m */; }; 72464F721E1F327C00FB341C /* SUOperatingSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = 726F2CE41BC9C33D001971A4 /* SUOperatingSystem.m */; }; @@ -1082,6 +1083,7 @@ 723B5DA51CF7AB0100365F95 /* SPUDownloaderProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SPUDownloaderProtocol.h; path = Downloader/SPUDownloaderProtocol.h; sourceTree = SOURCE_ROOT; }; 723C8A521E2D60DB00C14942 /* SUTouchBarButtonGroup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUTouchBarButtonGroup.h; sourceTree = ""; }; 723C8A531E2D60DB00C14942 /* SUTouchBarButtonGroup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUTouchBarButtonGroup.m; sourceTree = ""; }; + 723EDC3E26885A8E000BCBA4 /* testappcast_channels.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = testappcast_channels.xml; sourceTree = ""; }; 7246E0A11C83B685003B4E75 /* SPUStandardUpdaterController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUStandardUpdaterController.h; sourceTree = ""; }; 7246E0A21C83B685003B4E75 /* SPUStandardUpdaterController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUStandardUpdaterController.m; sourceTree = ""; }; 724BB36C1D31D0B7005D534A /* org.sparkle-project.InstallerConnection.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = "org.sparkle-project.InstallerConnection.xpc"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1685,6 +1687,7 @@ 14958C6C19AEBC610061B14F /* test-pubkey.pem */, 5AF6C74E1AEA46D10014A3AB /* test.pkg */, 5AD0FA7E1C73F2E2004BCEFF /* testappcast.xml */, + 723EDC3E26885A8E000BCBA4 /* testappcast_channels.xml */, 725B3A81263FBF0C0041AB8E /* testappcast_minimumAutoupdateVersion.xml */, 722545B526805FF80036465C /* testappcast_info_updates.xml */, 5F1510A11C96E591006E1629 /* testnamespaces.xml */, @@ -2336,6 +2339,7 @@ 726E4A371C89116000C57C6A /* SPUStandardUserDriverDelegate.h in Headers */, 72EF30BE2675CF38008CE987 /* SPUAppcastItemStateResolver.h in Headers */, 72EF30C7267C716A008CE987 /* SUAppcastItem+Private.h in Headers */, + 61299A5C09CA6D4500B7442F /* SUConstants.h in Headers */, 726DF88E1C84277600188804 /* SPUUserUpdateState.h in Headers */, 72B767DA1C9CD2E400A07552 /* SPUUIBasedUpdateDriver.h in Headers */, 7229E1B61C97C91100CB50D0 /* SPUUpdateDriver.h in Headers */, @@ -2357,7 +2361,6 @@ 725602D51C83551C00DAA70E /* SUApplicationInfo.h in Headers */, 7214B8811D456A8500CB5CED /* SUBundleIcon.h in Headers */, 720767D31E2EE9C200F9A850 /* SUTouchBarForwardDeclarations.h in Headers */, - 61299A5C09CA6D4500B7442F /* SUConstants.h in Headers */, 55E6F33319EC9F6C00005E76 /* SUErrors.h in Headers */, 14652F8419A978C200959E44 /* SUExport.h in Headers */, 7267E5E51D3D90AA00D1BF90 /* SUFileManager.h in Headers */, @@ -2936,6 +2939,7 @@ files = ( 725B3A82263FBF0C0041AB8E /* testappcast_minimumAutoupdateVersion.xml in Resources */, 14958C6E19AEBC950061B14F /* signed-test-file.txt in Resources */, + 723EDC3F26885A8E000BCBA4 /* testappcast_channels.xml in Resources */, 72AC6B2E1B9B218C00F62325 /* SparkleTestCodeSignApp.dmg in Resources */, C23E885B1BE7B24F0050BB73 /* SparkleTestCodeSignApp.enc.dmg in Resources */, 72AC6B281B9AAD6700F62325 /* SparkleTestCodeSignApp.tar in Resources */, diff --git a/Sparkle/Base.lproj/Sparkle.strings b/Sparkle/Base.lproj/Sparkle.strings index 7d903471..31e24cb0 100644 --- a/Sparkle/Base.lproj/Sparkle.strings +++ b/Sparkle/Base.lproj/Sparkle.strings @@ -14,9 +14,6 @@ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@ is currently the newest version available.\n(You are currently running version %3$@.)"; -/* An appcast feed error when checking for updates and no appcast items are retrieved */ -"No valid update information could be loaded." = "No valid update information could be loaded."; - "An error occurred while running the updater. Please try again later." = "An error occurred while running the updater. Please try again later."; "An error occurred while starting the installer. Please try again later." = "An error occurred while starting the installer. Please try again later."; diff --git a/Sparkle/SPUBasicUpdateDriver.m b/Sparkle/SPUBasicUpdateDriver.m index 3aaade2f..2e1fe8fb 100644 --- a/Sparkle/SPUBasicUpdateDriver.m +++ b/Sparkle/SPUBasicUpdateDriver.m @@ -169,7 +169,6 @@ NSString *localizedDescription; NSString *recoverySuggestion; - NSString *recoveryOption; SPUNoUpdateFoundReason reason; if (latestAppcastItem != nil) { @@ -214,19 +213,20 @@ } break; } - - recoveryOption = SULocalizedString(@"OK", nil); } else { // When no updates are found in the appcast, or latest appcast item info // was not provided (i.e, for a background update check) - // In the case no info was provided for a background check, the error isn't shown anywhere - localizedDescription = SULocalizedString(@"Update Error!", nil); - recoverySuggestion = SULocalizedString(@"No valid update information could be loaded.", nil); - recoveryOption = SULocalizedString(@"Cancel Update", nil); + // We will need to assume the user is up to date if the feed doen't have any applicable update items + // There could be update items on channels the updater is not subscribed to for example. But we can't tell the user about them. + // There could also only be update items available for other platforms or none at all. + localizedDescription = SULocalizedString(@"You're up-to-date!", "Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates."); + recoverySuggestion = [NSString stringWithFormat:SULocalizedString(@"%@ %@ is currently the newest version available.", nil), [self.host name], [self.host displayVersion]]; - reason = SPUNoUpdateFoundReasonUnknown; + reason = SPUNoUpdateFoundReasonOnLatestVersion; } + NSString *recoveryOption = SULocalizedString(@"OK", nil); + NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:@{ NSLocalizedDescriptionKey: localizedDescription, diff --git a/Sparkle/SPUUpdater.h b/Sparkle/SPUUpdater.h index e77c6eb2..2828521b 100644 --- a/Sparkle/SPUUpdater.h +++ b/Sparkle/SPUUpdater.h @@ -186,10 +186,12 @@ SU_EXPORT @interface SPUUpdater : NSObject Setting this property will persist in the host bundle's user defaults. To avoid this, you should consider instead implementing - -[SPUUpdaterDelegate feedURLStringForUpdater:] or -[SPUUpdaterDelegate feedParametersForUpdater:sendingSystemProfile:] + -[SPUUpdaterDelegate feedURLStringForUpdater:] Passing nil will remove any feed URL that has been set in the host bundle's user defaults. + For beta updates, you may consider migrating to -[SPUUpdaterDelegate allowedChannelsForUpdater:] in the future. + This method must be called on the main thread; calls from background threads will have no effect. */ - (void)setFeedURL:(NSURL * _Nullable)feedURL; diff --git a/Sparkle/SPUUpdaterDelegate.h b/Sparkle/SPUUpdaterDelegate.h index 18bad993..28aed683 100644 --- a/Sparkle/SPUUpdaterDelegate.h +++ b/Sparkle/SPUUpdaterDelegate.h @@ -160,9 +160,38 @@ typedef NS_ENUM(NSInteger, SPUUpdateCheck) */ - (void)updater:(SPUUpdater *)updater didFinishLoadingAppcast:(SUAppcast *)appcast; +/*! + Returns the set of Sparkle channels the updater is allowed to find new updates from. + + An appcast item can specify a channel the update is posted to. Without specifying a channel, the appcast item is posted to the default channel. + For instance: + + 2.0 Beta 1 + beta + + + This example posts an update to the 'beta' channel, so only updaters that are allowed to use the 'beta' channel can find this update. + + If the is not present, the update item is posted to the default channel and can be found by any updater. + + You can pick any name you'd like for the channel. The valid characters for channel names are letters, numbers, dashes, underscores, and periods. + + Note to use this feature, all app versions that your users may update from in your feed must use a version of Sparkle that supports this feature. + This feature was added in Sparkle 2. + + \return The set of channel names the updater is allowed to find new updates in. An empty set is the default behavior, + which means the updater will only look for updates in the default channel. + */ +- (NSSet *)allowedChannelsForUpdater:(SPUUpdater *)updater; + /*! Returns the item in the appcast corresponding to the update that should be installed. + Please consider using or migrating to other supported features before adopting this method. + Specifically: + If you want to filter out certain tagged updates (like beta updates), consider -[SPUUpdaterDelegate allowedChannelsForUpdater:] instead. + If you want to treat certain updates as informational-only, consider supplying with a set of affected versions users are updating from. + If you're using special logic or extensions in your appcast, implement this to use your own logic for finding a valid update, if any, in the given appcast. diff --git a/Sparkle/SUAppcast.m b/Sparkle/SUAppcast.m index c3d927cc..57076764 100644 --- a/Sparkle/SUAppcast.m +++ b/Sparkle/SUAppcast.m @@ -145,15 +145,15 @@ [dict setObject:deltas forKey:name]; } else if ([name isEqualToString:SUAppcastElementTags]) { - NSMutableArray *tags = [NSMutableArray array]; + NSMutableArray *names = [NSMutableArray array]; NSEnumerator *childEnum = [[node children] objectEnumerator]; for (NSXMLNode *child in childEnum) { NSString *childName = child.name; if (childName) { - [tags addObject:childName]; + [names addObject:childName]; } } - [dict setObject:tags forKey:name]; + [dict setObject:names forKey:name]; } else if ([name isEqualToString:SUAppcastElementInformationalUpdate]) { NSMutableSet *informationalUpdateVersions = [NSMutableSet set]; diff --git a/Sparkle/SUAppcastDriver.m b/Sparkle/SUAppcastDriver.m index 82834df5..d7f125e4 100644 --- a/Sparkle/SUAppcastDriver.m +++ b/Sparkle/SUAppcastDriver.m @@ -166,10 +166,18 @@ NSDictionary *userInfo = @{ SUUpdaterAppcastNotificationKey: loadedAppcast }; [[NSNotificationCenter defaultCenter] postNotificationName:SUUpdaterDidFinishLoadingAppCastNotification object:self.updater userInfo:userInfo]; - // We will never care about other OS's - SUAppcast *macOSAppcast = [loadedAppcast copyByFilteringItems:^(SUAppcastItem *item) { - return (BOOL)[item isMacOsUpdate]; - }]; + NSSet *allowedChannels; + if ([self.updaterDelegate respondsToSelector:@selector(allowedChannelsForUpdater:)]) { + allowedChannels = [self.updaterDelegate allowedChannelsForUpdater:self.updater]; + if (allowedChannels == nil) { + SULog(SULogLevelError, @"Error: -allowedChannelsForUpdater: cannot return nil. Treating this as an empty set."); + allowedChannels = [NSSet set]; + } + } else { + allowedChannels = [NSSet set]; + } + + SUAppcast *macOSAppcast = [[self class] filterAppcast:loadedAppcast forMacOSAndAllowedChannels:allowedChannels]; id applicationVersionComparator = [self versionComparator]; @@ -221,6 +229,26 @@ } } +// This method is used by unit tests ++ (SUAppcast *)filterAppcast:(SUAppcast *)appcast forMacOSAndAllowedChannels:(NSSet *)allowedChannels +{ + return [appcast copyByFilteringItems:^(SUAppcastItem *item) { + // We will never care about other OS's + BOOL macOSUpdate = [item isMacOsUpdate]; + if (!macOSUpdate) { + return NO; + } + + NSString *channel = item.channel; + if (channel == nil) { + // Item is on the default channel + return YES; + } + + return [allowedChannels containsObject:channel]; + }]; +} + // This method is used by unit tests + (SUAppcast *)filterSupportedAppcast:(SUAppcast *)appcast phasedUpdateGroup:(NSNumber * _Nullable)phasedUpdateGroup skippedUpdate:(SPUSkippedUpdate * _Nullable)skippedUpdate hostVersion:(NSString *)hostVersion versionComparator:(id)versionComparator testOSVersion:(BOOL)testOSVersion testMinimumAutoupdateVersion:(BOOL)testMinimumAutoupdateVersion { diff --git a/Sparkle/SUAppcastItem.h b/Sparkle/SUAppcastItem.h index 4d3efba1..95b2dda1 100644 --- a/Sparkle/SUAppcastItem.h +++ b/Sparkle/SUAppcastItem.h @@ -52,6 +52,7 @@ SU_EXPORT @interface SUAppcastItem : NSObject @property (copy, readonly, nullable) NSNumber* phasedRolloutInterval; @property (nonatomic, copy, readonly) NSString *installationType; @property (copy, readonly, nullable) NSString *minimumAutoupdateVersion; +@property (nonatomic, readonly, nullable) NSString *channel; @property (getter=isDeltaUpdate, readonly) BOOL deltaUpdate; @property (getter=isCriticalUpdate, readonly) BOOL criticalUpdate; diff --git a/Sparkle/SUAppcastItem.m b/Sparkle/SUAppcastItem.m index e1fd3b89..3fa305a4 100644 --- a/Sparkle/SUAppcastItem.m +++ b/Sparkle/SUAppcastItem.m @@ -73,6 +73,7 @@ static NSString *SUAppcastItemStateKey = @"SUAppcastItemState"; @synthesize state = _state; @synthesize hasCriticalInformation = _hasCriticalInformation; @synthesize informationalUpdateVersions = _informationalUpdateVersions; +@synthesize channel = _channel; + (BOOL)supportsSecureCoding { @@ -130,6 +131,8 @@ static NSString *SUAppcastItemStateKey = @"SUAppcastItemState"; _propertiesDictionary = propertiesDictionary; _phasedRolloutInterval = [decoder decodeObjectOfClass:[NSNumber class] forKey:SUAppcastElementPhasedRolloutInterval]; + + _channel = [(NSString *)[decoder decodeObjectOfClass:[NSString class] forKey:SUAppcastElementChannel] copy]; } return self; @@ -206,6 +209,10 @@ static NSString *SUAppcastItemStateKey = @"SUAppcastItemState"; if (self.phasedRolloutInterval != nil) { [encoder encodeObject:self.phasedRolloutInterval forKey:SUAppcastElementPhasedRolloutInterval]; } + + if (self.channel != nil) { + [encoder encodeObject:self.channel forKey:SUAppcastElementChannel]; + } } - (BOOL)isDeltaUpdate @@ -421,6 +428,24 @@ static NSString *SUAppcastItemStateKey = @"SUAppcastItemState"; _maximumSystemVersion = [(NSString *)[dict objectForKey:SUAppcastElementMaximumSystemVersion] copy]; _minimumAutoupdateVersion = [(NSString *)[dict objectForKey:SUAppcastElementMinimumAutoupdateVersion] copy]; + NSString *channel = [dict objectForKey:SUAppcastElementChannel]; + if (channel != nil) { + if (channel.length == 0) { + SULog(SULogLevelError, @"warning: Item with version '%@' has zero-length channel; this will be ignored.", newVersion); + _channel = nil; + } else { + // Reject characters in the channel name that may cause parsing problems in tools later + NSMutableCharacterSet *allowedCharacterSet = [NSMutableCharacterSet alphanumericCharacterSet]; + [allowedCharacterSet addCharactersInString:@"_.-"]; + if ([channel rangeOfCharacterFromSet:allowedCharacterSet.invertedSet].location != NSNotFound) { + SULog(SULogLevelError, @"warning: Item with version '%@' has channel with invalid name. This channel will be ignored. Only [a-zA-Z0-9._-] is allowed.", newVersion); + _channel = nil; + } else { + _channel = [channel copy]; + } + } + } + // Grab critical update information NSDictionary * _Nullable criticalUpdateDictionaryFromAppcast = (NSDictionary *)[dict objectForKey:SUAppcastElementCriticalUpdate]; NSArray *tags = [dict objectForKey:SUAppcastElementTags]; diff --git a/Sparkle/SUConstants.h b/Sparkle/SUConstants.h index a1f2669c..7d1ad6a6 100644 --- a/Sparkle/SUConstants.h +++ b/Sparkle/SUConstants.h @@ -88,6 +88,7 @@ extern NSString *const SUAppcastElementReleaseNotesLink; extern NSString *const SUAppcastElementTags; extern NSString *const SUAppcastElementPhasedRolloutInterval; extern NSString *const SUAppcastElementInformationalUpdate; +extern NSString *const SUAppcastElementChannel; extern NSString *const SURSSAttributeURL; extern NSString *const SURSSAttributeLength; diff --git a/Sparkle/SUConstants.m b/Sparkle/SUConstants.m index d1c75802..1d3c3cd1 100644 --- a/Sparkle/SUConstants.m +++ b/Sparkle/SUConstants.m @@ -84,6 +84,7 @@ NSString *const SUAppcastElementReleaseNotesLink = @"sparkle:releaseNotesLink"; NSString *const SUAppcastElementTags = @"sparkle:tags"; NSString *const SUAppcastElementPhasedRolloutInterval = @"sparkle:phasedRolloutInterval"; NSString *const SUAppcastElementInformationalUpdate = @"sparkle:informationalUpdate"; +NSString *const SUAppcastElementChannel = @"sparkle:channel"; NSString *const SURSSAttributeURL = @"url"; NSString *const SURSSAttributeLength = @"length"; diff --git a/Sparkle/ja.lproj/Sparkle.strings b/Sparkle/ja.lproj/Sparkle.strings index d7c2654f..f3a631ae 100644 --- a/Sparkle/ja.lproj/Sparkle.strings +++ b/Sparkle/ja.lproj/Sparkle.strings @@ -14,9 +14,6 @@ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@は現在入手できる最新バージョンです。\n(現在使用中のバージョンは%3$@です。)"; -/* An appcast feed error when checking for updates and no appcast items are retrieved */ -"No valid update information could be loaded." = "有効なアップデート情報が取得できませんでした。"; - "An error occurred while running the updater. Please try again later." = "アップデータを実行中にエラーが発生しました。あとでやり直してください。"; "An error occurred while starting the installer. Please try again later." = "インストーラを始動中にエラーが発生しました。あとでやり直してください。"; diff --git a/Tests/Resources/testappcast_channels.xml b/Tests/Resources/testappcast_channels.xml new file mode 100644 index 00000000..48729986 --- /dev/null +++ b/Tests/Resources/testappcast_channels.xml @@ -0,0 +1,59 @@ + + + + For unit test only + + + Version 2.0 + desc + Sat, 26 Jul 2014 15:20:11 +0000 + + + + + + Version 3.0 + 86400 + + + + + + + + + + + + + Version 4.0 + 4.0 + Sat, 26 Jul 2014 15:20:13 +0000 + + beta + + + + Version 5.0 + 5.0 + + nightly + + + + Version 6.0 + 6.0 + + + + diff --git a/Tests/SUAppcastTest.swift b/Tests/SUAppcastTest.swift index b593a098..d4ac65fe 100644 --- a/Tests/SUAppcastTest.swift +++ b/Tests/SUAppcastTest.swift @@ -79,6 +79,64 @@ class SUAppcastTest: XCTestCase { } } + func testChannelsAndMacOSReleases() { + let testURL = Bundle(for: SUAppcastTest.self).url(forResource: "testappcast_channels", withExtension: "xml")! + + do { + let testData = try Data(contentsOf: testURL) + + let versionComparator = SUStandardVersionComparator.default() + let hostVersion = "1.0" + let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) + + let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver) + XCTAssertEqual(5, appcast.items.count) + + do { + let filteredAppcast = SUAppcastDriver.filterAppcast(appcast, forMacOSAndAllowedChannels: ["beta", "nightly"]) + XCTAssertEqual(4, filteredAppcast.items.count) + + XCTAssertEqual("2.0", filteredAppcast.items[0].versionString) + XCTAssertEqual("3.0", filteredAppcast.items[1].versionString) + XCTAssertEqual("4.0", filteredAppcast.items[2].versionString) + XCTAssertEqual("5.0", filteredAppcast.items[3].versionString) + } + + do { + let filteredAppcast = SUAppcastDriver.filterAppcast(appcast, forMacOSAndAllowedChannels: []) + XCTAssertEqual(2, filteredAppcast.items.count) + XCTAssertEqual("2.0", filteredAppcast.items[0].versionString) + XCTAssertEqual("3.0", filteredAppcast.items[1].versionString) + } + + do { + let filteredAppcast = SUAppcastDriver.filterAppcast(appcast, forMacOSAndAllowedChannels: ["beta"]) + XCTAssertEqual(3, filteredAppcast.items.count) + XCTAssertEqual("2.0", filteredAppcast.items[0].versionString) + XCTAssertEqual("3.0", filteredAppcast.items[1].versionString) + XCTAssertEqual("4.0", filteredAppcast.items[2].versionString) + } + + do { + let filteredAppcast = SUAppcastDriver.filterAppcast(appcast, forMacOSAndAllowedChannels: ["nightly"]) + XCTAssertEqual(3, filteredAppcast.items.count) + XCTAssertEqual("2.0", filteredAppcast.items[0].versionString) + XCTAssertEqual("3.0", filteredAppcast.items[1].versionString) + XCTAssertEqual("5.0", filteredAppcast.items[2].versionString) + } + + do { + let filteredAppcast = SUAppcastDriver.filterAppcast(appcast, forMacOSAndAllowedChannels: ["madeup"]) + XCTAssertEqual("2.0", filteredAppcast.items[0].versionString) + XCTAssertEqual("3.0", filteredAppcast.items[1].versionString) + XCTAssertEqual(2, filteredAppcast.items.count) + } + } catch let err as NSError { + NSLog("%@", err) + XCTFail(err.localizedDescription) + } + } + func testCriticalUpdateVersion() { let testURL = Bundle(for: SUAppcastTest.self).url(forResource: "testappcast", withExtension: "xml")! diff --git a/Tests/Sparkle Unit Tests-Bridging-Header.h b/Tests/Sparkle Unit Tests-Bridging-Header.h index d0e9251e..395426db 100644 --- a/Tests/Sparkle Unit Tests-Bridging-Header.h +++ b/Tests/Sparkle Unit Tests-Bridging-Header.h @@ -42,6 +42,8 @@ static const char *SUAppleQuarantineIdentifier = "com.apple.quarantine"; + (SUAppcast *)filterSupportedAppcast:(SUAppcast *)appcast phasedUpdateGroup:(NSNumber * _Nullable)phasedUpdateGroup skippedUpdate:(SPUSkippedUpdate * _Nullable)skippedUpdate hostVersion:(NSString *)hostVersion versionComparator:(id)versionComparator testOSVersion:(BOOL)testOSVersion testMinimumAutoupdateVersion:(BOOL)testMinimumAutoupdateVersion; ++ (SUAppcast *)filterAppcast:(SUAppcast *)appcast forMacOSAndAllowedChannels:(NSSet *)allowedChannels; + @end @interface SUBinaryDeltaUnarchiver (Private) diff --git a/sparkle-cli/SPUCommandLineDriver.h b/sparkle-cli/SPUCommandLineDriver.h index bd387cea..56396381 100644 --- a/sparkle-cli/SPUCommandLineDriver.h +++ b/sparkle-cli/SPUCommandLineDriver.h @@ -14,7 +14,7 @@ NS_ASSUME_NONNULL_BEGIN @interface SPUCommandLineDriver : NSObject -- (nullable instancetype)initWithUpdateBundlePath:(NSString *)updateBundlePath applicationBundlePath:(nullable NSString *)applicationBundlePath customFeedURL:(nullable NSString *)customFeedURL updatePermissionResponse:(nullable SUUpdatePermissionResponse *)updatePermissionResponse deferInstallation:(BOOL)deferInstallation interactiveInstallation:(BOOL)interactiveInstallation verbose:(BOOL)verbose; +- (nullable instancetype)initWithUpdateBundlePath:(NSString *)updateBundlePath applicationBundlePath:(nullable NSString *)applicationBundlePath allowedChannels:(NSSet *)allowedChannels customFeedURL:(nullable NSString *)customFeedURL updatePermissionResponse:(nullable SUUpdatePermissionResponse *)updatePermissionResponse deferInstallation:(BOOL)deferInstallation interactiveInstallation:(BOOL)interactiveInstallation verbose:(BOOL)verbose; - (void)runAndCheckForUpdatesNow:(BOOL)checkForUpdatesNow; diff --git a/sparkle-cli/SPUCommandLineDriver.m b/sparkle-cli/SPUCommandLineDriver.m index 83e05c97..9f576185 100644 --- a/sparkle-cli/SPUCommandLineDriver.m +++ b/sparkle-cli/SPUCommandLineDriver.m @@ -16,6 +16,7 @@ @property (nonatomic, readonly) BOOL verbose; @property (nonatomic) BOOL probingForUpdates; @property (nonatomic, readonly) BOOL interactive; +@property (nonatomic, readonly) NSSet *allowedChannels; @property (nonatomic, copy, readonly, nullable) NSString *customFeedURL; @end @@ -26,9 +27,10 @@ @synthesize verbose = _verbose; @synthesize probingForUpdates = _probingForUpdates; @synthesize interactive = _interactive; +@synthesize allowedChannels = _allowedChannels; @synthesize customFeedURL = _customFeedURL; -- (instancetype)initWithUpdateBundlePath:(NSString *)updateBundlePath applicationBundlePath:(nullable NSString *)applicationBundlePath customFeedURL:(nullable NSString *)customFeedURL updatePermissionResponse:(nullable SUUpdatePermissionResponse *)updatePermissionResponse deferInstallation:(BOOL)deferInstallation interactiveInstallation:(BOOL)interactiveInstallation verbose:(BOOL)verbose +- (instancetype)initWithUpdateBundlePath:(NSString *)updateBundlePath applicationBundlePath:(nullable NSString *)applicationBundlePath allowedChannels:(NSSet *)allowedChannels customFeedURL:(nullable NSString *)customFeedURL updatePermissionResponse:(nullable SUUpdatePermissionResponse *)updatePermissionResponse deferInstallation:(BOOL)deferInstallation interactiveInstallation:(BOOL)interactiveInstallation verbose:(BOOL)verbose { self = [super init]; if (self != nil) { @@ -49,6 +51,7 @@ _verbose = verbose; _interactive = interactiveInstallation; + _allowedChannels = allowedChannels; _customFeedURL = [customFeedURL copy]; id userDriver = [[SPUCommandLineUserDriver alloc] initWithUpdatePermissionResponse:updatePermissionResponse deferInstallation:deferInstallation verbose:verbose]; @@ -84,6 +87,11 @@ } } +- (NSSet *)allowedChannelsForUpdater:(SPUUpdater *)__unused updater +{ + return self.allowedChannels; +} + - (nullable NSString *)feedURLStringForUpdater:(SPUUpdater *)__unused updater { return self.customFeedURL; diff --git a/sparkle-cli/main.m b/sparkle-cli/main.m index e6025bb1..904440c3 100644 --- a/sparkle-cli/main.m +++ b/sparkle-cli/main.m @@ -20,10 +20,11 @@ #define PROBE_FLAG "probe" #define INTERACTIVE_FLAG "interactive" #define FEED_URL_FLAG "feed-url" +#define CHANNELS_FLAG "channels" static void printUsage(char **argv) { - fprintf(stderr, "Usage: %s bundle [--%s ] [--%s] [--%s] [--%s] [--%s] [--%s] [--%s] [--%s]\n", argv[0], APPLICATION_FLAG, CHECK_NOW_FLAG, PROBE_FLAG, GRANT_AUTOMATIC_CHECKING_FLAG, SEND_PROFILE_FLAG, DEFER_FLAG, INTERACTIVE_FLAG, VERBOSE_FLAG); + fprintf(stderr, "Usage: %s bundle [--%s app-path] [--%s] [--%s] [--%s chan1,chan2,…] [--%s feed-url] [--%s] [--%s] [--%s] [--%s] [--%s]\n", argv[0], APPLICATION_FLAG, CHECK_NOW_FLAG, PROBE_FLAG, CHANNELS_FLAG, FEED_URL_FLAG, GRANT_AUTOMATIC_CHECKING_FLAG, SEND_PROFILE_FLAG, DEFER_FLAG, INTERACTIVE_FLAG, VERBOSE_FLAG); fprintf(stderr, "Description:\n"); fprintf(stderr, " Check if any new updates for a Sparkle supported bundle need to be installed.\n\n"); fprintf(stderr, " If any new updates need to be installed, the user application\n is terminated and the update is installed immediately unless --%s\n is specified. If the application was alive, then it will be relaunched after.\n\n", DEFER_FLAG); @@ -36,6 +37,7 @@ static void printUsage(char **argv) fprintf(stderr, " --%s\n Path to the application to watch for termination and to relaunch.\n If not provided, this is assumed to be the same as the bundle.\n", APPLICATION_FLAG); fprintf(stderr, " --%s\n Immediately checks for updates to install.\n Without this, updates are checked only when needed on a scheduled basis.\n", CHECK_NOW_FLAG); fprintf(stderr, " --%s\n Probe for updates. Check if any updates are available but do not install.\n An exit status of 0 is returned if a new update is available.\n", PROBE_FLAG); + fprintf(stderr, " --%s\n List of allowed Sparkle channels to look for updates in. By default,\n only the default channel is used.\n", CHANNELS_FLAG); fprintf(stderr, " --%s\n URL for appcast feed. This URL will be used for the feed instead of the one\n in the bundle's Info.plist or in the bundle's user defaults.\n", FEED_URL_FLAG); fprintf(stderr, " --%s\n Allows prompting the user for an authorization dialog prompt if the\n installer needs elevated privileges, or allows performing an interactive\n installer package.\n", INTERACTIVE_FLAG); fprintf(stderr, " --%s\n If update permission is requested, this enables automatic update checks.\n Note that this behavior may overwrite the user's defaults for the bundle.\n This option has no effect if --%s is passed, or if the\n user has replied to this request already, or if the developer configured\n to skip it.\n", GRANT_AUTOMATIC_CHECKING_FLAG, CHECK_NOW_FLAG); @@ -55,6 +57,7 @@ int main(int argc, char **argv) struct option longOptions[] = { {APPLICATION_FLAG, required_argument, NULL, 0}, + {CHANNELS_FLAG, required_argument, NULL, 0}, {FEED_URL_FLAG, required_argument, NULL, 0}, {DEFER_FLAG, no_argument, NULL, 0}, {VERBOSE_FLAG, no_argument, NULL, 0}, @@ -68,6 +71,7 @@ int main(int argc, char **argv) NSString *applicationPath = nil; NSString *feedURL = nil; + NSSet *channels = [NSSet set]; BOOL deferInstall = NO; BOOL verbose = NO; BOOL checkForUpdatesNow = NO; @@ -100,6 +104,18 @@ int main(int argc, char **argv) printUsage(argv); return EXIT_FAILURE; } + } else if (strcmp(CHANNELS_FLAG, longOptions[optionIndex].name) == 0) { + assert(optarg != NULL); + + NSString *channelsString = [[NSString alloc] initWithUTF8String:optarg]; + if (channelsString == nil) { + printUsage(argv); + return EXIT_FAILURE; + } + + if (channelsString.length > 0) { + channels = [NSSet setWithArray:[channelsString componentsSeparatedByString:@","]]; + } } else if (strcmp(DEFER_FLAG, longOptions[optionIndex].name) == 0) { deferInstall = YES; } else if (strcmp(VERBOSE_FLAG, longOptions[optionIndex].name) == 0) { @@ -146,7 +162,7 @@ int main(int argc, char **argv) updatePermissionResponse = [[SUUpdatePermissionResponse alloc] initWithAutomaticUpdateChecks:YES sendSystemProfile:sendProfile]; } - SPUCommandLineDriver *driver = [[SPUCommandLineDriver alloc] initWithUpdateBundlePath:updatePath applicationBundlePath:applicationPath customFeedURL:feedURL updatePermissionResponse:updatePermissionResponse deferInstallation:deferInstall interactiveInstallation:interactive verbose:verbose]; + SPUCommandLineDriver *driver = [[SPUCommandLineDriver alloc] initWithUpdateBundlePath:updatePath applicationBundlePath:applicationPath allowedChannels:channels customFeedURL:feedURL updatePermissionResponse:updatePermissionResponse deferInstallation:deferInstall interactiveInstallation:interactive verbose:verbose]; if (driver == nil) { fprintf(stderr, "Error: Failed to initialize updater. Are the bundle paths provided valid?\n"); return EXIT_FAILURE;