xref: /aosp_15_r20/external/webrtc/tools_webrtc/ios/build_ios_libs.py (revision d9f758449e529ab9291ac668be2861e7a55c2422)
1#!/usr/bin/env vpython3
2
3# Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
4#
5# Use of this source code is governed by a BSD-style license
6# that can be found in the LICENSE file in the root of the source
7# tree. An additional intellectual property rights grant can be found
8# in the file PATENTS.  All contributing project authors may
9# be found in the AUTHORS file in the root of the source tree.
10"""WebRTC iOS XCFramework build script.
11Each architecture is compiled separately before being merged together.
12By default, the library is created in out_ios_libs/. (Change with -o.)
13"""
14
15import argparse
16import logging
17import os
18import shutil
19import subprocess
20import sys
21
22os.environ['PATH'] = '/usr/libexec' + os.pathsep + os.environ['PATH']
23
24SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
25SRC_DIR = os.path.abspath(os.path.join(SCRIPT_DIR, '..', '..'))
26sys.path.append(os.path.join(SRC_DIR, 'build'))
27import find_depot_tools
28
29SDK_OUTPUT_DIR = os.path.join(SRC_DIR, 'out_ios_libs')
30SDK_FRAMEWORK_NAME = 'WebRTC.framework'
31SDK_DSYM_NAME = 'WebRTC.dSYM'
32SDK_XCFRAMEWORK_NAME = 'WebRTC.xcframework'
33
34ENABLED_ARCHS = [
35    'device:arm64', 'simulator:arm64', 'simulator:x64',
36    'catalyst:arm64', 'catalyst:x64',
37    'arm64', 'x64'
38]
39DEFAULT_ARCHS = [
40    'device:arm64', 'simulator:arm64', 'simulator:x64'
41]
42IOS_DEPLOYMENT_TARGET = {
43    'device': '12.0',
44    'simulator': '12.0',
45    'catalyst': '14.0'
46}
47LIBVPX_BUILD_VP9 = False
48
49sys.path.append(os.path.join(SCRIPT_DIR, '..', 'libs'))
50from generate_licenses import LicenseBuilder
51
52
53def _ParseArgs():
54  parser = argparse.ArgumentParser(description=__doc__)
55  parser.add_argument('--build_config',
56                      default='release',
57                      choices=['debug', 'release'],
58                      help='The build config. Can be "debug" or "release". '
59                      'Defaults to "release".')
60  parser.add_argument('--arch',
61                      nargs='+',
62                      default=DEFAULT_ARCHS,
63                      choices=ENABLED_ARCHS,
64                      help='Architectures to build. Defaults to %(default)s.')
65  parser.add_argument(
66      '-c',
67      '--clean',
68      action='store_true',
69      default=False,
70      help='Removes the previously generated build output, if any.')
71  parser.add_argument('-p',
72                      '--purify',
73                      action='store_true',
74                      default=False,
75                      help='Purifies the previously generated build output by '
76                      'removing the temporary results used when (re)building.')
77  parser.add_argument(
78      '-o',
79      '--output-dir',
80      type=os.path.abspath,
81      default=SDK_OUTPUT_DIR,
82      help='Specifies a directory to output the build artifacts to. '
83      'If specified together with -c, deletes the dir.')
84  parser.add_argument(
85      '-r',
86      '--revision',
87      type=int,
88      default=0,
89      help='Specifies a revision number to embed if building the framework.')
90  parser.add_argument('--verbose',
91                      action='store_true',
92                      default=False,
93                      help='Debug logging.')
94  parser.add_argument('--use-goma',
95                      action='store_true',
96                      default=False,
97                      help='Use goma to build.')
98  parser.add_argument('--use-remoteexec',
99                      action='store_true',
100                      default=False,
101                      help='Use RBE to build.')
102  parser.add_argument(
103      '--extra-gn-args',
104      default=[],
105      nargs='*',
106      help='Additional GN args to be used during Ninja generation.')
107
108  return parser.parse_args()
109
110
111def _RunCommand(cmd):
112  logging.debug('Running: %r', cmd)
113  subprocess.check_call(cmd, cwd=SRC_DIR)
114
115
116def _CleanArtifacts(output_dir):
117  if os.path.isdir(output_dir):
118    logging.info('Deleting %s', output_dir)
119    shutil.rmtree(output_dir)
120
121
122def _CleanTemporary(output_dir, architectures):
123  if os.path.isdir(output_dir):
124    logging.info('Removing temporary build files.')
125    for arch in architectures:
126      arch_lib_path = os.path.join(output_dir, arch)
127      if os.path.isdir(arch_lib_path):
128        shutil.rmtree(arch_lib_path)
129
130
131def _ParseArchitecture(architectures):
132  result = dict()
133  for arch in architectures:
134    if ":" in arch:
135      target_environment, target_cpu = arch.split(":")
136    else:
137      logging.warning('The environment for build is not specified.')
138      logging.warning('It is assumed based on cpu type.')
139      logging.warning('See crbug.com/1138425 for more details.')
140      if arch == "x64":
141        target_environment = "simulator"
142      else:
143        target_environment = "device"
144      target_cpu = arch
145    archs = result.get(target_environment)
146    if archs is None:
147      result[target_environment] = {target_cpu}
148    else:
149      archs.add(target_cpu)
150
151  return result
152
153
154def BuildWebRTC(output_dir, target_environment, target_arch, flavor,
155                gn_target_name, ios_deployment_target, libvpx_build_vp9,
156                use_goma, use_remoteexec, extra_gn_args):
157  gn_args = [
158      'target_os="ios"',
159      'ios_enable_code_signing=false',
160      'is_component_build=false',
161      'rtc_include_tests=false',
162  ]
163
164  # Add flavor option.
165  if flavor == 'debug':
166    gn_args.append('is_debug=true')
167  elif flavor == 'release':
168    gn_args.append('is_debug=false')
169  else:
170    raise ValueError('Unexpected flavor type: %s' % flavor)
171
172  gn_args.append('target_environment="%s"' % target_environment)
173
174  gn_args.append('target_cpu="%s"' % target_arch)
175
176  gn_args.append('ios_deployment_target="%s"' % ios_deployment_target)
177
178  gn_args.append('rtc_libvpx_build_vp9=' +
179                 ('true' if libvpx_build_vp9 else 'false'))
180
181  gn_args.append('use_lld=true')
182  gn_args.append('use_goma=' + ('true' if use_goma else 'false'))
183  gn_args.append('use_remoteexec=' + ('true' if use_remoteexec else 'false'))
184  gn_args.append('rtc_enable_objc_symbol_export=true')
185
186  args_string = ' '.join(gn_args + extra_gn_args)
187  logging.info('Building WebRTC with args: %s', args_string)
188
189  cmd = [
190      sys.executable,
191      os.path.join(find_depot_tools.DEPOT_TOOLS_PATH, 'gn.py'),
192      'gen',
193      output_dir,
194      '--args=' + args_string,
195  ]
196  _RunCommand(cmd)
197  logging.info('Building target: %s', gn_target_name)
198
199  cmd = [
200      os.path.join(find_depot_tools.DEPOT_TOOLS_PATH, 'ninja'),
201      '-C',
202      output_dir,
203      gn_target_name,
204  ]
205  if use_goma or use_remoteexec:
206    cmd.extend(['-j', '200'])
207  _RunCommand(cmd)
208
209
210def main():
211  args = _ParseArgs()
212
213  logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
214
215  if args.clean:
216    _CleanArtifacts(args.output_dir)
217    return 0
218
219  # architectures is typed as Dict[str, Set[str]],
220  # where key is for the environment (device or simulator)
221  # and value is for the cpu type.
222  architectures = _ParseArchitecture(args.arch)
223  gn_args = args.extra_gn_args
224
225  if args.purify:
226    _CleanTemporary(args.output_dir, list(architectures.keys()))
227    return 0
228
229  gn_target_name = 'framework_objc'
230  gn_args.append('enable_dsyms=true')
231  gn_args.append('enable_stripping=true')
232
233  # Build all architectures.
234  framework_paths = []
235  all_lib_paths = []
236  for (environment, archs) in list(architectures.items()):
237    framework_path = os.path.join(args.output_dir, environment)
238    framework_paths.append(framework_path)
239    lib_paths = []
240    for arch in archs:
241      lib_path = os.path.join(framework_path, arch + '_libs')
242      lib_paths.append(lib_path)
243      BuildWebRTC(lib_path, environment, arch, args.build_config,
244                  gn_target_name, IOS_DEPLOYMENT_TARGET[environment],
245                  LIBVPX_BUILD_VP9, args.use_goma, args.use_remoteexec, gn_args)
246    all_lib_paths.extend(lib_paths)
247
248    # Combine the slices.
249    dylib_path = os.path.join(SDK_FRAMEWORK_NAME, 'WebRTC')
250    # Dylibs will be combined, all other files are the same across archs.
251    shutil.rmtree(os.path.join(framework_path, SDK_FRAMEWORK_NAME),
252                  ignore_errors=True)
253    shutil.copytree(os.path.join(lib_paths[0], SDK_FRAMEWORK_NAME),
254                    os.path.join(framework_path, SDK_FRAMEWORK_NAME),
255                    symlinks=True)
256    logging.info('Merging framework slices for %s.', environment)
257    dylib_paths = [os.path.join(path, dylib_path) for path in lib_paths]
258    out_dylib_path = os.path.join(framework_path, dylib_path)
259    if os.path.islink(out_dylib_path):
260      out_dylib_path = os.path.join(os.path.dirname(out_dylib_path),
261                                    os.readlink(out_dylib_path))
262    try:
263      os.remove(out_dylib_path)
264    except OSError:
265      pass
266    cmd = ['lipo'] + dylib_paths + ['-create', '-output', out_dylib_path]
267    _RunCommand(cmd)
268
269    # Merge the dSYM slices.
270    lib_dsym_dir_path = os.path.join(lib_paths[0], SDK_DSYM_NAME)
271    if os.path.isdir(lib_dsym_dir_path):
272      shutil.rmtree(os.path.join(framework_path, SDK_DSYM_NAME),
273                    ignore_errors=True)
274      shutil.copytree(lib_dsym_dir_path,
275                      os.path.join(framework_path, SDK_DSYM_NAME))
276      logging.info('Merging dSYM slices.')
277      dsym_path = os.path.join(SDK_DSYM_NAME, 'Contents', 'Resources', 'DWARF',
278                               'WebRTC')
279      lib_dsym_paths = [os.path.join(path, dsym_path) for path in lib_paths]
280      out_dsym_path = os.path.join(framework_path, dsym_path)
281      try:
282        os.remove(out_dsym_path)
283      except OSError:
284        pass
285      cmd = ['lipo'] + lib_dsym_paths + ['-create', '-output', out_dsym_path]
286      _RunCommand(cmd)
287
288      # Check for Mac-style WebRTC.framework/Resources/ (for Catalyst)...
289      resources_dir = os.path.join(framework_path, SDK_FRAMEWORK_NAME,
290                                   'Resources')
291      if not os.path.exists(resources_dir):
292        # ...then fall back to iOS-style WebRTC.framework/
293        resources_dir = os.path.dirname(resources_dir)
294
295      # Modify the version number.
296      # Format should be <Branch cut MXX>.<Hotfix #>.<Rev #>.
297      # e.g. 55.0.14986 means
298      # branch cut 55, no hotfixes, and revision 14986.
299      infoplist_path = os.path.join(resources_dir, 'Info.plist')
300      cmd = [
301          'PlistBuddy', '-c', 'Print :CFBundleShortVersionString',
302          infoplist_path
303      ]
304      major_minor = subprocess.check_output(cmd).decode('utf-8').strip()
305      version_number = '%s.%s' % (major_minor, args.revision)
306      logging.info('Substituting revision number: %s', version_number)
307      cmd = [
308          'PlistBuddy', '-c', 'Set :CFBundleVersion ' + version_number,
309          infoplist_path
310      ]
311      _RunCommand(cmd)
312      _RunCommand(['plutil', '-convert', 'binary1', infoplist_path])
313
314  xcframework_dir = os.path.join(args.output_dir, SDK_XCFRAMEWORK_NAME)
315  if os.path.isdir(xcframework_dir):
316    shutil.rmtree(xcframework_dir)
317
318  logging.info('Creating xcframework.')
319  cmd = ['xcodebuild', '-create-xcframework', '-output', xcframework_dir]
320
321  # Apparently, xcodebuild needs absolute paths for input arguments
322  for framework_path in framework_paths:
323    cmd += [
324        '-framework',
325        os.path.abspath(os.path.join(framework_path, SDK_FRAMEWORK_NAME)),
326    ]
327    dsym_full_path = os.path.join(framework_path, SDK_DSYM_NAME)
328    if os.path.exists(dsym_full_path):
329      cmd += ['-debug-symbols', os.path.abspath(dsym_full_path)]
330
331  _RunCommand(cmd)
332
333  # Generate the license file.
334  logging.info('Generate license file.')
335  gn_target_full_name = '//sdk:' + gn_target_name
336  builder = LicenseBuilder(all_lib_paths, [gn_target_full_name])
337  builder.GenerateLicenseText(
338      os.path.join(args.output_dir, SDK_XCFRAMEWORK_NAME))
339
340  logging.info('Done.')
341  return 0
342
343
344if __name__ == '__main__':
345  sys.exit(main())
346