crashpad/build/ios/convert_gn_xcodeproj.py
Mark Mentovai a5a1c3b07f Add .style.yapf and reformat according to yapf, using “google” style
% yapf --in-place $(git ls-files **/*.py)
% yapf --version
yapf 0.30.0

Note that this is not using the “chromium” yapf style because Chromium
is moving to PEP-8.
https://groups.google.com/a/chromium.org/d/topic/chromium-dev/RcJgJdkNIdg
yapf 0.30.0 no longer recognizes “chromium” as a style option.
22ef70f3c4
Since this is a mass reformatting, it might as well move things all the
way into the future all at once.

This uses the “google” style, which is a superset of “pep8”.

Change-Id: Ifa37371079ea1859e4afe8e31d2eef2cfd7af384
Reviewed-on: https://chromium-review.googlesource.com/c/crashpad/crashpad/+/2165637
Commit-Queue: Mark Mentovai <mark@chromium.org>
Reviewed-by: Scott Graham <scottmg@chromium.org>
2020-04-27 13:59:09 +00:00

284 lines
11 KiB
Python
Executable File

#!/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_settings = build_config_template['buildSettings']
build_settings['CONFIGURATION_BUILD_DIR'] = (
'$(PROJECT_DIR)/../$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)')
build_settings['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": "<group>"
}
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": "<group>"
}
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:]))