diff --git a/DEPS b/DEPS
index 2f8a2e64..22992237 100644
--- a/DEPS
+++ b/DEPS
@@ -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',
diff --git a/build/ios/convert_gn_xcodeproj.py b/build/ios/convert_gn_xcodeproj.py
index 48c17525..8c5c6d07 100755
--- a/build/ios/convert_gn_xcodeproj.py
+++ b/build/ios/convert_gn_xcodeproj.py
@@ -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:]))
diff --git a/build/ios/setup_ios_gn.py b/build/ios/setup_ios_gn.py
index 76e63811..aacc8ec7 100755
--- a/build/ios/setup_ios_gn.py
+++ b/build/ios/setup_ios_gn.py
@@ -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:]))
diff --git a/build/ios/xcodescheme-testable.template b/build/ios/xcodescheme-testable.template
new file mode 100644
index 00000000..61b6f471
--- /dev/null
+++ b/build/ios/xcodescheme-testable.template
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/build/ios/xcodescheme.template b/build/ios/xcodescheme.template
new file mode 100644
index 00000000..514bea4e
--- /dev/null
+++ b/build/ios/xcodescheme.template
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+ @{TESTABLES}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/ios/host/cptest_application_delegate.mm b/test/ios/host/cptest_application_delegate.mm
index 397646c8..503d2feb 100644
--- a/test/ios/host/cptest_application_delegate.mm
+++ b/test/ios/host/cptest_application_delegate.mm
@@ -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(
+ [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],