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],