1#!/usr/bin/python 2# Copyright 2016 The Chromium Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6import argparse 7import convert_gn_xcodeproj 8import errno 9import os 10import re 11import shutil 12import subprocess 13import sys 14import tempfile 15 16try: 17 import configparser 18except ImportError: 19 import ConfigParser as configparser 20 21try: 22 import StringIO as io 23except ImportError: 24 import io 25 26 27SUPPORTED_TARGETS = ('iphoneos', 'iphonesimulator', 'maccatalyst') 28SUPPORTED_CONFIGS = ('Debug', 'Release', 'Profile', 'Official', 'Coverage') 29 30 31class ConfigParserWithStringInterpolation(configparser.SafeConfigParser): 32 33 '''A .ini file parser that supports strings and environment variables.''' 34 35 ENV_VAR_PATTERN = re.compile(r'\$([A-Za-z0-9_]+)') 36 37 def values(self, section): 38 return map( 39 lambda kv: self._UnquoteString(self._ExpandEnvVar(kv[1])), 40 configparser.ConfigParser.items(self, section)) 41 42 def getstring(self, section, option): 43 return self._UnquoteString(self._ExpandEnvVar(self.get(section, option))) 44 45 def _UnquoteString(self, string): 46 if not string or string[0] != '"' or string[-1] != '"': 47 return string 48 return string[1:-1] 49 50 def _ExpandEnvVar(self, value): 51 match = self.ENV_VAR_PATTERN.search(value) 52 if not match: 53 return value 54 name, (begin, end) = match.group(1), match.span(0) 55 prefix, suffix = value[:begin], self._ExpandEnvVar(value[end:]) 56 return prefix + os.environ.get(name, '') + suffix 57 58class GnGenerator(object): 59 60 '''Holds configuration for a build and method to generate gn default files.''' 61 62 FAT_BUILD_DEFAULT_ARCH = '64-bit' 63 64 TARGET_CPU_VALUES = { 65 'iphoneos': '"arm64"', 66 'iphonesimulator': '"x64"', 67 'maccatalyst': '"x64"', 68 } 69 70 TARGET_ENVIRONMENT_VALUES = { 71 'iphoneos': '"device"', 72 'iphonesimulator': '"simulator"', 73 'maccatalyst': '"catalyst"' 74 } 75 76 def __init__(self, settings, config, target): 77 assert target in SUPPORTED_TARGETS 78 assert config in SUPPORTED_CONFIGS 79 self._settings = settings 80 self._config = config 81 self._target = target 82 83 def _GetGnArgs(self): 84 """Build the list of arguments to pass to gn. 85 86 Returns: 87 A list of tuple containing gn variable names and variable values (it 88 is not a dictionary as the order needs to be preserved). 89 """ 90 args = [] 91 92 # build/config/ios/ios_sdk.gni asserts that goma is not enabled when 93 # building Official, so ignore the value of goma.enabled when creating 94 # args.gn for Official. 95 if self._config != 'Official': 96 if self._settings.getboolean('goma', 'enabled'): 97 args.append(('use_goma', True)) 98 goma_dir = self._settings.getstring('goma', 'install') 99 if goma_dir: 100 args.append(('goma_dir', '"%s"' % os.path.expanduser(goma_dir))) 101 102 args.append(('target_os', '"ios"')) 103 args.append(('is_debug', self._config in ('Debug', 'Coverage'))) 104 args.append(('enable_dsyms', self._config in ('Profile', 'Official'))) 105 args.append(('enable_stripping', 'enable_dsyms')) 106 args.append(('is_official_build', self._config == 'Official')) 107 args.append(('is_chrome_branded', 'is_official_build')) 108 args.append(('use_clang_coverage', self._config == 'Coverage')) 109 args.append(('is_component_build', False)) 110 111 if os.environ.get('FORCE_MAC_TOOLCHAIN', '0') == '1': 112 args.append(('use_system_xcode', False)) 113 114 args.append(('target_cpu', self.TARGET_CPU_VALUES[self._target])) 115 args.append(( 116 'target_environment', 117 self.TARGET_ENVIRONMENT_VALUES[self._target])) 118 119 if self._target == 'maccatalyst': 120 # Building for "catalyst" environment has not been open-sourced thus can't 121 # use ToT clang and need to use Xcode's version instead. This version of 122 # clang does not generate the same warning as ToT clang, so do not treat 123 # warnings as errors. 124 # TODO(crbug.com/1145947): remove once clang ToT supports "macabi". 125 args.append(('use_xcode_clang', True)) 126 args.append(('treat_warnings_as_errors', False)) 127 128 # The "catalyst" environment is only supported from iOS 13.0 SDK. Until 129 # Chrome uses this SDK, it needs to be overridden for "catalyst" builds. 130 args.append(('ios_deployment_target', '"13.0"')) 131 132 # Add user overrides after the other configurations so that they can 133 # refer to them and override them. 134 args.extend(self._settings.items('gn_args')) 135 return args 136 137 138 def Generate(self, gn_path, root_path, build_dir): 139 self.WriteArgsGn(build_dir) 140 subprocess.check_call( 141 self.GetGnCommand(gn_path, root_path, build_dir, True)) 142 143 def CreateGnRules(self, gn_path, root_path, build_dir): 144 gn_command = self.GetGnCommand(gn_path, root_path, build_dir, False) 145 self.WriteArgsGn(build_dir) 146 self.WriteBuildNinja(gn_command, build_dir) 147 self.WriteBuildNinjaDeps(build_dir) 148 149 def WriteArgsGn(self, build_dir): 150 with open(os.path.join(build_dir, 'args.gn'), 'w') as stream: 151 stream.write('# This file was generated by setup-gn.py. Do not edit\n') 152 stream.write('# but instead use ~/.setup-gn or $repo/.setup-gn files\n') 153 stream.write('# to configure settings.\n') 154 stream.write('\n') 155 156 if self._target != 'maccatalyst': 157 if self._settings.has_section('$imports$'): 158 for import_rule in self._settings.values('$imports$'): 159 stream.write('import("%s")\n' % import_rule) 160 stream.write('\n') 161 162 gn_args = self._GetGnArgs() 163 for name, value in gn_args: 164 if isinstance(value, bool): 165 stream.write('%s = %s\n' % (name, str(value).lower())) 166 elif isinstance(value, list): 167 stream.write('%s = [%s' % (name, '\n' if len(value) > 1 else '')) 168 if len(value) == 1: 169 prefix = ' ' 170 suffix = ' ' 171 else: 172 prefix = ' ' 173 suffix = ',\n' 174 for item in value: 175 if isinstance(item, bool): 176 stream.write('%s%s%s' % (prefix, str(item).lower(), suffix)) 177 else: 178 stream.write('%s%s%s' % (prefix, item, suffix)) 179 stream.write(']\n') 180 else: 181 # ConfigParser removes quote around empty string which confuse 182 # `gn gen` so restore them. 183 if not value: 184 value = '""' 185 stream.write('%s = %s\n' % (name, value)) 186 187 def WriteBuildNinja(self, gn_command, build_dir): 188 with open(os.path.join(build_dir, 'build.ninja'), 'w') as stream: 189 stream.write('ninja_required_version = 1.7.2\n') 190 stream.write('\n') 191 stream.write('rule gn\n') 192 stream.write(' command = %s\n' % NinjaEscapeCommand(gn_command)) 193 stream.write(' description = Regenerating ninja files\n') 194 stream.write('\n') 195 stream.write('build build.ninja: gn\n') 196 stream.write(' generator = 1\n') 197 stream.write(' depfile = build.ninja.d\n') 198 199 def WriteBuildNinjaDeps(self, build_dir): 200 with open(os.path.join(build_dir, 'build.ninja.d'), 'w') as stream: 201 stream.write('build.ninja: nonexistant_file.gn\n') 202 203 def GetGnCommand(self, gn_path, src_path, out_path, generate_xcode_project): 204 gn_command = [ gn_path, '--root=%s' % os.path.realpath(src_path), '-q' ] 205 if generate_xcode_project: 206 gn_command.append('--ide=xcode') 207 gn_command.append('--ninja-executable=autoninja') 208 if self._settings.has_section('filters'): 209 target_filters = self._settings.values('filters') 210 if target_filters: 211 gn_command.append('--filters=%s' % ';'.join(target_filters)) 212 else: 213 gn_command.append('--check') 214 gn_command.append('gen') 215 gn_command.append('//%s' % 216 os.path.relpath(os.path.abspath(out_path), os.path.abspath(src_path))) 217 return gn_command 218 219 220def NinjaNeedEscape(arg): 221 '''Returns True if |arg| needs to be escaped when written to .ninja file.''' 222 return ':' in arg or '*' in arg or ';' in arg 223 224 225def NinjaEscapeCommand(command): 226 '''Escapes |command| in order to write it to .ninja file.''' 227 result = [] 228 for arg in command: 229 if NinjaNeedEscape(arg): 230 arg = arg.replace(':', '$:') 231 arg = arg.replace(';', '\\;') 232 arg = arg.replace('*', '\\*') 233 else: 234 result.append(arg) 235 return ' '.join(result) 236 237 238def FindGn(): 239 '''Returns absolute path to gn binary looking at the PATH env variable.''' 240 for path in os.environ['PATH'].split(os.path.pathsep): 241 gn_path = os.path.join(path, 'gn') 242 if os.path.isfile(gn_path) and os.access(gn_path, os.X_OK): 243 return gn_path 244 return None 245 246 247def GenerateXcodeProject(gn_path, root_dir, out_dir, settings): 248 '''Convert GN generated Xcode project into multi-configuration Xcode 249 project.''' 250 251 temp_path = tempfile.mkdtemp(prefix=os.path.abspath( 252 os.path.join(out_dir, '_temp'))) 253 try: 254 generator = GnGenerator(settings, 'Debug', 'iphonesimulator') 255 generator.Generate(gn_path, root_dir, temp_path) 256 convert_gn_xcodeproj.ConvertGnXcodeProject( 257 root_dir, 258 os.path.join(temp_path), 259 os.path.join(out_dir, 'build'), 260 SUPPORTED_CONFIGS) 261 finally: 262 if os.path.exists(temp_path): 263 shutil.rmtree(temp_path) 264 265 266def GenerateGnBuildRules(gn_path, root_dir, out_dir, settings): 267 '''Generates all template configurations for gn.''' 268 for config in SUPPORTED_CONFIGS: 269 for target in SUPPORTED_TARGETS: 270 build_dir = os.path.join(out_dir, '%s-%s' % (config, target)) 271 if not os.path.isdir(build_dir): 272 os.makedirs(build_dir) 273 274 generator = GnGenerator(settings, config, target) 275 generator.CreateGnRules(gn_path, root_dir, build_dir) 276 277 278def Main(args): 279 default_root = os.path.normpath(os.path.join( 280 os.path.dirname(__file__), os.pardir, os.pardir, os.pardir)) 281 282 parser = argparse.ArgumentParser( 283 description='Generate build directories for use with gn.') 284 parser.add_argument( 285 'root', default=default_root, nargs='?', 286 help='root directory where to generate multiple out configurations') 287 parser.add_argument( 288 '--import', action='append', dest='import_rules', default=[], 289 help='path to file defining default gn variables') 290 parser.add_argument( 291 '--gn-path', default=None, 292 help='path to gn binary (default: look up in $PATH)') 293 parser.add_argument( 294 '--build-dir', default='out', 295 help='path where the build should be created (default: %(default)s)') 296 args = parser.parse_args(args) 297 298 # Load configuration (first global and then any user overrides). 299 settings = ConfigParserWithStringInterpolation() 300 settings.read([ 301 os.path.splitext(__file__)[0] + '.config', 302 os.path.expanduser('~/.setup-gn'), 303 ]) 304 305 # Add private sections corresponding to --import argument. 306 if args.import_rules: 307 settings.add_section('$imports$') 308 for i, import_rule in enumerate(args.import_rules): 309 if not import_rule.startswith('//'): 310 import_rule = '//%s' % os.path.relpath( 311 os.path.abspath(import_rule), os.path.abspath(args.root)) 312 settings.set('$imports$', '$rule%d$' % i, import_rule) 313 314 # Validate settings. 315 if settings.getstring('build', 'arch') not in ('64-bit', '32-bit', 'fat'): 316 sys.stderr.write('ERROR: invalid value for build.arch: %s\n' % 317 settings.getstring('build', 'arch')) 318 sys.exit(1) 319 320 # Find path to gn binary either from command-line or in PATH. 321 if args.gn_path: 322 gn_path = args.gn_path 323 else: 324 gn_path = FindGn() 325 if gn_path is None: 326 sys.stderr.write('ERROR: cannot find gn in PATH\n') 327 sys.exit(1) 328 329 out_dir = os.path.join(args.root, args.build_dir) 330 if not os.path.isdir(out_dir): 331 os.makedirs(out_dir) 332 333 GenerateXcodeProject(gn_path, args.root, out_dir, settings) 334 GenerateGnBuildRules(gn_path, args.root, out_dir, settings) 335 336 337if __name__ == '__main__': 338 sys.exit(Main(sys.argv[1:])) 339