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:])) |
+ |
+ |