1#!/usr/bin/env python 2 3# Copyright 2020 The Crashpad Authors. All rights reserved. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17 18import argparse 19import convert_gn_xcodeproj 20import errno 21import os 22import re 23import shutil 24import subprocess 25import sys 26import tempfile 27import ConfigParser 28 29try: 30 import cStringIO as StringIO 31except ImportError: 32 import StringIO 33 34 35SUPPORTED_TARGETS = ('iphoneos', 'iphonesimulator') 36SUPPORTED_CONFIGS = ('Debug', 'Release', 'Profile', 'Official', 'Coverage') 37 38 39class ConfigParserWithStringInterpolation(ConfigParser.SafeConfigParser): 40 41 '''A .ini file parser that supports strings and environment variables.''' 42 43 ENV_VAR_PATTERN = re.compile(r'\$([A-Za-z0-9_]+)') 44 45 def values(self, section): 46 return map( 47 lambda (k, v): self._UnquoteString(self._ExpandEnvVar(v)), 48 ConfigParser.SafeConfigParser.items(self, section)) 49 50 def getstring(self, section, option): 51 return self._UnquoteString(self._ExpandEnvVar(self.get(section, option))) 52 53 def _UnquoteString(self, string): 54 if not string or string[0] != '"' or string[-1] != '"': 55 return string 56 return string[1:-1] 57 58 def _ExpandEnvVar(self, value): 59 match = self.ENV_VAR_PATTERN.search(value) 60 if not match: 61 return value 62 name, (begin, end) = match.group(1), match.span(0) 63 prefix, suffix = value[:begin], self._ExpandEnvVar(value[end:]) 64 return prefix + os.environ.get(name, '') + suffix 65 66class GnGenerator(object): 67 68 '''Holds configuration for a build and method to generate gn default files.''' 69 70 FAT_BUILD_DEFAULT_ARCH = '64-bit' 71 72 TARGET_CPU_VALUES = { 73 'iphoneos': { 74 '32-bit': '"arm"', 75 '64-bit': '"arm64"', 76 }, 77 'iphonesimulator': { 78 '32-bit': '"x86"', 79 '64-bit': '"x64"', 80 } 81 } 82 83 def __init__(self, settings, config, target): 84 assert target in SUPPORTED_TARGETS 85 assert config in SUPPORTED_CONFIGS 86 self._settings = settings 87 self._config = config 88 self._target = target 89 90 def _GetGnArgs(self): 91 """Build the list of arguments to pass to gn. 92 93 Returns: 94 A list of tuple containing gn variable names and variable values (it 95 is not a dictionary as the order needs to be preserved). 96 """ 97 args = [] 98 99 args.append(('is_debug', self._config in ('Debug', 'Coverage'))) 100 101 if os.environ.get('FORCE_MAC_TOOLCHAIN', '0') == '1': 102 args.append(('use_system_xcode', False)) 103 104 cpu_values = self.TARGET_CPU_VALUES[self._target] 105 build_arch = self._settings.getstring('build', 'arch') 106 if build_arch == 'fat': 107 target_cpu = cpu_values[self.FAT_BUILD_DEFAULT_ARCH] 108 args.append(('target_cpu', target_cpu)) 109 args.append(('additional_target_cpus', 110 [cpu for cpu in cpu_values.itervalues() if cpu != target_cpu])) 111 else: 112 args.append(('target_cpu', cpu_values[build_arch])) 113 114 # Add user overrides after the other configurations so that they can 115 # refer to them and override them. 116 args.extend(self._settings.items('gn_args')) 117 return args 118 119 120 def Generate(self, gn_path, root_path, out_path): 121 buf = StringIO.StringIO() 122 self.WriteArgsGn(buf) 123 WriteToFileIfChanged( 124 os.path.join(out_path, 'args.gn'), 125 buf.getvalue(), 126 overwrite=True) 127 128 subprocess.check_call( 129 self.GetGnCommand(gn_path, root_path, out_path, True)) 130 131 def CreateGnRules(self, gn_path, root_path, out_path): 132 buf = StringIO.StringIO() 133 self.WriteArgsGn(buf) 134 WriteToFileIfChanged( 135 os.path.join(out_path, 'args.gn'), 136 buf.getvalue(), 137 overwrite=True) 138 139 buf = StringIO.StringIO() 140 gn_command = self.GetGnCommand(gn_path, root_path, out_path, False) 141 self.WriteBuildNinja(buf, gn_command) 142 WriteToFileIfChanged( 143 os.path.join(out_path, 'build.ninja'), 144 buf.getvalue(), 145 overwrite=False) 146 147 buf = StringIO.StringIO() 148 self.WriteBuildNinjaDeps(buf) 149 WriteToFileIfChanged( 150 os.path.join(out_path, 'build.ninja.d'), 151 buf.getvalue(), 152 overwrite=False) 153 154 def WriteArgsGn(self, stream): 155 stream.write('# This file was generated by setup-gn.py. Do not edit\n') 156 stream.write('# but instead use ~/.setup-gn or $repo/.setup-gn files\n') 157 stream.write('# to configure settings.\n') 158 stream.write('\n') 159 160 if self._settings.has_section('$imports$'): 161 for import_rule in self._settings.values('$imports$'): 162 stream.write('import("%s")\n' % import_rule) 163 stream.write('\n') 164 165 gn_args = self._GetGnArgs() 166 for name, value in gn_args: 167 if isinstance(value, bool): 168 stream.write('%s = %s\n' % (name, str(value).lower())) 169 elif isinstance(value, list): 170 stream.write('%s = [%s' % (name, '\n' if len(value) > 1 else '')) 171 if len(value) == 1: 172 prefix = ' ' 173 suffix = ' ' 174 else: 175 prefix = ' ' 176 suffix = ',\n' 177 for item in value: 178 if isinstance(item, bool): 179 stream.write('%s%s%s' % (prefix, str(item).lower(), suffix)) 180 else: 181 stream.write('%s%s%s' % (prefix, item, suffix)) 182 stream.write(']\n') 183 else: 184 stream.write('%s = %s\n' % (name, value)) 185 186 def WriteBuildNinja(self, stream, gn_command): 187 stream.write('rule gn\n') 188 stream.write(' command = %s\n' % NinjaEscapeCommand(gn_command)) 189 stream.write(' description = Regenerating ninja files\n') 190 stream.write('\n') 191 stream.write('build build.ninja: gn\n') 192 stream.write(' generator = 1\n') 193 stream.write(' depfile = build.ninja.d\n') 194 195 def WriteBuildNinjaDeps(self, stream): 196 stream.write('build.ninja: nonexistant_file.gn\n') 197 198 def GetGnCommand(self, gn_path, src_path, out_path, generate_xcode_project): 199 gn_command = [ gn_path, '--root=%s' % os.path.realpath(src_path), '-q' ] 200 if generate_xcode_project: 201 gn_command.append('--ide=xcode') 202 gn_command.append('--root-target=gn_all') 203 if self._settings.getboolean('goma', 'enabled'): 204 ninja_jobs = self._settings.getint('xcode', 'jobs') or 200 205 gn_command.append('--ninja-extra-args=-j%s' % ninja_jobs) 206 if self._settings.has_section('filters'): 207 target_filters = self._settings.values('filters') 208 if target_filters: 209 gn_command.append('--filters=%s' % ';'.join(target_filters)) 210 # TODO(justincohen): --check is currently failing in crashpad. 211 # else: 212 # gn_command.append('--check') 213 gn_command.append('gen') 214 gn_command.append('//%s' % 215 os.path.relpath(os.path.abspath(out_path), os.path.abspath(src_path))) 216 return gn_command 217 218 219def WriteToFileIfChanged(filename, content, overwrite): 220 '''Write |content| to |filename| if different. If |overwrite| is False 221 and the file already exists it is left untouched.''' 222 if os.path.exists(filename): 223 if not overwrite: 224 return 225 with open(filename) as file: 226 if file.read() == content: 227 return 228 if not os.path.isdir(os.path.dirname(filename)): 229 os.makedirs(os.path.dirname(filename)) 230 with open(filename, 'w') as file: 231 file.write(content) 232 233 234def NinjaNeedEscape(arg): 235 '''Returns True if |arg| needs to be escaped when written to .ninja file.''' 236 return ':' in arg or '*' in arg or ';' in arg 237 238 239def NinjaEscapeCommand(command): 240 '''Escapes |command| in order to write it to .ninja file.''' 241 result = [] 242 for arg in command: 243 if NinjaNeedEscape(arg): 244 arg = arg.replace(':', '$:') 245 arg = arg.replace(';', '\\;') 246 arg = arg.replace('*', '\\*') 247 else: 248 result.append(arg) 249 return ' '.join(result) 250 251 252def FindGn(): 253 '''Returns absolute path to gn binary looking at the PATH env variable.''' 254 for path in os.environ['PATH'].split(os.path.pathsep): 255 gn_path = os.path.join(path, 'gn') 256 if os.path.isfile(gn_path) and os.access(gn_path, os.X_OK): 257 return gn_path 258 return None 259 260 261def GenerateXcodeProject(gn_path, root_dir, out_dir, settings): 262 '''Convert GN generated Xcode project into multi-configuration Xcode 263 project.''' 264 265 temp_path = tempfile.mkdtemp(prefix=os.path.abspath( 266 os.path.join(out_dir, '_temp'))) 267 try: 268 generator = GnGenerator(settings, 'Debug', 'iphonesimulator') 269 generator.Generate(gn_path, root_dir, temp_path) 270 convert_gn_xcodeproj.ConvertGnXcodeProject( 271 root_dir, 272 os.path.join(temp_path), 273 os.path.join(out_dir, 'build'), 274 SUPPORTED_CONFIGS) 275 finally: 276 if os.path.exists(temp_path): 277 shutil.rmtree(temp_path) 278 279 280def GenerateGnBuildRules(gn_path, root_dir, out_dir, settings): 281 '''Generates all template configurations for gn.''' 282 for config in SUPPORTED_CONFIGS: 283 for target in SUPPORTED_TARGETS: 284 build_dir = os.path.join(out_dir, '%s-%s' % (config, target)) 285 generator = GnGenerator(settings, config, target) 286 generator.CreateGnRules(gn_path, root_dir, build_dir) 287 288 289def Main(args): 290 default_root = os.path.normpath(os.path.join( 291 os.path.dirname(__file__), os.pardir, os.pardir)) 292 293 parser = argparse.ArgumentParser( 294 description='Generate build directories for use with gn.') 295 parser.add_argument( 296 'root', default=default_root, nargs='?', 297 help='root directory where to generate multiple out configurations') 298 parser.add_argument( 299 '--import', action='append', dest='import_rules', default=[], 300 help='path to file defining default gn variables') 301 args = parser.parse_args(args) 302 303 # Load configuration (first global and then any user overrides). 304 settings = ConfigParserWithStringInterpolation() 305 settings.read([ 306 os.path.splitext(__file__)[0] + '.config', 307 os.path.expanduser('~/.setup-gn'), 308 ]) 309 310 # Add private sections corresponding to --import argument. 311 if args.import_rules: 312 settings.add_section('$imports$') 313 for i, import_rule in enumerate(args.import_rules): 314 if not import_rule.startswith('//'): 315 import_rule = '//%s' % os.path.relpath( 316 os.path.abspath(import_rule), os.path.abspath(args.root)) 317 settings.set('$imports$', '$rule%d$' % i, import_rule) 318 319 # Validate settings. 320 if settings.getstring('build', 'arch') not in ('64-bit', '32-bit', 'fat'): 321 sys.stderr.write('ERROR: invalid value for build.arch: %s\n' % 322 settings.getstring('build', 'arch')) 323 sys.exit(1) 324 325 if settings.getboolean('goma', 'enabled'): 326 if settings.getint('xcode', 'jobs') < 0: 327 sys.stderr.write('ERROR: invalid value for xcode.jobs: %s\n' % 328 settings.get('xcode', 'jobs')) 329 sys.exit(1) 330 goma_install = os.path.expanduser(settings.getstring('goma', 'install')) 331 if not os.path.isdir(goma_install): 332 sys.stderr.write('WARNING: goma.install directory not found: %s\n' % 333 settings.get('goma', 'install')) 334 sys.stderr.write('WARNING: disabling goma\n') 335 settings.set('goma', 'enabled', 'false') 336 337 # Find gn binary in PATH. 338 gn_path = FindGn() 339 if gn_path is None: 340 sys.stderr.write('ERROR: cannot find gn in PATH\n') 341 sys.exit(1) 342 343 out_dir = os.path.join(args.root, 'out') 344 if not os.path.isdir(out_dir): 345 os.makedirs(out_dir) 346 347 GenerateXcodeProject(gn_path, args.root, out_dir, settings) 348 GenerateGnBuildRules(gn_path, args.root, out_dir, settings) 349 350 351if __name__ == '__main__': 352 sys.exit(Main(sys.argv[1:])) 353