Skip to content
This repository has been archived by the owner on Nov 3, 2020. It is now read-only.

Added option to remove Command-Line Tool again (#338) #344

Merged
merged 3 commits into from
Jul 11, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
* Fix error when trying to create a new Podfile from an xcodeproj with spaces
[marcboquet](https://github.com/marcboquet)
[#337](https://github.com/CocoaPods/CocoaPods-app/pull/337)

* Added option to remove installed Command-Line Tool again (#338)
[Buju77](https://github.com/Buju77)
[#344](https://github.com/CocoaPods/CocoaPods-app/pull/344)

## [1.0.0](https://github.com/CocoaPods/CocoaPods-app/releases/tag/1.0.0)
[CocoaPods](https://github.com/CocoaPods/CocoaPods/blob/master/CHANGELOG.md#100-2016-05-10)
Expand Down
10 changes: 10 additions & 0 deletions app/CocoaPods/Base.lproj/MainMenu.xib
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<customObject id="Voe-Tx-rLC" userLabel="App Delegate" customClass="CPAppDelegate">
<connections>
<outlet property="documentController" destination="Qtz-kf-riq" id="Zl6-hS-hbl"/>
<outlet property="removeCommandLineToolMenuItem" destination="JNe-tW-Tp9" id="gqu-Cm-DzJ"/>
</connections>
</customObject>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
Expand Down Expand Up @@ -59,6 +60,12 @@
<action selector="installBinstubIfNecessary:" target="-1" id="1aC-DY-EQL"/>
</connections>
</menuItem>
<menuItem title="Remove the Command-Line Tool…" hidden="YES" id="JNe-tW-Tp9">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="removeBinstubIfNecessary:" target="Voe-Tx-rLC" id="nAW-bJ-hUa"/>
</connections>
</menuItem>
<menuItem title="Check for Updates…" id="6Sx-7e-VkP">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
Expand Down Expand Up @@ -111,6 +118,9 @@
</connections>
</menuItem>
</items>
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="ahL-fs-Ah5"/>
</connections>
</menu>
</menuItem>
<menuItem title="File" id="dMs-cI-mzQ">
Expand Down
15 changes: 11 additions & 4 deletions app/CocoaPods/CLI Integrations/CPCLIToolInstallationController.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,34 @@ extern NSString * const kCPCLIToolInstalledToDestinationsKey;
/// Checks if binstub is not installed yet and is not configured to not request the
/// user for installation again (`kCPRequestCLIToolInstallationAgainKey`).
///
/// Returns whether or not installation should be performed.
/// @returns whether or not installation should be performed.

- (BOOL)shouldInstallBinstubIfNecessary;

/// Only performs the installation if it's not installed yet and is not configured to not request the
/// user for installation again (`kCPRequestCLIToolInstallationAgainKey`).
///
/// Returns whether or not installation has been performed.
/// @return Whether or not installation has been performed.
///
- (BOOL)installBinstubIfNecessary;

/// Always tries to perform the installation, unless the user cancels an overwrite.
///
/// Returns whether or not installation has been performed.
/// @return Whether or not installation has been performed.
///
- (BOOL)installBinstub;

/// Tries to remove the already installed binstub. Will only remove the binstub when it was installed using the app.
///
/// @return Whether uninstall was successful or not.
///
- (BOOL)removeBinstub;

/// Allows the user to choose a different destination than the suggested destination.
///
/// Updates the `destinationURL` if the user chooses a new one.
///
/// Returns whether or not a destination was chosen or if the user cancelled.
/// @returns whether or not a destination was chosen or if the user cancelled.
///
- (BOOL)runModalDestinationChangeSavePanel;

Expand All @@ -43,5 +48,7 @@ extern NSString * const kCPCLIToolInstalledToDestinationsKey;

- (BOOL)binstubAlreadyExists;

/// Returns `YES` if binstub was installed using this controller.
- (BOOL)hasInstalledBinstubBefore;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for all the doc fixes


@end
223 changes: 211 additions & 12 deletions app/CocoaPods/CLI Integrations/CPCLIToolInstallationController.m
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ - (instancetype)initWithSuggestedDestinationURL:(NSURL *)suggestedDestinationURL

- (BOOL)shouldInstallBinstubIfNecessary;
{
[self verifyExistingInstallDestinations];

if (self.previouslyInstalledToDestinations.count > 0) {
if ([self hasInstalledBinstubBefore]) {
NSLog(@"Already installed binstub.");
return NO;
}
Expand Down Expand Up @@ -75,6 +73,41 @@ - (BOOL)installBinstub;
return installed;
}

#pragma mark - Uninstallation

- (BOOL)hasInstalledBinstubBefore
{
[self verifyExistingInstallDestinations];

return self.previouslyInstalledToDestinations.count > 0;
}

- (BOOL)removeBinstub
{
[self verifyExistingInstallDestinations];

if (![self hasInstalledBinstubBefore]) {
NSLog(@"Tried to remove binstub, but it was never installed using the app before.");
return NO;
}

if (![self promptIfUserReallyWantsToUninstall]) {
NSLog(@"User canceled removing binstub.");
return NO;
}

// go ahead and delete it
NSLog(@"Now removing binstub ...");

NSDictionary *remainingURLs = [self removeBinstubAccordingToPrivileges];

[self saveBookmarksWithURLs:remainingURLs];

// success only when we have successfully removed all urls from file system
NSLog(@"Finished removing binstub: %@", self.previouslyInstalledToDestinations.count == 0 ? @"success" : @"failed");
return self.previouslyInstalledToDestinations.count == 0;
}

#pragma mark - Installation destination bookmarks

static NSData *
Expand Down Expand Up @@ -111,7 +144,6 @@ - (void)verifyExistingInstallDestinations;
} else {
NSLog(@"Verifying existing destinations.");
NSUInteger bookmarkCount = bookmarks.count;
NSMutableArray *verifiedBookmarks = [NSMutableArray arrayWithCapacity:bookmarkCount];
NSMutableDictionary *URLs = [NSMutableDictionary dictionaryWithCapacity:bookmarkCount];
for (NSUInteger i = 0; i < bookmarkCount; i++) {
NSData *bookmark = [bookmarks objectAtIndex:i];
Expand Down Expand Up @@ -141,12 +173,9 @@ - (void)verifyExistingInstallDestinations;
}
#endif
URLs[URL] = bookmark;
[verifiedBookmarks addObject:bookmark];
}
}
self.previouslyInstalledToDestinations = [URLs copy];
[defaults setObject:[verifiedBookmarks copy]
forKey:kCPCLIToolInstalledToDestinationsKey];
[self saveBookmarksWithURLs:URLs];
}
}

Expand All @@ -160,9 +189,7 @@ - (void)saveInstallationDestination;
NSMutableDictionary *URLs = [self.previouslyInstalledToDestinations mutableCopy];
// Update any previous bookmark data pointing to the same destination.
URLs[self.destinationURL] = bookmark;
NSArray *bookmarks = [URLs allValues];
[[NSUserDefaults standardUserDefaults] setObject:bookmarks
forKey:kCPCLIToolInstalledToDestinationsKey];
[self saveBookmarksWithURLs:URLs];
}
}

Expand Down Expand Up @@ -196,8 +223,33 @@ - (BOOL)promptIfOverwriting
return [alert runModal] == NSAlertFirstButtonReturn;
}

- (BOOL)promptIfUserReallyWantsToUninstall
{
NSAlert *alert = [NSAlert new];
alert.alertStyle = NSCriticalAlertStyle;
alert.messageText = NSLocalizedString(@"UNINSTALL_CLI_WARNING_MESSAGE_TEXT", nil);

[alert addButtonWithTitle:NSLocalizedString(@"UNINSTALL_CLI_REMOVE", nil)];
[alert addButtonWithTitle:NSLocalizedString(@"CANCEL", nil)];

return [alert runModal] == NSAlertFirstButtonReturn;
}

#pragma mark - Utility

- (void)saveBookmarksWithURLs:(NSDictionary *)URLs
{
self.previouslyInstalledToDestinations = [URLs copy];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSArray *bookmarks = [URLs allValues];
if (bookmarks.count == 0) {
[defaults removeObjectForKey:kCPCLIToolInstalledToDestinationsKey];
} else {
[defaults setObject:bookmarks
forKey:kCPCLIToolInstalledToDestinationsKey];
}
}

- (NSURL *)binstubSourceURL;
{
NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
Expand Down Expand Up @@ -229,7 +281,12 @@ - (BOOL)binstubAlreadyIsTheLatestVersion;

- (BOOL)hasWriteAccessToBinstub;
{
NSURL *destinationDirURL = [self.destinationURL URLByDeletingLastPathComponent];
return [self hasWriteAccessToBinstubWithURL:self.destinationURL];
}

- (BOOL)hasWriteAccessToBinstubWithURL:(NSURL *)url;
{
NSURL *destinationDirURL = [url URLByDeletingLastPathComponent];
return access([destinationDirURL.path UTF8String], W_OK) == 0;
}

Expand Down Expand Up @@ -358,4 +415,146 @@ - (BOOL)installBinstubToPrivilegedDestination;
return succeeded;
}

#pragma mark - Binstub uninstallation

/// Loops through all installed destinations and tries to remove them.
///
/// @return Dictionary of remaining bookmarks which couldn't be removed. Empty dict means everything was succuessfully removed.
///
- (NSDictionary *)removeBinstubAccordingToPrivileges
{
self.errorMessage = nil;

NSFileManager *fileManager = [NSFileManager defaultManager];

NSMutableArray *privilegedURLs = [NSMutableArray array];
NSMutableDictionary *URLs = [self.previouslyInstalledToDestinations mutableCopy];
for (NSURL *url in self.previouslyInstalledToDestinations) {

if (![fileManager fileExistsAtPath:url.path]) {
// remove url from our list
[URLs removeObjectForKey:url];
continue;
}

NSLog(@"Removing binstub: %@", url.path);

BOOL removed = NO;
if ([self hasWriteAccessToBinstubWithURL:url]) {
removed = [self removeBinstubFromAccessibleDestinationWithURL:url];
} else {
removed = NO; // will be done few lines below
[privilegedURLs addObject:url];
}

if (removed) {
[URLs removeObjectForKey:url];
}
}

// now remove privileged urls all at once
if (privilegedURLs.count > 0) {
BOOL removed = [self removeBinstubFromPrivilegedDestinationWithURLs:privilegedURLs];
if (removed) {
[URLs removeObjectsForKeys:privilegedURLs];
}
}

return [URLs copy];
}

- (BOOL)removeBinstubFromAccessibleDestinationWithURL:(NSURL *)url
{
NSError *error = nil;
BOOL success = [[NSFileManager defaultManager] removeItemAtURL:url error:&error];

if (error) {
NSLog(@"Failed to remove binstub: %@", error);
self.errorMessage = @"Failed to remove pod command";
success = NO;
}

return success;
}

// Possible Solutions how to gain privileged access to remove files:
// 1. `AuthorizationExecuteWithPrivileges` but its deprecated since OS X 10.7: [1] and [2]
// 2. `ServiceManagement.framework`'s `SMJobBless()`: [3] and [4]
// 3. AppleScript: [5] and [6]
//
// Disadvantage of solution #3 is that authorization dialog will pop up for each
//
// References:
// [1] http://www.michaelvobrien.com/blog/2009/07/authorizationexecutewithprivileges-a-simple-example/
// [2] https://developer.apple.com/library/mac/documentation/Security/Conceptual/authorization_concepts/03authtasks/authtasks.html
// [3] http://stackoverflow.com/a/6842129
// [4] https://developer.apple.com/library/mac/samplecode/EvenBetterAuthorizationSample/Listings/Read_Me_About_EvenBetterAuthorizationSample_txt.html#//apple_ref/doc/uid/DTS40013768-Read_Me_About_EvenBetterAuthorizationSample_txt-DontLinkElementID_17
// [5] http://stackoverflow.com/a/8865284
// [6] http://stackoverflow.com/a/15248621
- (BOOL)removeBinstubFromPrivilegedDestinationWithURLs:(NSArray<NSURL *> *)urls {
if (urls.count == 0) {
return NO;
}

NSArray<NSString *> *paths = [urls valueForKey:@"path"]; // NSURL.path
NSString *pathsArgumentString = [NSString stringWithFormat:@"'%@'", [paths componentsJoinedByString:@"' '"]]; // [asdf, wasd] --> 'asdf' 'wasd'

NSString *output = nil;
NSString *processErrorDescription = nil;

// Command: `'/bin/rm' -f '/usr/local/bin/pod' '/usr/local/bin/path with space/someOtherBinary'`
BOOL success = [self runProcessAsAdministrator:@"/bin/rm"
withArguments:@[@"-f", pathsArgumentString]
output:&output
errorDescription:&processErrorDescription];

// Process failed to run
if (!success) {
NSLog(@"Failed to remove Binstub from privileged destination: %@", processErrorDescription);
}
return success;
}

// Using AppleScript
// Source: [6] (StackOverflow)
- (BOOL)runProcessAsAdministrator:(NSString *)scriptPath
withArguments:(NSArray *)arguments
output:(NSString **)output
errorDescription:(NSString **)errorDescription {

NSString *allArgs = [arguments componentsJoinedByString:@" "];
NSString *fullScript = [NSString stringWithFormat:@"'%@' %@", scriptPath, allArgs];

NSDictionary *errorInfo = [NSDictionary new];
NSString *script = [NSString stringWithFormat:@"do shell script \"%@\" with administrator privileges", fullScript];

NSAppleScript *appleScript = [[NSAppleScript new] initWithSource:script];
NSAppleEventDescriptor *eventResult = [appleScript executeAndReturnError:&errorInfo];

if (eventResult) {
// Set output to the AppleScript's output
*output = [eventResult stringValue];

return YES;
}

// Check errorInfo & describe common errors
*errorDescription = nil;
if ([errorInfo valueForKey:NSAppleScriptErrorNumber]) {
NSNumber *errorNumber = (NSNumber *)[errorInfo valueForKey:NSAppleScriptErrorNumber];
if ([errorNumber intValue] == -128) {
*errorDescription = @"The administrator password is required to do this.";
}
}

// Set error message from provided message
if (*errorDescription == nil) {
if ([errorInfo valueForKey:NSAppleScriptErrorMessage]) {
*errorDescription = (NSString *)[errorInfo valueForKey:NSAppleScriptErrorMessage];
}
}

return NO;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is all really well documented, thanks!

@end
Loading