Patch ios-usb-dependencies binaries with relative paths

This CL does:
1) uses regex to detect paths that need to be updated
2) patches paths for iproxy, libcrypto.3.dylib, idevicescreenshot, idevicesyslog
3) also uploads libimobiledeviceglue lib to GCS

Led run: https://ci.chromium.org/raw/build/logs.chromium.org/flutter/led/keyonghan_google.com/ab1258510096a02701ed9fcdc3bdb8fc0df0f1e9e1dab127587204cfd6960014/+/build.proto?server=chromium-swarm.appspot.com

Change-Id: I4c1b7157bc9a39e611fcd3eec5e5e1fada1f8458
Bug: https://github.com/flutter/flutter/issues/36019
Reviewed-on: https://flutter-review.googlesource.com/c/recipes/+/34240
Commit-Queue: Keyong Han <keyonghan@google.com>
Reviewed-by: Jenn Magder <magder@google.com>
diff --git a/recipes/ios_usb_dependencies/ios-usb-dependencies.expected/basic.json b/recipes/ios_usb_dependencies/ios-usb-dependencies.expected/basic.json
index 3b5b3f8..43ea546 100644
--- a/recipes/ios_usb_dependencies/ios-usb-dependencies.expected/basic.json
+++ b/recipes/ios_usb_dependencies/ios-usb-dependencies.expected/basic.json
@@ -470,6 +470,32 @@
   },
   {
     "cmd": [
+      "python",
+      "RECIPE_MODULE[flutter::zip]/resources/zip.py"
+    ],
+    "name": "zipping libimobiledeviceglue dir",
+    "stdin": "{\"entries\": [{\"path\": \"[START_DIR]/src/libimobiledeviceglue_output\", \"type\": \"dir\"}], \"output\": \"[START_DIR]/libimobiledeviceglue.zip\", \"root\": \"[START_DIR]/src/libimobiledeviceglue_output\"}"
+  },
+  {
+    "cmd": [
+      "python3",
+      "-u",
+      "RECIPE_MODULE[depot_tools::gsutil]/resources/gsutil_smart_retry.py",
+      "--",
+      "RECIPE_REPO[depot_tools]/gsutil.py",
+      "----",
+      "cp",
+      "[START_DIR]/libimobiledeviceglue.zip",
+      "gs://flutter_infra_release/ios-usb-dependencies/unsigned/libimobiledeviceglue/None/libimobiledeviceglue.zip"
+    ],
+    "infra_step": true,
+    "name": "gsutil upload of libimobiledeviceglue.zip",
+    "~followup_annotations": [
+      "@@@STEP_LINK@libimobiledeviceglue.zip@https://storage.cloud.google.com/flutter_infra_release/ios-usb-dependencies/unsigned/libimobiledeviceglue/None/libimobiledeviceglue.zip@@@"
+    ]
+  },
+  {
+    "cmd": [
       "vpython3",
       "-u",
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
@@ -549,6 +575,32 @@
   },
   {
     "cmd": [
+      "otool",
+      "-L",
+      "[START_DIR]/src/libusbmuxd_output/iproxy"
+    ],
+    "name": "Get linked paths from iproxy before patch"
+  },
+  {
+    "cmd": [
+      "install_name_tool",
+      "-change",
+      "/opt/s/w/ir/x/w/src/libusbmuxd_install/lib/libusbmuxd-2.0.6.dylib",
+      "@loader_path/../libusbmuxd-2.0.6.dylib",
+      "[START_DIR]/src/libusbmuxd_output/iproxy"
+    ],
+    "name": "Patch [START_DIR]/src/libusbmuxd_output/iproxy with install_name_tool"
+  },
+  {
+    "cmd": [
+      "otool",
+      "-L",
+      "[START_DIR]/src/libusbmuxd_output/iproxy"
+    ],
+    "name": "Get linked paths from [START_DIR]/src/libusbmuxd_output/iproxy after patch"
+  },
+  {
+    "cmd": [
       "python",
       "RECIPE_MODULE[flutter::zip]/resources/zip.py"
     ],
@@ -656,6 +708,22 @@
   },
   {
     "cmd": [
+      "otool",
+      "-L",
+      "[START_DIR]/src/openssl_output/libcrypto.3.dylib"
+    ],
+    "name": "Get linked paths from libcrypto.3.dylib before patch"
+  },
+  {
+    "cmd": [
+      "otool",
+      "-L",
+      "[START_DIR]/src/openssl_output/libcrypto.3.dylib"
+    ],
+    "name": "Get linked paths from [START_DIR]/src/openssl_output/libcrypto.3.dylib after patch"
+  },
+  {
+    "cmd": [
       "python",
       "RECIPE_MODULE[flutter::zip]/resources/zip.py"
     ],
@@ -766,6 +834,38 @@
   },
   {
     "cmd": [
+      "otool",
+      "-L",
+      "[START_DIR]/src/libimobiledevice_output/idevicescreenshot"
+    ],
+    "name": "Get linked paths from idevicescreenshot before patch"
+  },
+  {
+    "cmd": [
+      "otool",
+      "-L",
+      "[START_DIR]/src/libimobiledevice_output/idevicescreenshot"
+    ],
+    "name": "Get linked paths from [START_DIR]/src/libimobiledevice_output/idevicescreenshot after patch"
+  },
+  {
+    "cmd": [
+      "otool",
+      "-L",
+      "[START_DIR]/src/libimobiledevice_output/idevicesyslog"
+    ],
+    "name": "Get linked paths from idevicesyslog before patch"
+  },
+  {
+    "cmd": [
+      "otool",
+      "-L",
+      "[START_DIR]/src/libimobiledevice_output/idevicesyslog"
+    ],
+    "name": "Get linked paths from [START_DIR]/src/libimobiledevice_output/idevicesyslog after patch"
+  },
+  {
+    "cmd": [
       "python",
       "RECIPE_MODULE[flutter::zip]/resources/zip.py"
     ],
diff --git a/recipes/ios_usb_dependencies/ios-usb-dependencies.py b/recipes/ios_usb_dependencies/ios-usb-dependencies.py
index 848f01c..d74a87b 100644
--- a/recipes/ios_usb_dependencies/ios-usb-dependencies.py
+++ b/recipes/ios_usb_dependencies/ios-usb-dependencies.py
@@ -3,6 +3,7 @@
 # found in the LICENSE file.
 
 from contextlib import contextmanager
+import re
 
 DEPS = [
     'depot_tools/gsutil',
@@ -11,12 +12,71 @@
     'recipe_engine/context',
     'recipe_engine/file',
     'recipe_engine/path',
+    'recipe_engine/raw_io',
     'recipe_engine/runtime',
     'recipe_engine/step',
 ]
 
 BUCKET_NAME = 'flutter_infra_release'
 
+# These regex are used to detect lib paths that need to be patched.
+LIBUSBMUXD_PATTERN = r'^\t(\/.*(libusbmuxd.*\.dylib)).*'
+LIBPLIST_PATTERN = r'^\t(\/.*(libplist.*\.dylib)).*'
+LIBSSL_PATTERN = r'^\t(\/.*(libssl.*\.dylib)).*'
+LIBCRYPTO_PATTERN = r'^\t(\/.*(libcrypto.*\.dylib)).*'
+LIBIMOBILEDEVICE_PATTERN = r'^\t(\/.*(libimobiledevice.*\.dylib)).*'
+LIBIMOBILEDEVICEGLUE_PATTERN = r'^\t(\/.*(libimobiledevice-glue.*\.dylib)).*'
+
+DIRNAME_PATTERN_TUPLES = [
+        ('libusbmuxd', LIBUSBMUXD_PATTERN),
+        ('libplist', LIBPLIST_PATTERN),
+        ('lisbssl', LIBSSL_PATTERN),
+        ('libcrypto', LIBCRYPTO_PATTERN),
+        ('libimobiledevice', LIBIMOBILEDEVICE_PATTERN),
+        ('libimobiledevice-glue', LIBIMOBILEDEVICEGLUE_PATTERN)
+        ]
+
+# Map between package and its artifacts that need path patch.
+BIANRY_ARTIFACT_MAP = {
+  'libusbmuxd': ['iproxy'],
+  'openssl': ['libcrypto.3.dylib'],
+  'libimobiledevice': ['idevicescreenshot', 'idevicesyslog']
+}
+
+def ParseOtoolPath(input_string):
+  '''Parse paths that need to be patched to relative paths via otool.'''
+  input_lines = input_string.split('\n')
+  output = {}
+  for input_line in input_lines:
+    for dirname, pattern in DIRNAME_PATTERN_TUPLES:
+      # re.match searches from beginning of string
+      match = re.match(pattern, input_line)
+      if match:
+        old_path = match.group(1)
+        new_path = '@loader_path/../%s' % match.group(2)
+        output[old_path] = new_path
+  return output
+
+def PatchLoadPath(api, ouput_path, package_name):
+  '''Update dynamically linked paths in a binary'''
+  if package_name not in BIANRY_ARTIFACT_MAP:
+    return
+  artifacts = BIANRY_ARTIFACT_MAP[package_name]
+  for artifact in artifacts:
+    artifact_path = ouput_path.join(artifact)
+    otool_step_data = api.step(
+        'Get linked paths from %s before patch' % artifact,
+        ['otool', '-L', artifact_path], stdout=api.raw_io.output_text())
+    old_paths_to_new_paths = ParseOtoolPath(otool_step_data.stdout.rstrip())
+    for old_path in old_paths_to_new_paths:
+        new_path = old_paths_to_new_paths[old_path]
+        api.step(
+                'Patch %s with install_name_tool' % artifact_path,
+                ['install_name_tool', '-change', old_path, new_path, artifact_path],
+                )
+    api.step(
+        'Get linked paths from %s after patch' % artifact_path,
+        ['otool', '-L', artifact_path])
 
 def GetCloudPath(api, package_name, commit_sha):
   """Location of cloud bucket for unsigned binaries"""
@@ -137,6 +197,7 @@
       api, env, env_prefixes, package_name, package_install_dir, update_path,
       update_library_path, update_pkg_config_path
   )
+  PatchLoadPath(api, package_out_dir, package_name)
   UploadPackage(
       api, package_name, work_dir, package_out_dir, upload, commit_sha
   )
@@ -158,6 +219,7 @@
         env,
         env_prefixes,
         'libimobiledeviceglue',
+        upload=True,
         update_library_path=True,
         update_pkg_config_path=True
     )
@@ -190,4 +252,8 @@
           api.path['start_dir'].join('src').join('ios-deploy'
                                                 ).join('commit_sha.txt'),
       ),
+      api.step_data(
+          'Get linked paths from iproxy before patch',
+          stdout=api.raw_io.output_text('\t/opt/s/w/ir/x/w/src/libusbmuxd_install/lib/libusbmuxd-2.0.6.dylib (compatibility version 7.0.0, current version 7.0.0)'),
+          retcode=0)
   )
diff --git a/recipes/ios_usb_dependencies/ios-usb-dependencies.resources/libimobiledeviceglue.sh b/recipes/ios_usb_dependencies/ios-usb-dependencies.resources/libimobiledeviceglue.sh
index c7bd4f6..9a9890c 100755
--- a/recipes/ios_usb_dependencies/ios-usb-dependencies.resources/libimobiledeviceglue.sh
+++ b/recipes/ios_usb_dependencies/ios-usb-dependencies.resources/libimobiledeviceglue.sh
@@ -23,3 +23,5 @@
 "$SRC_DIR"/configure --prefix="$INSTALL_DIR"
 
 make install
+
+cp "$INSTALL_DIR"/lib/libimobiledevice-glue-1.0.0.dylib "$OUTPUT_DIR"