├── CLI ├── DependenciesCLI.entitlements └── main.m ├── Dependencies ├── Dependencies.entitlements ├── Preferences │ ├── DEPPreferences+Bookmark.h │ ├── DEPPreferences+Bookmark.m │ ├── DEPPreferences.h │ ├── DEPPreferences.m │ ├── DEPPreferencesCommandLineViewController.h │ ├── DEPPreferencesCommandLineViewController.m │ └── DEPPreferencesWindowControllerProtocol.h └── Utilities │ ├── DEPScopeBookmark.h │ └── DEPScopeBookmark.m ├── LICENSE.txt └── README.md /CLI/DependenciesCLI.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | $(TeamIdentifierPrefix)app.dependencies.dependencies.preferences 10 | 11 | com.apple.security.files.bookmarks.app-scope 12 | 13 | com.apple.security.files.user-selected.read-write 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /CLI/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // dependencies 4 | // 5 | // Copyright © 2020 Alexandre Colucci. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | #import "DEPPreferences.h" 11 | #import "DEPPreferences+Bookmark.h" 12 | 13 | int main(int argc, const char * argv[]) 14 | { 15 | @autoreleasepool 16 | { 17 | NSString *commandLineBookmarkPath = nil; 18 | 19 | // If there is a shared bookmark, try to convert it to a security scope bookmark 20 | NSURL *bookmarkURL = [DEPPreferences resolveSharedCommandLineBookmark]; 21 | if(bookmarkURL != nil) 22 | { 23 | [DEPPreferences createSecurityScopeCommandLineBookmark:bookmarkURL]; 24 | bookmarkURL = [DEPPreferences resolveSecurityScopeCommandLineBookmark]; 25 | if(bookmarkURL != nil) 26 | { 27 | NSLog(@"Successfully imported the shared bookmark for %@", bookmarkURL); 28 | commandLineBookmarkPath = [bookmarkURL path]; 29 | } 30 | 31 | [DEPPreferences removeSharedCommandLineBookmark]; 32 | } 33 | 34 | // Try to load the security scope bookmark 35 | NSURL *securityScopeBookmarkURL = [DEPPreferences resolveSecurityScopeCommandLineBookmark]; 36 | if(securityScopeBookmarkURL != nil) 37 | { 38 | NSLog(@"Command line support enabled for %@", securityScopeBookmarkURL); 39 | commandLineBookmarkPath = [securityScopeBookmarkURL path]; 40 | } 41 | 42 | if(commandLineBookmarkPath == nil) 43 | { 44 | NSLog(@"Please enable the command line support in the Dependencies preferences."); 45 | return 1; 46 | } 47 | 48 | //[...] 49 | } 50 | 51 | return 0; 52 | } 53 | -------------------------------------------------------------------------------- /Dependencies/Dependencies.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | $(TeamIdentifierPrefix)app.dependencies.dependencies.preferences 10 | 11 | com.apple.security.files.bookmarks.app-scope 12 | 13 | com.apple.security.files.user-selected.read-write 14 | 15 | com.apple.security.network.client 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Dependencies/Preferences/DEPPreferences+Bookmark.h: -------------------------------------------------------------------------------- 1 | // 2 | // DEPPreferences+Bookmark.h 3 | // Dependencies 4 | // 5 | // Copyright © 2020 Alexandre Colucci. All rights reserved. 6 | // 7 | 8 | #import 9 | #import "DEPPreferences.h" 10 | 11 | @interface DEPPreferences (Bookmark) 12 | 13 | 14 | // MARK: - Command Line Bookmark 15 | 16 | +(void)createSecurityScopeCommandLineBookmark:(NSURL *)inURL; 17 | +(NSURL *)resolveSecurityScopeCommandLineBookmark; 18 | 19 | +(void)createSharedCommandLineBookmark:(NSURL *)inURL; 20 | +(NSURL *)resolveSharedCommandLineBookmark; 21 | +(void)removeSharedCommandLineBookmark; 22 | 23 | @end 24 | 25 | -------------------------------------------------------------------------------- /Dependencies/Preferences/DEPPreferences+Bookmark.m: -------------------------------------------------------------------------------- 1 | // 2 | // DEPPreferences+Bookmark.m 3 | // Dependencies 4 | // 5 | // Copyright © 2020 Alexandre Colucci. All rights reserved. 6 | 7 | #import "DEPPreferences+Bookmark.h" 8 | #import "DEPScopeBookmark.h" 9 | 10 | // The preference where the Scope Bookmark is saved to enable the command line support 11 | NSString* const kCommandLineSupportSelectedFolder = @"CommandLineSupportSelectedFolder"; 12 | 13 | 14 | @implementation DEPPreferences (Bookmark) 15 | 16 | // MARK: - Bookmarks 17 | 18 | +(NSURL *)resolveBookmarkDataForKey:(NSString *)inPrefKey inUserDefaults:(NSUserDefaults *)inUserDefaults 19 | { 20 | NSURL *outBookmarkURL = nil; 21 | if(inPrefKey != nil && inUserDefaults != nil) 22 | { 23 | NSData *bookmarkData = [inUserDefaults objectForKey:inPrefKey]; 24 | if (bookmarkData != nil) 25 | { 26 | NSData *updatedBookmarkData = bookmarkData; 27 | outBookmarkURL = [DEPScopeBookmark resolveBookmarkData:&updatedBookmarkData]; 28 | if(updatedBookmarkData != bookmarkData) 29 | { 30 | [inUserDefaults setObject:updatedBookmarkData forKey:inPrefKey]; 31 | } 32 | } 33 | else 34 | { 35 | NSLog(@"No Bookmark data stored for %@", inPrefKey); 36 | } 37 | } 38 | 39 | return outBookmarkURL; 40 | } 41 | 42 | 43 | // MARK: - Command Line Bookmark 44 | 45 | +(void)createSecurityScopeCommandLineBookmark:(NSURL *)inURL 46 | { 47 | if(inURL != nil) 48 | { 49 | NSData *bookmark = [DEPScopeBookmark createSecurityScopeBookmarkDataForURL:inURL]; 50 | if(bookmark != nil) 51 | { 52 | [[NSUserDefaults standardUserDefaults] setObject:bookmark forKey:kCommandLineSupportSelectedFolder]; 53 | } 54 | } 55 | } 56 | 57 | +(NSURL *)resolveSecurityScopeCommandLineBookmark 58 | { 59 | return [DEPPreferences resolveBookmarkDataForKey:kCommandLineSupportSelectedFolder inUserDefaults:[NSUserDefaults standardUserDefaults]]; 60 | } 61 | 62 | +(void)createSharedCommandLineBookmark:(NSURL *)inURL 63 | { 64 | if(inURL != nil) 65 | { 66 | NSError *error = nil; 67 | NSData *bookmarkData = [inURL bookmarkDataWithOptions:NSURLBookmarkCreationMinimalBookmark includingResourceValuesForKeys:nil relativeToURL:nil error:&error]; 68 | if(bookmarkData != nil) 69 | { 70 | [[DEPPreferences sharedAppGroupPreferencesSuite] setObject:bookmarkData forKey:kCommandLineSupportSelectedFolder]; 71 | } 72 | else 73 | { 74 | NSLog(@"Could not create the command line bookmark data for %@ due to %@", [inURL path], error); 75 | } 76 | } 77 | } 78 | 79 | +(NSURL *)resolveSharedCommandLineBookmark 80 | { 81 | NSData *bookmarkData = [[DEPPreferences sharedAppGroupPreferencesSuite] objectForKey:kCommandLineSupportSelectedFolder]; 82 | if(bookmarkData != nil) 83 | { 84 | BOOL bookmarkDataIsStale = NO; 85 | NSError *error = nil; 86 | NSURL *theURL = [NSURL URLByResolvingBookmarkData:bookmarkData options:NSURLBookmarkResolutionWithoutUI relativeToURL:nil bookmarkDataIsStale:&bookmarkDataIsStale error:&error]; 87 | 88 | if(theURL != nil) 89 | { 90 | return theURL; 91 | } 92 | else if(error != nil) 93 | { 94 | NSLog(@"Could not get the url for the command line bookmark data due to %@", error); 95 | } 96 | else 97 | { 98 | NSLog(@"Could not get the url for the command line bookmark data"); 99 | } 100 | } 101 | 102 | return nil; 103 | } 104 | 105 | +(void)removeSharedCommandLineBookmark 106 | { 107 | [[DEPPreferences sharedAppGroupPreferencesSuite] removeObjectForKey:kCommandLineSupportSelectedFolder]; 108 | } 109 | 110 | @end 111 | -------------------------------------------------------------------------------- /Dependencies/Preferences/DEPPreferences.h: -------------------------------------------------------------------------------- 1 | // 2 | // DEPPreferences.h 3 | // Dependencies 4 | // 5 | // Copyright © 2020 Alexandre Colucci. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | 11 | @interface DEPPreferences : NSObject 12 | 13 | +(NSUserDefaults*)sharedAppGroupPreferencesSuite; 14 | 15 | @end 16 | 17 | -------------------------------------------------------------------------------- /Dependencies/Preferences/DEPPreferences.m: -------------------------------------------------------------------------------- 1 | // 2 | // DEPPreferences.m 3 | // Dependencies 4 | // 5 | // Copyright © 2020 Alexandre Colucci. All rights reserved. 6 | 7 | #import "DEPPreferences.h" 8 | 9 | // The App Group to share the preferences between the QuickLook plugin and the app 10 | NSString *const kPreferencesAppGroup = @"QFL3YR6JR6.app.dependencies.dependencies.preferences"; 11 | 12 | 13 | @implementation DEPPreferences 14 | 15 | 16 | +(NSUserDefaults*)sharedAppGroupPreferencesSuite 17 | { 18 | static NSUserDefaults *sUserDefault = nil; 19 | if(sUserDefault == nil) 20 | { 21 | sUserDefault = [[NSUserDefaults alloc] initWithSuiteName:kPreferencesAppGroup]; 22 | } 23 | 24 | return sUserDefault; 25 | } 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /Dependencies/Preferences/DEPPreferencesCommandLineViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // DEPPreferencesCommandLineViewController.h 3 | // Dependencies 4 | // 5 | // Copyright © 2020 Alexandre Colucci. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | #import "DEPPreferencesWindowControllerProtocol.h" 11 | 12 | @interface DEPPreferencesCommandLineViewController : NSViewController 13 | 14 | - (instancetype)initViewController; 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /Dependencies/Preferences/DEPPreferencesCommandLineViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // DEPPreferencesCommandLineViewController.m 3 | // Dependencies 4 | // 5 | // Copyright © 2020 Alexandre Colucci. All rights reserved. 6 | 7 | #import "DEPPreferencesCommandLineViewController.h" 8 | 9 | #import "DEPPreferences.h" 10 | #import "DEPPreferences+Bookmark.h" 11 | 12 | @interface DEPPreferencesCommandLineViewController () 13 | 14 | @property (weak) IBOutlet NSTextField *grandAccessTextField; 15 | @property (weak) IBOutlet NSTextField *statusTextField; 16 | 17 | @property (weak) IBOutlet NSTextField *runCommandTextField; 18 | @property (weak) IBOutlet NSTextField *runSysmlinkCommandTextField; 19 | 20 | @property (weak) IBOutlet NSPathControl *pathControl; 21 | @property (strong) NSURL *bookmarkURL; 22 | 23 | @end 24 | 25 | 26 | @implementation DEPPreferencesCommandLineViewController 27 | 28 | - (instancetype)initViewController 29 | { 30 | self = [super initWithNibName:@"DEPPreferencesCommandLineView" bundle:nil]; 31 | if (self) 32 | { 33 | _bookmarkURL = [DEPPreferences resolveSecurityScopeCommandLineBookmark]; 34 | } 35 | 36 | return self; 37 | } 38 | 39 | - (void)viewDidLoad 40 | { 41 | [super viewDidLoad]; 42 | 43 | self.grandAccessTextField.stringValue = [NSString stringWithFormat:@"Grant access to the command line tool:"]; 44 | [self updateStatus]; 45 | } 46 | 47 | - (void)viewWillAppear 48 | { 49 | [super viewWillAppear]; 50 | 51 | // Recreate the shared command line bookmark 52 | self.bookmarkURL = [DEPPreferences resolveSecurityScopeCommandLineBookmark]; 53 | [self updateStatus]; 54 | if(self.bookmarkURL != nil) 55 | { 56 | [DEPPreferences createSharedCommandLineBookmark:self.bookmarkURL]; 57 | } 58 | } 59 | 60 | - (void)updateStatus 61 | { 62 | if(self.bookmarkURL == nil) 63 | { 64 | [self.statusTextField setStringValue:@"Command Line support is currently not enabled."]; 65 | [self.pathControl setToolTip:nil]; 66 | [self.pathControl setHidden:YES]; 67 | } 68 | else 69 | { 70 | [self.statusTextField setStringValue:@"Access is granted for"]; 71 | [self.pathControl setURL:self.bookmarkURL]; 72 | [self.pathControl setToolTip:[self.bookmarkURL path]]; 73 | [self.pathControl setHidden:NO]; 74 | } 75 | } 76 | 77 | -(NSString*)identifier 78 | { 79 | return [[[self title] lowercaseString] stringByReplacingOccurrencesOfString:@" " withString:@""]; 80 | } 81 | 82 | -(NSString*)title 83 | { 84 | return @"Command Line"; 85 | } 86 | 87 | -(IBAction)doEnableCommandLine:(id)sender 88 | { 89 | NSOpenPanel *openPanel = [NSOpenPanel openPanel]; 90 | [openPanel setAllowsMultipleSelection:NO]; 91 | [openPanel setCanChooseFiles:NO]; 92 | [openPanel setCanChooseDirectories:YES]; 93 | [openPanel setResolvesAliases:YES]; 94 | [openPanel setDirectoryURL:[NSURL fileURLWithPath:[@"~" stringByExpandingTildeInPath]]]; 95 | [openPanel setPrompt:@"Select"]; 96 | [openPanel setMessage:@"Select a folder that Dependencies will be able to access from the command line."]; 97 | 98 | NSModalResponse response = [openPanel runModal]; 99 | if(response == NSModalResponseOK) 100 | { 101 | NSURL *url = [openPanel URLs].firstObject; 102 | if(url != nil) 103 | { 104 | // 105 | // 1. The user selected a folder to enable the command line 106 | // 2. This folder is saved as a secure scope bookmark in the app so that we can reuse the folder when the app is relaunched. 107 | // This is used to display the selected folder in the UI and to resave the shared bookmark. 108 | // 3. The folder is also saved as a shared (non-secure) bookmark. Security scope bookmark can only be decrypted by the app that created it so the main app can't pass a secure scope bookmark to a command line tool. 109 | // 4. The user needs to launch the command line from the Terminal while the main app is still running (to ensure that the shared bookmark is still valid). 110 | // 5. The command line reads the shared bookmark and create a security scope bookmark. 111 | // 6. The command line now has read/write access to the folder even if the computer is restarted. 112 | [DEPPreferences createSharedCommandLineBookmark:url]; 113 | [DEPPreferences createSecurityScopeCommandLineBookmark:url]; 114 | self.bookmarkURL = [DEPPreferences resolveSecurityScopeCommandLineBookmark]; 115 | [self updateStatus]; 116 | } 117 | } 118 | } 119 | 120 | -(void)copyToClipboard:(NSTextField *)inTextField 121 | { 122 | NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; 123 | [pasteBoard declareTypes:[NSArray arrayWithObjects:NSPasteboardTypeString, nil] owner:nil]; 124 | [pasteBoard setString:[inTextField stringValue] forType:NSPasteboardTypeString]; 125 | } 126 | 127 | -(IBAction)doCopyCommand:(id)sender 128 | { 129 | [self copyToClipboard:self.runCommandTextField]; 130 | } 131 | 132 | -(IBAction)doCopySymlinkCommand:(id)sender 133 | { 134 | [self copyToClipboard:self.runSysmlinkCommandTextField]; 135 | } 136 | 137 | @end 138 | -------------------------------------------------------------------------------- /Dependencies/Preferences/DEPPreferencesWindowControllerProtocol.h: -------------------------------------------------------------------------------- 1 | // 2 | // DEPPreferencesWindowControllerProtocol.h 3 | // Dependencies 4 | // 5 | // Copyright © 2020 Alexandre Colucci. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @protocol DEPPreferencesWindowControllerProtocol 11 | 12 | @required 13 | 14 | @property (nonatomic, readonly, strong, nonnull) NSString *identifier; 15 | @property (nonatomic, readonly, strong, nonnull) NSString *title; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /Dependencies/Utilities/DEPScopeBookmark.h: -------------------------------------------------------------------------------- 1 | // 2 | // DEPScopeBookmark.h 3 | // Dependencies 4 | // 5 | // Copyright © 2020 Alexandre Colucci. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface DEPScopeBookmark : NSObject 11 | 12 | 13 | /** 14 | Create a Scope Bookmark for the specified URL 15 | */ 16 | +(NSData *)createSecurityScopeBookmarkDataForURL:(NSURL *)inURL; 17 | 18 | 19 | /** 20 | Resolve the Bookmark data 21 | */ 22 | +(NSURL *)resolveBookmarkData:(NSData **)inOutBookmarkData; 23 | 24 | @end 25 | 26 | -------------------------------------------------------------------------------- /Dependencies/Utilities/DEPScopeBookmark.m: -------------------------------------------------------------------------------- 1 | // 2 | // DEPScopeBookmark.m 3 | // Dependencies 4 | // 5 | // Copyright © 2020 Alexandre Colucci. All rights reserved. 6 | // 7 | 8 | #import "DEPScopeBookmark.h" 9 | 10 | @implementation DEPScopeBookmark 11 | 12 | +(NSData *)createSecurityScopeBookmarkDataForURL:(NSURL *)inURL 13 | { 14 | if(inURL != nil) 15 | { 16 | NSError *error = nil; 17 | NSData *bookmarkData = [inURL bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&error]; 18 | if(bookmarkData != nil) 19 | { 20 | return bookmarkData; 21 | } 22 | else 23 | { 24 | NSLog(@"Could not create the Bookmark data for %@ due to %@", [inURL path], error); 25 | } 26 | } 27 | 28 | return nil; 29 | } 30 | 31 | +(NSURL *)resolveBookmarkData:(NSData **)inOutBookmarkData 32 | { 33 | if(inOutBookmarkData == nil) 34 | return nil; 35 | 36 | NSURL *outBookmarkURL = nil; 37 | 38 | NSError *error = nil; 39 | NSData *bookmarkData = *inOutBookmarkData; 40 | if (bookmarkData != nil) 41 | { 42 | BOOL bookmarkDataIsStale = NO; 43 | NSURL *theURL = [NSURL URLByResolvingBookmarkData:bookmarkData options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&bookmarkDataIsStale error:&error]; 44 | if (theURL != nil && bookmarkDataIsStale) 45 | { 46 | // Update the bookmark data 47 | NSData *updatedBookmark = [DEPScopeBookmark createSecurityScopeBookmarkDataForURL:theURL]; 48 | if(updatedBookmark != nil) 49 | { 50 | *inOutBookmarkData = updatedBookmark; 51 | } 52 | } 53 | 54 | if(theURL != nil) 55 | { 56 | BOOL startAccessingSecurityScopedResourceResult = [theURL startAccessingSecurityScopedResource]; 57 | if(!startAccessingSecurityScopedResourceResult) 58 | { 59 | NSLog(@"Could not start accessing the scoped URL"); 60 | } 61 | else 62 | { 63 | outBookmarkURL = theURL; 64 | } 65 | } 66 | else 67 | { 68 | if(error != nil) 69 | { 70 | NSLog(@"Could not get the url for the bookmark data due to %@", error); 71 | } 72 | else 73 | { 74 | NSLog(@"Could not get the url for the bookmark data"); 75 | } 76 | } 77 | } 78 | else 79 | { 80 | NSLog(@"No Bookmark data"); 81 | } 82 | 83 | return outBookmarkURL; 84 | } 85 | 86 | @end 87 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Alexandre Colucci, blog.timac.org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mac App Store: Embedding a Command Line tool using paths as arguments 2 | Code snippets to embed a Command Line tool using paths as arguments on the Mac App Store 3 | 4 | # Blog post 5 | 6 | This code goes along the blog post available at [https://blog.timac.org/2021/0516-mac-app-store-embedding-a-command-line-tool-using-paths-as-arguments/](https://blog.timac.org/2021/0516-mac-app-store-embedding-a-command-line-tool-using-paths-as-arguments/) 7 | 8 | A couple of months ago, I released a new app called [Dependencies](https://apps.apple.com/app/dependencies/id1538972026) on the Mac App Store. You can download and try it for free at [https://apps.apple.com/app/dependencies/id1538972026](https://apps.apple.com/app/dependencies/id1538972026). 9 | 10 | In this blog article, I explain how I built the command line support and released it in the Mac App Store. Implementing this feature turned out to be tricky, mostly due to the lack of documentation on this specific subject. This post might be of interest if you are planning to add a Command Line tool to your app distributed on the Mac App Store. --------------------------------------------------------------------------------