Add --app_deltas option that can be used to specify a directory to store data to enable incremental installs. (#434)

* Add --app_deltas option that can be used to specify a directory to store data to enable incremental installs.

* Fixed the location of the getopts string and ensured adding appening the path to the status only occurs during incremental installs

* Updated to use CF over NS types.

* Resolve comments.

* Apply realpath to the app delta path.
diff --git a/src/ios-deploy/ios-deploy.m b/src/ios-deploy/ios-deploy.m
index 23b79a8..ddcf64b 100644
--- a/src/ios-deploy/ios-deploy.m
+++ b/src/ios-deploy/ios-deploy.m
@@ -77,6 +77,7 @@
 CFSocketNativeHandle  AMDServiceConnectionGetSocket(ServiceConnRef con);
 int AMDeviceSecureTransferPath(int zero, AMDeviceRef device, CFURLRef url, CFDictionaryRef options, void *callback, int cbarg);
 int AMDeviceSecureInstallApplication(int zero, AMDeviceRef device, CFURLRef url, CFDictionaryRef options, void *callback, int cbarg);
+int AMDeviceSecureInstallApplicationBundle(AMDeviceRef device, CFURLRef url, CFDictionaryRef options, void *callback, int cbarg);
 int AMDeviceMountImage(AMDeviceRef device, CFStringRef image, CFDictionaryRef options, void *callback, int cbarg);
 mach_error_t AMDeviceLookupApplications(AMDeviceRef device, CFDictionaryRef options, CFDictionaryRef *result);
 int AMDeviceGetInterfaceType(AMDeviceRef device);
@@ -90,6 +91,7 @@
 bool interactive = true;
 bool justlaunch = false;
 char *app_path = NULL;
+char *app_deltas = NULL;
 char *device_id = NULL;
 char *args = NULL;
 char *envs = NULL;
@@ -611,11 +613,23 @@
     CFNumberGetValue(CFDictionaryGetValue(dict, CFSTR("PercentComplete")), kCFNumberSInt32Type, &percent);
 
     int overall_percent = (percent / 2) + 50;
-    NSLogOut(@"[%3d%%] %@", overall_percent, status);
+
+    // During standard install, the "Status" value contains the actual status,
+    // such as "Copying" or "CreatingStagingDirectory", as well as any
+    // applicable paths. The incremental install version, includes only the
+    // status and a seperate value in "Path" for any applicable paths. This
+    // merges the status and path during incremental installs so the output is
+    // similar between both installation types.
+    CFStringRef path = CFDictionaryGetValue(dict, CFSTR("Path"));
+    NSString *status_with_path = (path != NULL && app_deltas != NULL) ?
+      [NSString stringWithFormat:@"%@ %@", status, path] :
+      (__bridge NSString *)status;
+
+    NSLogOut(@"[%3d%%] %@", overall_percent, status_with_path);
     NSLogJSON(@{@"Event": @"BundleInstall",
                 @"OverallPercent": @(overall_percent),
                 @"Percent": @(percent),
-                @"Status": (__bridge NSString *)status
+                @"Status": status_with_path
                 });
     return 0;
 }
@@ -1275,6 +1289,19 @@
     return conn;
 }
 
+// Uses realpath() to resolve any symlinks in a path. Returns the resolved
+// path or the original path if an error occurs. This allocates memory for the
+// resolved path and the caller is responsible for freeing it.
+char *resolve_path(char *path)
+{
+  char buffer[PATH_MAX];
+  // Use the original path if realpath() fails, otherwise use resolved value.
+  char *resolved_path = realpath(path, buffer) == NULL ? path : buffer;
+  char *new_path = malloc(strlen(resolved_path) + 1);
+  strcpy(new_path, resolved_path);
+  return new_path;
+}
+
 char const* get_filename_from_path(char const* path)
 {
     char const*ptr = path + strlen(path);
@@ -1707,39 +1734,83 @@
         check_error(AMDeviceValidatePairing(device));
         check_error(AMDeviceStartSession(device));
 
+        CFDictionaryRef options;
+        if (app_deltas == NULL) { // standard install
+          // NOTE: the secure version doesn't seem to require us to start the AFC service
+          ServiceConnRef afcFd;
+          check_error(AMDeviceSecureStartService(device, CFSTR("com.apple.afc"), NULL, &afcFd));
+          check_error(AMDeviceStopSession(device));
+          check_error(AMDeviceDisconnect(device));
 
-        // NOTE: the secure version doesn't seem to require us to start the AFC service
-        ServiceConnRef afcFd;
-        check_error(AMDeviceSecureStartService(device, CFSTR("com.apple.afc"), NULL, &afcFd));
-        check_error(AMDeviceStopSession(device));
-        check_error(AMDeviceDisconnect(device));
+          CFStringRef keys[] = { CFSTR("PackageType") };
+          CFStringRef values[] = { CFSTR("Developer") };
+          options = CFDictionaryCreate(NULL, (const void **)&keys, (const void **)&values, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+          check_error(AMDeviceSecureTransferPath(0, device, url, options, transfer_callback, 0));
+          close(*afcFd);
 
-        CFStringRef keys[] = { CFSTR("PackageType") };
-        CFStringRef values[] = { CFSTR("Developer") };
-        CFDictionaryRef options = CFDictionaryCreate(NULL, (const void **)&keys, (const void **)&values, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+          AMDeviceConnect(device);
+          assert(AMDeviceIsPaired(device));
+          check_error(AMDeviceValidatePairing(device));
+          check_error(AMDeviceStartSession(device));
+          check_error(AMDeviceSecureInstallApplication(0, device, url, options, install_callback, 0));
+        } else { // incremental install
+          check_error(AMDeviceStopSession(device));
+          check_error(AMDeviceDisconnect(device));
 
-        //assert(AMDeviceTransferApplication(afcFd, path, NULL, transfer_callback, NULL) == 0);
-        check_error(AMDeviceSecureTransferPath(0, device, url, options, transfer_callback, 0));
-        close(*afcFd);
+          CFStringRef extracted_bundle_id = NULL;
+          CFStringRef extracted_bundle_id_ref = get_bundle_id(url);
+          if (bundle_id != NULL) {
+            extracted_bundle_id = CFStringCreateWithCString(NULL, bundle_id, kCFStringEncodingUTF8);
+            CFRelease(extracted_bundle_id_ref);
+          } else {
+            if (extracted_bundle_id_ref == NULL) {
+              on_error(@"[ ERROR] Could not determine bundle id.");
+            }
+            extracted_bundle_id = extracted_bundle_id_ref;
+          }
 
+          CFStringRef deltas_path =
+            CFStringCreateWithCString(NULL, app_deltas, kCFStringEncodingUTF8);
+          CFURLRef deltas_relative_url =
+            CFURLCreateWithFileSystemPath(NULL, deltas_path, kCFURLPOSIXPathStyle, false);
+          CFURLRef app_deltas_url = CFURLCopyAbsoluteURL(deltas_relative_url);
+          CFStringRef prefer_wifi = no_wifi ? CFSTR("0") : CFSTR("1");
 
+          // These values were determined by inspecting Xcode 11.1 logs with the Console app.
+          CFStringRef keys[] = {
+            CFSTR("CFBundleIdentifier"),
+            CFSTR("CloseOnInvalidate"),
+            CFSTR("InvalidateOnDetach"),
+            CFSTR("IsUserInitiated"),
+            CFSTR("PackageType"),
+            CFSTR("PreferWifi"),
+            CFSTR("ShadowParentKey"),
+          };
+          CFStringRef values[] = {
+            extracted_bundle_id,
+            CFSTR("1"),
+            CFSTR("1"),
+            CFSTR("1"),
+            CFSTR("Developer"),
+            prefer_wifi,
+            (CFStringRef)app_deltas_url,
+          };
 
-        AMDeviceConnect(device);
-        assert(AMDeviceIsPaired(device));
-        check_error(AMDeviceValidatePairing(device));
-        check_error(AMDeviceStartSession(device));
+          CFIndex size = sizeof(keys)/sizeof(CFStringRef);
+          options = CFDictionaryCreate(NULL, (const void **)&keys, (const void **)&values, size, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
 
-        // // NOTE: the secure version doesn't seem to require us to start the installation_proxy service
-        // // Although I can't find it right now, I in some code that the first param of AMDeviceSecureInstallApplication was a "dontStartInstallProxy"
-        // // implying this is done for us by iOS already
-
-        //service_conn_t installFd;
-        //assert(AMDeviceSecureStartService(device, CFSTR("com.apple.mobile.installation_proxy"), NULL, &installFd) == 0);
-
-        //mach_error_t result = AMDeviceInstallApplication(installFd, path, options, install_callback, NULL);
-        check_error(AMDeviceSecureInstallApplication(0, device, url, options, install_callback, 0));
-
-        // close(installFd);
+          AMDeviceConnect(device);
+          assert(AMDeviceIsPaired(device));
+          check_error(AMDeviceValidatePairing(device));
+          check_error(AMDeviceStartSession(device));
+          check_error(AMDeviceSecureInstallApplicationBundle(device, url, options, install_callback, 0));
+          CFRelease(extracted_bundle_id);
+          CFRelease(deltas_path);
+          CFRelease(deltas_relative_url);
+          CFRelease(app_deltas_url);
+          free(app_deltas);
+          app_deltas = NULL;
+        }
 
         check_error(AMDeviceStopSession(device));
         check_error(AMDeviceDisconnect(device));
@@ -1836,6 +1907,7 @@
         @"  -L, --justlaunch             just launch the app and exit lldb\n"
         @"  -v, --verbose                enable verbose output\n"
         @"  -m, --noinstall              directly start debugging without app install (-d not required)\n"
+        @"  -A, --app_deltas             incremental install. must specify a directory to store app deltas to determine what needs to be installed\n"
         @"  -p, --port <number>          port used for device, default: dynamic\n"
         @"  -r, --uninstall              uninstall the app before install (do not use with -m; app cache and data are cleared) \n"
         @"  -9, --uninstall_only         uninstall the app ONLY. Use only with -1 <bundle_id> \n"
@@ -1906,11 +1978,12 @@
         { "error_output", required_argument, NULL, 'E' },
         { "detect_deadlocks", required_argument, NULL, 1000 },
         { "json", no_argument, NULL, 'j'},
+        { "app_deltas", required_argument, NULL, 'A'},
         { NULL, 0, NULL, 0 },
     };
     int ch;
 
-    while ((ch = getopt_long(argc, argv, "VmcdvunrILeD:R:i:b:a:t:p:1:2:o:l:w:9BWjNs:OE:C", longopts, NULL)) != -1)
+    while ((ch = getopt_long(argc, argv, "VmcdvunrILeD:R:i:b:a:t:p:1:2:o:l:w:9BWjNs:OE:CA:", longopts, NULL)) != -1)
     {
         switch (ch) {
         case 'm':
@@ -2032,6 +2105,9 @@
         case 'j':
             _json_output = true;
             break;
+        case 'A':
+            app_deltas = resolve_path(optarg);
+            break;
         default:
             usage(argv[0]);
             return exitcode_error;