diff --git a/Squirrel/NSBundle+SQRLVersionExtensions.h b/Squirrel/NSBundle+SQRLVersionExtensions.h index d53f35d9..be7e4669 100644 --- a/Squirrel/NSBundle+SQRLVersionExtensions.h +++ b/Squirrel/NSBundle+SQRLVersionExtensions.h @@ -14,4 +14,7 @@ // Info.plist, or nil if the key is not present. @property (nonatomic, copy, readonly) NSString *sqrl_bundleVersion; +/// The value of the `kCFBundleExecutableKey` key. +@property (nonatomic, copy, readonly) NSString *sqrl_executableName; + @end diff --git a/Squirrel/NSBundle+SQRLVersionExtensions.m b/Squirrel/NSBundle+SQRLVersionExtensions.m index a61da0e3..70a43223 100644 --- a/Squirrel/NSBundle+SQRLVersionExtensions.m +++ b/Squirrel/NSBundle+SQRLVersionExtensions.m @@ -14,4 +14,8 @@ - (NSString *)sqrl_bundleVersion { return [self objectForInfoDictionaryKey:(id)kCFBundleVersionKey]; } +- (NSString *)sqrl_executableName { + return [self objectForInfoDictionaryKey:(id)kCFBundleExecutableKey]; +} + @end diff --git a/Squirrel/SQRLInstaller.m b/Squirrel/SQRLInstaller.m index 82ac17a9..b64c84df 100644 --- a/Squirrel/SQRLInstaller.m +++ b/Squirrel/SQRLInstaller.m @@ -262,27 +262,49 @@ - (RACSignal *)acquireTargetBundleURLForRequest:(SQRLShipItRequest *)request { setNameWithFormat:@"%@ -acquireTargetBundleURLForRequest: %@", self, request]; } +- (RACSignal *)renameIfNeeded:(SQRLShipItRequest *)request updateBundleURL:(NSURL *)updateBundleURL { + if (!request.useUpdateBundleName) return [RACSignal return:request]; + + return [[self + renamedTargetIfNeededWithTargetURL:request.targetBundleURL sourceURL:updateBundleURL] + flattenMap:^(NSURL *newTargetURL) { + if ([newTargetURL isEqual:request.targetBundleURL]) return [RACSignal return:request]; + + SQRLShipItRequest *updatedRequest = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:request.updateBundleURL targetBundleURL:newTargetURL bundleIdentifier:request.bundleIdentifier launchAfterInstallation:request.launchAfterInstallation useUpdateBundleName:request.useUpdateBundleName]; + return [[self + installItemToURL:newTargetURL fromURL:request.targetBundleURL] + concat:[RACSignal return:updatedRequest]]; + }]; +} + - (RACSignal *)installRequest:(SQRLShipItRequest *)request { NSParameterAssert(request != nil); return [[[[self prepareAndValidateUpdateBundleURLForRequest:request] flattenMap:^(NSURL *updateBundleURL) { - return [[[[[[[self - acquireTargetBundleURLForRequest:request] - concat:[self installItemToURL:request.targetBundleURL fromURL:updateBundleURL]] - concat:[RACSignal return:request.updateBundleURL]] - concat:[RACSignal return:updateBundleURL]] - concat:[RACSignal defer:^{ - return [RACSignal return:self.ownedBundle.temporaryURL]; - }]] - flattenMap:^(NSURL *location) { - return [[[self - deleteOwnedBundleAtURL:location] - doError:^(NSError *error) { - NSLog(@"Couldn't remove owned bundle at location %@, error %@", location, error.sqrl_verboseDescription); + return [[[[self + renameIfNeeded:request updateBundleURL:updateBundleURL] + flattenMap:^(SQRLShipItRequest *request) { + return [[self acquireTargetBundleURLForRequest:request] concat:[RACSignal return:request]]; + }] + flattenMap:^(SQRLShipItRequest *request) { + return [[[[[[self + installItemToURL:request.targetBundleURL fromURL:updateBundleURL] + concat:[RACSignal return:request.updateBundleURL]] + concat:[RACSignal return:updateBundleURL]] + concat:[RACSignal defer:^{ + return [RACSignal return:self.ownedBundle.temporaryURL]; + }]] + flattenMap:^(NSURL *location) { + return [[[self + deleteOwnedBundleAtURL:location] + doError:^(NSError *error) { + NSLog(@"Couldn't remove owned bundle at location %@, error %@", location, error.sqrl_verboseDescription); + }] + catchTo:[RACSignal empty]]; }] - catchTo:[RACSignal empty]]; + concat:[RACSignal return:request]]; }] doCompleted:^{ self.ownedBundle = nil; @@ -413,6 +435,36 @@ - (RACSignal *)verifyBundleAtURL:(NSURL *)bundleURL usingSignature:(SQRLCodeSign #pragma mark Installation +/// Check if the target should be renamed and provide the renamed URL. +/// +/// targetURL - The URL for the target. Cannot be nil. +/// sourceURL - The URL for the source. Cannot be nil. +/// +/// Returns a signal which will send the URL for the renamed target. If a rename +/// isn't needed then it will send `targetURL`. +- (RACSignal *)renamedTargetIfNeededWithTargetURL:(NSURL *)targetURL sourceURL:(NSURL *)sourceURL { + return [RACSignal defer:^{ + NSBundle *sourceBundle = [NSBundle bundleWithURL:sourceURL]; + NSString *targetExecutableName = targetURL.lastPathComponent.stringByDeletingPathExtension; + NSString *sourceExecutableName = sourceBundle.sqrl_executableName; + + // If they're already the same then we're good. + if ([targetExecutableName isEqual:sourceExecutableName]) { + return [RACSignal return:targetURL]; + } + + NSString *newAppName = [sourceExecutableName stringByAppendingPathExtension:@"app"]; + NSURL *newTargetURL = [targetURL.URLByDeletingLastPathComponent URLByAppendingPathComponent:newAppName]; + + // If there's already something there then don't rename to it. + if ([NSFileManager.defaultManager fileExistsAtPath:newTargetURL.path]) { + return [RACSignal return:targetURL]; + } + + return [RACSignal return:newTargetURL]; + }]; +} + - (RACSignal *)installItemToURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL { NSParameterAssert(targetURL != nil); NSParameterAssert(sourceURL != nil); diff --git a/Squirrel/SQRLShipItRequest.h b/Squirrel/SQRLShipItRequest.h index 056cefd6..8edc3634 100644 --- a/Squirrel/SQRLShipItRequest.h +++ b/Squirrel/SQRLShipItRequest.h @@ -73,10 +73,11 @@ extern NSString * const SQRLShipItRequestPropertyErrorKey; // installing. Can be nil. // launchAfterInstallation - Whether the updated application should be launched // after installation. +// useUpdateBundleName - Should the target use the update bundle's name? // // Returns a request which can be written to disk for ShipIt to read and // perform. -- (instancetype)initWithUpdateBundleURL:(NSURL *)updateBundleURL targetBundleURL:(NSURL *)targetBundleURL bundleIdentifier:(NSString *)bundleIdentifier launchAfterInstallation:(BOOL)launchAfterInstallation; +- (instancetype)initWithUpdateBundleURL:(NSURL *)updateBundleURL targetBundleURL:(NSURL *)targetBundleURL bundleIdentifier:(NSString *)bundleIdentifier launchAfterInstallation:(BOOL)launchAfterInstallation useUpdateBundleName:(BOOL)useUpdateBundleName; // The URL to the downloaded update's app bundle. @property (nonatomic, copy, readonly) NSURL *updateBundleURL; @@ -93,4 +94,7 @@ extern NSString * const SQRLShipItRequestPropertyErrorKey; // Whether to launch the application after an update is successfully installed. @property (nonatomic, assign, readonly) BOOL launchAfterInstallation; +// Whether the app should use the update bundle's name. +@property (nonatomic, assign, readonly) BOOL useUpdateBundleName; + @end diff --git a/Squirrel/SQRLShipItRequest.m b/Squirrel/SQRLShipItRequest.m index 6351e037..f6fd03cd 100644 --- a/Squirrel/SQRLShipItRequest.m +++ b/Squirrel/SQRLShipItRequest.m @@ -47,12 +47,13 @@ - (id)initWithDictionary:(NSDictionary *)dictionary error:(NSError **)error { return self; } -- (instancetype)initWithUpdateBundleURL:(NSURL *)updateBundleURL targetBundleURL:(NSURL *)targetBundleURL bundleIdentifier:(NSString *)bundleIdentifier launchAfterInstallation:(BOOL)launchAfterInstallation { +- (instancetype)initWithUpdateBundleURL:(NSURL *)updateBundleURL targetBundleURL:(NSURL *)targetBundleURL bundleIdentifier:(NSString *)bundleIdentifier launchAfterInstallation:(BOOL)launchAfterInstallation useUpdateBundleName:(BOOL)useUpdateBundleName { return [self initWithDictionary:@{ @keypath(self.updateBundleURL): updateBundleURL, @keypath(self.targetBundleURL): targetBundleURL, @keypath(self.bundleIdentifier): bundleIdentifier ?: NSNull.null, @keypath(self.launchAfterInstallation): @(launchAfterInstallation), + @keypath(self.useUpdateBundleName): @(useUpdateBundleName), } error:NULL]; } diff --git a/Squirrel/SQRLUpdater.m b/Squirrel/SQRLUpdater.m index af3473dd..723fe03b 100644 --- a/Squirrel/SQRLUpdater.m +++ b/Squirrel/SQRLUpdater.m @@ -528,7 +528,12 @@ - (RACSignal *)prepareUpdateForInstallation:(SQRLDownloadedUpdate *)update { return [[[[RACSignal defer:^{ NSRunningApplication *currentApplication = NSRunningApplication.currentApplication; - SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:update.bundle.bundleURL targetBundleURL:currentApplication.bundleURL bundleIdentifier:currentApplication.bundleIdentifier launchAfterInstallation:NO]; + NSBundle *appBundle = [NSBundle bundleWithURL:currentApplication.bundleURL]; + // Only use the update bundle's name if the user hasn't renamed the + // app themselves. + BOOL useUpdateBundleName = [appBundle.sqrl_executableName isEqual:currentApplication.bundleURL.lastPathComponent.stringByDeletingPathExtension]; + + SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:update.bundle.bundleURL targetBundleURL:currentApplication.bundleURL bundleIdentifier:currentApplication.bundleIdentifier launchAfterInstallation:NO useUpdateBundleName:useUpdateBundleName]; return [request writeUsingURL:self.shipItStateURL]; }] then:^{ @@ -542,7 +547,7 @@ - (RACSignal *)relaunchToInstallUpdate { return [[[[[[[[SQRLShipItRequest readUsingURL:self.shipItStateURL] map:^(SQRLShipItRequest *request) { - return [[SQRLShipItRequest alloc] initWithUpdateBundleURL:request.updateBundleURL targetBundleURL:request.targetBundleURL bundleIdentifier:request.bundleIdentifier launchAfterInstallation:YES]; + return [[SQRLShipItRequest alloc] initWithUpdateBundleURL:request.updateBundleURL targetBundleURL:request.targetBundleURL bundleIdentifier:request.bundleIdentifier launchAfterInstallation:YES useUpdateBundleName:request.useUpdateBundleName]; }] flattenMap:^(SQRLShipItRequest *request) { return [[request diff --git a/Squirrel/ShipIt-main.m b/Squirrel/ShipIt-main.m index eaeaaad1..44fa16f4 100644 --- a/Squirrel/ShipIt-main.m +++ b/Squirrel/ShipIt-main.m @@ -75,7 +75,7 @@ static void installRequest(RACSignal *readRequestSignal, SQRLDirectoryManager *d RACSignal *action; if (attempt > SQRLShipItMaximumInstallationAttempts) { - action = [[[installer.abortInstallationCommand + action = [[[[installer.abortInstallationCommand execute:request] initially:^{ NSLog(@"Too many attempts to install, aborting update"); @@ -86,7 +86,8 @@ static void installRequest(RACSignal *readRequestSignal, SQRLDirectoryManager *d // Exit successfully so launchd doesn't restart us // again. return [RACSignal empty]; - }]; + }] + concat:[RACSignal return:request]]; } else { action = [[[[installer.installUpdateCommand execute:request] @@ -114,8 +115,8 @@ static void installRequest(RACSignal *readRequestSignal, SQRLDirectoryManager *d // Launch regardless of whether installation succeeds or fails. action = [[action deliverOn:RACScheduler.mainThreadScheduler] - finally:^{ - NSURL *bundleURL = request.targetBundleURL; + doNext:^(SQRLShipItRequest *finalRequest) { + NSURL *bundleURL = finalRequest.targetBundleURL; if (bundleURL == nil) { NSLog(@"Missing target bundle URL, cannot launch application"); return; diff --git a/SquirrelTests/SQRLInstallerSpec.m b/SquirrelTests/SQRLInstallerSpec.m index 9b472ea5..34fcb8e8 100644 --- a/SquirrelTests/SQRLInstallerSpec.m +++ b/SquirrelTests/SQRLInstallerSpec.m @@ -40,7 +40,7 @@ }); it(@"should install an update using ShipIt", ^{ - SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO]; + SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO]; [self installWithRequest:request remote:YES]; @@ -48,7 +48,7 @@ }); it(@"should install an update in process", ^{ - SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO]; + SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO]; [self installWithRequest:request remote:NO]; @@ -60,7 +60,7 @@ NSArray *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:bundleIdentifier]; expect(@(apps.count)).to(equal(@0)); - SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:YES]; + SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:YES useUpdateBundleName:NO]; [self installWithRequest:request remote:YES]; @@ -72,7 +72,7 @@ NSURL *diskImageURL = [self createAndMountDiskImageNamed:@"TestApplication 2.1" fromDirectory:updateURL.URLByDeletingLastPathComponent]; updateURL = [diskImageURL URLByAppendingPathComponent:updateURL.lastPathComponent]; - SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO]; + SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO]; [self installWithRequest:request remote:YES]; @@ -83,7 +83,7 @@ NSURL *diskImageURL = [self createAndMountDiskImageNamed:@"TestApplication" fromDirectory:self.testApplicationURL.URLByDeletingLastPathComponent]; NSURL *targetURL = [diskImageURL URLByAppendingPathComponent:self.testApplicationURL.lastPathComponent]; - SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:targetURL bundleIdentifier:nil launchAfterInstallation:NO]; + SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:targetURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO]; [self installWithRequest:request remote:YES]; @@ -118,7 +118,7 @@ }); it(@"should not install an update after too many attempts", ^{ - SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:targetURL bundleIdentifier:nil launchAfterInstallation:NO]; + SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:targetURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO]; [self installWithRequest:request remote:YES]; __block NSError *error; @@ -129,7 +129,7 @@ }); it(@"should relaunch even after failing to install an update", ^{ - SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:targetURL bundleIdentifier:nil launchAfterInstallation:YES]; + SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:targetURL bundleIdentifier:nil launchAfterInstallation:YES useUpdateBundleName:NO]; [self installWithRequest:request remote:YES]; expect(@([NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.github.Squirrel.TestApplication"].count)).toEventually(equal(@1)); @@ -149,7 +149,7 @@ expect(@(modeOfURL(updateURL))).to(equal(@0777)); expect(@(modeOfURL([updateURL URLByAppendingPathComponent:@"Contents/MacOS/TestApplication"]))).to(equal(@0777)); - SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO]; + SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO]; [self installWithRequest:request remote:YES]; @@ -178,7 +178,7 @@ // accessing the property. targetURL = self.testApplicationURL; - SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO]; + SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO]; [self installWithRequest:request remote:YES]; diff --git a/SquirrelTests/SQRLShipItRequestSpec.m b/SquirrelTests/SQRLShipItRequestSpec.m index c1735ed6..6bce72af 100644 --- a/SquirrelTests/SQRLShipItRequestSpec.m +++ b/SquirrelTests/SQRLShipItRequestSpec.m @@ -25,7 +25,7 @@ directoryManager = SQRLDirectoryManager.currentApplicationManager; NSURL *updateURL = [self createTestApplicationUpdate]; - request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO]; + request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO]; expect(request).notTo(beNil()); expect(request.targetBundleURL).to(equal(self.testApplicationURL));