mirror of
https://github.com/chromium/crashpad.git
synced 2025-03-08 21:26:04 +00:00
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 <rohitrao@chromium.org> Reviewed-by: Mark Mentovai <mark@chromium.org> Commit-Queue: Justin Cohen <justincohen@chromium.org>
This commit is contained in:
parent
23a2da5e95
commit
77baffaf93
15
DEPS
15
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 = [
|
||||
|
273
build/ios/convert_gn_xcodeproj.py
Normal file
273
build/ios/convert_gn_xcodeproj.py
Normal file
@ -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": "<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:]))
|
39
build/ios/setup-ios-gn.config
Normal file
39
build/ios/setup-ios-gn.config
Normal file
@ -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.
|
352
build/ios/setup-ios-gn.py
Normal file
352
build/ios/setup-ios-gn.py
Normal file
@ -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:]))
|
Loading…
x
Reference in New Issue
Block a user