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