def build_xcframeworks()

in scripts/gha/build_ios_tvos.py [0:0]


def build_xcframeworks(frameworks_path, xcframeworks_path, template_info_plist,
                       targets):
  """Build xcframeworks combining libraries for different operating systems.

  Combine frameworks for different operating systems (ios, tvos), architectures
  (arm64, armv7, x86_64 etc) per platform variant (device, simulator).
  This makes it super convenient for developers to use a single deliverable in
  XCode and develop for multiple platforms/operating systems in one project.

  Args:
      frameworks_path (str): Absolute path to path containing frameworks.
      xcframeworks_path (str): Absolute path to create xcframeworks in.
      template_info_plist (str): Absolute path to a template Info.plist that
        will be copied over to each xcframework and provides metadata to XCode.
      targets iterable(str): List of firebase target libraries.
        (eg: [firebase_auth, firebase_remote_config])

        Eg: <build_dir>/frameworks              <------------- <frameworks_path>
                - ios                           <---------- <frameworks_os_path>
                  - device-arm64
                    - firebase.framework
                    - firebase_admob.framework
                    ...
                  - simulator-i386
                  ...
                - tvos
                  - device-arm64
                    - firebase.framework
                    - firebase_admob.framework
                    ...
                  - simulator-x86_64
                  ...

        Output: <build_dir>/xcframeworks        <----------- <xcframeworks_path>
                - firebase.xcframework
                  - Info.plist                  <----------- <Info.plist file>
                  - ios-arm64_armv7           <-- <all_libraries_for_ios_device>
                    - firebase.framework
                      - firebase                <---- <library>
                      - Headers                 <---- <all_include_headers>
                  - ios-arm64_i386_x86_64-simulator <--- <for_ios_simulator>
                    - firebase.framework
                      - firebase
                      - Headers
                  - tvos-arm64                <- <all_libraries_for_tvos_device>
                    - firebase.framework
                      - firebase
                      - Headers
                    ...
                  ...
                - firebase_auth.xcframework   <-- <firebase_auth target>
                  - Info.plist
                  - ios-arm64_armv7
                    - firebase_auth.framework
                      - firebase_auth
                  - ios-arm64_i386_x86_64-simulator
                    - firebase_auth.framework
                      - firebase_auth
                  - tvos-arm64
                    - firebase.framework
                      - firebase
                      - Headers
                    ...
                  ...
                ...

  """
  for apple_os in os.listdir(frameworks_path):
    framework_os_path = os.path.join(frameworks_path, apple_os)
    platform_variant_architecture_dirs = os.listdir(framework_os_path)
    # Extract list of all built platform-architecture combinations into a map.
    # Map looks like this,
    # {'device': ['arm64', 'armv7'], 'simulator': ['x86_64']}
    platform_variant_arch_map = defaultdict(list)
    for variant_architecture in platform_variant_architecture_dirs:
      # Skip directories not of the format platform-arch (eg: universal)
      if not '-' in variant_architecture:
        continue
      platform_variant, architecture = variant_architecture.split('-')
      platform_variant_arch_map[platform_variant].append(architecture)

    reference_dir_path = os.path.join(framework_os_path,
                                      platform_variant_architecture_dirs[0])
    logging.debug('Using {0} as reference path for scanning '
                  'targets'.format(reference_dir_path))

    # Filter only .framework directories and make sure the framework is
    # in list of supported targets.
    target_frameworks = [x for x in os.listdir(reference_dir_path)
                        if x.endswith('.framework') and
                        x.split('.')[0] in targets]
    logging.debug('Targets found: {0}'.format(' '.join(target_frameworks)))

    # For each target, we collect all libraries for a specific platform variants
    # (device or simulator) and os (ios or tvos).
    for target_framework in target_frameworks:
      target_libraries = []
      # Eg: split firebase_auth.framework -> firebase_auth, .framework
      target, _ = os.path.splitext(target_framework)
      xcframework_target_map = {}
      for platform_variant in platform_variant_arch_map:
        architectures = platform_variant_arch_map[platform_variant]
        xcframework_libraries = []
        for architecture in architectures:
          # <build_dir>/<apple_os>/frameworks/<platform-arch>/
          # <target>.framework/target
          library_path = os.path.join(framework_os_path,
                                      '{0}-{1}'.format(platform_variant,
                                      architecture), target_framework, target)
          xcframework_libraries.append(library_path)

        xcframework_key_parts = [apple_os]
        xcframework_key_parts.append('_'.join(sorted(architectures)))
        if platform_variant != 'device':
          # device is treated as default platform variant and we do not add any
          # suffix at the end. For all other variants, add them as suffix.
          xcframework_key_parts.append(platform_variant)
        # Eg: ios-arm64_armv7, tvos-x86_64-simulator
        xcframework_key = '-'.join(xcframework_key_parts)

        # <build_dir>/xcframeworks/<target>.xcframework/<os>-<list_of_archs>/
        # <target>.framework
        library_output_dir = os.path.join(xcframeworks_path,
                                          '{0}.xcframework'.format(target),
                                          xcframework_key,
                                          '{0}.framework'.format(target))
        logging.debug('Ensuring all directories exist: ' + library_output_dir)
        os.makedirs(library_output_dir)
        cmd = ['lipo', '-create']
        cmd.extend(xcframework_libraries)
        cmd.append('-output')
        cmd.append(os.path.join(library_output_dir, target))

        logging.info('Creating xcframework at' +
                     os.path.join(library_output_dir, target))
        utils.run_command(cmd)

        # <build_dir>/xcframeworks/<target>.xcframework
        target_xcframeworks_path = os.path.join(xcframeworks_path,
          '{0}.xcframework'.format(target))
        # Create Info.plist for xcframework
        dest_path = os.path.join(target_xcframeworks_path, 'Info.plist')
        logging.info('Copying template {0}'.format(template_info_plist))
        shutil.copy(template_info_plist, dest_path)
        contents = None
        # Replace token LIBRARY_PATH with current target framework.
        with open(dest_path, 'r') as info_plist_file:
          contents = info_plist_file.read()
        if contents:
          logging.debug('Updating LIBRARY_PATH with '
                        '{0}.framework'.format(target))
          contents = contents.replace('LIBRARY_PATH',
                                      '{0}.framework'.format(target))
          with open(dest_path, 'w') as info_plist_file:
            info_plist_file.write(contents)

    # Copy Headers for firebase.xcframework from firebase.framework.
    # Using a random platform specific firebase.framework.
    firebase_framework_headers_path = os.path.join(reference_dir_path,
                                                   'firebase.framework',
                                                   'Headers')
    firebase_xcframework_path = os.path.join(xcframeworks_path,
                                             'firebase.xcframework')

    for xcframework_key in os.listdir(firebase_xcframework_path):
      if os.path.isfile(os.path.join(firebase_xcframework_path,
                                     xcframework_key)):
        continue
      dest_headers_path = os.path.join(firebase_xcframework_path,
                                       xcframework_key,
                                       'firebase.framework', 'Headers')
      if os.path.exists(dest_headers_path):
        continue
      logging.info('Copying {0} to {1}'.format(firebase_framework_headers_path,
                                               dest_headers_path))
      shutil.copytree(firebase_framework_headers_path,
                     dest_headers_path)