|  | #!/usr/bin/python | 
|  | # -*- coding: utf-8 -*- | 
|  | # Copyright 2016 The Tulsi Authors. All rights reserved. | 
|  | # | 
|  | # Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | # you may not use this file except in compliance with the License. | 
|  | # You may obtain a copy of the License at | 
|  | # | 
|  | #      http://www.apache.org/licenses/LICENSE-2.0 | 
|  | # | 
|  | # Unless required by applicable law or agreed to in writing, software | 
|  | # distributed under the License is distributed on an "AS IS" BASIS, | 
|  | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | # See the License for the specific language governing permissions and | 
|  | # limitations under the License. | 
|  |  | 
|  | """Bridge between Xcode and Bazel for the "build" action.""" | 
|  |  | 
|  | import atexit | 
|  | import errno | 
|  | import fcntl | 
|  | import hashlib | 
|  | import inspect | 
|  | import io | 
|  | import json | 
|  | import os | 
|  | import pipes | 
|  | import plistlib | 
|  | import re | 
|  | import shutil | 
|  | import signal | 
|  | import subprocess | 
|  | import sys | 
|  | import textwrap | 
|  | import threading | 
|  | import time | 
|  | import zipfile | 
|  |  | 
|  | from apfs_clone_copy import CopyOnWrite | 
|  | import bazel_build_events | 
|  | import bazel_build_settings | 
|  | import bazel_options | 
|  | from bootstrap_lldbinit import BootstrapLLDBInit | 
|  | from bootstrap_lldbinit import TULSI_LLDBINIT_FILE | 
|  | import tulsi_logging | 
|  | from update_symbol_cache import UpdateSymbolCache | 
|  |  | 
|  |  | 
|  | # List of frameworks that Xcode injects into test host targets that should be | 
|  | # re-signed when running the tests on devices. | 
|  | XCODE_INJECTED_FRAMEWORKS = [ | 
|  | 'libXCTestBundleInject.dylib', | 
|  | 'libXCTestSwiftSupport.dylib', | 
|  | 'IDEBundleInjection.framework', | 
|  | 'XCTAutomationSupport.framework', | 
|  | 'XCTest.framework', | 
|  | ] | 
|  |  | 
|  | _logger = None | 
|  |  | 
|  |  | 
|  | def _PrintUnbuffered(msg): | 
|  | sys.stdout.write('%s\n' % msg) | 
|  | sys.stdout.flush() | 
|  |  | 
|  |  | 
|  | def _PrintXcodeWarning(msg): | 
|  | sys.stdout.write(':: warning: %s\n' % msg) | 
|  | sys.stdout.flush() | 
|  |  | 
|  |  | 
|  | def _PrintXcodeError(msg): | 
|  | sys.stderr.write(':: error: %s\n' % msg) | 
|  | sys.stderr.flush() | 
|  |  | 
|  |  | 
|  | def _Fatal(msg, fatal_frame=None): | 
|  | """Print a fatal error pointing to the failure line inside the script.""" | 
|  | if not fatal_frame: | 
|  | fatal_frame = inspect.currentframe().f_back | 
|  | filename, line_number, _, _, _ = inspect.getframeinfo(fatal_frame) | 
|  | _PrintUnbuffered('%s:%d: error: %s' % (os.path.abspath(filename), | 
|  | line_number, msg)) | 
|  |  | 
|  |  | 
|  | CLEANUP_BEP_FILE_AT_EXIT = False | 
|  |  | 
|  |  | 
|  | # Function to be called atexit to clean up the BEP file if one is present. | 
|  | # This is especially useful in cases of abnormal termination (such as what | 
|  | # happens when Xcode is killed). | 
|  | def _BEPFileExitCleanup(bep_file_path): | 
|  | if not CLEANUP_BEP_FILE_AT_EXIT: | 
|  | return | 
|  | try: | 
|  | os.remove(bep_file_path) | 
|  | except OSError as e: | 
|  | _PrintXcodeWarning('Failed to remove BEP file from %s. Error: %s' % | 
|  | (bep_file_path, e.strerror)) | 
|  |  | 
|  |  | 
|  | def _InterruptHandler(signum, frame): | 
|  | """Gracefully exit on SIGINT.""" | 
|  | del signum, frame  # Unused. | 
|  | _PrintUnbuffered('Caught interrupt signal. Exiting...') | 
|  | sys.exit(0) | 
|  |  | 
|  |  | 
|  | class Timer(object): | 
|  | """Simple profiler.""" | 
|  |  | 
|  | def __init__(self, action_name, action_id): | 
|  | """Creates a new Timer object. | 
|  |  | 
|  | Args: | 
|  | action_name: A human-readable action name, shown in the build log. | 
|  | action_id: A machine-readable action identifier, can be used for metrics. | 
|  |  | 
|  | Returns: | 
|  | A Timer instance. | 
|  |  | 
|  | Raises: | 
|  | RuntimeError: if Timer is created without initializing _logger. | 
|  | """ | 
|  | if _logger is None: | 
|  | raise RuntimeError('Attempted to create Timer without a logger.') | 
|  | self.action_name = action_name | 
|  | self.action_id = action_id | 
|  | self._start = None | 
|  |  | 
|  | def Start(self): | 
|  | self._start = time.time() | 
|  | return self | 
|  |  | 
|  | def End(self, log_absolute_times=False): | 
|  | end = time.time() | 
|  | seconds = end - self._start | 
|  | if log_absolute_times: | 
|  | _logger.log_action(self.action_name, self.action_id, seconds, | 
|  | self._start, end) | 
|  | else: | 
|  | _logger.log_action(self.action_name, self.action_id, seconds) | 
|  |  | 
|  |  | 
|  | def _LockFileCreate(): | 
|  | # This relies on this script running at the root of the bazel workspace. | 
|  | cwd = os.environ['PWD'] | 
|  | cwd_hash = hashlib.sha256(cwd.encode()).hexdigest() | 
|  | return '/tmp/tulsi_bazel_build_{}.lock'.format(cwd_hash) | 
|  |  | 
|  |  | 
|  | # Function to be called atexit to release the file lock on script termination. | 
|  | def _LockFileExitCleanup(lock_file_handle): | 
|  | lock_file_handle.close() | 
|  |  | 
|  |  | 
|  | def _LockFileAcquire(lock_path): | 
|  | """Force script to wait on file lock to serialize build target actions. | 
|  |  | 
|  | Args: | 
|  | lock_path: Path to the lock file. | 
|  | """ | 
|  | _PrintUnbuffered('Queuing Tulsi build...') | 
|  | lockfile = open(lock_path, 'w') | 
|  | # Register "fclose(...)" as early as possible, before acquiring lock. | 
|  | atexit.register(_LockFileExitCleanup, lockfile) | 
|  | while True: | 
|  | try: | 
|  | fcntl.lockf(lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB) | 
|  | break | 
|  | except IOError as err: | 
|  | if err.errno != errno.EAGAIN: | 
|  | raise | 
|  | else: | 
|  | time.sleep(0.1) | 
|  |  | 
|  |  | 
|  | class CodesignBundleAttributes(object): | 
|  | """Wrapper class for codesigning attributes of a signed bundle.""" | 
|  |  | 
|  | # List of codesigning attributes that this script requires. | 
|  | _ATTRIBUTES = ['Authority', 'Identifier', 'TeamIdentifier'] | 
|  |  | 
|  | def __init__(self, codesign_output): | 
|  | self.attributes = {} | 
|  |  | 
|  | pending_attributes = list(self._ATTRIBUTES) | 
|  | for line in codesign_output.split('\n'): | 
|  | if not pending_attributes: | 
|  | break | 
|  |  | 
|  | for attribute in pending_attributes: | 
|  | if line.startswith(attribute): | 
|  | value = line[len(attribute) + 1:] | 
|  | self.attributes[attribute] = value | 
|  | pending_attributes.remove(attribute) | 
|  | break | 
|  |  | 
|  | for attribute in self._ATTRIBUTES: | 
|  | if attribute not in self.attributes: | 
|  | _PrintXcodeError( | 
|  | 'Failed to extract %s from %s.\n' % (attribute, codesign_output)) | 
|  |  | 
|  | def Get(self, attribute): | 
|  | """Returns the value for the given attribute, or None if it wasn't found.""" | 
|  | value = self.attributes.get(attribute) | 
|  | if attribute not in self._ATTRIBUTES: | 
|  | _PrintXcodeError( | 
|  | 'Attribute %s not declared to be parsed. ' % attribute + | 
|  | 'Available attributes are %s.\n' % self._ATTRIBUTES) | 
|  | return value | 
|  |  | 
|  |  | 
|  | class _OptionsParser(object): | 
|  | """Handles parsing script options.""" | 
|  |  | 
|  | # List of all supported Xcode configurations. | 
|  | KNOWN_CONFIGS = ['Debug', 'Release'] | 
|  |  | 
|  | def __init__(self, build_settings, sdk_version, platform_name, arch): | 
|  | self.targets = [] | 
|  | self.build_settings = build_settings | 
|  | self.common_build_options = [ | 
|  | '--verbose_failures', | 
|  | '--bes_outerr_buffer_size=0',  # Don't buffer Bazel output. | 
|  | ] | 
|  |  | 
|  | self.sdk_version = sdk_version | 
|  | self.platform_name = platform_name | 
|  |  | 
|  | if self.platform_name.startswith('watch'): | 
|  | config_platform = 'watchos' | 
|  | elif self.platform_name.startswith('iphone'): | 
|  | config_platform = 'ios' | 
|  | elif self.platform_name.startswith('macos'): | 
|  | config_platform = 'macos' | 
|  | elif self.platform_name.startswith('appletv'): | 
|  | config_platform = 'tvos' | 
|  | else: | 
|  | self._WarnUnknownPlatform() | 
|  | config_platform = 'ios' | 
|  | self.bazel_build_config = '{}_{}'.format(config_platform, arch) | 
|  | if self.bazel_build_config not in build_settings.platformConfigFlags: | 
|  | _PrintXcodeError('Unknown active compilation target of "{}". ' | 
|  | 'Please report a Tulsi bug.' | 
|  | .format(self.bazel_build_config)) | 
|  | sys.exit(1) | 
|  |  | 
|  | self.verbose = 0 | 
|  | self.bazel_bin_path = 'bazel-bin' | 
|  | self.bazel_executable = None | 
|  |  | 
|  | @staticmethod | 
|  | def _UsageMessage(): | 
|  | """Returns a usage message string.""" | 
|  | usage = textwrap.dedent("""\ | 
|  | Usage: %s <target> [<target2> ...] --bazel <bazel_binary_path> [options] | 
|  |  | 
|  | Where options are: | 
|  | --verbose [-v] | 
|  | Increments the verbosity of the script by one level. This argument | 
|  | may be provided multiple times to enable additional output levels. | 
|  |  | 
|  | --bazel_bin_path <path> | 
|  | Path at which Bazel-generated artifacts may be retrieved. | 
|  | """ % sys.argv[0]) | 
|  |  | 
|  | return usage | 
|  |  | 
|  | def ParseOptions(self, args): | 
|  | """Parses arguments, returning (message, exit_code).""" | 
|  |  | 
|  | bazel_executable_index = args.index('--bazel') | 
|  |  | 
|  | self.targets = args[:bazel_executable_index] | 
|  | if not self.targets or len(args) < bazel_executable_index + 2: | 
|  | return (self._UsageMessage(), 10) | 
|  | self.bazel_executable = args[bazel_executable_index + 1] | 
|  |  | 
|  | return self._ParseVariableOptions(args[bazel_executable_index + 2:]) | 
|  |  | 
|  | def GetBaseFlagsForTargets(self, config): | 
|  | is_debug = config == 'Debug' | 
|  | return self.build_settings.flags_for_target( | 
|  | self.targets[0], | 
|  | is_debug, | 
|  | self.bazel_build_config) | 
|  |  | 
|  | def GetEnabledFeatures(self): | 
|  | """Returns a list of enabled Bazel features for the active target.""" | 
|  | return self.build_settings.features_for_target(self.targets[0]) | 
|  |  | 
|  | def GetBazelOptions(self, config): | 
|  | """Returns the full set of build options for the given config.""" | 
|  | bazel, start_up, build = self.GetBaseFlagsForTargets(config) | 
|  | all_build = [] | 
|  | all_build.extend(self.common_build_options) | 
|  | all_build.extend(build) | 
|  |  | 
|  | xcode_version_flag = self._ComputeXcodeVersionFlag() | 
|  | if xcode_version_flag: | 
|  | all_build.append('--xcode_version=%s' % xcode_version_flag) | 
|  |  | 
|  | return bazel, start_up, all_build | 
|  |  | 
|  | def _WarnUnknownPlatform(self): | 
|  | _PrintUnbuffered('Warning: unknown platform "%s" will be treated as ' | 
|  | 'iOS' % self.platform_name) | 
|  |  | 
|  | def _ParseVariableOptions(self, args): | 
|  | """Parses flag-based args, returning (message, exit_code).""" | 
|  |  | 
|  | verbose_re = re.compile('-(v+)$') | 
|  |  | 
|  | while args: | 
|  | arg = args[0] | 
|  | args = args[1:] | 
|  |  | 
|  | if arg == '--bazel_bin_path': | 
|  | if not args: | 
|  | return ('Missing required parameter for %s' % arg, 2) | 
|  | self.bazel_bin_path = args[0] | 
|  | args = args[1:] | 
|  |  | 
|  | elif arg == '--verbose': | 
|  | self.verbose += 1 | 
|  |  | 
|  | else: | 
|  | match = verbose_re.match(arg) | 
|  | if match: | 
|  | self.verbose += len(match.group(1)) | 
|  | else: | 
|  | return ('Unknown option "%s"\n%s' % (arg, self._UsageMessage()), 1) | 
|  |  | 
|  | return (None, 0) | 
|  |  | 
|  | @staticmethod | 
|  | def _GetXcodeBuildVersionString(): | 
|  | """Returns Xcode build version from the environment as a string.""" | 
|  | return os.environ['XCODE_PRODUCT_BUILD_VERSION'] | 
|  |  | 
|  | @staticmethod | 
|  | def _GetXcodeVersionString(): | 
|  | """Returns Xcode version info from the Xcode's version.plist. | 
|  |  | 
|  | Just reading XCODE_VERSION_ACTUAL from the environment seems like | 
|  | a more reasonable implementation, but has shown to be unreliable, | 
|  | at least when using Xcode 11.3.1 and opening the project within an | 
|  | Xcode workspace. | 
|  | """ | 
|  | developer_dir = os.environ['DEVELOPER_DIR'] | 
|  | app_dir = developer_dir.split('.app')[0] + '.app' | 
|  | version_plist_path = os.path.join(app_dir, 'Contents', 'version.plist') | 
|  | try: | 
|  | # python2 API to plistlib - needs updating if/when Tulsi bumps to python3 | 
|  | plist = plistlib.readPlist(version_plist_path) | 
|  | except IOError: | 
|  | _PrintXcodeWarning('Tulsi cannot determine Xcode version, error ' | 
|  | 'reading from {}'.format(version_plist_path)) | 
|  | return None | 
|  | try: | 
|  | # Example: "11.3.1", "11.3", "11.0" | 
|  | key = 'CFBundleShortVersionString' | 
|  | version_string = plist[key] | 
|  | except KeyError: | 
|  | _PrintXcodeWarning('Tulsi cannot determine Xcode version from {}, no ' | 
|  | '"{}" key'.format(version_plist_path, key)) | 
|  | return None | 
|  |  | 
|  | # But we need to normalize to major.minor.patch, e.g. 11.3.0 or | 
|  | # 11.0.0, so add one or two ".0" if needed (two just in case | 
|  | # there is ever just a single version number like "12") | 
|  | dots_count = version_string.count('.') | 
|  | dot_zeroes_to_add = 2 - dots_count | 
|  | version_string += '.0' * dot_zeroes_to_add | 
|  | return version_string | 
|  |  | 
|  | @staticmethod | 
|  | def _ComputeXcodeVersionFlag(): | 
|  | """Returns a string for the --xcode_version build flag, if any. | 
|  |  | 
|  | The flag should be used if the active Xcode version was not the same one | 
|  | used during project generation. | 
|  |  | 
|  | Note this a best-attempt only; this may not be accurate as Bazel itself | 
|  | caches the active DEVELOPER_DIR path and the user may have changed their | 
|  | installed Xcode version. | 
|  | """ | 
|  | xcode_version = _OptionsParser._GetXcodeVersionString() | 
|  | build_version = _OptionsParser._GetXcodeBuildVersionString() | 
|  |  | 
|  | if not xcode_version or not build_version: | 
|  | return None | 
|  |  | 
|  | # Of the form Major.Minor.Fix.Build (new Bazel form) or Major.Min.Fix (old). | 
|  | full_bazel_version = os.environ.get('TULSI_XCODE_VERSION') | 
|  | if not full_bazel_version:  # Unexpected: Tulsi gen didn't set the flag. | 
|  | return xcode_version | 
|  |  | 
|  | # Newer Bazel versions specify the version as Major.Minor.Fix.Build. | 
|  | if full_bazel_version.count('.') == 3: | 
|  | components = full_bazel_version.rsplit('.', 1) | 
|  | bazel_xcode_version = components[0] | 
|  | bazel_build_version = components[1] | 
|  |  | 
|  | if (xcode_version != bazel_xcode_version | 
|  | or build_version != bazel_build_version): | 
|  | return '{}.{}'.format(xcode_version, build_version) | 
|  | else: | 
|  | return None | 
|  | else:  # Old version of Bazel. We need to use form Major.Minor.Fix. | 
|  | return xcode_version if xcode_version != full_bazel_version else None | 
|  |  | 
|  |  | 
|  | class BazelBuildBridge(object): | 
|  | """Handles invoking Bazel and unpacking generated binaries.""" | 
|  |  | 
|  | BUILD_EVENTS_FILE = 'build_events.json' | 
|  |  | 
|  | def __init__(self, build_settings): | 
|  | self.build_settings = build_settings | 
|  | self.verbose = 0 | 
|  | self.bazel_bin_path = None | 
|  | self.codesign_attributes = {} | 
|  |  | 
|  | self.codesigning_folder_path = os.environ['CODESIGNING_FOLDER_PATH'] | 
|  |  | 
|  | self.xcode_action = os.environ['ACTION']  # The Xcode build action. | 
|  | # When invoked as an external build system script, Xcode will set ACTION to | 
|  | # an empty string. | 
|  | if not self.xcode_action: | 
|  | self.xcode_action = 'build' | 
|  |  | 
|  | if int(os.environ['XCODE_VERSION_MAJOR']) < 900: | 
|  | xcode_build_version = os.environ['XCODE_PRODUCT_BUILD_VERSION'] | 
|  | _PrintXcodeWarning('Tulsi officially supports Xcode 9+. You are using an ' | 
|  | 'earlier Xcode, build %s.' % xcode_build_version) | 
|  |  | 
|  | self.tulsi_version = os.environ.get('TULSI_VERSION', 'UNKNOWN') | 
|  |  | 
|  | # TODO(b/69857078): Remove this when wrapped_clang is updated. | 
|  | self.direct_debug_prefix_map = False | 
|  | self.normalized_prefix_map = False | 
|  |  | 
|  | self.update_symbol_cache = UpdateSymbolCache() | 
|  |  | 
|  | # Target architecture.  Must be defined for correct setting of | 
|  | # the --cpu flag. Note that Xcode will set multiple values in | 
|  | # ARCHS when building for a Generic Device. | 
|  | archs = os.environ.get('ARCHS') | 
|  | if not archs: | 
|  | _PrintXcodeError('Tulsi requires env variable ARCHS to be ' | 
|  | 'set.  Please file a bug against Tulsi.') | 
|  | sys.exit(1) | 
|  | self.arch = archs.split()[-1] | 
|  |  | 
|  | # Path into which generated artifacts should be copied. | 
|  | self.built_products_dir = os.environ['BUILT_PRODUCTS_DIR'] | 
|  | # Path where Xcode expects generated sources to be placed. | 
|  | self.derived_sources_folder_path = os.environ.get('DERIVED_SOURCES_DIR') | 
|  | # Full name of the target artifact (e.g., "MyApp.app" or "Test.xctest"). | 
|  | self.full_product_name = os.environ['FULL_PRODUCT_NAME'] | 
|  | # Whether to generate runfiles for this target. | 
|  | self.gen_runfiles = os.environ.get('GENERATE_RUNFILES') | 
|  | # Target SDK version. | 
|  | self.sdk_version = os.environ.get('SDK_VERSION') | 
|  | # TEST_HOST for unit tests. | 
|  | self.test_host_binary = os.environ.get('TEST_HOST') | 
|  | # Whether this target is a test or not. | 
|  | self.is_test = os.environ.get('WRAPPER_EXTENSION') == 'xctest' | 
|  | # Target platform. | 
|  | self.platform_name = os.environ['PLATFORM_NAME'] | 
|  | # Type of the target artifact. | 
|  | self.product_type = os.environ['PRODUCT_TYPE'] | 
|  | # Path to the parent of the xcodeproj bundle. | 
|  | self.project_dir = os.environ['PROJECT_DIR'] | 
|  | # Path to the xcodeproj bundle. | 
|  | self.project_file_path = os.environ['PROJECT_FILE_PATH'] | 
|  | # Path to the directory containing the WORKSPACE file. | 
|  | self.workspace_root = os.path.abspath(os.environ['TULSI_WR']) | 
|  | # Set to the name of the generated bundle for bundle-type targets, None for | 
|  | # single file targets (like static libraries). | 
|  | self.wrapper_name = os.environ.get('WRAPPER_NAME') | 
|  | self.wrapper_suffix = os.environ.get('WRAPPER_SUFFIX', '') | 
|  |  | 
|  | # Path where Xcode expects the artifacts to be written to. This is not the | 
|  | # codesigning_path as device vs simulator builds have different signing | 
|  | # requirements, so Xcode expects different paths to be signed. This is | 
|  | # mostly apparent on XCUITests where simulator builds set the codesigning | 
|  | # path to be the .xctest bundle, but for device builds it is actually the | 
|  | # UI runner app (since it needs to be codesigned to run on the device.) The | 
|  | # FULL_PRODUCT_NAME variable is a stable path on where to put the expected | 
|  | # artifacts. For static libraries (objc_library, swift_library), | 
|  | # FULL_PRODUCT_NAME corresponds to the .a file name, which coincides with | 
|  | # the expected location for a single artifact output. | 
|  | # TODO(b/35811023): Check these paths are still valid. | 
|  | self.artifact_output_path = os.path.join( | 
|  | os.environ['TARGET_BUILD_DIR'], | 
|  | os.environ['FULL_PRODUCT_NAME']) | 
|  |  | 
|  | # Path to where Xcode expects the binary to be placed. | 
|  | self.binary_path = os.path.join( | 
|  | os.environ['TARGET_BUILD_DIR'], os.environ['EXECUTABLE_PATH']) | 
|  |  | 
|  | self.is_simulator = self.platform_name.endswith('simulator') | 
|  | # Check to see if code signing actions should be skipped or not. | 
|  | if self.is_simulator: | 
|  | self.codesigning_allowed = False | 
|  | else: | 
|  | self.codesigning_allowed = os.environ.get('CODE_SIGNING_ALLOWED') == 'YES' | 
|  |  | 
|  | if self.codesigning_allowed: | 
|  | platform_prefix = 'iOS' | 
|  | if self.platform_name.startswith('macos'): | 
|  | platform_prefix = 'macOS' | 
|  | entitlements_filename = '%sXCTRunner.entitlements' % platform_prefix | 
|  | self.runner_entitlements_template = os.path.join(self.project_file_path, | 
|  | '.tulsi', | 
|  | 'Resources', | 
|  | entitlements_filename) | 
|  |  | 
|  | self.bazel_executable = None | 
|  |  | 
|  | def Run(self, args): | 
|  | """Executes a Bazel build based on the environment and given arguments.""" | 
|  | if self.xcode_action != 'build': | 
|  | sys.stderr.write('Xcode action is %s, ignoring.' % self.xcode_action) | 
|  | return 0 | 
|  |  | 
|  | parser = _OptionsParser(self.build_settings, | 
|  | self.sdk_version, | 
|  | self.platform_name, | 
|  | self.arch) | 
|  | timer = Timer('Parsing options', 'parsing_options').Start() | 
|  | message, exit_code = parser.ParseOptions(args[1:]) | 
|  | timer.End() | 
|  | if exit_code: | 
|  | _PrintXcodeError('Option parsing failed: %s' % message) | 
|  | return exit_code | 
|  |  | 
|  | self.verbose = parser.verbose | 
|  | self.bazel_bin_path = os.path.abspath(parser.bazel_bin_path) | 
|  | self.bazel_executable = parser.bazel_executable | 
|  | self.bazel_exec_root = self.build_settings.bazelExecRoot | 
|  |  | 
|  | # Update feature flags. | 
|  | features = parser.GetEnabledFeatures() | 
|  | self.direct_debug_prefix_map = 'DirectDebugPrefixMap' in features | 
|  | self.normalized_prefix_map = 'DebugPathNormalization' in features | 
|  |  | 
|  | # Path to the Build Events JSON file uses pid and is removed if the | 
|  | # build is successful. | 
|  | filename = '%d_%s' % (os.getpid(), BazelBuildBridge.BUILD_EVENTS_FILE) | 
|  | self.build_events_file_path = os.path.join( | 
|  | self.project_file_path, | 
|  | '.tulsi', | 
|  | filename) | 
|  |  | 
|  | (command, retval) = self._BuildBazelCommand(parser) | 
|  | if retval: | 
|  | return retval | 
|  |  | 
|  | timer = Timer('Running Bazel', 'running_bazel').Start() | 
|  | exit_code, outputs = self._RunBazelAndPatchOutput(command) | 
|  | timer.End() | 
|  | if exit_code: | 
|  | _Fatal('Bazel build failed with exit code %d. Please check the build ' | 
|  | 'log in Report Navigator (⌘9) for more information.' | 
|  | % exit_code) | 
|  | return exit_code | 
|  |  | 
|  | post_bazel_timer = Timer('Total Tulsi Post-Bazel time', 'total_post_bazel') | 
|  | post_bazel_timer.Start() | 
|  |  | 
|  | if not os.path.exists(self.bazel_exec_root): | 
|  | _Fatal('No Bazel execution root was found at %r. Debugging experience ' | 
|  | 'will be compromised. Please report a Tulsi bug.' | 
|  | % self.bazel_exec_root) | 
|  | return 404 | 
|  |  | 
|  | # This needs to run after `bazel build`, since it depends on the Bazel | 
|  | # workspace directory | 
|  | exit_code = self._LinkTulsiWorkspace() | 
|  | if exit_code: | 
|  | return exit_code | 
|  |  | 
|  | exit_code, outputs_data = self._ExtractAspectOutputsData(outputs) | 
|  | if exit_code: | 
|  | return exit_code | 
|  |  | 
|  | # Generated headers are installed on a thread since we are launching | 
|  | # a separate process to do so. This gives us clean timings. | 
|  | install_thread = threading.Thread( | 
|  | target=self._InstallGeneratedHeaders, args=(outputs,)) | 
|  | install_thread.start() | 
|  | timer = Timer('Installing artifacts', 'installing_artifacts').Start() | 
|  | exit_code = self._InstallArtifact(outputs_data) | 
|  | timer.End() | 
|  | install_thread.join() | 
|  | if exit_code: | 
|  | return exit_code | 
|  |  | 
|  | exit_code, dsym_paths = self._InstallDSYMBundles( | 
|  | self.built_products_dir, outputs_data) | 
|  | if exit_code: | 
|  | return exit_code | 
|  |  | 
|  | if not dsym_paths: | 
|  | # Clean any bundles from a previous build that can interfere with | 
|  | # debugging in LLDB. | 
|  | self._CleanExistingDSYMs() | 
|  | else: | 
|  | for path in dsym_paths: | 
|  | # Starting with Xcode 9.x, a plist based remapping exists for dSYM | 
|  | # bundles that works with Swift as well as (Obj-)C(++). | 
|  | # | 
|  | # This solution also works for Xcode 8.x for (Obj-)C(++) but not | 
|  | # for Swift. | 
|  | timer = Timer('Adding remappings as plists to dSYM', | 
|  | 'plist_dsym').Start() | 
|  | exit_code = self._PlistdSYMPaths(path) | 
|  | timer.End() | 
|  | if exit_code: | 
|  | _PrintXcodeError('Remapping dSYMs process returned %i, please ' | 
|  | 'report a Tulsi bug and attach a full Xcode ' | 
|  | 'build log.' % exit_code) | 
|  | return exit_code | 
|  |  | 
|  | # Starting with Xcode 7.3, XCTests inject several supporting frameworks | 
|  | # into the test host that need to be signed with the same identity as | 
|  | # the host itself. | 
|  | if (self.is_test and not self.platform_name.startswith('macos') and | 
|  | self.codesigning_allowed): | 
|  | exit_code = self._ResignTestArtifacts() | 
|  | if exit_code: | 
|  | return exit_code | 
|  |  | 
|  | # Starting with Xcode 8, .lldbinit files are honored during Xcode debugging | 
|  | # sessions. This allows use of the target.source-map field to remap the | 
|  | # debug symbol paths encoded in the binary to the paths expected by Xcode. | 
|  | # | 
|  | # This will not work with dSYM bundles, or a direct -fdebug-prefix-map from | 
|  | # the Bazel-built locations to Xcode-visible sources. | 
|  | timer = Timer('Updating .lldbinit', 'updating_lldbinit').Start() | 
|  | clear_source_map = dsym_paths or self.direct_debug_prefix_map | 
|  | exit_code = self._UpdateLLDBInit(clear_source_map) | 
|  | timer.End() | 
|  | if exit_code: | 
|  | _PrintXcodeWarning('Updating .lldbinit action failed with code %d' % | 
|  | exit_code) | 
|  |  | 
|  | post_bazel_timer.End(log_absolute_times=True) | 
|  |  | 
|  | return 0 | 
|  |  | 
|  | def _BuildBazelCommand(self, options): | 
|  | """Builds up a commandline string suitable for running Bazel.""" | 
|  | configuration = os.environ['CONFIGURATION'] | 
|  | # Treat the special testrunner build config as a Debug compile. | 
|  | test_runner_config_prefix = '__TulsiTestRunner_' | 
|  | if configuration.startswith(test_runner_config_prefix): | 
|  | configuration = configuration[len(test_runner_config_prefix):] | 
|  | elif os.environ.get('TULSI_TEST_RUNNER_ONLY') == 'YES': | 
|  | _PrintXcodeError('Building test targets with configuration "%s" is not ' | 
|  | 'allowed. Please use the "Test" action or "Build for" > ' | 
|  | '"Testing" instead.' % configuration) | 
|  | return (None, 1) | 
|  |  | 
|  | if configuration not in _OptionsParser.KNOWN_CONFIGS: | 
|  | _PrintXcodeError('Unknown build configuration "%s"' % configuration) | 
|  | return (None, 1) | 
|  |  | 
|  | bazel, start_up, build = options.GetBazelOptions(configuration) | 
|  | bazel_command = [bazel] | 
|  | bazel_command.extend(start_up) | 
|  | bazel_command.append('build') | 
|  | bazel_command.extend(build) | 
|  |  | 
|  | bazel_command.extend([ | 
|  | # The following flags are used by Tulsi to identify itself and read | 
|  | # build information from Bazel. They shold not affect Bazel anaylsis | 
|  | # caching. | 
|  | '--tool_tag=tulsi:bazel_build', | 
|  | '--build_event_json_file=%s' % self.build_events_file_path, | 
|  | '--noexperimental_build_event_json_file_path_conversion', | 
|  | '--aspects', '@tulsi//:tulsi/tulsi_aspects.bzl%tulsi_outputs_aspect']) | 
|  |  | 
|  | if self.is_test and self.gen_runfiles: | 
|  | bazel_command.append('--output_groups=+tulsi_outputs') | 
|  | else: | 
|  | bazel_command.append('--output_groups=tulsi_outputs,default') | 
|  |  | 
|  | bazel_command.extend(options.targets) | 
|  |  | 
|  | extra_options = bazel_options.BazelOptions(os.environ) | 
|  | bazel_command.extend(extra_options.bazel_feature_flags()) | 
|  |  | 
|  | return (bazel_command, 0) | 
|  |  | 
|  | def _RunBazelAndPatchOutput(self, command): | 
|  | """Runs subprocess command, patching output as it's received.""" | 
|  | self._PrintVerbose('Running "%s", patching output for workspace root at ' | 
|  | '"%s" with project path at "%s".' % | 
|  | (' '.join([pipes.quote(x) for x in command]), | 
|  | self.workspace_root, | 
|  | self.project_dir)) | 
|  | # Clean up bazel output to make it look better in Xcode. | 
|  | bazel_line_regex = re.compile( | 
|  | r'(INFO|DEBUG|WARNING|ERROR|FAILED): ([^:]+:\d+:(?:\d+:)?)\s+(.+)') | 
|  |  | 
|  | bazel_generic_regex = re.compile(r'(INFO|DEBUG|WARNING|ERROR|FAILED): (.*)') | 
|  |  | 
|  | def PatchBazelDiagnosticStatements(output_line): | 
|  | """Make Bazel output more Xcode friendly.""" | 
|  |  | 
|  | def BazelLabelToXcodeLabel(bazel_label): | 
|  | """Map Bazel labels to xcode labels for build output.""" | 
|  | xcode_labels = { | 
|  | 'INFO': 'note', | 
|  | 'DEBUG': 'note', | 
|  | 'WARNING': 'warning', | 
|  | 'ERROR': 'error', | 
|  | 'FAILED': 'error' | 
|  | } | 
|  | return xcode_labels.get(bazel_label, bazel_label) | 
|  |  | 
|  | match = bazel_line_regex.match(output_line) | 
|  | if match: | 
|  | xcode_label = BazelLabelToXcodeLabel(match.group(1)) | 
|  | output_line = '%s %s: %s' % (match.group(2), xcode_label, | 
|  | match.group(3)) | 
|  | else: | 
|  | match = bazel_generic_regex.match(output_line) | 
|  | if match: | 
|  | xcode_label = BazelLabelToXcodeLabel(match.group(1)) | 
|  | output_line = '%s: %s' % (xcode_label, match.group(2)) | 
|  | return output_line | 
|  |  | 
|  | if self.workspace_root != self.project_dir: | 
|  | # Match (likely) filename:line_number: lines. | 
|  | xcode_parsable_line_regex = re.compile(r'([^/][^:]+):\d+:') | 
|  |  | 
|  | def PatchOutputLine(output_line): | 
|  | output_line = PatchBazelDiagnosticStatements(output_line) | 
|  | if xcode_parsable_line_regex.match(output_line): | 
|  | output_line = '%s/%s' % (self.workspace_root, output_line) | 
|  | return output_line | 
|  | patch_xcode_parsable_line = PatchOutputLine | 
|  | else: | 
|  | patch_xcode_parsable_line = PatchBazelDiagnosticStatements | 
|  |  | 
|  | def HandleOutput(output): | 
|  | for line in output.splitlines(): | 
|  | _logger.log_bazel_message(patch_xcode_parsable_line(line)) | 
|  |  | 
|  | def WatcherUpdate(watcher): | 
|  | """Processes any new events in the given watcher. | 
|  |  | 
|  | Args: | 
|  | watcher: a BazelBuildEventsWatcher object. | 
|  |  | 
|  | Returns: | 
|  | A list of new tulsiout file names seen. | 
|  | """ | 
|  | new_events = watcher.check_for_new_events() | 
|  | new_outputs = [] | 
|  | for build_event in new_events: | 
|  | if build_event.stderr: | 
|  | HandleOutput(build_event.stderr) | 
|  | if build_event.stdout: | 
|  | HandleOutput(build_event.stdout) | 
|  | if build_event.files: | 
|  | outputs = [x for x in build_event.files if x.endswith('.tulsiouts')] | 
|  | new_outputs.extend(outputs) | 
|  | return new_outputs | 
|  |  | 
|  | def ReaderThread(file_handle, out_buffer): | 
|  | out_buffer.append(file_handle.read()) | 
|  | file_handle.close() | 
|  |  | 
|  | # Make sure the BEP JSON file exists and is empty. We do this to prevent | 
|  | # any sort of race between the watcher, bazel, and the old file contents. | 
|  | open(self.build_events_file_path, 'w').close() | 
|  |  | 
|  | # Capture the stderr and stdout from Bazel. We only display it if it we're | 
|  | # unable to read any BEP events. | 
|  | process = subprocess.Popen(command, | 
|  | stdout=subprocess.PIPE, | 
|  | stderr=subprocess.STDOUT, | 
|  | bufsize=1) | 
|  |  | 
|  | # Register atexit function to clean up BEP file. | 
|  | atexit.register(_BEPFileExitCleanup, self.build_events_file_path) | 
|  | global CLEANUP_BEP_FILE_AT_EXIT | 
|  | CLEANUP_BEP_FILE_AT_EXIT = True | 
|  |  | 
|  | # Start capturing output from Bazel. | 
|  | reader_buffer = [] | 
|  | reader_thread = threading.Thread(target=ReaderThread, | 
|  | args=(process.stdout, reader_buffer)) | 
|  | reader_thread.daemon = True | 
|  | reader_thread.start() | 
|  |  | 
|  | with io.open(self.build_events_file_path, 'r', -1, 'utf-8', 'ignore' | 
|  | ) as bep_file: | 
|  | watcher = bazel_build_events.BazelBuildEventsWatcher(bep_file, | 
|  | _PrintXcodeWarning) | 
|  | output_locations = [] | 
|  | while process.returncode is None: | 
|  | output_locations.extend(WatcherUpdate(watcher)) | 
|  | time.sleep(0.1) | 
|  | process.poll() | 
|  |  | 
|  | output_locations.extend(WatcherUpdate(watcher)) | 
|  |  | 
|  | # If BEP JSON parsing failed, we should display the raw stdout and | 
|  | # stderr from Bazel. | 
|  | reader_thread.join() | 
|  | if not watcher.has_read_events(): | 
|  | HandleOutput(reader_buffer[0]) | 
|  |  | 
|  | if process.returncode == 0 and not output_locations: | 
|  | CLEANUP_BEP_FILE_AT_EXIT = False | 
|  | _PrintXcodeError('Unable to find location of the .tulsiouts file.' | 
|  | 'Please report this as a Tulsi bug, including the' | 
|  | 'contents of %s.' % self.build_events_file_path) | 
|  | return 1, output_locations | 
|  | return process.returncode, output_locations | 
|  |  | 
|  | def _ExtractAspectOutputsData(self, output_files): | 
|  | """Converts aspect output from paths to json to a list of dictionaries. | 
|  |  | 
|  | Args: | 
|  | output_files: A list of strings to files representing Bazel aspect output | 
|  | in UTF-8 JSON format. | 
|  |  | 
|  | Returns: | 
|  | return_code, [dict]: A tuple with a return code as its first argument and | 
|  | for its second argument, a list of dictionaries for | 
|  | each output_file that could be interpreted as valid | 
|  | JSON, representing the returned Bazel aspect | 
|  | information. | 
|  | return_code, None: If an error occurred while converting the list of | 
|  | files into JSON. | 
|  | """ | 
|  | outputs_data = [] | 
|  | for output_file in output_files: | 
|  | try: | 
|  | output_data = json.load(open(output_file)) | 
|  | except (ValueError, IOError) as e: | 
|  | _PrintXcodeError('Failed to load output map ""%s". ' | 
|  | '%s' % (output_file, e)) | 
|  | return 600, None | 
|  | outputs_data.append(output_data) | 
|  | return 0, outputs_data | 
|  |  | 
|  | def _InstallArtifact(self, outputs_data): | 
|  | """Installs Bazel-generated artifacts into the Xcode output directory.""" | 
|  | xcode_artifact_path = self.artifact_output_path | 
|  |  | 
|  | if not outputs_data: | 
|  | _PrintXcodeError('Failed to load top level output file.') | 
|  | return 600 | 
|  |  | 
|  | primary_output_data = outputs_data[0] | 
|  |  | 
|  | if 'artifact' not in primary_output_data: | 
|  | _PrintXcodeError( | 
|  | 'Failed to find an output artifact for target %s in output map %r' % | 
|  | (xcode_artifact_path, primary_output_data)) | 
|  | return 601 | 
|  |  | 
|  | primary_artifact = primary_output_data['artifact'] | 
|  | artifact_archive_root = primary_output_data.get('archive_root') | 
|  | bundle_name = primary_output_data.get('bundle_name') | 
|  |  | 
|  | # The PRODUCT_NAME used by the Xcode project is not trustable as it may be | 
|  | # modified by the user and, more importantly, may have been modified by | 
|  | # Tulsi to disambiguate multiple targets with the same name. | 
|  | self.bazel_product_name = bundle_name | 
|  |  | 
|  | # We need to handle IPAs (from {ios, tvos}_application) differently from | 
|  | # ZIPs (from the other bundled rules) because they output slightly different | 
|  | # directory structures. | 
|  | is_ipa = primary_artifact.endswith('.ipa') | 
|  | is_zip = primary_artifact.endswith('.zip') | 
|  |  | 
|  | if is_ipa or is_zip: | 
|  | expected_bundle_name = bundle_name + self.wrapper_suffix | 
|  |  | 
|  | # The directory structure within the IPA is then determined based on | 
|  | # Bazel's package and/or product type. | 
|  | if is_ipa: | 
|  | bundle_subpath = os.path.join('Payload', expected_bundle_name) | 
|  | else: | 
|  | # If the artifact is a ZIP, assume that the bundle is the top-level | 
|  | # directory (this is the way in which Skylark rules package artifacts | 
|  | # that are not standalone IPAs). | 
|  | bundle_subpath = expected_bundle_name | 
|  |  | 
|  | # Prefer to copy over files from the archive root instead of unzipping the | 
|  | # ipa/zip in order to help preserve timestamps. Note that the archive root | 
|  | # is only present for local builds; for remote builds we must extract from | 
|  | # the zip file. | 
|  | if self._IsValidArtifactArchiveRoot(artifact_archive_root, bundle_name): | 
|  | source_location = os.path.join(artifact_archive_root, bundle_subpath) | 
|  | exit_code = self._RsyncBundle(os.path.basename(primary_artifact), | 
|  | source_location, | 
|  | xcode_artifact_path) | 
|  | else: | 
|  | exit_code = self._UnpackTarget(primary_artifact, | 
|  | xcode_artifact_path, | 
|  | bundle_subpath) | 
|  | if exit_code: | 
|  | return exit_code | 
|  |  | 
|  | elif os.path.isfile(primary_artifact): | 
|  | # Remove the old artifact before copying. | 
|  | if os.path.isfile(xcode_artifact_path): | 
|  | try: | 
|  | os.remove(xcode_artifact_path) | 
|  | except OSError as e: | 
|  | _PrintXcodeError('Failed to remove stale output file ""%s". ' | 
|  | '%s' % (xcode_artifact_path, e)) | 
|  | return 600 | 
|  | exit_code = self._CopyFile(os.path.basename(primary_artifact), | 
|  | primary_artifact, | 
|  | xcode_artifact_path) | 
|  | if exit_code: | 
|  | return exit_code | 
|  | else: | 
|  | self._RsyncBundle(os.path.basename(primary_artifact), | 
|  | primary_artifact, | 
|  | xcode_artifact_path) | 
|  |  | 
|  | # When the rules output a tree artifact, Tulsi will copy the bundle as is | 
|  | # into the expected Xcode output location. But because they're copied as | 
|  | # is from the bazel output, they come with bazel's permissions, which are | 
|  | # read only. Here we set them to write as well, so Xcode can modify the | 
|  | # bundle too (for example, for codesigning). | 
|  | chmod_timer = Timer('Modifying permissions of output bundle', | 
|  | 'bundle_chmod').Start() | 
|  |  | 
|  | self._PrintVerbose('Spawning subprocess to add write permissions to ' | 
|  | 'copied bundle...') | 
|  | process = subprocess.Popen(['chmod', '-R', 'uga+w', xcode_artifact_path]) | 
|  | process.wait() | 
|  | chmod_timer.End() | 
|  |  | 
|  | # No return code check as this is not an essential operation. | 
|  | self._InstallEmbeddedBundlesIfNecessary(primary_output_data) | 
|  |  | 
|  | return 0 | 
|  |  | 
|  | def _IsValidArtifactArchiveRoot(self, archive_root, bundle_name): | 
|  | """Returns true if the archive root is valid for use.""" | 
|  | if not archive_root or not os.path.isdir(archive_root): | 
|  | return False | 
|  |  | 
|  | # The archive root will not be updated for any remote builds, but will be | 
|  | # valid for local builds. We detect this by using an implementation detail | 
|  | # of the rules_apple bundler: archives will always be transformed from | 
|  | # <name>.unprocessed.zip (locally or remotely) to <name>.archive-root. | 
|  | # | 
|  | # Thus if the mod time on the archive root is not greater than the mod | 
|  | # time on the on the zip, the archive root is not valid. Remote builds | 
|  | # will end up copying the <name>.unprocessed.zip but not the | 
|  | # <name>.archive-root, making this a valid temporary solution. | 
|  | # | 
|  | # In the future, it would be better to have this handled by the rules; | 
|  | # until then this should suffice as a work around to improve build times. | 
|  | unprocessed_zip = os.path.join(os.path.dirname(archive_root), | 
|  | '%s.unprocessed.zip' % bundle_name) | 
|  | if not os.path.isfile(unprocessed_zip): | 
|  | return False | 
|  | return os.path.getmtime(archive_root) > os.path.getmtime(unprocessed_zip) | 
|  |  | 
|  | def _InstallEmbeddedBundlesIfNecessary(self, output_data): | 
|  | """Install embedded bundles next to the current target's output.""" | 
|  |  | 
|  | # In order to find and load symbols for the binary installed on device, | 
|  | # Instruments needs to "see" it in Spotlight index somewhere on the local | 
|  | # filesystem. This is only needed for on-device instrumentation. | 
|  | # | 
|  | # Unfortunatelly, it does not seem to be possible to detect when a build is | 
|  | # being made for profiling, thus we can't exclude this step for on-device | 
|  | # non-profiling builds. | 
|  |  | 
|  | if self.is_simulator or ('embedded_bundles' not in output_data): | 
|  | return | 
|  |  | 
|  | timer = Timer('Installing embedded bundles', | 
|  | 'installing_embedded_bundles').Start() | 
|  |  | 
|  | for bundle_info in output_data['embedded_bundles']: | 
|  | bundle_name = bundle_info['bundle_name'] | 
|  | bundle_extension = bundle_info['bundle_extension'] | 
|  | full_name = bundle_name + bundle_extension | 
|  | output_path = os.path.join(self.built_products_dir, full_name) | 
|  | # TODO(b/68936732): See if copying just the binary (not the whole bundle) | 
|  | # is enough to make Instruments work. | 
|  | if self._IsValidArtifactArchiveRoot(bundle_info['archive_root'], | 
|  | bundle_name): | 
|  | source_path = os.path.join(bundle_info['archive_root'], full_name) | 
|  | self._RsyncBundle(full_name, source_path, output_path) | 
|  | else: | 
|  | # Try to find the embedded bundle within the installed main bundle. | 
|  | bundle_path = self._FindEmbeddedBundleInMain(bundle_name, | 
|  | bundle_extension) | 
|  | if bundle_path: | 
|  | self._RsyncBundle(full_name, bundle_path, output_path) | 
|  | else: | 
|  | _PrintXcodeWarning('Could not find bundle %s in main bundle. ' % | 
|  | (full_name) + | 
|  | 'Device-level Instruments debugging will be ' | 
|  | 'disabled for this bundle. Please report a ' | 
|  | 'Tulsi bug and attach a full Xcode build log.') | 
|  |  | 
|  | timer.End() | 
|  |  | 
|  | # Maps extensions to anticipated subfolders. | 
|  | _EMBEDDED_BUNDLE_PATHS = { | 
|  | '.appex': 'PlugIns', | 
|  | '.framework': 'Frameworks' | 
|  | } | 
|  |  | 
|  | def _FindEmbeddedBundleInMain(self, bundle_name, bundle_extension): | 
|  | """Retrieves the first embedded bundle found within our main bundle.""" | 
|  | main_bundle = os.environ.get('EXECUTABLE_FOLDER_PATH') | 
|  |  | 
|  | if not main_bundle: | 
|  | return None | 
|  |  | 
|  | main_bundle_path = os.path.join(self.built_products_dir, | 
|  | main_bundle) | 
|  |  | 
|  | return self._FindEmbeddedBundle(bundle_name, | 
|  | bundle_extension, | 
|  | main_bundle_path) | 
|  |  | 
|  | def _FindEmbeddedBundle(self, bundle_name, bundle_extension, bundle_path): | 
|  | """Retrieves the first embedded bundle found within this bundle path.""" | 
|  | embedded_subfolder = self._EMBEDDED_BUNDLE_PATHS.get(bundle_extension) | 
|  |  | 
|  | if not embedded_subfolder: | 
|  | return None | 
|  |  | 
|  | projected_bundle_path = os.path.join(bundle_path, | 
|  | embedded_subfolder, | 
|  | bundle_name + bundle_extension) | 
|  |  | 
|  | if os.path.isdir(projected_bundle_path): | 
|  | return projected_bundle_path | 
|  |  | 
|  | # For frameworks not in the main app bundle, and possibly other executable | 
|  | # bundle content in the future, we recurse through every .appex in PlugIns | 
|  | # to find those frameworks. | 
|  | # | 
|  | # This won't support frameworks that could potentially have the same name | 
|  | # but are different between the app and extensions, but we intentionally | 
|  | # choose not to handle that case. Xcode build system only supports | 
|  | # uniquely named frameworks, and we shouldn't confuse the dynamic loader | 
|  | # with frameworks that have the same image names but different content. | 
|  | appex_root_path = os.path.join(bundle_path, 'PlugIns') | 
|  | if not os.path.isdir(appex_root_path): | 
|  | return None | 
|  |  | 
|  | # Find each directory within appex_root_path and attempt to find a bundle. | 
|  | # If one can't be found, return None. | 
|  | appex_dirs = os.listdir(appex_root_path) | 
|  | for appex_dir in appex_dirs: | 
|  | appex_path = os.path.join(appex_root_path, appex_dir) | 
|  | path = self._FindEmbeddedBundle(bundle_name, | 
|  | bundle_extension, | 
|  | appex_path) | 
|  | if path: | 
|  | return path | 
|  | return None | 
|  |  | 
|  | def _InstallGeneratedHeaders(self, outputs): | 
|  | """Invokes install_genfiles.py to install generated Bazel files.""" | 
|  | genfiles_timer = Timer('Installing generated headers', | 
|  | 'installing_generated_headers').Start() | 
|  | # Resolve the path to the install_genfiles.py script. | 
|  | # It should be in the same directory as this script. | 
|  | path = os.path.join(os.path.dirname(os.path.realpath(__file__)), | 
|  | 'install_genfiles.py') | 
|  |  | 
|  | args = [path, self.bazel_exec_root] | 
|  | args.extend(outputs) | 
|  |  | 
|  | self._PrintVerbose('Spawning subprocess install_genfiles.py to copy ' | 
|  | 'generated files in the background...') | 
|  | process = subprocess.Popen(args) | 
|  | process.wait() | 
|  | genfiles_timer.End() | 
|  |  | 
|  | def _InstallBundle(self, source_path, output_path): | 
|  | """Copies the bundle at source_path to output_path.""" | 
|  | if not os.path.isdir(source_path): | 
|  | return 0, None | 
|  |  | 
|  | if os.path.isdir(output_path): | 
|  | try: | 
|  | shutil.rmtree(output_path) | 
|  | except OSError as e: | 
|  | _PrintXcodeError('Failed to remove stale bundle ""%s". ' | 
|  | '%s' % (output_path, e)) | 
|  | return 700, None | 
|  |  | 
|  | exit_code = self._CopyBundle(os.path.basename(source_path), | 
|  | source_path, | 
|  | output_path) | 
|  | return exit_code, output_path | 
|  |  | 
|  | def _RsyncBundle(self, source_path, full_source_path, output_path): | 
|  | """Rsyncs the given bundle to the given expected output path.""" | 
|  | self._PrintVerbose('Rsyncing %s to %s' % (source_path, output_path)) | 
|  |  | 
|  | # rsync behavior changes based on presence of a trailing slash. | 
|  | if not full_source_path.endswith('/'): | 
|  | full_source_path += '/' | 
|  |  | 
|  | try: | 
|  | # Use -c to check differences by checksum, -v for verbose, | 
|  | # and --delete to delete stale files. | 
|  | # The rest of the flags are the same as -a but without preserving | 
|  | # timestamps, which is done intentionally so the timestamp will | 
|  | # only change when the file is changed. | 
|  | subprocess.check_output(['rsync', | 
|  | '-vcrlpgoD', | 
|  | '--delete', | 
|  | full_source_path, | 
|  | output_path], | 
|  | stderr=subprocess.STDOUT) | 
|  | except subprocess.CalledProcessError as e: | 
|  | _PrintXcodeError('Rsync failed. %s' % e) | 
|  | return 650 | 
|  | return 0 | 
|  |  | 
|  | def _CopyBundle(self, source_path, full_source_path, output_path): | 
|  | """Copies the given bundle to the given expected output path.""" | 
|  | self._PrintVerbose('Copying %s to %s' % (source_path, output_path)) | 
|  | try: | 
|  | CopyOnWrite(full_source_path, output_path, tree=True) | 
|  | except OSError as e: | 
|  | _PrintXcodeError('Copy failed. %s' % e) | 
|  | return 650 | 
|  | return 0 | 
|  |  | 
|  | def _CopyFile(self, source_path, full_source_path, output_path): | 
|  | """Copies the given file to the given expected output path.""" | 
|  | self._PrintVerbose('Copying %s to %s' % (source_path, output_path)) | 
|  | output_path_dir = os.path.dirname(output_path) | 
|  | if not os.path.exists(output_path_dir): | 
|  | try: | 
|  | os.makedirs(output_path_dir) | 
|  | except OSError as e: | 
|  | _PrintXcodeError('Failed to create output directory "%s". ' | 
|  | '%s' % (output_path_dir, e)) | 
|  | return 650 | 
|  | try: | 
|  | CopyOnWrite(full_source_path, output_path) | 
|  | except OSError as e: | 
|  | _PrintXcodeError('Copy failed. %s' % e) | 
|  | return 650 | 
|  | return 0 | 
|  |  | 
|  | def _UnpackTarget(self, bundle_path, output_path, bundle_subpath): | 
|  | """Unpacks generated bundle into the given expected output path.""" | 
|  | self._PrintVerbose('Unpacking %s to %s' % (bundle_path, output_path)) | 
|  |  | 
|  | if not os.path.isfile(bundle_path): | 
|  | _PrintXcodeError('Generated bundle not found at "%s"' % bundle_path) | 
|  | return 670 | 
|  |  | 
|  | if os.path.isdir(output_path): | 
|  | try: | 
|  | shutil.rmtree(output_path) | 
|  | except OSError as e: | 
|  | _PrintXcodeError('Failed to remove stale output directory ""%s". ' | 
|  | '%s' % (output_path, e)) | 
|  | return 600 | 
|  |  | 
|  | # We need to handle IPAs (from {ios, tvos}_application) differently from | 
|  | # ZIPs (from the other bundled rules) because they output slightly different | 
|  | # directory structures. | 
|  | is_ipa = bundle_path.endswith('.ipa') | 
|  |  | 
|  | with zipfile.ZipFile(bundle_path, 'r') as zf: | 
|  | for item in zf.infolist(): | 
|  | filename = item.filename | 
|  |  | 
|  | # Support directories do not seem to be needed by the debugger and are | 
|  | # skipped. | 
|  | basedir = filename.split(os.sep)[0] | 
|  | if basedir.endswith('Support') or basedir.endswith('Support2'): | 
|  | continue | 
|  |  | 
|  | if len(filename) < len(bundle_subpath): | 
|  | continue | 
|  |  | 
|  | attributes = (item.external_attr >> 16) & 0o777 | 
|  | self._PrintVerbose('Extracting %s (%o)' % (filename, attributes), | 
|  | level=1) | 
|  |  | 
|  | if not filename.startswith(bundle_subpath): | 
|  | _PrintXcodeWarning('Mismatched extraction path. Bundle content ' | 
|  | 'at "%s" expected to have subpath of "%s"' % | 
|  | (filename, bundle_subpath)) | 
|  |  | 
|  | dir_components = self._SplitPathComponents(filename) | 
|  |  | 
|  | # Get the file's path, ignoring the payload components if the archive | 
|  | # is an IPA. | 
|  | if is_ipa: | 
|  | subpath = os.path.join(*dir_components[2:]) | 
|  | else: | 
|  | subpath = os.path.join(*dir_components[1:]) | 
|  | target_path = os.path.join(output_path, subpath) | 
|  |  | 
|  | # Ensure the target directory exists. | 
|  | try: | 
|  | target_dir = os.path.dirname(target_path) | 
|  | if not os.path.isdir(target_dir): | 
|  | os.makedirs(target_dir) | 
|  | except OSError as e: | 
|  | _PrintXcodeError( | 
|  | 'Failed to create target path "%s" during extraction. %s' % ( | 
|  | target_path, e)) | 
|  | return 671 | 
|  |  | 
|  | # If the archive item looks like a file, extract it. | 
|  | if not filename.endswith(os.sep): | 
|  | with zf.open(item) as src, file(target_path, 'wb') as dst: | 
|  | shutil.copyfileobj(src, dst) | 
|  |  | 
|  | # Patch up the extracted file's attributes to match the zip content. | 
|  | if attributes: | 
|  | os.chmod(target_path, attributes) | 
|  |  | 
|  | return 0 | 
|  |  | 
|  | def _InstallDSYMBundles(self, output_dir, outputs_data): | 
|  | """Copies any generated dSYM bundles to the given directory.""" | 
|  | # Indicates that our aspect reports a dSYM was generated for this build. | 
|  | primary_output_data = outputs_data[0] | 
|  | has_dsym = primary_output_data['has_dsym'] | 
|  |  | 
|  | if not has_dsym: | 
|  | return 0, None | 
|  |  | 
|  | # Start the timer now that we know we have dSYM bundles to install. | 
|  | timer = Timer('Installing DSYM bundles', 'installing_dsym').Start() | 
|  |  | 
|  | # Declares the Xcode-generated name of our main target's dSYM. | 
|  | # This environment variable is always set, for any possible Xcode output | 
|  | # that could generate a dSYM bundle. | 
|  | # | 
|  | # Note that this may differ from the Bazel name as Tulsi may modify the | 
|  | # Xcode `BUNDLE_NAME`, so we need to make sure we use Bazel as the source | 
|  | # of truth for Bazel's dSYM name, but copy it over to where Xcode expects. | 
|  | xcode_target_dsym = os.environ.get('DWARF_DSYM_FILE_NAME') | 
|  | dsym_to_process = set() | 
|  | if xcode_target_dsym: | 
|  | dsym_path = primary_output_data.get('dsym_path') | 
|  | if dsym_path: | 
|  | dsym_to_process.add((dsym_path, xcode_target_dsym)) | 
|  | else: | 
|  | _PrintXcodeWarning( | 
|  | 'Unable to resolve dSYM paths for main bundle %s' | 
|  | % primary_output_data) | 
|  |  | 
|  | # Collect additional dSYM bundles generated by the dependencies of this | 
|  | # build such as extensions or frameworks. | 
|  | child_dsyms = set() | 
|  | for data in outputs_data: | 
|  | for bundle_info in data.get('embedded_bundles', []): | 
|  | if not bundle_info['has_dsym']: | 
|  | continue | 
|  | dsym_path = bundle_info.get('dsym_path') | 
|  | if dsym_path: | 
|  | child_dsyms.add((dsym_path, os.path.basename(dsym_path))) | 
|  | else: | 
|  | _PrintXcodeWarning( | 
|  | 'Unable to resolve dSYM paths for embedded bundle %s' | 
|  | % bundle_info) | 
|  | dsym_to_process.update(child_dsyms) | 
|  |  | 
|  | dsyms_found = [] | 
|  | for input_dsym_full_path, xcode_dsym_name in dsym_to_process: | 
|  | output_full_path = os.path.join(output_dir, xcode_dsym_name) | 
|  | exit_code, path = self._InstallBundle(input_dsym_full_path, | 
|  | output_full_path) | 
|  | if exit_code: | 
|  | _PrintXcodeWarning('Failed to install dSYM to "%s" (%s)' | 
|  | % (input_dsym_full_path, exit_code)) | 
|  | elif path is None: | 
|  | _PrintXcodeWarning('Did not find a dSYM bundle at %s' | 
|  | % input_dsym_full_path) | 
|  | else: | 
|  | dsyms_found.append(path) | 
|  |  | 
|  | timer.End() | 
|  | return 0, dsyms_found | 
|  |  | 
|  | def _ResignBundle(self, bundle_path, signing_identity, entitlements=None): | 
|  | """Re-signs the bundle with the given signing identity and entitlements.""" | 
|  | if not self.codesigning_allowed: | 
|  | return 0 | 
|  |  | 
|  | timer = Timer('\tSigning ' + bundle_path, 'signing_bundle').Start() | 
|  | command = [ | 
|  | 'xcrun', | 
|  | 'codesign', | 
|  | '-f', | 
|  | '--timestamp=none', | 
|  | '-s', | 
|  | signing_identity, | 
|  | ] | 
|  |  | 
|  | if entitlements: | 
|  | command.extend(['--entitlements', entitlements]) | 
|  | else: | 
|  | command.append('--preserve-metadata=entitlements') | 
|  |  | 
|  | command.append(bundle_path) | 
|  |  | 
|  | returncode, output = self._RunSubprocess(command) | 
|  | timer.End() | 
|  | if returncode: | 
|  | _PrintXcodeError('Re-sign command %r failed. %s' % (command, output)) | 
|  | return 800 + returncode | 
|  | return 0 | 
|  |  | 
|  | def _ResignTestArtifacts(self): | 
|  | """Resign test related artifacts that Xcode injected into the outputs.""" | 
|  | if not self.is_test: | 
|  | return 0 | 
|  | # Extract the signing identity from the bundle at the expected output path | 
|  | # since that's where the signed bundle from bazel was placed. | 
|  | signing_identity = self._ExtractSigningIdentity(self.artifact_output_path) | 
|  | if not signing_identity: | 
|  | return 800 | 
|  |  | 
|  | exit_code = 0 | 
|  | timer = Timer('Re-signing injected test host artifacts', | 
|  | 'resigning_test_host').Start() | 
|  |  | 
|  | if self.test_host_binary: | 
|  | # For Unit tests, we need to resign the frameworks that Xcode injected | 
|  | # into the test host bundle. | 
|  | test_host_bundle = os.path.dirname(self.test_host_binary) | 
|  | exit_code = self._ResignXcodeTestFrameworks( | 
|  | test_host_bundle, signing_identity) | 
|  | else: | 
|  | # For UI tests, we need to resign the UI test runner app and the | 
|  | # frameworks that Xcode injected into the runner app. The UI Runner app | 
|  | # also needs to be signed with entitlements. | 
|  | exit_code = self._ResignXcodeTestFrameworks( | 
|  | self.codesigning_folder_path, signing_identity) | 
|  | if exit_code == 0: | 
|  | entitlements_path = self._InstantiateUIRunnerEntitlements() | 
|  | if entitlements_path: | 
|  | exit_code = self._ResignBundle( | 
|  | self.codesigning_folder_path, | 
|  | signing_identity, | 
|  | entitlements_path) | 
|  | else: | 
|  | _PrintXcodeError('Could not instantiate UI runner entitlements.') | 
|  | exit_code = 800 | 
|  |  | 
|  | timer.End() | 
|  | return exit_code | 
|  |  | 
|  | def _ResignXcodeTestFrameworks(self, bundle, signing_identity): | 
|  | """Re-signs the support frameworks injected by Xcode in the given bundle.""" | 
|  | if not self.codesigning_allowed: | 
|  | return 0 | 
|  |  | 
|  | for framework in XCODE_INJECTED_FRAMEWORKS: | 
|  | framework_path = os.path.join( | 
|  | bundle, 'Frameworks', framework) | 
|  | if os.path.isdir(framework_path) or os.path.isfile(framework_path): | 
|  | exit_code = self._ResignBundle(framework_path, signing_identity) | 
|  | if exit_code != 0: | 
|  | return exit_code | 
|  | return 0 | 
|  |  | 
|  | def _InstantiateUIRunnerEntitlements(self): | 
|  | """Substitute team and bundle identifiers into UI runner entitlements. | 
|  |  | 
|  | This method throws an IOError exception if the template wasn't found in | 
|  | its expected location, or an OSError if the expected output folder could | 
|  | not be created. | 
|  |  | 
|  | Returns: | 
|  | The path to where the entitlements file was generated. | 
|  | """ | 
|  | if not self.codesigning_allowed: | 
|  | return None | 
|  | if not os.path.exists(self.derived_sources_folder_path): | 
|  | os.makedirs(self.derived_sources_folder_path) | 
|  |  | 
|  | output_file = os.path.join( | 
|  | self.derived_sources_folder_path, | 
|  | self.bazel_product_name + '_UIRunner.entitlements') | 
|  | if os.path.exists(output_file): | 
|  | os.remove(output_file) | 
|  |  | 
|  | with open(self.runner_entitlements_template, 'r') as template: | 
|  | contents = template.read() | 
|  | contents = contents.replace( | 
|  | '$(TeamIdentifier)', | 
|  | self._ExtractSigningTeamIdentifier(self.artifact_output_path)) | 
|  | contents = contents.replace( | 
|  | '$(BundleIdentifier)', | 
|  | self._ExtractSigningBundleIdentifier(self.artifact_output_path)) | 
|  | with open(output_file, 'w') as output: | 
|  | output.write(contents) | 
|  | return output_file | 
|  |  | 
|  | def _ExtractSigningIdentity(self, signed_bundle): | 
|  | """Returns the identity used to sign the given bundle path.""" | 
|  | return self._ExtractSigningAttribute(signed_bundle, 'Authority') | 
|  |  | 
|  | def _ExtractSigningTeamIdentifier(self, signed_bundle): | 
|  | """Returns the team identifier used to sign the given bundle path.""" | 
|  | return self._ExtractSigningAttribute(signed_bundle, 'TeamIdentifier') | 
|  |  | 
|  | def _ExtractSigningBundleIdentifier(self, signed_bundle): | 
|  | """Returns the bundle identifier used to sign the given bundle path.""" | 
|  | return self._ExtractSigningAttribute(signed_bundle, 'Identifier') | 
|  |  | 
|  | def _ExtractSigningAttribute(self, signed_bundle, attribute): | 
|  | """Returns the attribute used to sign the given bundle path.""" | 
|  | if not self.codesigning_allowed: | 
|  | return '<CODE_SIGNING_ALLOWED=NO>' | 
|  |  | 
|  | cached = self.codesign_attributes.get(signed_bundle) | 
|  | if cached: | 
|  | return cached.Get(attribute) | 
|  |  | 
|  | timer = Timer('\tExtracting signature for ' + signed_bundle, | 
|  | 'extracting_signature').Start() | 
|  | output = subprocess.check_output(['xcrun', | 
|  | 'codesign', | 
|  | '-dvv', | 
|  | signed_bundle], | 
|  | stderr=subprocess.STDOUT) | 
|  | timer.End() | 
|  |  | 
|  | bundle_attributes = CodesignBundleAttributes(output) | 
|  | self.codesign_attributes[signed_bundle] = bundle_attributes | 
|  | return bundle_attributes.Get(attribute) | 
|  |  | 
|  | def _UpdateLLDBInit(self, clear_source_map=False): | 
|  | """Updates ~/.lldbinit-tulsiproj to enable debugging of Bazel binaries.""" | 
|  |  | 
|  | # Make sure a reference to ~/.lldbinit-tulsiproj exists in ~/.lldbinit or | 
|  | # ~/.lldbinit-Xcode. Priority is given to ~/.lldbinit-Xcode if it exists, | 
|  | # otherwise the bootstrapping will be written to ~/.lldbinit. | 
|  | BootstrapLLDBInit() | 
|  |  | 
|  | with open(TULSI_LLDBINIT_FILE, 'w') as out: | 
|  | out.write('# This file is autogenerated by Tulsi and should not be ' | 
|  | 'edited.\n') | 
|  |  | 
|  | if clear_source_map: | 
|  | out.write('settings clear target.source-map\n') | 
|  | return 0 | 
|  |  | 
|  | if self.normalized_prefix_map: | 
|  | source_map = ('./', self._NormalizePath(self.workspace_root)) | 
|  | out.write('# This maps the normalized root to that used by ' | 
|  | '%r.\n' % os.path.basename(self.project_file_path)) | 
|  | else: | 
|  | # NOTE: settings target.source-map is different from | 
|  | # DBGSourcePathRemapping; the former is an LLDB target-level | 
|  | # remapping API that rewrites breakpoints, the latter is an LLDB | 
|  | # module-level remapping API that changes DWARF debug info in memory. | 
|  | # | 
|  | # If we had multiple remappings, it would not make sense for the | 
|  | # two APIs to share the same mappings. They have very different | 
|  | # side-effects in how they individually handle debug information. | 
|  | source_map = self._ExtractTargetSourceMap() | 
|  | out.write('# This maps Bazel\'s execution root to that used by ' | 
|  | '%r.\n' % os.path.basename(self.project_file_path)) | 
|  |  | 
|  | out.write('settings set target.source-map "%s" "%s"\n' % source_map) | 
|  |  | 
|  | return 0 | 
|  |  | 
|  | def _DWARFdSYMBinaries(self, dsym_bundle_path): | 
|  | """Returns an array of abs paths to DWARF binaries in the dSYM bundle. | 
|  |  | 
|  | Args: | 
|  | dsym_bundle_path: absolute path to the dSYM bundle. | 
|  |  | 
|  | Returns: | 
|  | str[]: a list of strings representing the absolute paths to each binary | 
|  | found within the dSYM bundle. | 
|  | """ | 
|  | dwarf_dir = os.path.join(dsym_bundle_path, | 
|  | 'Contents', | 
|  | 'Resources', | 
|  | 'DWARF') | 
|  |  | 
|  | dsym_binaries = [] | 
|  |  | 
|  | for f in os.listdir(dwarf_dir): | 
|  | # Ignore hidden files, such as .DS_Store files. | 
|  | if not f.startswith('.'): | 
|  | # Append full path info. | 
|  | dsym_binary = os.path.join(dwarf_dir, f) | 
|  | dsym_binaries.append(dsym_binary) | 
|  |  | 
|  | return dsym_binaries | 
|  |  | 
|  | def _UUIDInfoForBinary(self, source_binary_path): | 
|  | """Returns exit code of dwarfdump along with every UUID + arch found. | 
|  |  | 
|  | Args: | 
|  | source_binary_path: absolute path to the binary file. | 
|  |  | 
|  | Returns: | 
|  | (Int, str[(str, str)]): a tuple containing the return code of dwarfdump | 
|  | as its first element, and a list of strings | 
|  | representing each UUID found for each given | 
|  | binary slice found within the binary with its | 
|  | given architecture, if no error has occcured. | 
|  | """ | 
|  |  | 
|  | returncode, output = self._RunSubprocess([ | 
|  | 'xcrun', | 
|  | 'dwarfdump', | 
|  | '--uuid', | 
|  | source_binary_path | 
|  | ]) | 
|  | if returncode: | 
|  | _PrintXcodeWarning('dwarfdump returned %d while finding the UUID for %s' | 
|  | % (returncode, source_binary_path)) | 
|  | return (returncode, []) | 
|  |  | 
|  | # All UUIDs for binary slices will be returned as the second from left, | 
|  | # from output; "UUID: D4DE5AA2-79EE-36FE-980C-755AED318308 (x86_64) | 
|  | # /Applications/Calendar.app/Contents/MacOS/Calendar" | 
|  |  | 
|  | uuids_found = [] | 
|  | for dwarfdump_output in output.split('\n'): | 
|  | if not dwarfdump_output: | 
|  | continue | 
|  | found_output = re.match(r'^(?:UUID: )([^ ]+) \(([^)]+)', dwarfdump_output) | 
|  | if not found_output: | 
|  | continue | 
|  | found_uuid = found_output.group(1) | 
|  | if not found_uuid: | 
|  | continue | 
|  | found_arch = found_output.group(2) | 
|  | if not found_arch: | 
|  | continue | 
|  | uuids_found.append((found_uuid, found_arch)) | 
|  |  | 
|  | return (0, uuids_found) | 
|  |  | 
|  | def _CreateUUIDPlist(self, dsym_bundle_path, uuid, arch, source_maps): | 
|  | """Creates a UUID.plist in a dSYM bundle to redirect sources. | 
|  |  | 
|  | Args: | 
|  | dsym_bundle_path: absolute path to the dSYM bundle. | 
|  | uuid: string representing the UUID of the binary slice with paths to | 
|  | remap in the dSYM bundle. | 
|  | arch: the architecture of the binary slice. | 
|  | source_maps:  list of tuples representing all absolute paths to source | 
|  | files compiled by Bazel as strings ($0) associated with the | 
|  | paths to Xcode-visible sources used for the purposes of | 
|  | Tulsi debugging as strings ($1). | 
|  |  | 
|  | Returns: | 
|  | Bool: True if no error was found, or False, representing a failure to | 
|  | write when creating the plist. | 
|  | """ | 
|  |  | 
|  | # Create a UUID plist at (dsym_bundle_path)/Contents/Resources/. | 
|  | remap_plist = os.path.join(dsym_bundle_path, | 
|  | 'Contents', | 
|  | 'Resources', | 
|  | '%s.plist' % uuid) | 
|  |  | 
|  | # Via an XML plist, add the mappings from  _ExtractTargetSourceMap(). | 
|  | try: | 
|  | with open(remap_plist, 'w') as out: | 
|  | out.write('<?xml version="1.0" encoding="UTF-8"?>\n' | 
|  | '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" ' | 
|  | '"http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n' | 
|  | '<plist version="1.0">\n' | 
|  | '<dict>\n' | 
|  | '<key>DBGSourcePathRemapping</key>\n' | 
|  | '<dict>\n') | 
|  | for source_map in source_maps: | 
|  | # Add the mapping as a DBGSourcePathRemapping to the UUID plist here. | 
|  | out.write('<key>%s</key>\n<string>%s</string>\n' % source_map) | 
|  |  | 
|  | # Make sure that we also set DBGVersion to 3. | 
|  | out.write('</dict>\n' | 
|  | '<key>DBGVersion</key>\n' | 
|  | '<string>3</string>\n' | 
|  | '</dict>\n' | 
|  | '</plist>\n') | 
|  | except OSError as e: | 
|  | _PrintXcodeError('Failed to write %s, received error %s' % | 
|  | (remap_plist, e)) | 
|  | return False | 
|  |  | 
|  | # Update the dSYM symbol cache with a reference to this dSYM bundle. | 
|  | err_msg = self.update_symbol_cache.UpdateUUID(uuid, | 
|  | dsym_bundle_path, | 
|  | arch) | 
|  | if err_msg: | 
|  | _PrintXcodeWarning('Attempted to save (uuid, dsym_bundle_path, arch) ' | 
|  | 'to DBGShellCommands\' dSYM cache, but got error ' | 
|  | '\"%s\".' % err_msg) | 
|  |  | 
|  | return True | 
|  |  | 
|  | def _CleanExistingDSYMs(self): | 
|  | """Clean dSYM bundles that were left over from a previous build.""" | 
|  |  | 
|  | output_dir = self.built_products_dir | 
|  | output_dir_list = os.listdir(output_dir) | 
|  | for item in output_dir_list: | 
|  | if item.endswith('.dSYM'): | 
|  | shutil.rmtree(os.path.join(output_dir, item)) | 
|  |  | 
|  | def _PlistdSYMPaths(self, dsym_bundle_path): | 
|  | """Adds Plists to a given dSYM bundle to redirect DWARF data.""" | 
|  |  | 
|  | # Retrieve the paths that we are expected to remap. | 
|  |  | 
|  | # Always include a direct path from the execroot to Xcode-visible sources. | 
|  | source_maps = [self._ExtractTargetSourceMap()] | 
|  |  | 
|  | # Remap relative paths from the workspace root. | 
|  | if self.normalized_prefix_map: | 
|  | # Take the normalized path and map that to Xcode-visible sources. | 
|  | source_maps.append(('./', self._NormalizePath(self.workspace_root))) | 
|  |  | 
|  | # Find the binaries within the dSYM bundle. UUIDs will match that of the | 
|  | # binary it was based on. | 
|  | dsym_binaries = self._DWARFdSYMBinaries(dsym_bundle_path) | 
|  |  | 
|  | if not dsym_binaries: | 
|  | _PrintXcodeWarning('Could not find the binaries that the dSYM %s was ' | 
|  | 'based on to determine DWARF binary slices to patch. ' | 
|  | 'Debugging will probably fail.' % (dsym_bundle_path)) | 
|  | return 404 | 
|  |  | 
|  | # Find the binary slice UUIDs with dwarfdump from each binary. | 
|  | for source_binary_path in dsym_binaries: | 
|  |  | 
|  | returncode, uuid_info_found = self._UUIDInfoForBinary(source_binary_path) | 
|  | if returncode: | 
|  | return returncode | 
|  |  | 
|  | # Create a plist per UUID, each indicating a binary slice to remap paths. | 
|  | for uuid, arch in uuid_info_found: | 
|  | plist_created = self._CreateUUIDPlist(dsym_bundle_path, | 
|  | uuid, | 
|  | arch, | 
|  | source_maps) | 
|  | if not plist_created: | 
|  | return 405 | 
|  |  | 
|  | return 0 | 
|  |  | 
|  | def _NormalizePath(self, path): | 
|  | """Returns paths with a common form, normalized with a trailing slash. | 
|  |  | 
|  | Args: | 
|  | path: a file system path given in the form of a string. | 
|  |  | 
|  | Returns: | 
|  | str: a normalized string with a trailing slash, based on |path|. | 
|  | """ | 
|  | return os.path.normpath(path) + os.sep | 
|  |  | 
|  | def _ExtractTargetSourceMap(self, normalize=True): | 
|  | """Extracts the source path as a tuple associated with the WORKSPACE path. | 
|  |  | 
|  | Args: | 
|  | normalize: Defines if all paths should be normalized. Preferred for APIs | 
|  | like DBGSourcePathRemapping and target.source-map but won't | 
|  | work for the purposes of -fdebug-prefix-map. | 
|  |  | 
|  | Returns: | 
|  | None: if an error occurred. | 
|  | (str, str): a single tuple representing all absolute paths to source | 
|  | files compiled by Bazel as strings ($0) associated with | 
|  | the paths to Xcode-visible sources used for the purposes | 
|  | of Tulsi debugging as strings ($1). | 
|  | """ | 
|  | # All paths route to the "workspace root" for sources visible from Xcode. | 
|  | sm_destpath = self.workspace_root | 
|  | if normalize: | 
|  | sm_destpath = self._NormalizePath(sm_destpath) | 
|  |  | 
|  | # Add a redirection for the Bazel execution root, the path where sources | 
|  | # are referenced by Bazel. | 
|  | sm_execroot = self.bazel_exec_root | 
|  | if normalize: | 
|  | sm_execroot = self._NormalizePath(sm_execroot) | 
|  | return (sm_execroot, sm_destpath) | 
|  |  | 
|  | def _LinkTulsiWorkspace(self): | 
|  | """Links the Bazel Workspace to the Tulsi Workspace (`tulsi-workspace`).""" | 
|  | tulsi_workspace = os.path.join(self.project_file_path, | 
|  | '.tulsi', | 
|  | 'tulsi-workspace') | 
|  | if os.path.islink(tulsi_workspace): | 
|  | os.unlink(tulsi_workspace) | 
|  | os.symlink(self.bazel_exec_root, tulsi_workspace) | 
|  | if not os.path.exists(tulsi_workspace): | 
|  | _PrintXcodeError( | 
|  | 'Linking Tulsi Workspace to %s failed.' % tulsi_workspace) | 
|  | return -1 | 
|  |  | 
|  | @staticmethod | 
|  | def _SplitPathComponents(path): | 
|  | """Splits the given path into an array of all of its components.""" | 
|  | components = path.split(os.sep) | 
|  | # Patch up the first component if path started with an os.sep | 
|  | if not components[0]: | 
|  | components[0] = os.sep | 
|  | return components | 
|  |  | 
|  | def _RunSubprocess(self, cmd): | 
|  | """Runs the given command as a subprocess, returning (exit_code, output).""" | 
|  | self._PrintVerbose('%r' % cmd, 1) | 
|  | process = subprocess.Popen(cmd, | 
|  | stdout=subprocess.PIPE, | 
|  | stderr=subprocess.STDOUT) | 
|  | output, _ = process.communicate() | 
|  | return (process.returncode, output) | 
|  |  | 
|  | def _PrintVerbose(self, msg, level=0): | 
|  | if self.verbose > level: | 
|  | _PrintUnbuffered(msg) | 
|  |  | 
|  |  | 
|  | def main(argv): | 
|  | build_settings = bazel_build_settings.BUILD_SETTINGS | 
|  | if build_settings is None: | 
|  | _Fatal('Unable to resolve build settings. Please report a Tulsi bug.') | 
|  | return 1 | 
|  | return BazelBuildBridge(build_settings).Run(argv) | 
|  |  | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | # Register the interrupt handler immediately in case we receive SIGINT while | 
|  | # trying to acquire the lock. | 
|  | signal.signal(signal.SIGINT, _InterruptHandler) | 
|  | _LockFileAcquire(_LockFileCreate()) | 
|  | _logger = tulsi_logging.Logger() | 
|  | logger_warning = tulsi_logging.validity_check() | 
|  | if logger_warning: | 
|  | _PrintXcodeWarning(logger_warning) | 
|  | _timer = Timer('Everything', 'complete_build').Start() | 
|  | _exit_code = main(sys.argv) | 
|  | _timer.End() | 
|  | sys.exit(_exit_code) |