ios: Update build scripts to support Apple Silicon simulators.

Also update mini_chromium to 5654edb422 for target_environment arg.

Change-Id: If350938bbeaddbdda123c2f0e9ff978075a60370
Reviewed-on: https://chromium-review.googlesource.com/c/crashpad/crashpad/+/3558170
Reviewed-by: Rohit Rao <rohitrao@chromium.org>
Commit-Queue: Justin Cohen <justincohen@chromium.org>
This commit is contained in:
Justin Cohen 2022-03-31 13:50:10 -04:00 committed by Crashpad LUCI CQ
parent 20d6dee037
commit fa01762894
6 changed files with 681 additions and 326 deletions

2
DEPS
View File

@ -39,7 +39,7 @@ deps = {
'e1e7b0ad8ee99a875b272c8e33e308472e897660',
'crashpad/third_party/mini_chromium/mini_chromium':
Var('chromium_git') + '/chromium/mini_chromium@' +
'f87a38442a9e7ba88d1c4f479e9167927eae84ed',
'5654edb4225bcad13901155c819febb5748e502b',
'crashpad/third_party/libfuzzer/src':
Var('chromium_git') + '/chromium/llvm-project/compiler-rt/lib/fuzzer.git@' +
'fda403cf93ecb8792cb1d061564d89a6553ca020',

View File

@ -26,14 +26,146 @@ import argparse
import collections
import copy
import filecmp
import json
import functools
import hashlib
import json
import os
import re
import shutil
import string
import subprocess
import sys
import tempfile
import xml.etree.ElementTree
LLDBINIT_PATH = '$(PROJECT_DIR)/.lldbinit'
PYTHON_RE = re.compile('[ /]python[23]?$')
XCTEST_PRODUCT_TYPES = frozenset((
'com.apple.product-type.bundle.unit-test',
'com.apple.product-type.bundle.ui-testing',
))
SCHEME_PRODUCT_TYPES = frozenset((
'com.apple.product-type.app-extension',
'com.apple.product-type.application',
'com.apple.product-type.framework'
))
class Template(string.Template):
"""A subclass of string.Template that changes delimiter."""
delimiter = '@'
@functools.lru_cache
def LoadSchemeTemplate(root, name):
"""Return a string.Template object for scheme file loaded relative to root."""
path = os.path.join(root, 'build', 'ios', name)
with open(path) as file:
return Template(file.read())
def CreateIdentifier(str_id):
"""Return a 24 characters string that can be used as an identifier."""
return hashlib.sha1(str_id.encode("utf-8")).hexdigest()[:24].upper()
def GenerateSchemeForTarget(root, project, old_project, name, path, tests):
"""Generates the .xcsheme file for target named |name|.
The file is generated in the new project schemes directory from a template.
If there is an existing previous project, then the old scheme file is copied
and the lldbinit setting is set. If lldbinit setting is already correct, the
file is not modified, just copied.
"""
project_name = os.path.basename(project)
relative_path = os.path.join('xcshareddata', 'xcschemes', name + '.xcscheme')
identifier = CreateIdentifier('%s %s' % (name, path))
scheme_path = os.path.join(project, relative_path)
if not os.path.isdir(os.path.dirname(scheme_path)):
os.makedirs(os.path.dirname(scheme_path))
old_scheme_path = os.path.join(old_project, relative_path)
if os.path.exists(old_scheme_path):
made_changes = False
tree = xml.etree.ElementTree.parse(old_scheme_path)
tree_root = tree.getroot()
for reference in tree_root.findall('.//BuildableReference'):
for (attr, value) in (
('BuildableName', path),
('BlueprintName', name),
('BlueprintIdentifier', identifier)):
if reference.get(attr) != value:
reference.set(attr, value)
made_changes = True
for child in tree_root:
if child.tag not in ('TestAction', 'LaunchAction'):
continue
if child.get('customLLDBInitFile') != LLDBINIT_PATH:
child.set('customLLDBInitFile', LLDBINIT_PATH)
made_changes = True
# Override the list of testables.
if child.tag == 'TestAction':
for subchild in child:
if subchild.tag != 'Testables':
continue
for elt in list(subchild):
subchild.remove(elt)
if tests:
template = LoadSchemeTemplate(root, 'xcodescheme-testable.template')
for (key, test_path, test_name) in sorted(tests):
testable = ''.join(template.substitute(
BLUEPRINT_IDENTIFIER=key,
BUILDABLE_NAME=test_path,
BLUEPRINT_NAME=test_name,
PROJECT_NAME=project_name))
testable_elt = xml.etree.ElementTree.fromstring(testable)
subchild.append(testable_elt)
if made_changes:
tree.write(scheme_path, xml_declaration=True, encoding='UTF-8')
else:
shutil.copyfile(old_scheme_path, scheme_path)
else:
testables = ''
if tests:
template = LoadSchemeTemplate(root, 'xcodescheme-testable.template')
testables = '\n' + ''.join(
template.substitute(
BLUEPRINT_IDENTIFIER=key,
BUILDABLE_NAME=test_path,
BLUEPRINT_NAME=test_name,
PROJECT_NAME=project_name)
for (key, test_path, test_name) in sorted(tests)).rstrip()
template = LoadSchemeTemplate(root, 'xcodescheme.template')
with open(scheme_path, 'w') as scheme_file:
scheme_file.write(
template.substitute(
TESTABLES=testables,
LLDBINIT_PATH=LLDBINIT_PATH,
BLUEPRINT_IDENTIFIER=identifier,
BUILDABLE_NAME=path,
BLUEPRINT_NAME=name,
PROJECT_NAME=project_name))
class XcodeProject(object):
@ -46,7 +178,7 @@ class XcodeProject(object):
while True:
self.counter += 1
str_id = "%s %s %d" % (parent_name, obj['isa'], self.counter)
new_id = hashlib.sha1(str_id.encode("utf-8")).hexdigest()[:24].upper()
new_id = CreateIdentifier(str_id)
# Make sure ID is unique. It's possible there could be an id conflict
# since this is run after GN runs.
@ -54,6 +186,93 @@ class XcodeProject(object):
self.objects[new_id] = obj
return new_id
def IterObjectsByIsa(self, isa):
"""Iterates overs objects of the |isa| type."""
for key, obj in self.objects.items():
if obj['isa'] == isa:
yield (key, obj)
def IterNativeTargetByProductType(self, product_types):
"""Iterates over PBXNativeTarget objects of any |product_types| types."""
for key, obj in self.IterObjectsByIsa('PBXNativeTarget'):
if obj['productType'] in product_types:
yield (key, obj)
def UpdateBuildScripts(self):
"""Update build scripts to respect configuration and platforms."""
for key, obj in self.IterObjectsByIsa('PBXShellScriptBuildPhase'):
shell_path = obj['shellPath']
shell_code = obj['shellScript']
if shell_path.endswith('/sh'):
shell_code = shell_code.replace(
'ninja -C .',
'ninja -C "../${CONFIGURATION}${EFFECTIVE_PLATFORM_NAME}"')
elif PYTHON_RE.search(shell_path):
shell_code = shell_code.replace(
'''ninja_params = [ '-C', '.' ]''',
'''ninja_params = [ '-C', '../' + os.environ['CONFIGURATION']'''
''' + os.environ['EFFECTIVE_PLATFORM_NAME'] ]''')
# Replace the build script in the object.
obj['shellScript'] = shell_code
def UpdateBuildConfigurations(self, configurations):
"""Add new configurations, using the first one as default."""
# Create a list with all the objects of interest. This is needed
# because objects will be added to/removed from the project upon
# iterating this list and python dictionaries cannot be mutated
# during iteration.
for key, obj in list(self.IterObjectsByIsa('XCConfigurationList')):
# Use the first build configuration as template for creating all the
# new build configurations.
build_config_template = self.objects[obj['buildConfigurations'][0]]
build_config_template['buildSettings']['CONFIGURATION_BUILD_DIR'] = \
'$(PROJECT_DIR)/../$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)'
# Remove the existing build configurations from the project before
# creating the new ones.
for build_config_id in obj['buildConfigurations']:
del self.objects[build_config_id]
obj['buildConfigurations'] = []
for configuration in configurations:
build_config = copy.copy(build_config_template)
build_config['name'] = configuration
build_config_id = self.AddObject('products', build_config)
obj['buildConfigurations'].append(build_config_id)
def GetHostMappingForXCTests(self):
"""Returns a dict from targets to the list of their xctests modules."""
mapping = collections.defaultdict(list)
for key, obj in self.IterNativeTargetByProductType(XCTEST_PRODUCT_TYPES):
build_config_lists_id = obj['buildConfigurationList']
build_configs = self.objects[build_config_lists_id]['buildConfigurations']
# Use the first build configuration to get the name of the host target.
# This is arbitrary, but since the build configuration are all identical
# after UpdateBuildConfiguration, except for their 'name', it is fine.
build_config = self.objects[build_configs[0]]
if obj['productType'] == 'com.apple.product-type.bundle.unit-test':
# The test_host value will look like this:
# `$(BUILD_PRODUCTS_DIR)/host_app_name.app/host_app_name`
#
# Extract the `host_app_name.app` part as key for the output.
test_host_path = build_config['buildSettings']['TEST_HOST']
test_host_name = os.path.basename(os.path.dirname(test_host_path))
else:
test_host_name = build_config['buildSettings']['TEST_TARGET_NAME']
test_name = obj['name']
test_path = self.objects[obj['productReference']]['path']
mapping[test_host_name].append((key, test_name, test_path))
return dict(mapping)
def check_output(command):
"""Wrapper around subprocess.check_output that decode output as utf-8."""
@ -100,7 +319,7 @@ def WriteXcodeProject(output_path, json_string):
os.path.join(output_path, 'project.pbxproj'))
def UpdateXcodeProject(project_dir, configurations, root_dir):
def UpdateXcodeProject(project_dir, old_project_dir, configurations, root_dir):
"""Update inplace Xcode project to support multiple configurations.
Args:
@ -113,41 +332,25 @@ def UpdateXcodeProject(project_dir, configurations, root_dir):
json_data = json.loads(LoadXcodeProjectAsJSON(project_dir))
project = XcodeProject(json_data['objects'])
objects_to_remove = []
for value in list(project.objects.values()):
isa = value['isa']
project.UpdateBuildScripts()
project.UpdateBuildConfigurations(configurations)
# Teach build shell script to look for the configuration and platform.
if isa == 'PBXShellScriptBuildPhase':
shell_path = value['shellPath']
if shell_path.endswith('/sh'):
value['shellScript'] = value['shellScript'].replace(
'ninja -C .',
'ninja -C "../${CONFIGURATION}${EFFECTIVE_PLATFORM_NAME}"')
elif re.search('[ /]python[23]?$', shell_path):
value['shellScript'] = value['shellScript'].replace(
'ninja_params = [ \'-C\', \'.\' ]',
'ninja_params = [ \'-C\', \'../\' + os.environ[\'CONFIGURATION\']'
' + os.environ[\'EFFECTIVE_PLATFORM_NAME\'] ]')
mapping = project.GetHostMappingForXCTests()
# Add new configuration, using the first one as default.
if isa == 'XCConfigurationList':
value['defaultConfigurationName'] = configurations[0]
objects_to_remove.extend(value['buildConfigurations'])
# Generate schemes for application, extensions and framework targets
for key, obj in project.IterNativeTargetByProductType(SCHEME_PRODUCT_TYPES):
product = project.objects[obj['productReference']]
product_path = product['path']
build_config_template = project.objects[value['buildConfigurations'][0]]
build_config_template['buildSettings']['CONFIGURATION_BUILD_DIR'] = \
'$(PROJECT_DIR)/../$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)'
# For XCTests, the key is the product path, while for XCUITests, the key
# is the target name. Use a sum of both possible keys (there should not
# be overlaps since different hosts are used for XCTests and XCUITests
# but this make the code simpler).
tests = mapping.get(product_path, []) + mapping.get(obj['name'], [])
GenerateSchemeForTarget(
root_dir, project_dir, old_project_dir,
obj['name'], product_path, tests)
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]
source = GetOrCreateRootGroup(project, json_data['rootObject'], 'Source')
AddMarkdownToProject(project, root_dir, source)
@ -274,7 +477,7 @@ def GetFolderForPath(project, group_object, path):
return group_object
def ConvertGnXcodeProject(root_dir, input_dir, output_dir, configurations):
def ConvertGnXcodeProject(root_dir, proj_name, input_dir, output_dir, configs):
'''Tweak the Xcode project generated by gn to support multiple configurations.
The Xcode projects generated by "gn gen --ide" only supports a single
@ -284,34 +487,22 @@ def ConvertGnXcodeProject(root_dir, input_dir, output_dir, configurations):
to select them in Xcode).
Args:
root_dir: directory that is the root of the project
proj_name: name of the Xcode project "file" (usually `all.xcodeproj`)
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.
configs: list of string corresponding to the configurations that need to be
supported by the tweaked Xcode projects, must contains at least one
value.
'''
# Update the project (supports legacy name "products.xcodeproj" or the new
# project name "all.xcodeproj").
for project_name in ('all.xcodeproj', 'products.xcodeproj'):
if os.path.exists(os.path.join(input_dir, project_name)):
UpdateXcodeProject(
os.path.join(input_dir, project_name),
configurations, root_dir)
UpdateXcodeProject(
os.path.join(input_dir, proj_name),
os.path.join(output_dir, proj_name),
configs, root_dir)
CopyTreeIfChanged(os.path.join(input_dir, project_name),
os.path.join(output_dir, project_name))
else:
shutil.rmtree(os.path.join(output_dir, project_name), ignore_errors=True)
# Copy all.xcworkspace if it exists (will be removed in a future gn version).
workspace_name = 'all.xcworkspace'
if os.path.exists(os.path.join(input_dir, workspace_name)):
CopyTreeIfChanged(os.path.join(input_dir, workspace_name),
os.path.join(output_dir, workspace_name))
else:
shutil.rmtree(os.path.join(output_dir, workspace_name), ignore_errors=True)
CopyTreeIfChanged(os.path.join(input_dir, proj_name),
os.path.join(output_dir, proj_name))
def Main(args):
@ -329,33 +520,30 @@ def Main(args):
parser.add_argument(
'--root', type=os.path.abspath, required=True,
help='root directory of the project')
parser.add_argument(
'--project-name', default='all.xcodeproj', dest='proj_name',
help='name of the Xcode project (default: %(default)s)')
args = parser.parse_args(args)
if not os.path.isdir(args.input):
sys.stderr.write('Input directory does not exists.\n')
return 1
# Depending on the version of "gn", there should be either one project file
# named "all.xcodeproj" or a project file named "products.xcodeproj" and a
# workspace named "all.xcworkspace".
required_files_sets = [
set(("all.xcodeproj",)),
set(("products.xcodeproj", "all.xcworkspace")),
]
for required_files in required_files_sets:
if required_files.issubset(os.listdir(args.input)):
break
else:
if args.proj_name not in os.listdir(args.input):
sys.stderr.write(
'Input directory does not contain all necessary Xcode projects.\n')
'Input directory does not contain the Xcode project.\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)
ConvertGnXcodeProject(
args.root,
args.proj_name,
args.input,
args.output,
args.configurations)
if __name__ == '__main__':
sys.exit(Main(sys.argv[1:]))

View File

@ -15,325 +15,391 @@
# limitations under the License.
import argparse
import configparser
import convert_gn_xcodeproj
import errno
import io
import os
import platform
import re
import shutil
import subprocess
import sys
import tempfile
import configparser
import io
SUPPORTED_TARGETS = ('iphoneos', 'iphonesimulator')
SUPPORTED_CONFIGS = ('Debug', 'Release', 'Profile', 'Official', 'Coverage')
SUPPORTED_TARGETS = ('iphoneos', 'iphonesimulator', 'maccatalyst')
SUPPORTED_CONFIGS = ('Debug', 'Release', 'Profile', 'Official')
# Pattern matching lines from ~/.lldbinit that must not be copied to the
# generated .lldbinit file. They match what the user were told to add to
# their global ~/.lldbinit file before setup-gn.py was updated to generate
# a project specific file and thus must not be copied as they would cause
# the settings to be overwritten.
LLDBINIT_SKIP_PATTERNS = (
re.compile('^script sys.path\\[:0\\] = \\[\'.*/src/tools/lldb\'\\]$'),
re.compile('^script import lldbinit$'),
re.compile('^settings append target.source-map .* /google/src/.*$'),
)
def HostCpuArch():
'''Returns the arch of the host cpu for GN.'''
HOST_CPU_ARCH = {
'arm64': '"arm64"',
'x86_64': '"x64"',
}
return HOST_CPU_ARCH[platform.machine()]
class ConfigParserWithStringInterpolation(configparser.ConfigParser):
'''A .ini file parser that supports strings and environment variables.'''
ENV_VAR_PATTERN = re.compile(r'\$([A-Za-z0-9_]+)')
'''A .ini file parser that supports strings and environment variables.'''
def values(self, section):
return [self._UnquoteString(self._ExpandEnvVar(kv[1])) for kv in configparser.ConfigParser.items(self, section)]
ENV_VAR_PATTERN = re.compile(r'\$([A-Za-z0-9_]+)')
def getstring(self, section, option):
return self._UnquoteString(self._ExpandEnvVar(self.get(section,
option)))
def values(self, section):
return filter(
lambda val: val != '',
map(lambda kv: self._UnquoteString(self._ExpandEnvVar(kv[1])),
configparser.ConfigParser.items(self, section)))
def _UnquoteString(self, string):
if not string or string[0] != '"' or string[-1] != '"':
return string
return string[1:-1]
def getstring(self, section, option, fallback=''):
try:
raw_value = self.get(section, option)
except configparser.NoOptionError:
return fallback
return self._UnquoteString(self._ExpandEnvVar(raw_value))
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
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'
'''Holds configuration for a build and method to generate gn default files.'''
TARGET_CPU_VALUES = {
'iphoneos': {
'32-bit': '"arm"',
'64-bit': '"arm64"',
},
'iphonesimulator': {
'32-bit': '"x86"',
'64-bit': '"x64"',
}
}
FAT_BUILD_DEFAULT_ARCH = '64-bit'
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
TARGET_CPU_VALUES = {
'iphoneos': '"arm64"',
'iphonesimulator': HostCpuArch(),
'maccatalyst': HostCpuArch(),
}
def _GetGnArgs(self):
"""Build the list of arguments to pass to gn.
TARGET_ENVIRONMENT_VALUES = {
'iphoneos': '"device"',
'iphonesimulator': '"simulator"',
'maccatalyst': '"catalyst"'
}
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 = []
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
args.append(('is_debug', self._config in ('Debug', 'Coverage')))
def _GetGnArgs(self, extra_args=None):
"""Build the list of arguments to pass to gn.
if os.environ.get('FORCE_MAC_TOOLCHAIN', '0') == '1':
args.append(('use_system_xcode', False))
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 = []
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.values() if cpu != target_cpu]))
else:
args.append(('target_cpu', cpu_values[build_arch]))
is_debug = self._config == 'Debug'
official = self._config == 'Official'
is_optim = self._config in ('Profile', 'Official')
# 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
args.append(('target_os', '"ios"'))
args.append(('is_debug', is_debug))
def Generate(self, gn_path, root_path, out_path):
buf = io.StringIO()
self.WriteArgsGn(buf)
WriteToFileIfChanged(os.path.join(out_path, 'args.gn'),
buf.getvalue(),
overwrite=True)
if os.environ.get('FORCE_MAC_TOOLCHAIN', '0') == '1':
args.append(('use_system_xcode', False))
subprocess.check_call(
self.GetGnCommand(gn_path, root_path, out_path, True))
args.append(('target_cpu', self.TARGET_CPU_VALUES[self._target]))
args.append((
'target_environment',
self.TARGET_ENVIRONMENT_VALUES[self._target]))
def CreateGnRules(self, gn_path, root_path, out_path):
buf = io.StringIO()
self.WriteArgsGn(buf)
WriteToFileIfChanged(os.path.join(out_path, 'args.gn'),
buf.getvalue(),
overwrite=True)
# If extra arguments are passed to the function, pass them before the
# user overrides (if any).
if extra_args is not None:
args.extend(extra_args)
buf = io.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)
# 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
buf = io.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')
def Generate(self, gn_path, proj_name, root_path, build_dir):
self.WriteArgsGn(build_dir, xcode_project_name=proj_name)
subprocess.check_call(self.GetGnCommand(
gn_path, root_path, build_dir, xcode_project_name=proj_name))
def CreateGnRules(self, gn_path, root_path, build_dir):
gn_command = self.GetGnCommand(gn_path, root_path, build_dir)
self.WriteArgsGn(build_dir)
self.WriteBuildNinja(gn_command, build_dir)
self.WriteBuildNinjaDeps(build_dir)
def WriteArgsGn(self, build_dir, xcode_project_name=None):
with open(os.path.join(build_dir, 'args.gn'), 'w') as 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._target != 'maccatalyst':
if self._settings.has_section('$imports$'):
for import_rule in self._settings.values('$imports$'):
stream.write('import("%s")\n' % import_rule)
stream.write('\n')
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')
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\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('--ninja-executable=autoninja')
if self._settings.has_section('filters'):
target_filters = self._settings.values('filters')
if target_filters:
gn_command.append('--filters=%s' % ';'.join(target_filters))
stream.write('%s%s%s' % (prefix, item, suffix))
stream.write(']\n')
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
# ConfigParser removes quote around empty string which confuse
# `gn gen` so restore them.
if not value:
value = '""'
stream.write('%s = %s\n' % (name, value))
def WriteBuildNinja(self, gn_command, build_dir):
with open(os.path.join(build_dir, 'build.ninja'), 'w') as stream:
stream.write('ninja_required_version = 1.7.2\n')
stream.write('\n')
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 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 WriteBuildNinjaDeps(self, build_dir):
with open(os.path.join(build_dir, 'build.ninja.d'), 'w') as stream:
stream.write('build.ninja: nonexistant_file.gn\n')
def GetGnCommand(self, gn_path, src_path, out_path, xcode_project_name=None):
gn_command = [ gn_path, '--root=%s' % os.path.realpath(src_path), '-q' ]
if xcode_project_name is not None:
gn_command.append('--ide=xcode')
gn_command.append('--ninja-executable=autoninja')
gn_command.append('--xcode-build-system=new')
gn_command.append('--xcode-project=%s' % xcode_project_name)
if self._settings.has_section('filters'):
target_filters = self._settings.values('filters')
if target_filters:
gn_command.append('--filters=%s' % ';'.join(target_filters))
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 NinjaNeedEscape(arg):
'''Returns True if |arg| needs to be escaped when written to .ninja file.'''
return ':' in arg or '*' in arg or ';' in 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)
'''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
'''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.'''
def GenerateXcodeProject(gn_path, root_dir, proj_name, out_dir, settings):
'''Generate Xcode project with Xcode and convert to multi-configurations.'''
prefix = os.path.abspath(os.path.join(out_dir, '_temp'))
temp_path = tempfile.mkdtemp(prefix=prefix)
try:
generator = GnGenerator(settings, 'Debug', 'iphonesimulator')
generator.Generate(gn_path, proj_name, root_dir, temp_path)
convert_gn_xcodeproj.ConvertGnXcodeProject(
root_dir,
'%s.xcodeproj' % proj_name,
os.path.join(temp_path),
os.path.join(out_dir, 'build'),
SUPPORTED_CONFIGS)
finally:
if os.path.exists(temp_path):
shutil.rmtree(temp_path)
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 CreateLLDBInitFile(root_dir, out_dir, settings):
'''
Generate an .lldbinit file for the project that load the script that fixes
the mapping of source files (see docs/ios/build_instructions.md#debugging).
'''
with open(os.path.join(out_dir, 'build', '.lldbinit'), 'w') as lldbinit:
lldb_script_dir = os.path.join(os.path.abspath(root_dir), 'tools', 'lldb')
lldbinit.write('script sys.path[:0] = [\'%s\']\n' % lldb_script_dir)
lldbinit.write('script import lldbinit\n')
workspace_name = settings.getstring(
'gn_args',
'ios_internal_citc_workspace_name')
if workspace_name != '':
username = os.environ['USER']
for shortname in ('googlemac', 'third_party', 'blaze-out'):
lldbinit.write('settings append target.source-map %s %s\n' % (
shortname,
'/google/src/cloud/%s/%s/google3/%s' % (
username, workspace_name, shortname)))
# Append the content of //ios/build/tools/lldbinit.defaults if it exists.
tools_dir = os.path.join(root_dir, 'ios', 'build', 'tools')
defaults_lldbinit_path = os.path.join(tools_dir, 'lldbinit.defaults')
if os.path.isfile(defaults_lldbinit_path):
with open(defaults_lldbinit_path) as defaults_lldbinit:
for line in defaults_lldbinit:
lldbinit.write(line)
# Append the content of ~/.lldbinit if it exists. Line that look like they
# are trying to configure source mapping are skipped as they probably date
# back from when setup-gn.py was not generating an .lldbinit file.
global_lldbinit_path = os.path.join(os.environ['HOME'], '.lldbinit')
if os.path.isfile(global_lldbinit_path):
with open(global_lldbinit_path) as global_lldbinit:
for line in global_lldbinit:
if any(pattern.match(line) for pattern in LLDBINIT_SKIP_PATTERNS):
continue
lldbinit.write(line)
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)
'''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))
if not os.path.isdir(build_dir):
os.makedirs(build_dir)
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))
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')
parser.add_argument('--gn-path',
default=None,
help='path to gn binary (default: look up in $PATH)')
parser.add_argument(
'--build-dir',
default='out',
help='path where the build should be created (default: %(default)s)')
args = parser.parse_args(args)
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')
parser.add_argument(
'--gn-path', default=None,
help='path to gn binary (default: look up in $PATH)')
parser.add_argument(
'--build-dir', default='out',
help='path where the build should be created (default: %(default)s)')
parser.add_argument(
'--config-path', default=os.path.expanduser('~/.setup-gn'),
help='path to the user config file (default: %(default)s)')
parser.add_argument(
'--system-config-path', default=os.path.splitext(__file__)[0] + '.config',
help='path to the default config file (default: %(default)s)')
parser.add_argument(
'--project-name', default='all', dest='proj_name',
help='name of the generated Xcode project (default: %(default)s)')
parser.add_argument(
'--no-xcode-project', action='store_true', default=False,
help='do not generate the build directory with XCode project')
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'),
])
# Load configuration (first global and then any user overrides).
settings = ConfigParserWithStringInterpolation()
settings.read([
args.system_config_path,
args.config_path,
])
# 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)
# 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)
# 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)
# Find path to gn binary either from command-line or in PATH.
if args.gn_path:
gn_path = args.gn_path
else:
gn_path = FindGn()
if gn_path is None:
sys.stderr.write('ERROR: cannot find gn in PATH\n')
sys.exit(1)
# Find path to gn binary either from command-line or in PATH.
if args.gn_path:
gn_path = args.gn_path
else:
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, args.build_dir)
if not os.path.isdir(out_dir):
os.makedirs(out_dir)
out_dir = os.path.join(args.root, args.build_dir)
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 not args.no_xcode_project:
GenerateXcodeProject(gn_path, args.root, args.proj_name, out_dir, settings)
CreateLLDBInitFile(args.root, out_dir, settings)
GenerateGnBuildRules(gn_path, args.root, out_dir, settings)
if __name__ == '__main__':
sys.exit(Main(sys.argv[1:]))
sys.exit(Main(sys.argv[1:]))

View File

@ -0,0 +1,10 @@
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "@{BLUEPRINT_IDENTIFIER}"
BuildableName = "@{BUILDABLE_NAME}"
BlueprintName = "@{BLUEPRINT_NAME}"
ReferencedContainer = "container:@{PROJECT_NAME}">
</BuildableReference>
</TestableReference>

View File

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1220"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "@{BLUEPRINT_IDENTIFIER}"
BuildableName = "@{BUILDABLE_NAME}"
BlueprintName = "@{BLUEPRINT_NAME}"
ReferencedContainer = "container:@{PROJECT_NAME}">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "@{LLDBINIT_PATH}"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>@{TESTABLES}
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "@{LLDBINIT_PATH}"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "@{BLUEPRINT_IDENTIFIER}"
BuildableName = "@{BUILDABLE_NAME}"
BlueprintName = "@{BLUEPRINT_NAME}"
ReferencedContainer = "container:@{PROJECT_NAME}">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "@{BLUEPRINT_IDENTIFIER}"
BuildableName = "@{BUILDABLE_NAME}"
BlueprintName = "@{BLUEPRINT_NAME}"
ReferencedContainer = "container:@{PROJECT_NAME}">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Official"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -104,6 +104,17 @@ GetProcessSnapshotMinidumpFromSinglePending() {
return process_snapshot;
}
UIWindow* GetAnyWindow() {
#if defined(__IPHONE_15_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_15_0
if (@available(iOS 15.0, *)) {
UIWindowScene* scene = reinterpret_cast<UIWindowScene*>(
[UIApplication sharedApplication].connectedScenes.anyObject);
return scene.keyWindow;
}
#endif
return [UIApplication sharedApplication].windows[0];
}
[[clang::optnone]] void recurse(int counter) {
// Fill up the stack faster.
int arr[1024];
@ -355,7 +366,7 @@ GetProcessSnapshotMinidumpFromSinglePending() {
// crash, so dispatch this out of the sinkhole.
dispatch_async(dispatch_get_main_queue(), ^{
UIView* unattachedView = [[UIView alloc] init];
UIWindow* window = [UIApplication sharedApplication].windows[0];
UIWindow* window = GetAnyWindow();
[NSLayoutConstraint activateConstraints:@[
[window.rootViewController.view.bottomAnchor
constraintEqualToAnchor:unattachedView.bottomAnchor],