├── 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.
--------------------------------------------------------------------------------