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