Chromium Code Reviews| Index: recipe_modules/bot_update/api.py |
| diff --git a/recipe_modules/bot_update/api.py b/recipe_modules/bot_update/api.py |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..bba7b9b0ae759878a4546f7f448324d821f60240 |
| --- /dev/null |
| +++ b/recipe_modules/bot_update/api.py |
| @@ -0,0 +1,266 @@ |
| +# Copyright 2014 The Chromium Authors. All rights reserved. |
| +# Use of this source code is governed by a BSD-style license that can be |
| +# found in the LICENSE file. |
| + |
| + |
| +"""Recipe module to ensure a checkout is consistant on a bot.""" |
| + |
| +from recipe_engine import recipe_api |
| + |
| + |
| +# This is just for testing, to indicate if a master is using a Git scheduler |
| +# or not. |
| +SVN_MASTERS = ( |
| + 'experimental.svn', |
| +) |
| + |
| + |
| +def jsonish_to_python(spec, is_top=False): |
| + """Turn a json spec into a python parsable object. |
| + |
| + This exists because Gclient specs, while resembling json, is actually |
| + ingested using a python "eval()". Therefore a bit of plumming is required |
| + to turn our newly constructed Gclient spec into a gclient-readable spec. |
| + """ |
| + ret = '' |
| + if is_top: # We're the 'top' level, so treat this dict as a suite. |
| + ret = '\n'.join( |
| + '%s = %s' % (k, jsonish_to_python(spec[k])) for k in sorted(spec) |
| + ) |
| + else: |
| + if isinstance(spec, dict): |
| + ret += '{' |
| + ret += ', '.join( |
| + "%s: %s" % (repr(str(k)), jsonish_to_python(spec[k])) |
| + for k in sorted(spec) |
| + ) |
| + ret += '}' |
| + elif isinstance(spec, list): |
| + ret += '[' |
| + ret += ', '.join(jsonish_to_python(x) for x in spec) |
| + ret += ']' |
| + elif isinstance(spec, basestring): |
| + ret = repr(str(spec)) |
| + else: |
| + ret = repr(spec) |
| + return ret |
| + |
| + |
| +class BotUpdateApi(recipe_api.RecipeApi): |
| + |
| + def __init__(self, *args, **kwargs): |
|
iannucci
2016/01/30 00:40:17
no PROPERTIES ?
|
| + self._properties = {} |
| + super(BotUpdateApi, self).__init__(*args, **kwargs) |
| + |
| + def __call__(self, name, cmd, **kwargs): |
| + """Wrapper for easy calling of bot_update.""" |
| + assert isinstance(cmd, (list, tuple)) |
| + bot_update_path = self.resource('bot_update.py') |
| + kwargs.setdefault('infra_step', True) |
| + return self.m.python(name, bot_update_path, cmd, **kwargs) |
| + |
| + @property |
| + def properties(self): |
| + return self._properties |
| + |
| + def ensure_checkout(self, gclient_config=None, suffix=None, |
| + patch=True, update_presentation=True, |
| + force=False, patch_root=None, no_shallow=False, |
| + with_branch_heads=False, refs=None, |
| + patch_project_roots=None, patch_oauth2=False, |
| + output_manifest=True, clobber=False, |
| + root_solution_revision=None, **kwargs): |
| + refs = refs or [] |
| + # We can re-use the gclient spec from the gclient module, since all the |
| + # data bot_update needs is already configured into the gclient spec. |
| + cfg = gclient_config or self.m.gclient.c |
| + spec_string = jsonish_to_python(cfg.as_jsonish(), True) |
| + |
| + # Construct our bot_update command. This basically be inclusive of |
| + # everything required for bot_update to know: |
| + root = patch_root |
| + if root is None: |
| + root = cfg.solutions[0].name |
| + additional = self.m.rietveld.calculate_issue_root(patch_project_roots) |
| + if additional: |
| + root = self.m.path.join(root, additional) |
| + |
| + if patch: |
| + issue = self.m.properties.get('issue') |
| + patchset = self.m.properties.get('patchset') |
| + patch_url = self.m.properties.get('patch_url') |
| + gerrit_repo = self.m.properties.get('repository') |
| + gerrit_ref = self.m.properties.get('event.patchSet.ref') |
| + else: |
| + # The trybot recipe sometimes wants to de-apply the patch. In which case |
| + # we pretend the issue/patchset/patch_url never existed. |
| + issue = patchset = patch_url = email_file = key_file = None |
| + gerrit_repo = gerrit_ref = None |
| + |
| + # Issue and patchset must come together. |
| + if issue: |
| + assert patchset |
| + if patchset: |
| + assert issue |
| + if patch_url: |
| + # If patch_url is present, bot_update will actually ignore issue/ps. |
| + issue = patchset = None |
| + |
| + # The gerrit_ref and gerrit_repo must be together or not at all. If one is |
| + # missing, clear both of them. |
| + if not gerrit_ref or not gerrit_repo: |
| + gerrit_repo = gerrit_ref = None |
| + assert (gerrit_ref != None) == (gerrit_repo != None) |
| + |
| + # Point to the oauth2 auth files if specified. |
| + # These paths are where the bots put their credential files. |
| + if patch_oauth2: |
| + email_file = self.m.path['build'].join( |
| + 'site_config', '.rietveld_client_email') |
| + key_file = self.m.path['build'].join( |
| + 'site_config', '.rietveld_secret_key') |
| + else: |
| + email_file = key_file = None |
| + |
| + rev_map = {} |
| + if self.m.gclient.c: |
| + rev_map = self.m.gclient.c.got_revision_mapping.as_jsonish() |
| + |
| + flags = [ |
| + # 1. What do we want to check out (spec/root/rev/rev_map). |
| + ['--spec', spec_string], |
| + ['--root', root], |
| + ['--revision_mapping_file', self.m.json.input(rev_map)], |
| + |
| + # 2. How to find the patch, if any (issue/patchset/patch_url). |
| + ['--issue', issue], |
| + ['--patchset', patchset], |
| + ['--patch_url', patch_url], |
| + ['--rietveld_server', self.m.properties.get('rietveld')], |
| + ['--gerrit_repo', gerrit_repo], |
| + ['--gerrit_ref', gerrit_ref], |
| + ['--apply_issue_email_file', email_file], |
| + ['--apply_issue_key_file', key_file], |
| + |
| + # 3. Hookups to JSON output back into recipes. |
| + ['--output_json', self.m.json.output()],] |
| + |
| + |
| + # Collect all fixed revisions to simulate them in the json output. |
| + # Fixed revision are the explicit input revisions of bot_update.py, i.e. |
| + # every command line parameter "--revision name@value". |
| + fixed_revisions = {} |
| + |
| + revisions = {} |
| + for solution in cfg.solutions: |
| + if solution.revision: |
| + revisions[solution.name] = solution.revision |
| + elif solution == cfg.solutions[0]: |
| + revisions[solution.name] = ( |
| + self.m.properties.get('parent_got_revision') or |
| + self.m.properties.get('revision') or |
| + 'HEAD') |
| + if self.m.gclient.c and self.m.gclient.c.revisions: |
| + revisions.update(self.m.gclient.c.revisions) |
| + if cfg.solutions and root_solution_revision: |
| + revisions[cfg.solutions[0].name] = root_solution_revision |
| + # Allow for overrides required to bisect into rolls. |
| + revisions.update(self.m.properties.get('deps_revision_overrides', {})) |
| + for name, revision in sorted(revisions.items()): |
| + fixed_revision = self.m.gclient.resolve_revision(revision) |
| + if fixed_revision: |
| + fixed_revisions[name] = fixed_revision |
| + flags.append(['--revision', '%s@%s' % (name, fixed_revision)]) |
| + |
| + # Add extra fetch refspecs. |
| + for ref in refs: |
| + flags.append(['--refs', ref]) |
| + |
| + # Filter out flags that are None. |
| + cmd = [item for flag_set in flags |
| + for item in flag_set if flag_set[1] is not None] |
| + |
| + if clobber: |
| + cmd.append('--clobber') |
| + if force: |
| + cmd.append('--force') |
| + if no_shallow: |
| + cmd.append('--no_shallow') |
| + if output_manifest: |
| + cmd.append('--output_manifest') |
| + if with_branch_heads or cfg.with_branch_heads: |
| + cmd.append('--with_branch_heads') |
| + |
| + # Inject Json output for testing. |
| + git_mode = self.m.properties.get('mastername') not in SVN_MASTERS |
| + first_sln = cfg.solutions[0].name |
| + step_test_data = lambda: self.test_api.output_json( |
| + root, first_sln, rev_map, git_mode, force, |
| + self.m.properties.get('fail_patch', False), |
| + output_manifest=output_manifest, fixed_revisions=fixed_revisions) |
| + |
| + # Add suffixes to the step name, if specified. |
| + name = 'bot_update' |
| + if not patch: |
| + name += ' (without patch)' |
| + if suffix: |
| + name += ' - %s' % suffix |
| + |
| + # Ah hah! Now that everything is in place, lets run bot_update! |
| + try: |
| + # 87 and 88 are the 'patch failure' codes for patch download and patch |
| + # apply, respectively. We don't actually use the error codes, and instead |
| + # rely on emitted json to determine cause of failure. |
| + self(name, cmd, step_test_data=step_test_data, |
| + ok_ret=(0, 87, 88), **kwargs) |
| + finally: |
| + step_result = self.m.step.active_result |
| + self._properties = step_result.json.output.get('properties', {}) |
| + |
| + if update_presentation: |
| + # Set properties such as got_revision. |
| + for prop_name, prop_value in self.properties.iteritems(): |
| + step_result.presentation.properties[prop_name] = prop_value |
| + # Add helpful step description in the step UI. |
| + if 'step_text' in step_result.json.output: |
| + step_text = step_result.json.output['step_text'] |
| + step_result.presentation.step_text = step_text |
| + # Add log line output. |
| + if 'log_lines' in step_result.json.output: |
| + for log_name, log_lines in step_result.json.output['log_lines']: |
| + step_result.presentation.logs[log_name] = log_lines.splitlines() |
| + |
| + # Set the "checkout" path for the main solution. |
| + # This is used by the Chromium module to figure out where to look for |
| + # the checkout. |
| + # If there is a patch failure, emit another step that said things failed. |
| + if step_result.json.output.get('patch_failure'): |
| + return_code = step_result.json.output.get('patch_apply_return_code') |
| + if return_code == 3: |
| + # This is download failure, hence an infra failure. |
| + # Sadly, python.failing_step doesn't support kwargs. |
| + self.m.python.inline( |
| + 'Patch failure', |
| + ('import sys;' |
| + 'print "Patch download failed. See bot_update step for details";' |
| + 'sys.exit(1)'), |
| + infra_step=True, |
| + step_test_data=lambda: self.m.raw_io.test_api.output( |
| + 'Patch download failed. See bot_update step for details', |
| + retcode=1) |
| + ) |
| + else: |
| + # This is actual patch failure. |
| + step_result.presentation.properties['failure_type'] = 'PATCH_FAILURE' |
| + self.m.python.failing_step( |
| + 'Patch failure', 'Check the bot_update step for details') |
| + |
| + # bot_update actually just sets root to be the folder name of the |
| + # first solution. |
| + if step_result.json.output['did_run']: |
| + co_root = step_result.json.output['root'] |
| + cwd = kwargs.get('cwd', self.m.path['slave_build']) |
| + if 'checkout' not in self.m.path: |
| + self.m.path['checkout'] = cwd.join(*co_root.split(self.m.path.sep)) |
| + |
| + return step_result |