From 77baffaf931b3e1c21cdc0e0f32aadd9856ea4eb Mon Sep 17 00:00:00 2001 From: Justin Cohen Date: Tue, 28 Jan 2020 15:34:34 -0500 Subject: [PATCH] Add iOS gn configs. Allows more native iOS development with Xcode by auto generating all the various configs during runhooks. Change-Id: I840001caabc7ef656c3145b847cee5596335aa23 Reviewed-on: https://chromium-review.googlesource.com/c/crashpad/crashpad/+/2024186 Reviewed-by: Rohit Rao Reviewed-by: Mark Mentovai Commit-Queue: Justin Cohen --- DEPS | 15 +- build/ios/convert_gn_xcodeproj.py | 273 +++++++++++++++++++++++ build/ios/setup-ios-gn.config | 39 ++++ build/ios/setup-ios-gn.py | 352 ++++++++++++++++++++++++++++++ 4 files changed, 678 insertions(+), 1 deletion(-) create mode 100644 build/ios/convert_gn_xcodeproj.py create mode 100644 build/ios/setup-ios-gn.config create mode 100644 build/ios/setup-ios-gn.py diff --git a/DEPS b/DEPS index a20b19c0..2e93b810 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,11 @@ vars = { 'chromium_git': 'https://chromium.googlesource.com', 'pull_linux_clang': False, - 'pull_win_toolchain': False + 'pull_win_toolchain': False, + # Controls whether crashpad/build/ios/setup-ios-gn.py is run as part of + # gclient hooks. It is enabled by default for developer's convenience. It can + # be disabled with custom_vars (done automatically on the bots). + 'run_setup_ios_gn': True, } deps = { @@ -217,6 +221,15 @@ hooks = [ 'crashpad/build/install_linux_sysroot.py', ], }, + { + 'name': 'setup_gn_ios', + 'pattern': '.', + 'condition': 'run_setup_ios_gn and checkout_ios', + 'action': [ + 'python', + 'crashpad/build/ios/setup-ios-gn.py' + ], + }, ] recursedeps = [ diff --git a/build/ios/convert_gn_xcodeproj.py b/build/ios/convert_gn_xcodeproj.py new file mode 100644 index 00000000..b41d04ec --- /dev/null +++ b/build/ios/convert_gn_xcodeproj.py @@ -0,0 +1,273 @@ +#!/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. + +"""Convert GN Xcode projects to platform and configuration independent targets. + +GN generates Xcode projects that build one configuration only. However, typical +iOS development involves using the Xcode IDE to toggle the platform and +configuration. This script replaces the 'gn' configuration with 'Debug', +'Release' and 'Profile', and changes the ninja invocation to honor these +configurations. +""" + +import argparse +import collections +import copy +import filecmp +import json +import hashlib +import os +import plistlib +import random +import shutil +import subprocess +import sys +import tempfile + + +class XcodeProject(object): + + def __init__(self, objects, counter = 0): + self.objects = objects + self.counter = 0 + + def AddObject(self, parent_name, obj): + while True: + self.counter += 1 + str_id = "%s %s %d" % (parent_name, obj['isa'], self.counter) + new_id = hashlib.sha1(str_id).hexdigest()[:24].upper() + + # Make sure ID is unique. It's possible there could be an id conflict + # since this is run after GN runs. + if new_id not in self.objects: + self.objects[new_id] = obj + return new_id + + +def CopyFileIfChanged(source_path, target_path): + """Copy |source_path| to |target_path| is different.""" + target_dir = os.path.dirname(target_path) + if not os.path.isdir(target_dir): + os.makedirs(target_dir) + if not os.path.exists(target_path) or \ + not filecmp.cmp(source_path, target_path): + shutil.copyfile(source_path, target_path) + + +def LoadXcodeProjectAsJSON(path): + """Return Xcode project at |path| as a JSON string.""" + return subprocess.check_output([ + 'plutil', '-convert', 'json', '-o', '-', path]) + + +def WriteXcodeProject(output_path, json_string): + """Save Xcode project to |output_path| as XML.""" + with tempfile.NamedTemporaryFile() as temp_file: + temp_file.write(json_string) + temp_file.flush() + subprocess.check_call(['plutil', '-convert', 'xml1', temp_file.name]) + CopyFileIfChanged(temp_file.name, output_path) + + +def UpdateProductsProject(file_input, file_output, configurations, root_dir): + """Update Xcode project to support multiple configurations. + + Args: + file_input: path to the input Xcode project + file_output: path to the output file + configurations: list of string corresponding to the configurations that + need to be supported by the tweaked Xcode projects, must contains at + least one value. + """ + json_data = json.loads(LoadXcodeProjectAsJSON(file_input)) + project = XcodeProject(json_data['objects']) + + objects_to_remove = [] + for value in project.objects.values(): + isa = value['isa'] + + # Teach build shell script to look for the configuration and platform. + if isa == 'PBXShellScriptBuildPhase': + value['shellScript'] = value['shellScript'].replace( + 'ninja -C .', + 'ninja -C "../${CONFIGURATION}${EFFECTIVE_PLATFORM_NAME}"') + + # Add new configuration, using the first one as default. + if isa == 'XCConfigurationList': + value['defaultConfigurationName'] = configurations[0] + objects_to_remove.extend(value['buildConfigurations']) + + build_config_template = project.objects[value['buildConfigurations'][0]] + build_config_template['buildSettings']['CONFIGURATION_BUILD_DIR'] = \ + '$(PROJECT_DIR)/../$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)' + build_config_template['buildSettings']['CODE_SIGN_IDENTITY'] = '' + + value['buildConfigurations'] = [] + for configuration in configurations: + new_build_config = copy.copy(build_config_template) + new_build_config['name'] = configuration + value['buildConfigurations'].append( + project.AddObject('products', new_build_config)) + + for object_id in objects_to_remove: + del project.objects[object_id] + + AddMarkdownToProject(project, root_dir, json_data['rootObject']) + + objects = collections.OrderedDict(sorted(project.objects.iteritems())) + WriteXcodeProject(file_output, json.dumps(json_data)) + + +def AddMarkdownToProject(project, root_dir, root_object): + list_files_cmd = ['git', '-C', root_dir, 'ls-files', '*.md'] + paths = subprocess.check_output(list_files_cmd).splitlines() + ios_internal_dir = os.path.join(root_dir, 'ios_internal') + if os.path.exists(ios_internal_dir): + list_files_cmd = ['git', '-C', ios_internal_dir, 'ls-files', '*.md'] + ios_paths = subprocess.check_output(list_files_cmd).splitlines() + paths.extend(["ios_internal/" + path for path in ios_paths]) + for path in paths: + new_markdown_entry = { + "fileEncoding": "4", + "isa": "PBXFileReference", + "lastKnownFileType": "net.daringfireball.markdown", + "name": os.path.basename(path), + "path": path, + "sourceTree": "" + } + new_markdown_entry_id = project.AddObject('sources', new_markdown_entry) + folder = GetFolderForPath(project, root_object, os.path.dirname(path)) + folder['children'].append(new_markdown_entry_id) + + +def GetFolderForPath(project, rootObject, path): + objects = project.objects + # 'Sources' is always the first child of + # project->rootObject->mainGroup->children. + root = objects[objects[objects[rootObject]['mainGroup']]['children'][0]] + if not path: + return root + for folder in path.split('/'): + children = root['children'] + new_root = None + for child in children: + if objects[child]['isa'] == 'PBXGroup' and \ + objects[child]['name'] == folder: + new_root = objects[child] + break + if not new_root: + # If the folder isn't found we could just cram it into the leaf existing + # folder, but that leads to folders with tons of README.md inside. + new_group = { + "children": [ + ], + "isa": "PBXGroup", + "name": folder, + "sourceTree": "" + } + new_group_id = project.AddObject('sources', new_group) + children.append(new_group_id) + new_root = objects[new_group_id] + root = new_root + return root + + +def DisableNewBuildSystem(output_dir): + """Disables the new build system due to crbug.com/852522 """ + xcwspacesharedsettings = os.path.join(output_dir, 'all.xcworkspace', + 'xcshareddata', 'WorkspaceSettings.xcsettings') + if os.path.isfile(xcwspacesharedsettings): + json_data = json.loads(LoadXcodeProjectAsJSON(xcwspacesharedsettings)) + else: + json_data = {} + json_data['BuildSystemType'] = 'Original' + WriteXcodeProject(xcwspacesharedsettings, json.dumps(json_data)) + + +def ConvertGnXcodeProject(root_dir, input_dir, output_dir, configurations): + '''Tweak the Xcode project generated by gn to support multiple configurations. + + The Xcode projects generated by "gn gen --ide" only supports a single + platform and configuration (as the platform and configuration are set + per output directory). This method takes as input such projects and + add support for multiple configurations and platforms (to allow devs + to select them in Xcode). + + Args: + input_dir: directory containing the XCode projects created by "gn gen --ide" + output_dir: directory where the tweaked Xcode projects will be saved + configurations: list of string corresponding to the configurations that + need to be supported by the tweaked Xcode projects, must contains at + least one value. + ''' + # Update products project. + products = os.path.join('products.xcodeproj', 'project.pbxproj') + product_input = os.path.join(input_dir, products) + product_output = os.path.join(output_dir, products) + UpdateProductsProject(product_input, product_output, configurations, root_dir) + + # Copy all workspace. + xcwspace = os.path.join('all.xcworkspace', 'contents.xcworkspacedata') + CopyFileIfChanged(os.path.join(input_dir, xcwspace), + os.path.join(output_dir, xcwspace)) + + # TODO(crbug.com/852522): Disable new BuildSystemType. + DisableNewBuildSystem(output_dir) + + # TODO(crbug.com/679110): gn has been modified to remove 'sources.xcodeproj' + # and keep 'all.xcworkspace' and 'products.xcodeproj'. The following code is + # here to support both old and new projects setup and will be removed once gn + # has rolled past it. + sources = os.path.join('sources.xcodeproj', 'project.pbxproj') + if os.path.isfile(os.path.join(input_dir, sources)): + CopyFileIfChanged(os.path.join(input_dir, sources), + os.path.join(output_dir, sources)) + +def Main(args): + parser = argparse.ArgumentParser( + description='Convert GN Xcode projects for iOS.') + parser.add_argument( + 'input', + help='directory containing [product|all] Xcode projects.') + parser.add_argument( + 'output', + help='directory where to generate the iOS configuration.') + parser.add_argument( + '--add-config', dest='configurations', default=[], action='append', + help='configuration to add to the Xcode project') + parser.add_argument( + '--root', type=os.path.abspath, required=True, + help='root directory of the project') + args = parser.parse_args(args) + + if not os.path.isdir(args.input): + sys.stderr.write('Input directory does not exists.\n') + return 1 + + required = set(['products.xcodeproj', 'all.xcworkspace']) + if not required.issubset(os.listdir(args.input)): + sys.stderr.write( + 'Input directory does not contain all necessary Xcode projects.\n') + return 1 + + if not args.configurations: + sys.stderr.write('At least one configuration required, see --add-config.\n') + return 1 + + ConvertGnXcodeProject(args.root, args.input, args.output, args.configurations) + +if __name__ == '__main__': + sys.exit(Main(sys.argv[1:])) diff --git a/build/ios/setup-ios-gn.config b/build/ios/setup-ios-gn.config new file mode 100644 index 00000000..30c31115 --- /dev/null +++ b/build/ios/setup-ios-gn.config @@ -0,0 +1,39 @@ +# 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. + +[goma] +# Controls whether goma is enabled or not. If you generally use goma but +# want to disable goma for a single build, consider using the environment +# variable GOMA_DISABLED. +enabled = False +install = "$GOMA_DIR" + +[xcode] +# Controls settings for the generated Xcode project. If jobs is non-zero +# it will be passed to the ninja invocation in Xcode project. +jobs = 0 + +[build] +# Controls the build output. The only supported values are "64-bit", "32-bit" +# and "fat" (for a fat binary supporting both "32-bit" and "64-bit" cpus). +arch = "64-bit" + +[gn_args] +# Values in that section will be copied verbatim in the generated args.gn file. +target_os = "ios" + +[filters] +# List of target files to pass to --filters argument of gn gen when generating +# the Xcode project. By default, list all targets from ios/ and ios_internal/ +# and the targets corresponding to the unit tests run on the bots. diff --git a/build/ios/setup-ios-gn.py b/build/ios/setup-ios-gn.py new file mode 100644 index 00000000..995e3c84 --- /dev/null +++ b/build/ios/setup-ios-gn.py @@ -0,0 +1,352 @@ +#!/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 +import ConfigParser + +try: + import cStringIO as StringIO +except ImportError: + import StringIO + + +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 (k, v): self._UnquoteString(self._ExpandEnvVar(v)), + ConfigParser.SafeConfigParser.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 = StringIO.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 = StringIO.StringIO() + self.WriteArgsGn(buf) + WriteToFileIfChanged( + os.path.join(out_path, 'args.gn'), + buf.getvalue(), + overwrite=True) + + buf = StringIO.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 = StringIO.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('--root-target=gn_all') + if self._settings.getboolean('goma', 'enabled'): + ninja_jobs = self._settings.getint('xcode', 'jobs') or 200 + gn_command.append('--ninja-extra-args=-j%s' % ninja_jobs) + if self._settings.has_section('filters'): + target_filters = self._settings.values('filters') + if target_filters: + gn_command.append('--filters=%s' % ';'.join(target_filters)) + # TODO(justincohen): --check is currently failing in crashpad. + # 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') + 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) + + if settings.getboolean('goma', 'enabled'): + if settings.getint('xcode', 'jobs') < 0: + sys.stderr.write('ERROR: invalid value for xcode.jobs: %s\n' % + settings.get('xcode', 'jobs')) + sys.exit(1) + goma_install = os.path.expanduser(settings.getstring('goma', 'install')) + if not os.path.isdir(goma_install): + sys.stderr.write('WARNING: goma.install directory not found: %s\n' % + settings.get('goma', 'install')) + sys.stderr.write('WARNING: disabling goma\n') + settings.set('goma', 'enabled', 'false') + + # Find gn binary in PATH. + 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, 'out') + 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:]))