Add ability to build XCFramework for iOS

To build XCFramework, changed build_ios_libs.py to support
target pairs (environment, arch).
Also, changed default architecture to include the Arm64 iOS Simulator
and not the x86 iOS Simulator.
Mac Catalyst (target_environment = "catalyst") builds can also
be achieved in the same way, but at the moment, Mac Catalyst builds fail,
so I skipped them from the active arch.

Bug: webrtc:12372, webrtc:11516
Change-Id: I3f07ded81c7d0bdecc69a903b32e06c4ab63cee2
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/202160
Reviewed-by: Mirko Bonadei <mbonadei@webrtc.org>
Reviewed-by: Kári Helgason <kthelgason@webrtc.org>
Commit-Queue: Mirko Bonadei <mbonadei@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#34420}
diff --git a/tools_webrtc/ios/build_ios_libs.py b/tools_webrtc/ios/build_ios_libs.py
index 5aa40d6..c931853 100755
--- a/tools_webrtc/ios/build_ios_libs.py
+++ b/tools_webrtc/ios/build_ios_libs.py
@@ -7,7 +7,7 @@
 # tree. An additional intellectual property rights grant can be found
 # in the file PATENTS.  All contributing project authors may
 # be found in the AUTHORS file in the root of the source tree.
-"""WebRTC iOS FAT libraries build script.
+"""WebRTC iOS XCFramework build script.
 Each architecture is compiled separately before being merged together.
 By default, the library is created in out_ios_libs/. (Change with -o.)
 """
@@ -29,8 +29,16 @@
 
 SDK_OUTPUT_DIR = os.path.join(SRC_DIR, 'out_ios_libs')
 SDK_FRAMEWORK_NAME = 'WebRTC.framework'
+SDK_DSYM_NAME = 'WebRTC.dSYM'
+SDK_XCFRAMEWORK_NAME = 'WebRTC.xcframework'
 
-DEFAULT_ARCHS = ENABLED_ARCHS = ['arm64', 'x64']
+ENABLED_ARCHS = [
+    'device:arm64', 'simulator:arm64', 'simulator:x64',
+    'arm64', 'x64'
+]
+DEFAULT_ARCHS = [
+    'device:arm64', 'simulator:arm64', 'simulator:x64'
+]
 IOS_DEPLOYMENT_TARGET = '12.0'
 LIBVPX_BUILD_VP9 = False
 
@@ -114,15 +122,37 @@
     if os.path.isdir(output_dir):
         logging.info('Removing temporary build files.')
         for arch in architectures:
-            arch_lib_path = os.path.join(output_dir, arch + '_libs')
+            arch_lib_path = os.path.join(output_dir, arch)
             if os.path.isdir(arch_lib_path):
                 shutil.rmtree(arch_lib_path)
 
 
-def BuildWebRTC(output_dir, target_arch, flavor, gn_target_name,
-                ios_deployment_target, libvpx_build_vp9, use_bitcode, use_goma,
-                extra_gn_args):
-    output_dir = os.path.join(output_dir, target_arch + '_libs')
+def _ParseArchitecture(architectures):
+    result = dict()
+    for arch in architectures:
+        if ":" in arch:
+            target_environment, target_cpu = arch.split(":")
+        else:
+            logging.warning('The environment for build is not specified.')
+            logging.warning('It is assumed based on cpu type.')
+            logging.warning('See crbug.com/1138425 for more details.')
+            if arch == "x64":
+                target_environment = "simulator"
+            else:
+                target_environment = "device"
+            target_cpu = arch
+        archs = result.get(target_environment)
+        if archs is None:
+            result[target_environment] = {target_cpu}
+        else:
+            archs.add(target_cpu)
+
+    return result
+
+
+def BuildWebRTC(output_dir, target_environment, target_arch, flavor,
+                gn_target_name, ios_deployment_target, libvpx_build_vp9,
+                use_bitcode, use_goma, extra_gn_args):
     gn_args = [
         'target_os="ios"', 'ios_enable_code_signing=false',
         'use_xcode_clang=true', 'is_component_build=false',
@@ -137,6 +167,8 @@
     else:
         raise ValueError('Unexpected flavor type: %s' % flavor)
 
+    gn_args.append('target_environment="%s"' % target_environment)
+
     gn_args.append('target_cpu="%s"' % target_arch)
 
     gn_args.append('ios_deployment_target="%s"' % ios_deployment_target)
@@ -182,11 +214,14 @@
         _CleanArtifacts(args.output_dir)
         return 0
 
-    architectures = list(args.arch)
+    # architectures is typed as Dict[str, Set[str]],
+    # where key is for the environment (device or simulator)
+    # and value is for the cpu type.
+    architectures = _ParseArchitecture(args.arch)
     gn_args = args.extra_gn_args
 
     if args.purify:
-        _CleanTemporary(args.output_dir, architectures)
+        _CleanTemporary(args.output_dir, architectures.keys())
         return 0
 
     gn_target_name = 'framework_objc'
@@ -195,78 +230,101 @@
     gn_args.append('enable_stripping=true')
 
     # Build all architectures.
-    for arch in architectures:
-        BuildWebRTC(args.output_dir, arch, args.build_config, gn_target_name,
-                    IOS_DEPLOYMENT_TARGET, LIBVPX_BUILD_VP9, args.bitcode,
-                    args.use_goma, gn_args)
+    framework_paths = []
+    all_lib_paths = []
+    for (environment, archs) in architectures.items():
+        framework_path = os.path.join(args.output_dir, environment)
+        framework_paths.append(framework_path)
+        lib_paths = []
+        for arch in archs:
+            lib_path = os.path.join(framework_path, arch + '_libs')
+            lib_paths.append(lib_path)
+            BuildWebRTC(lib_path, environment, arch, args.build_config,
+                        gn_target_name, IOS_DEPLOYMENT_TARGET,
+                        LIBVPX_BUILD_VP9, args.bitcode, args.use_goma, gn_args)
+        all_lib_paths.extend(lib_paths)
 
-    # Create FAT archive.
-    lib_paths = [
-        os.path.join(args.output_dir, arch + '_libs') for arch in architectures
-    ]
-
-    # Combine the slices.
-    dylib_path = os.path.join(SDK_FRAMEWORK_NAME, 'WebRTC')
-    # Dylibs will be combined, all other files are the same across archs.
-    # Use distutils instead of shutil to support merging folders.
-    distutils.dir_util.copy_tree(
-        os.path.join(lib_paths[0], SDK_FRAMEWORK_NAME),
-        os.path.join(args.output_dir, SDK_FRAMEWORK_NAME))
-    logging.info('Merging framework slices.')
-    dylib_paths = [os.path.join(path, dylib_path) for path in lib_paths]
-    out_dylib_path = os.path.join(args.output_dir, dylib_path)
-    try:
-        os.remove(out_dylib_path)
-    except OSError:
-        pass
-    cmd = ['lipo'] + dylib_paths + ['-create', '-output', out_dylib_path]
-    _RunCommand(cmd)
-
-    # Merge the dSYM slices.
-    lib_dsym_dir_path = os.path.join(lib_paths[0], 'WebRTC.dSYM')
-    if os.path.isdir(lib_dsym_dir_path):
+        # Combine the slices.
+        dylib_path = os.path.join(SDK_FRAMEWORK_NAME, 'WebRTC')
+        # Dylibs will be combined, all other files are the same across archs.
+        # Use distutils instead of shutil to support merging folders.
         distutils.dir_util.copy_tree(
-            lib_dsym_dir_path, os.path.join(args.output_dir, 'WebRTC.dSYM'))
-        logging.info('Merging dSYM slices.')
-        dsym_path = os.path.join('WebRTC.dSYM', 'Contents', 'Resources',
-                                 'DWARF', 'WebRTC')
-        lib_dsym_paths = [os.path.join(path, dsym_path) for path in lib_paths]
-        out_dsym_path = os.path.join(args.output_dir, dsym_path)
+            os.path.join(lib_paths[0], SDK_FRAMEWORK_NAME),
+            os.path.join(framework_path, SDK_FRAMEWORK_NAME))
+        logging.info('Merging framework slices for %s.', environment)
+        dylib_paths = [os.path.join(path, dylib_path) for path in lib_paths]
+        out_dylib_path = os.path.join(framework_path, dylib_path)
         try:
-            os.remove(out_dsym_path)
+            os.remove(out_dylib_path)
         except OSError:
             pass
-        cmd = ['lipo'] + lib_dsym_paths + ['-create', '-output', out_dsym_path]
+        cmd = ['lipo'] + dylib_paths + ['-create', '-output', out_dylib_path]
         _RunCommand(cmd)
 
-        # Generate the license file.
-        ninja_dirs = [
-            os.path.join(args.output_dir, arch + '_libs')
-            for arch in architectures
-        ]
-        gn_target_full_name = '//sdk:' + gn_target_name
-        builder = LicenseBuilder(ninja_dirs, [gn_target_full_name])
-        builder.GenerateLicenseText(
-            os.path.join(args.output_dir, SDK_FRAMEWORK_NAME))
+        # Merge the dSYM slices.
+        lib_dsym_dir_path = os.path.join(lib_paths[0], SDK_DSYM_NAME)
+        if os.path.isdir(lib_dsym_dir_path):
+            distutils.dir_util.copy_tree(
+                lib_dsym_dir_path, os.path.join(framework_path, SDK_DSYM_NAME))
+            logging.info('Merging dSYM slices.')
+            dsym_path = os.path.join(SDK_DSYM_NAME, 'Contents', 'Resources',
+                                     'DWARF', 'WebRTC')
+            lib_dsym_paths = [
+                os.path.join(path, dsym_path) for path in lib_paths
+            ]
+            out_dsym_path = os.path.join(framework_path, dsym_path)
+            try:
+                os.remove(out_dsym_path)
+            except OSError:
+                pass
+            cmd = ['lipo'
+                   ] + lib_dsym_paths + ['-create', '-output', out_dsym_path]
+            _RunCommand(cmd)
 
-        # Modify the version number.
-        # Format should be <Branch cut MXX>.<Hotfix #>.<Rev #>.
-        # e.g. 55.0.14986 means branch cut 55, no hotfixes, and revision 14986.
-        infoplist_path = os.path.join(args.output_dir, SDK_FRAMEWORK_NAME,
-                                      'Info.plist')
-        cmd = [
-            'PlistBuddy', '-c', 'Print :CFBundleShortVersionString',
-            infoplist_path
+            # Modify the version number.
+            # Format should be <Branch cut MXX>.<Hotfix #>.<Rev #>.
+            # e.g. 55.0.14986 means
+            # branch cut 55, no hotfixes, and revision 14986.
+            infoplist_path = os.path.join(framework_path, SDK_FRAMEWORK_NAME,
+                                          'Info.plist')
+            cmd = [
+                'PlistBuddy', '-c', 'Print :CFBundleShortVersionString',
+                infoplist_path
+            ]
+            major_minor = subprocess.check_output(cmd).strip()
+            version_number = '%s.%s' % (major_minor, args.revision)
+            logging.info('Substituting revision number: %s', version_number)
+            cmd = [
+                'PlistBuddy', '-c', 'Set :CFBundleVersion ' + version_number,
+                infoplist_path
+            ]
+            _RunCommand(cmd)
+            _RunCommand(['plutil', '-convert', 'binary1', infoplist_path])
+
+    xcframework_dir = os.path.join(args.output_dir, SDK_XCFRAMEWORK_NAME)
+    if os.path.isdir(xcframework_dir):
+        shutil.rmtree(xcframework_dir)
+
+    logging.info('Creating xcframework.')
+    cmd = ['xcodebuild', '-create-xcframework', '-output', xcframework_dir]
+
+    # Apparently, xcodebuild needs absolute paths for input arguments
+    for framework_path in framework_paths:
+        cmd += [
+            '-framework',
+            os.path.abspath(os.path.join(framework_path, SDK_FRAMEWORK_NAME)),
+            '-debug-symbols',
+            os.path.abspath(os.path.join(framework_path, SDK_DSYM_NAME))
         ]
-        major_minor = subprocess.check_output(cmd).strip()
-        version_number = '%s.%s' % (major_minor, args.revision)
-        logging.info('Substituting revision number: %s', version_number)
-        cmd = [
-            'PlistBuddy', '-c', 'Set :CFBundleVersion ' + version_number,
-            infoplist_path
-        ]
-        _RunCommand(cmd)
-        _RunCommand(['plutil', '-convert', 'binary1', infoplist_path])
+
+    _RunCommand(cmd)
+
+    # Generate the license file.
+    logging.info('Generate license file.')
+    gn_target_full_name = '//sdk:' + gn_target_name
+    builder = LicenseBuilder(all_lib_paths, [gn_target_full_name])
+    builder.GenerateLicenseText(
+        os.path.join(args.output_dir, SDK_XCFRAMEWORK_NAME))
 
     logging.info('Done.')
     return 0