| Index: ios/build/tools/convert_gn_xcodeproj.py
|
| diff --git a/ios/build/tools/convert_gn_xcodeproj.py b/ios/build/tools/convert_gn_xcodeproj.py
|
| new file mode 100755
|
| index 0000000000000000000000000000000000000000..fa493565edad2f471a3bf4ffb166023adfe62d53
|
| --- /dev/null
|
| +++ b/ios/build/tools/convert_gn_xcodeproj.py
|
| @@ -0,0 +1,214 @@
|
| +#!/usr/bin/python
|
| +# Copyright 2016 The Chromium Authors. All rights reserved.
|
| +# Use of this source code is governed by a BSD-style license that can be
|
| +# found in the LICENSE file.
|
| +
|
| +"""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 invokation 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
|
| +
|
| +
|
| +XCTEST_PRODUCT_TYPE = 'com.apple.product-type.bundle.unit-test'
|
| +
|
| +
|
| +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):
|
| + """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']
|
| +
|
| + # TODO(crbug.com/619072): gn does not write the min deployment target in the
|
| + # generated Xcode project, so add it while doing the conversion, only if it
|
| + # is not present. Remove this code and comment once the bug is fixed and gn
|
| + # has rolled past it.
|
| + if isa == 'XCBuildConfiguration':
|
| + build_settings = value['buildSettings']
|
| + if 'IPHONEOS_DEPLOYMENT_TARGET' not in build_settings:
|
| + build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '9.0'
|
| +
|
| + # Remove path name key and change path to basename.
|
| + if isa == 'PBXFileReference':
|
| + if 'name' in value:
|
| + del value['name']
|
| + value['path'] = os.path.basename(value['path'])
|
| +
|
| + # 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}"')
|
| +
|
| + # Configure BUNDLE_LOADER and TEST_HOST for xctest target (assuming that
|
| + # the host is named "${target}_host") unless gn has already configured
|
| + # them.
|
| + if isa == 'PBXNativeTarget' and value['productType'] == XCTEST_PRODUCT_TYPE:
|
| + configuration_list = project.objects[value['buildConfigurationList']]
|
| + for config_name in configuration_list['buildConfigurations']:
|
| + config = project.objects[config_name]
|
| + if not config['buildSettings'].get('BUNDLE_LOADER'):
|
| + config['buildSettings']['BUNDLE_LOADER'] = '$(TEST_HOST)'
|
| + config['buildSettings']['TEST_HOST'] = \
|
| + '${BUILT_PRODUCTS_DIR}/%(name)s_host.app/%(name)s' % value
|
| +
|
| + # 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'] = \
|
| + '../$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)'
|
| +
|
| + 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]
|
| +
|
| + objects = collections.OrderedDict(sorted(project.objects.iteritems()))
|
| + WriteXcodeProject(file_output, json.dumps(json_data))
|
| +
|
| +
|
| +def ConvertGnXcodeProject(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)
|
| +
|
| + # Copy sources project and all workspace.
|
| + sources = os.path.join('sources.xcodeproj', 'project.pbxproj')
|
| + CopyFileIfChanged(os.path.join(input_dir, sources),
|
| + os.path.join(output_dir, sources))
|
| + xcwspace = os.path.join('all.xcworkspace', 'contents.xcworkspacedata')
|
| + CopyFileIfChanged(os.path.join(input_dir, xcwspace),
|
| + os.path.join(output_dir, xcwspace))
|
| +
|
| +
|
| +def Main(args):
|
| + parser = argparse.ArgumentParser(
|
| + description='Convert GN Xcode projects for iOS.')
|
| + parser.add_argument(
|
| + 'input',
|
| + help='directory containing [product|sources|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')
|
| + 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', 'sources.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.input, args.output, args.configurations)
|
| +
|
| +if __name__ == '__main__':
|
| + sys.exit(Main(sys.argv[1:]))
|
| +
|
| +
|
|
|