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 """Convert GN Xcode projects to platform and configuration independent targets. |
| 7 |
| 8 GN generates Xcode projects that build one configuration only. However, typical |
| 9 iOS development involves using the Xcode IDE to toggle the platform and |
| 10 configuration. This script replaces the 'gn' configuration with 'Debug', |
| 11 'Release' and 'Profile', and changes the ninja invokation to honor these |
| 12 configurations. |
| 13 """ |
| 14 |
| 15 import argparse |
| 16 import collections |
| 17 import copy |
| 18 import filecmp |
| 19 import json |
| 20 import hashlib |
| 21 import os |
| 22 import plistlib |
| 23 import random |
| 24 import shutil |
| 25 import subprocess |
| 26 import sys |
| 27 import tempfile |
| 28 |
| 29 |
| 30 XCTEST_PRODUCT_TYPE = 'com.apple.product-type.bundle.unit-test' |
| 31 |
| 32 |
| 33 class XcodeProject(object): |
| 34 |
| 35 def __init__(self, objects, counter = 0): |
| 36 self.objects = objects |
| 37 self.counter = 0 |
| 38 |
| 39 def AddObject(self, parent_name, obj): |
| 40 while True: |
| 41 self.counter += 1 |
| 42 str_id = "%s %s %d" % (parent_name, obj['isa'], self.counter) |
| 43 new_id = hashlib.sha1(str_id).hexdigest()[:24].upper() |
| 44 |
| 45 # Make sure ID is unique. It's possible there could be an id conflict |
| 46 # since this is run after GN runs. |
| 47 if new_id not in self.objects: |
| 48 self.objects[new_id] = obj |
| 49 return new_id |
| 50 |
| 51 |
| 52 def CopyFileIfChanged(source_path, target_path): |
| 53 """Copy |source_path| to |target_path| is different.""" |
| 54 target_dir = os.path.dirname(target_path) |
| 55 if not os.path.isdir(target_dir): |
| 56 os.makedirs(target_dir) |
| 57 if not os.path.exists(target_path) or \ |
| 58 not filecmp.cmp(source_path, target_path): |
| 59 shutil.copyfile(source_path, target_path) |
| 60 |
| 61 |
| 62 def LoadXcodeProjectAsJSON(path): |
| 63 """Return Xcode project at |path| as a JSON string.""" |
| 64 return subprocess.check_output([ |
| 65 'plutil', '-convert', 'json', '-o', '-', path]) |
| 66 |
| 67 |
| 68 def WriteXcodeProject(output_path, json_string): |
| 69 """Save Xcode project to |output_path| as XML.""" |
| 70 with tempfile.NamedTemporaryFile() as temp_file: |
| 71 temp_file.write(json_string) |
| 72 temp_file.flush() |
| 73 subprocess.check_call(['plutil', '-convert', 'xml1', temp_file.name]) |
| 74 CopyFileIfChanged(temp_file.name, output_path) |
| 75 |
| 76 |
| 77 def UpdateProductsProject(file_input, file_output, configurations): |
| 78 """Update Xcode project to support multiple configurations. |
| 79 |
| 80 Args: |
| 81 file_input: path to the input Xcode project |
| 82 file_output: path to the output file |
| 83 configurations: list of string corresponding to the configurations that |
| 84 need to be supported by the tweaked Xcode projects, must contains at |
| 85 least one value. |
| 86 """ |
| 87 json_data = json.loads(LoadXcodeProjectAsJSON(file_input)) |
| 88 project = XcodeProject(json_data['objects']) |
| 89 |
| 90 objects_to_remove = [] |
| 91 for value in project.objects.values(): |
| 92 isa = value['isa'] |
| 93 |
| 94 # TODO(crbug.com/619072): gn does not write the min deployment target in the |
| 95 # generated Xcode project, so add it while doing the conversion, only if it |
| 96 # is not present. Remove this code and comment once the bug is fixed and gn |
| 97 # has rolled past it. |
| 98 if isa == 'XCBuildConfiguration': |
| 99 build_settings = value['buildSettings'] |
| 100 if 'IPHONEOS_DEPLOYMENT_TARGET' not in build_settings: |
| 101 build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '9.0' |
| 102 |
| 103 # Remove path name key and change path to basename. |
| 104 if isa == 'PBXFileReference': |
| 105 if 'name' in value: |
| 106 del value['name'] |
| 107 value['path'] = os.path.basename(value['path']) |
| 108 |
| 109 # Teach build shell script to look for the configuration and platform. |
| 110 if isa == 'PBXShellScriptBuildPhase': |
| 111 value['shellScript'] = value['shellScript'].replace( |
| 112 'ninja -C .', |
| 113 'ninja -C "../${CONFIGURATION}${EFFECTIVE_PLATFORM_NAME}"') |
| 114 |
| 115 # Configure BUNDLE_LOADER and TEST_HOST for xctest target (assuming that |
| 116 # the host is named "${target}_host") unless gn has already configured |
| 117 # them. |
| 118 if isa == 'PBXNativeTarget' and value['productType'] == XCTEST_PRODUCT_TYPE: |
| 119 configuration_list = project.objects[value['buildConfigurationList']] |
| 120 for config_name in configuration_list['buildConfigurations']: |
| 121 config = project.objects[config_name] |
| 122 if not config['buildSettings'].get('BUNDLE_LOADER'): |
| 123 config['buildSettings']['BUNDLE_LOADER'] = '$(TEST_HOST)' |
| 124 config['buildSettings']['TEST_HOST'] = \ |
| 125 '${BUILT_PRODUCTS_DIR}/%(name)s_host.app/%(name)s' % value |
| 126 |
| 127 # Add new configuration, using the first one as default. |
| 128 if isa == 'XCConfigurationList': |
| 129 value['defaultConfigurationName'] = configurations[0] |
| 130 objects_to_remove.extend(value['buildConfigurations']) |
| 131 |
| 132 build_config_template = project.objects[value['buildConfigurations'][0]] |
| 133 build_config_template['buildSettings']['CONFIGURATION_BUILD_DIR'] = \ |
| 134 '../$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)' |
| 135 |
| 136 value['buildConfigurations'] = [] |
| 137 for configuration in configurations: |
| 138 new_build_config = copy.copy(build_config_template) |
| 139 new_build_config['name'] = configuration |
| 140 value['buildConfigurations'].append( |
| 141 project.AddObject('products', new_build_config)) |
| 142 |
| 143 for object_id in objects_to_remove: |
| 144 del project.objects[object_id] |
| 145 |
| 146 objects = collections.OrderedDict(sorted(project.objects.iteritems())) |
| 147 WriteXcodeProject(file_output, json.dumps(json_data)) |
| 148 |
| 149 |
| 150 def ConvertGnXcodeProject(input_dir, output_dir, configurations): |
| 151 '''Tweak the Xcode project generated by gn to support multiple configurations. |
| 152 |
| 153 The Xcode projects generated by "gn gen --ide" only supports a single |
| 154 platform and configuration (as the platform and configuration are set |
| 155 per output directory). This method takes as input such projects and |
| 156 add support for multiple configurations and platforms (to allow devs |
| 157 to select them in Xcode). |
| 158 |
| 159 Args: |
| 160 input_dir: directory containing the XCode projects created by "gn gen --ide" |
| 161 output_dir: directory where the tweaked Xcode projects will be saved |
| 162 configurations: list of string corresponding to the configurations that |
| 163 need to be supported by the tweaked Xcode projects, must contains at |
| 164 least one value. |
| 165 ''' |
| 166 # Update products project. |
| 167 products = os.path.join('products.xcodeproj', 'project.pbxproj') |
| 168 product_input = os.path.join(input_dir, products) |
| 169 product_output = os.path.join(output_dir, products) |
| 170 UpdateProductsProject(product_input, product_output, configurations) |
| 171 |
| 172 # Copy sources project and all workspace. |
| 173 sources = os.path.join('sources.xcodeproj', 'project.pbxproj') |
| 174 CopyFileIfChanged(os.path.join(input_dir, sources), |
| 175 os.path.join(output_dir, sources)) |
| 176 xcwspace = os.path.join('all.xcworkspace', 'contents.xcworkspacedata') |
| 177 CopyFileIfChanged(os.path.join(input_dir, xcwspace), |
| 178 os.path.join(output_dir, xcwspace)) |
| 179 |
| 180 |
| 181 def Main(args): |
| 182 parser = argparse.ArgumentParser( |
| 183 description='Convert GN Xcode projects for iOS.') |
| 184 parser.add_argument( |
| 185 'input', |
| 186 help='directory containing [product|sources|all] Xcode projects.') |
| 187 parser.add_argument( |
| 188 'output', |
| 189 help='directory where to generate the iOS configuration.') |
| 190 parser.add_argument( |
| 191 '--add-config', dest='configurations', default=[], action='append', |
| 192 help='configuration to add to the Xcode project') |
| 193 args = parser.parse_args(args) |
| 194 |
| 195 if not os.path.isdir(args.input): |
| 196 sys.stderr.write('Input directory does not exists.\n') |
| 197 return 1 |
| 198 |
| 199 required = set(['products.xcodeproj', 'sources.xcodeproj', 'all.xcworkspace']) |
| 200 if not required.issubset(os.listdir(args.input)): |
| 201 sys.stderr.write( |
| 202 'Input directory does not contain all necessary Xcode projects.\n') |
| 203 return 1 |
| 204 |
| 205 if not args.configurations: |
| 206 sys.stderr.write('At least one configuration required, see --add-config.\n') |
| 207 return 1 |
| 208 |
| 209 ConvertGnXcodeProject(args.input, args.output, args.configurations) |
| 210 |
| 211 if __name__ == '__main__': |
| 212 sys.exit(Main(sys.argv[1:])) |
| 213 |
| 214 |
OLD | NEW |