| """Build step class for setting up a clean build environment.""" |
| |
| import logging |
| import os |
| import re |
| |
| from helpers import gclient_utils |
| from slave import base_step |
| from slave.step import shell_step |
| |
| _GCLIENT_CWD = 'chromium' |
| _GCLIENT_ENV = { |
| # Set libassistant retries to wait for binaries to be compiled |
| # Expected wait time is about 900 sec. |
| 'LIBASSISTANT_FINDARCHIVE_RETRY_COUNT': |
| '15', |
| 'LIBASSISTANT_FINDARCHIVE_RETRY_INTERVAL': |
| '300', |
| # Export CHROME_HEADLESS so that the license agreement is |
| # automatically accepted in //build/android/play_services/update.py. |
| 'CHROME_HEADLESS': |
| '1', |
| |
| # Set this so `gclient sync` does not update the ninja files with GYP. |
| # This call is very expensive and is unecessary: On GN builds, GN is |
| # used to generate the ninja files, and on GYP builds, we do this as a |
| # later step. |
| 'GYP_CHROMIUM_NO_ACTION': |
| '1', |
| |
| # Set this so `gclient sync` will pick up the prebuilt, |
| # instrumented libraries that msan needs to run effectively. |
| # Specifically, gclient sync will run |
| # //third_party/intrumented_libraries/scripts/download_binaries.py$ |
| # to download the prebuilt library binaries. |
| 'GYP_DEFINES': ('use_prebuilt_instrumented_libraries=1 ' |
| 'msan=1 ' |
| 'msan_track_origins=2'), |
| } |
| |
| _FUCHSIA_SYNC_ENV_VAR = 'FUCHSIA_SYNC' |
| |
| _INSERT_SHA = '{INSERT_SHA}' |
| _INSERT_PROJECT = '{INSERT_PROJECT}' |
| |
| _GCLIENT_ERROR_RECOVERY = { |
| 'Cannot fast-forward merge, attempt to rebase?': [ |
| 'git', 'reset', '--hard', _INSERT_SHA |
| ], |
| 'Conflict while rebasing this branch.': ['git', 'am', '--abort'], |
| 'Already in a conflict, i.e. (no branch).': ['git', 'am', '--abort'], |
| 'Command \'git rev-list -n 1 HEAD\' returned non-zero exit status 128': [ |
| 'git', 'pull' |
| ], |
| 'subprocess.CalledProcessError': [ |
| 'git', 'submodule', 'deinit', '--all', '-f' |
| ], |
| 'fetch_post_sync_builddeps.SubmoduleSyncError': [ |
| 'rm', '-rf', _INSERT_PROJECT |
| ], |
| } |
| |
| UNIX_SUCCESS_CODE = 0 |
| |
| |
| class UnhandledGClientSyncError(base_step.StepError): |
| """Exception raised when an unrecognized gclient sync failure happens.""" |
| |
| |
| class GclientSyncStep(shell_step.ShellStep): |
| """Runs `gclient sync` after patches have been applied.""" |
| |
| def __init__(self, **kwargs): |
| """Creates a GclientSyncStep instance. |
| |
| Args: |
| **kwargs: Any additional args to pass to BaseStep. |
| """ |
| shell_step.ShellStep.__init__( |
| self, name='gclient sync', halt_on_failure=True, **kwargs) |
| self._gclient_root = kwargs.get('properties', |
| {}).get('gclient_root', _GCLIENT_CWD) |
| self._fuchsia_sync = kwargs.get('properties', {}).get('fuchsia_sync', False) |
| self._gclient_extra_args = kwargs.get('properties', |
| {}).get('gclient_extra_args') |
| |
| def _setup_for_repo_sync_retry(self, returncode, stdout, stderr): |
| """A function to run before retrying a failed gclient sync.""" |
| del returncode # Unused. |
| |
| for recoverable in _GCLIENT_ERROR_RECOVERY: |
| if recoverable in stderr: |
| reg_exp = r'^(?:\d+> )?_+ (?P<project_dir>[^\s]+) at (?P<sha>.*)$' |
| match = re.search(reg_exp, stderr, re.MULTILINE) |
| # See b/112112992 for context on the error. |
| if match: |
| project_dir = match.group('project_dir') |
| project_dir = os.path.join(self._gclient_root, project_dir) |
| sha = match.group('sha') |
| command = self._insert_template(_GCLIENT_ERROR_RECOVERY[recoverable], |
| _INSERT_SHA, sha) |
| self.exec_subprocess(command, cwd=project_dir) |
| break |
| |
| reg_exp = (r'Command \'git rev-list -n 1 HEAD\' ' |
| r'returned non-zero exit status 128 in (?P<project_dir>.*)') |
| match = re.search(reg_exp, stderr, re.MULTILINE) |
| if match: |
| project_dir = match.group('project_dir') |
| self.exec_subprocess( |
| _GCLIENT_ERROR_RECOVERY[recoverable], cwd=project_dir) |
| break |
| logging.info('Found error in stderr but fail to recover: %s', |
| recoverable) |
| elif recoverable in stdout: |
| reg_exp = (r'subprocess\.CalledProcessError: Command ' |
| r'\'\[\'git\', \'-C\', \'(?P<project_dir>.+)\', ' |
| r'\'submodule\', \'sync\'\]\' returned non-zero exit status') |
| match = re.search(reg_exp, stdout, re.MULTILINE) |
| if match: |
| project_dir = match.group('project_dir') |
| self.exec_subprocess( |
| _GCLIENT_ERROR_RECOVERY[recoverable], cwd=project_dir) |
| break |
| |
| # See b/135531243 for context on the error. |
| reg_exp = r'(?P<project_name>.+) submodules failed to sync' |
| match = re.search(reg_exp, stdout, re.MULTILINE) |
| if match: |
| project_dir = self.get_project_path(match.group('project_name')) |
| command = self._insert_template(_GCLIENT_ERROR_RECOVERY[recoverable], |
| _INSERT_PROJECT, project_dir) |
| self.exec_subprocess(command) |
| break |
| logging.info('Found error in stdout but fail to recover: %s', |
| recoverable) |
| |
| def _insert_template(self, command, template, value): |
| return [w if w != template else value for w in command] |
| |
| def get_commands(self): |
| # Ensures that additions to the gclient env here will remain deterministic. |
| env = dict(_GCLIENT_ENV) |
| if self._fuchsia_sync: |
| env[_FUCHSIA_SYNC_ENV_VAR] = '1' |
| return [ |
| # Bazel uses a shared location for extracting its installation. |
| # Sometimes those installations get corrupt. This forces Bazel to |
| # extract itself for every build. |
| self.ShellCommand(['rm', '-rf', |
| os.path.expanduser('~/.cache/bazel')]), |
| self.ShellCommand(gclient_utils.disable_depot_tools_auto_update_cmd()), |
| self.ShellCommand( # pylint: disable=unexpected-keyword-arg |
| gclient_utils.gclient_sync_cmd( |
| self._executor, |
| jobs=self.get_num_jobs(), |
| extra_args=self._gclient_extra_args, |
| ), |
| cwd=self._gclient_root, |
| env=env, |
| num_retries=3, |
| setup_for_retry=self._setup_for_repo_sync_retry, |
| halt_on_failure=True), |
| ] |