| #!/usr/bin/env python |
| |
| # Copyright 2020 The Crashpad Authors. All rights reserved. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import argparse |
| import convert_gn_xcodeproj |
| import errno |
| import os |
| import re |
| import shutil |
| import subprocess |
| import sys |
| import tempfile |
| |
| try: |
| import configparser |
| except ImportError: |
| import ConfigParser as configparser |
| |
| try: |
| import StringIO as io |
| except ImportError: |
| import io |
| |
| SUPPORTED_TARGETS = ('iphoneos', 'iphonesimulator') |
| SUPPORTED_CONFIGS = ('Debug', 'Release', 'Profile', 'Official', 'Coverage') |
| |
| |
| class ConfigParserWithStringInterpolation(configparser.SafeConfigParser): |
| '''A .ini file parser that supports strings and environment variables.''' |
| |
| ENV_VAR_PATTERN = re.compile(r'\$([A-Za-z0-9_]+)') |
| |
| def values(self, section): |
| return map(lambda kv: self._UnquoteString(self._ExpandEnvVar(kv[1])), |
| configparser.ConfigParser.items(self, section)) |
| |
| def getstring(self, section, option): |
| return self._UnquoteString(self._ExpandEnvVar(self.get(section, |
| option))) |
| |
| def _UnquoteString(self, string): |
| if not string or string[0] != '"' or string[-1] != '"': |
| return string |
| return string[1:-1] |
| |
| def _ExpandEnvVar(self, value): |
| match = self.ENV_VAR_PATTERN.search(value) |
| if not match: |
| return value |
| name, (begin, end) = match.group(1), match.span(0) |
| prefix, suffix = value[:begin], self._ExpandEnvVar(value[end:]) |
| return prefix + os.environ.get(name, '') + suffix |
| |
| |
| class GnGenerator(object): |
| '''Holds configuration for a build and method to generate gn default |
| files.''' |
| |
| FAT_BUILD_DEFAULT_ARCH = '64-bit' |
| |
| TARGET_CPU_VALUES = { |
| 'iphoneos': { |
| '32-bit': '"arm"', |
| '64-bit': '"arm64"', |
| }, |
| 'iphonesimulator': { |
| '32-bit': '"x86"', |
| '64-bit': '"x64"', |
| } |
| } |
| |
| def __init__(self, settings, config, target): |
| assert target in SUPPORTED_TARGETS |
| assert config in SUPPORTED_CONFIGS |
| self._settings = settings |
| self._config = config |
| self._target = target |
| |
| def _GetGnArgs(self): |
| """Build the list of arguments to pass to gn. |
| |
| Returns: |
| A list of tuple containing gn variable names and variable values (it |
| is not a dictionary as the order needs to be preserved). |
| """ |
| args = [] |
| |
| args.append(('is_debug', self._config in ('Debug', 'Coverage'))) |
| |
| if os.environ.get('FORCE_MAC_TOOLCHAIN', '0') == '1': |
| args.append(('use_system_xcode', False)) |
| |
| cpu_values = self.TARGET_CPU_VALUES[self._target] |
| build_arch = self._settings.getstring('build', 'arch') |
| if build_arch == 'fat': |
| target_cpu = cpu_values[self.FAT_BUILD_DEFAULT_ARCH] |
| args.append(('target_cpu', target_cpu)) |
| args.append( |
| ('additional_target_cpus', |
| [cpu for cpu in cpu_values.itervalues() if cpu != target_cpu])) |
| else: |
| args.append(('target_cpu', cpu_values[build_arch])) |
| |
| # Add user overrides after the other configurations so that they can |
| # refer to them and override them. |
| args.extend(self._settings.items('gn_args')) |
| return args |
| |
| def Generate(self, gn_path, root_path, out_path): |
| buf = io.StringIO() |
| self.WriteArgsGn(buf) |
| WriteToFileIfChanged(os.path.join(out_path, 'args.gn'), |
| buf.getvalue(), |
| overwrite=True) |
| |
| subprocess.check_call( |
| self.GetGnCommand(gn_path, root_path, out_path, True)) |
| |
| def CreateGnRules(self, gn_path, root_path, out_path): |
| buf = io.StringIO() |
| self.WriteArgsGn(buf) |
| WriteToFileIfChanged(os.path.join(out_path, 'args.gn'), |
| buf.getvalue(), |
| overwrite=True) |
| |
| buf = io.StringIO() |
| gn_command = self.GetGnCommand(gn_path, root_path, out_path, False) |
| self.WriteBuildNinja(buf, gn_command) |
| WriteToFileIfChanged(os.path.join(out_path, 'build.ninja'), |
| buf.getvalue(), |
| overwrite=False) |
| |
| buf = io.StringIO() |
| self.WriteBuildNinjaDeps(buf) |
| WriteToFileIfChanged(os.path.join(out_path, 'build.ninja.d'), |
| buf.getvalue(), |
| overwrite=False) |
| |
| def WriteArgsGn(self, stream): |
| stream.write('# This file was generated by setup-gn.py. Do not edit\n') |
| stream.write('# but instead use ~/.setup-gn or $repo/.setup-gn files\n') |
| stream.write('# to configure settings.\n') |
| stream.write('\n') |
| |
| if self._settings.has_section('$imports$'): |
| for import_rule in self._settings.values('$imports$'): |
| stream.write('import("%s")\n' % import_rule) |
| stream.write('\n') |
| |
| gn_args = self._GetGnArgs() |
| for name, value in gn_args: |
| if isinstance(value, bool): |
| stream.write('%s = %s\n' % (name, str(value).lower())) |
| elif isinstance(value, list): |
| stream.write('%s = [%s' % |
| (name, '\n' if len(value) > 1 else '')) |
| if len(value) == 1: |
| prefix = ' ' |
| suffix = ' ' |
| else: |
| prefix = ' ' |
| suffix = ',\n' |
| for item in value: |
| if isinstance(item, bool): |
| stream.write('%s%s%s' % |
| (prefix, str(item).lower(), suffix)) |
| else: |
| stream.write('%s%s%s' % (prefix, item, suffix)) |
| stream.write(']\n') |
| else: |
| stream.write('%s = %s\n' % (name, value)) |
| |
| def WriteBuildNinja(self, stream, gn_command): |
| stream.write('rule gn\n') |
| stream.write(' command = %s\n' % NinjaEscapeCommand(gn_command)) |
| stream.write(' description = Regenerating ninja files\n') |
| stream.write('\n') |
| stream.write('build build.ninja: gn\n') |
| stream.write(' generator = 1\n') |
| stream.write(' depfile = build.ninja.d\n') |
| |
| def WriteBuildNinjaDeps(self, stream): |
| stream.write('build.ninja: nonexistant_file.gn\n') |
| |
| def GetGnCommand(self, gn_path, src_path, out_path, generate_xcode_project): |
| gn_command = [gn_path, '--root=%s' % os.path.realpath(src_path), '-q'] |
| if generate_xcode_project: |
| gn_command.append('--ide=xcode') |
| gn_command.append('--ninja-executable=autoninja') |
| if self._settings.has_section('filters'): |
| target_filters = self._settings.values('filters') |
| if target_filters: |
| gn_command.append('--filters=%s' % ';'.join(target_filters)) |
| else: |
| gn_command.append('--check') |
| gn_command.append('gen') |
| gn_command.append('//%s' % os.path.relpath(os.path.abspath(out_path), |
| os.path.abspath(src_path))) |
| return gn_command |
| |
| |
| def WriteToFileIfChanged(filename, content, overwrite): |
| '''Write |content| to |filename| if different. If |overwrite| is False |
| and the file already exists it is left untouched.''' |
| if os.path.exists(filename): |
| if not overwrite: |
| return |
| with open(filename) as file: |
| if file.read() == content: |
| return |
| if not os.path.isdir(os.path.dirname(filename)): |
| os.makedirs(os.path.dirname(filename)) |
| with open(filename, 'w') as file: |
| file.write(content) |
| |
| |
| def NinjaNeedEscape(arg): |
| '''Returns True if |arg| needs to be escaped when written to .ninja file.''' |
| return ':' in arg or '*' in arg or ';' in arg |
| |
| |
| def NinjaEscapeCommand(command): |
| '''Escapes |command| in order to write it to .ninja file.''' |
| result = [] |
| for arg in command: |
| if NinjaNeedEscape(arg): |
| arg = arg.replace(':', '$:') |
| arg = arg.replace(';', '\\;') |
| arg = arg.replace('*', '\\*') |
| else: |
| result.append(arg) |
| return ' '.join(result) |
| |
| |
| def FindGn(): |
| '''Returns absolute path to gn binary looking at the PATH env variable.''' |
| for path in os.environ['PATH'].split(os.path.pathsep): |
| gn_path = os.path.join(path, 'gn') |
| if os.path.isfile(gn_path) and os.access(gn_path, os.X_OK): |
| return gn_path |
| return None |
| |
| |
| def GenerateXcodeProject(gn_path, root_dir, out_dir, settings): |
| '''Convert GN generated Xcode project into multi-configuration Xcode |
| project.''' |
| |
| temp_path = tempfile.mkdtemp( |
| prefix=os.path.abspath(os.path.join(out_dir, '_temp'))) |
| try: |
| generator = GnGenerator(settings, 'Debug', 'iphonesimulator') |
| generator.Generate(gn_path, root_dir, temp_path) |
| convert_gn_xcodeproj.ConvertGnXcodeProject( |
| root_dir, os.path.join(temp_path), os.path.join(out_dir, 'build'), |
| SUPPORTED_CONFIGS) |
| finally: |
| if os.path.exists(temp_path): |
| shutil.rmtree(temp_path) |
| |
| |
| def GenerateGnBuildRules(gn_path, root_dir, out_dir, settings): |
| '''Generates all template configurations for gn.''' |
| for config in SUPPORTED_CONFIGS: |
| for target in SUPPORTED_TARGETS: |
| build_dir = os.path.join(out_dir, '%s-%s' % (config, target)) |
| generator = GnGenerator(settings, config, target) |
| generator.CreateGnRules(gn_path, root_dir, build_dir) |
| |
| |
| def Main(args): |
| default_root = os.path.normpath( |
| os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) |
| |
| parser = argparse.ArgumentParser( |
| description='Generate build directories for use with gn.') |
| parser.add_argument( |
| 'root', |
| default=default_root, |
| nargs='?', |
| help='root directory where to generate multiple out configurations') |
| parser.add_argument('--import', |
| action='append', |
| dest='import_rules', |
| default=[], |
| help='path to file defining default gn variables') |
| parser.add_argument('--gn-path', |
| default=None, |
| help='path to gn binary (default: look up in $PATH)') |
| parser.add_argument( |
| '--build-dir', |
| default='out', |
| help='path where the build should be created (default: %(default)s)') |
| args = parser.parse_args(args) |
| |
| # Load configuration (first global and then any user overrides). |
| settings = ConfigParserWithStringInterpolation() |
| settings.read([ |
| os.path.splitext(__file__)[0] + '.config', |
| os.path.expanduser('~/.setup-gn'), |
| ]) |
| |
| # Add private sections corresponding to --import argument. |
| if args.import_rules: |
| settings.add_section('$imports$') |
| for i, import_rule in enumerate(args.import_rules): |
| if not import_rule.startswith('//'): |
| import_rule = '//%s' % os.path.relpath( |
| os.path.abspath(import_rule), os.path.abspath(args.root)) |
| settings.set('$imports$', '$rule%d$' % i, import_rule) |
| |
| # Validate settings. |
| if settings.getstring('build', 'arch') not in ('64-bit', '32-bit', 'fat'): |
| sys.stderr.write('ERROR: invalid value for build.arch: %s\n' % |
| settings.getstring('build', 'arch')) |
| sys.exit(1) |
| |
| # Find path to gn binary either from command-line or in PATH. |
| if args.gn_path: |
| gn_path = args.gn_path |
| else: |
| gn_path = FindGn() |
| if gn_path is None: |
| sys.stderr.write('ERROR: cannot find gn in PATH\n') |
| sys.exit(1) |
| |
| out_dir = os.path.join(args.root, args.build_dir) |
| if not os.path.isdir(out_dir): |
| os.makedirs(out_dir) |
| |
| GenerateXcodeProject(gn_path, args.root, out_dir, settings) |
| GenerateGnBuildRules(gn_path, args.root, out_dir, settings) |
| |
| |
| if __name__ == '__main__': |
| sys.exit(Main(sys.argv[1:])) |