1#!/usr/bin/env python
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 FAT libraries 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 distutils.dir_util
17import logging
18import os
19import shutil
20import subprocess
21import sys
22
23os.environ['PATH'] = '/usr/libexec' + os.pathsep + os.environ['PATH']
24
25SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
26SRC_DIR = os.path.abspath(os.path.join(SCRIPT_DIR, '..', '..'))
27sys.path.append(os.path.join(SRC_DIR, 'build'))
28import find_depot_tools
29
30SDK_OUTPUT_DIR = os.path.join(SRC_DIR, 'out_ios_libs')
31SDK_FRAMEWORK_NAME = 'WebRTC.framework'
32
33DEFAULT_ARCHS = ENABLED_ARCHS = ['arm64', 'arm', 'x64', 'x86']
34IOS_DEPLOYMENT_TARGET = '10.0'
35LIBVPX_BUILD_VP9 = False
36
37sys.path.append(os.path.join(SCRIPT_DIR, '..', 'libs'))
38from generate_licenses import LicenseBuilder
39
40
41def _ParseArgs():
42    parser = argparse.ArgumentParser(description=__doc__)
43    parser.add_argument('--build_config',
44                        default='release',
45                        choices=['debug', 'release'],
46                        help='The build config. Can be "debug" or "release". '
47                        'Defaults to "release".')
48    parser.add_argument(
49        '--arch',
50        nargs='+',
51        default=DEFAULT_ARCHS,
52        choices=ENABLED_ARCHS,
53        help='Architectures to build. Defaults to %(default)s.')
54    parser.add_argument(
55        '-c',
56        '--clean',
57        action='store_true',
58        default=False,
59        help='Removes the previously generated build output, if any.')
60    parser.add_argument(
61        '-p',
62        '--purify',
63        action='store_true',
64        default=False,
65        help='Purifies the previously generated build output by '
66        'removing the temporary results used when (re)building.')
67    parser.add_argument(
68        '-o',
69        '--output-dir',
70        default=SDK_OUTPUT_DIR,
71        help='Specifies a directory to output the build artifacts to. '
72        'If specified together with -c, deletes the dir.')
73    parser.add_argument(
74        '-r',
75        '--revision',
76        type=int,
77        default=0,
78        help='Specifies a revision number to embed if building the framework.')
79    parser.add_argument('-e',
80                        '--bitcode',
81                        action='store_true',
82                        default=False,
83                        help='Compile with bitcode.')
84    parser.add_argument('--verbose',
85                        action='store_true',
86                        default=False,
87                        help='Debug logging.')
88    parser.add_argument('--use-goma',
89                        action='store_true',
90                        default=False,
91                        help='Use goma to build.')
92    parser.add_argument(
93        '--extra-gn-args',
94        default=[],
95        nargs='*',
96        help='Additional GN args to be used during Ninja generation.')
97
98    return parser.parse_args()
99
100
101def _RunCommand(cmd):
102    logging.debug('Running: %r', cmd)
103    subprocess.check_call(cmd, cwd=SRC_DIR)
104
105
106def _CleanArtifacts(output_dir):
107    if os.path.isdir(output_dir):
108        logging.info('Deleting %s', output_dir)
109        shutil.rmtree(output_dir)
110
111
112def _CleanTemporary(output_dir, architectures):
113    if os.path.isdir(output_dir):
114        logging.info('Removing temporary build files.')
115        for arch in architectures:
116            arch_lib_path = os.path.join(output_dir, arch + '_libs')
117            if os.path.isdir(arch_lib_path):
118                shutil.rmtree(arch_lib_path)
119
120
121def BuildWebRTC(output_dir, target_arch, flavor, gn_target_name,
122                ios_deployment_target, libvpx_build_vp9, use_bitcode, use_goma,
123                extra_gn_args):
124    output_dir = os.path.join(output_dir, target_arch + '_libs')
125    gn_args = [
126        'target_os="ios"', 'ios_enable_code_signing=false',
127        'use_xcode_clang=true', 'is_component_build=false'
128    ]
129
130    # Add flavor option.
131    if flavor == 'debug':
132        gn_args.append('is_debug=true')
133    elif flavor == 'release':
134        gn_args.append('is_debug=false')
135    else:
136        raise ValueError('Unexpected flavor type: %s' % flavor)
137
138    gn_args.append('target_cpu="%s"' % target_arch)
139
140    gn_args.append('ios_deployment_target="%s"' % ios_deployment_target)
141
142    gn_args.append('rtc_libvpx_build_vp9=' +
143                   ('true' if libvpx_build_vp9 else 'false'))
144
145    gn_args.append('enable_ios_bitcode=' +
146                   ('true' if use_bitcode else 'false'))
147    gn_args.append('use_goma=' + ('true' if use_goma else 'false'))
148
149    args_string = ' '.join(gn_args + extra_gn_args)
150    logging.info('Building WebRTC with args: %s', args_string)
151
152    cmd = [
153        sys.executable,
154        os.path.join(find_depot_tools.DEPOT_TOOLS_PATH, 'gn.py'),
155        'gen',
156        output_dir,
157        '--args=' + args_string,
158    ]
159    _RunCommand(cmd)
160    logging.info('Building target: %s', gn_target_name)
161
162    cmd = [
163        os.path.join(find_depot_tools.DEPOT_TOOLS_PATH, 'ninja'),
164        '-C',
165        output_dir,
166        gn_target_name,
167    ]
168    if use_goma:
169        cmd.extend(['-j', '200'])
170    _RunCommand(cmd)
171
172
173def main():
174    args = _ParseArgs()
175
176    logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
177
178    if args.clean:
179        _CleanArtifacts(args.output_dir)
180        return 0
181
182    architectures = list(args.arch)
183    gn_args = args.extra_gn_args
184
185    if args.purify:
186        _CleanTemporary(args.output_dir, architectures)
187        return 0
188
189    gn_target_name = 'framework_objc'
190    if not args.bitcode:
191        gn_args.append('enable_dsyms=true')
192    gn_args.append('enable_stripping=true')
193
194    # Build all architectures.
195    for arch in architectures:
196        BuildWebRTC(args.output_dir, arch, args.build_config, gn_target_name,
197                    IOS_DEPLOYMENT_TARGET, LIBVPX_BUILD_VP9, args.bitcode,
198                    args.use_goma, gn_args)
199
200    # Create FAT archive.
201    lib_paths = [
202        os.path.join(args.output_dir, arch + '_libs') for arch in architectures
203    ]
204
205    # Combine the slices.
206    dylib_path = os.path.join(SDK_FRAMEWORK_NAME, 'WebRTC')
207    # Dylibs will be combined, all other files are the same across archs.
208    # Use distutils instead of shutil to support merging folders.
209    distutils.dir_util.copy_tree(
210        os.path.join(lib_paths[0], SDK_FRAMEWORK_NAME),
211        os.path.join(args.output_dir, SDK_FRAMEWORK_NAME))
212    logging.info('Merging framework slices.')
213    dylib_paths = [os.path.join(path, dylib_path) for path in lib_paths]
214    out_dylib_path = os.path.join(args.output_dir, dylib_path)
215    try:
216        os.remove(out_dylib_path)
217    except OSError:
218        pass
219    cmd = ['lipo'] + dylib_paths + ['-create', '-output', out_dylib_path]
220    _RunCommand(cmd)
221
222    # Merge the dSYM slices.
223    lib_dsym_dir_path = os.path.join(lib_paths[0], 'WebRTC.dSYM')
224    if os.path.isdir(lib_dsym_dir_path):
225        distutils.dir_util.copy_tree(
226            lib_dsym_dir_path, os.path.join(args.output_dir, 'WebRTC.dSYM'))
227        logging.info('Merging dSYM slices.')
228        dsym_path = os.path.join('WebRTC.dSYM', 'Contents', 'Resources',
229                                 'DWARF', 'WebRTC')
230        lib_dsym_paths = [os.path.join(path, dsym_path) for path in lib_paths]
231        out_dsym_path = os.path.join(args.output_dir, dsym_path)
232        try:
233            os.remove(out_dsym_path)
234        except OSError:
235            pass
236        cmd = ['lipo'] + lib_dsym_paths + ['-create', '-output', out_dsym_path]
237        _RunCommand(cmd)
238
239        # Generate the license file.
240        ninja_dirs = [
241            os.path.join(args.output_dir, arch + '_libs')
242            for arch in architectures
243        ]
244        gn_target_full_name = '//sdk:' + gn_target_name
245        builder = LicenseBuilder(ninja_dirs, [gn_target_full_name])
246        builder.GenerateLicenseText(
247            os.path.join(args.output_dir, SDK_FRAMEWORK_NAME))
248
249        # Modify the version number.
250        # Format should be <Branch cut MXX>.<Hotfix #>.<Rev #>.
251        # e.g. 55.0.14986 means branch cut 55, no hotfixes, and revision 14986.
252        infoplist_path = os.path.join(args.output_dir, SDK_FRAMEWORK_NAME,
253                                      'Info.plist')
254        cmd = [
255            'PlistBuddy', '-c', 'Print :CFBundleShortVersionString',
256            infoplist_path
257        ]
258        major_minor = subprocess.check_output(cmd).strip()
259        version_number = '%s.%s' % (major_minor, args.revision)
260        logging.info('Substituting revision number: %s', version_number)
261        cmd = [
262            'PlistBuddy', '-c', 'Set :CFBundleVersion ' + version_number,
263            infoplist_path
264        ]
265        _RunCommand(cmd)
266        _RunCommand(['plutil', '-convert', 'binary1', infoplist_path])
267
268    logging.info('Done.')
269    return 0
270
271
272if __name__ == '__main__':
273    sys.exit(main())
274