OLD | NEW |
(Empty) | |
| 1 # Copyright 2013 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. |
| 4 |
| 5 from recipe_engine import recipe_api |
| 6 |
| 7 |
| 8 class RevisionResolver(object): |
| 9 """Resolves the revision based on build properties.""" |
| 10 |
| 11 def resolve(self, properties): # pragma: no cover |
| 12 raise NotImplementedError() |
| 13 |
| 14 |
| 15 class RevisionFallbackChain(RevisionResolver): |
| 16 """Specify that a given project's sync revision follows the fallback chain.""" |
| 17 def __init__(self, default=None): |
| 18 self._default = default |
| 19 |
| 20 def resolve(self, properties): |
| 21 """Resolve the revision via the revision fallback chain. |
| 22 |
| 23 If the given revision was set using the revision_fallback_chain() function, |
| 24 this function will follow the chain, looking at relevant build properties |
| 25 until it finds one set or reaches the end of the chain and returns the |
| 26 default. If the given revision was not set using revision_fallback_chain(), |
| 27 this function just returns it as-is. |
| 28 """ |
| 29 return (properties.get('parent_got_revision') or |
| 30 properties.get('orig_revision') or |
| 31 properties.get('revision') or |
| 32 self._default) |
| 33 |
| 34 |
| 35 def jsonish_to_python(spec, is_top=False): |
| 36 ret = '' |
| 37 if is_top: # We're the 'top' level, so treat this dict as a suite. |
| 38 ret = '\n'.join( |
| 39 '%s = %s' % (k, jsonish_to_python(spec[k])) for k in sorted(spec) |
| 40 ) |
| 41 else: |
| 42 if isinstance(spec, dict): |
| 43 ret += '{' |
| 44 ret += ', '.join( |
| 45 "%s: %s" % (repr(str(k)), jsonish_to_python(spec[k])) |
| 46 for k in sorted(spec) |
| 47 ) |
| 48 ret += '}' |
| 49 elif isinstance(spec, list): |
| 50 ret += '[' |
| 51 ret += ', '.join(jsonish_to_python(x) for x in spec) |
| 52 ret += ']' |
| 53 elif isinstance(spec, basestring): |
| 54 ret = repr(str(spec)) |
| 55 else: |
| 56 ret = repr(spec) |
| 57 return ret |
| 58 |
| 59 class GclientApi(recipe_api.RecipeApi): |
| 60 # Singleton object to indicate to checkout() that we should run a revert if |
| 61 # we detect that we're on the tryserver. |
| 62 RevertOnTryserver = object() |
| 63 |
| 64 def __init__(self, **kwargs): |
| 65 super(GclientApi, self).__init__(**kwargs) |
| 66 self.USE_MIRROR = None |
| 67 self._spec_alias = None |
| 68 |
| 69 def __call__(self, name, cmd, infra_step=True, **kwargs): |
| 70 """Wrapper for easy calling of gclient steps.""" |
| 71 assert isinstance(cmd, (list, tuple)) |
| 72 prefix = 'gclient ' |
| 73 if self.spec_alias: |
| 74 prefix = ('[spec: %s] ' % self.spec_alias) + prefix |
| 75 |
| 76 return self.m.python(prefix + name, |
| 77 self.m.depot_tools.gclient_py, |
| 78 cmd, |
| 79 infra_step=infra_step, |
| 80 **kwargs) |
| 81 |
| 82 @property |
| 83 def use_mirror(self): |
| 84 """Indicates if gclient will use mirrors in its configuration.""" |
| 85 if self.USE_MIRROR is None: |
| 86 self.USE_MIRROR = self.m.properties.get('use_mirror', True) |
| 87 return self.USE_MIRROR |
| 88 |
| 89 @use_mirror.setter |
| 90 def use_mirror(self, val): # pragma: no cover |
| 91 self.USE_MIRROR = val |
| 92 |
| 93 @property |
| 94 def spec_alias(self): |
| 95 """Optional name for the current spec for step naming.""" |
| 96 return self._spec_alias |
| 97 |
| 98 @spec_alias.setter |
| 99 def spec_alias(self, name): |
| 100 self._spec_alias = name |
| 101 |
| 102 @spec_alias.deleter |
| 103 def spec_alias(self): |
| 104 self._spec_alias = None |
| 105 |
| 106 def get_config_defaults(self): |
| 107 ret = { |
| 108 'USE_MIRROR': self.use_mirror |
| 109 } |
| 110 ret['CACHE_DIR'] = self.m.path['root'].join('git_cache') |
| 111 return ret |
| 112 |
| 113 def resolve_revision(self, revision): |
| 114 if hasattr(revision, 'resolve'): |
| 115 return revision.resolve(self.m.properties) |
| 116 return revision |
| 117 |
| 118 def sync(self, cfg, with_branch_heads=False, **kwargs): |
| 119 revisions = [] |
| 120 for i, s in enumerate(cfg.solutions): |
| 121 if s.safesync_url: # prefer safesync_url in gclient mode |
| 122 continue |
| 123 if i == 0 and s.revision is None: |
| 124 s.revision = RevisionFallbackChain() |
| 125 |
| 126 if s.revision is not None and s.revision != '': |
| 127 fixed_revision = self.resolve_revision(s.revision) |
| 128 if fixed_revision: |
| 129 revisions.extend(['--revision', '%s@%s' % (s.name, fixed_revision)]) |
| 130 |
| 131 for name, revision in sorted(cfg.revisions.items()): |
| 132 fixed_revision = self.resolve_revision(revision) |
| 133 if fixed_revision: |
| 134 revisions.extend(['--revision', '%s@%s' % (name, fixed_revision)]) |
| 135 |
| 136 test_data_paths = set(cfg.got_revision_mapping.keys() + |
| 137 [s.name for s in cfg.solutions]) |
| 138 step_test_data = lambda: ( |
| 139 self.test_api.output_json(test_data_paths, cfg.GIT_MODE)) |
| 140 try: |
| 141 if not cfg.GIT_MODE: |
| 142 args = ['sync', '--nohooks', '--force', '--verbose'] |
| 143 if cfg.delete_unversioned_trees: |
| 144 args.append('--delete_unversioned_trees') |
| 145 if with_branch_heads: |
| 146 args.append('--with_branch_heads') |
| 147 self('sync', args + revisions + ['--output-json', self.m.json.output()], |
| 148 step_test_data=step_test_data, |
| 149 **kwargs) |
| 150 else: |
| 151 # clean() isn't used because the gclient sync flags passed in checkout() |
| 152 # do much the same thing, and they're more correct than doing a separate |
| 153 # 'gclient revert' because it makes sure the other args are correct when |
| 154 # a repo was deleted and needs to be re-cloned (notably |
| 155 # --with_branch_heads), whereas 'revert' uses default args for clone |
| 156 # operations. |
| 157 # |
| 158 # TODO(mmoss): To be like current official builders, this step could |
| 159 # just delete the whole <slave_name>/build/ directory and start each |
| 160 # build from scratch. That might be the least bad solution, at least |
| 161 # until we have a reliable gclient method to produce a pristine working |
| 162 # dir for git-based builds (e.g. maybe some combination of 'git |
| 163 # reset/clean -fx' and removing the 'out' directory). |
| 164 j = '-j2' if self.m.platform.is_win else '-j8' |
| 165 args = ['sync', '--verbose', '--with_branch_heads', '--nohooks', j, |
| 166 '--reset', '--force', '--upstream', '--no-nag-max'] |
| 167 if cfg.delete_unversioned_trees: |
| 168 args.append('--delete_unversioned_trees') |
| 169 self('sync', args + revisions + |
| 170 ['--output-json', self.m.json.output()], |
| 171 step_test_data=step_test_data, |
| 172 **kwargs) |
| 173 finally: |
| 174 result = self.m.step.active_result |
| 175 data = result.json.output |
| 176 for path, info in data['solutions'].iteritems(): |
| 177 # gclient json paths always end with a slash |
| 178 path = path.rstrip('/') |
| 179 if path in cfg.got_revision_mapping: |
| 180 propname = cfg.got_revision_mapping[path] |
| 181 result.presentation.properties[propname] = info['revision'] |
| 182 |
| 183 return result |
| 184 |
| 185 def inject_parent_got_revision(self, gclient_config=None, override=False): |
| 186 """Match gclient config to build revisions obtained from build_properties. |
| 187 |
| 188 Args: |
| 189 gclient_config (gclient config object) - The config to manipulate. A value |
| 190 of None manipulates the module's built-in config (self.c). |
| 191 override (bool) - If True, will forcibly set revision and custom_vars |
| 192 even if the config already contains values for them. |
| 193 """ |
| 194 cfg = gclient_config or self.c |
| 195 |
| 196 for prop, custom_var in cfg.parent_got_revision_mapping.iteritems(): |
| 197 val = str(self.m.properties.get(prop, '')) |
| 198 # TODO(infra): Fix coverage. |
| 199 if val: # pragma: no cover |
| 200 # Special case for 'src', inject into solutions[0] |
| 201 if custom_var is None: |
| 202 # This is not covered because we are deprecating this feature and |
| 203 # it is no longer used by the public recipes. |
| 204 if cfg.solutions[0].revision is None or override: # pragma: no cover |
| 205 cfg.solutions[0].revision = val |
| 206 else: |
| 207 if custom_var not in cfg.solutions[0].custom_vars or override: |
| 208 cfg.solutions[0].custom_vars[custom_var] = val |
| 209 |
| 210 def checkout(self, gclient_config=None, revert=RevertOnTryserver, |
| 211 inject_parent_got_revision=True, with_branch_heads=False, |
| 212 **kwargs): |
| 213 """Return a step generator function for gclient checkouts.""" |
| 214 cfg = gclient_config or self.c |
| 215 assert cfg.complete() |
| 216 |
| 217 if revert is self.RevertOnTryserver: |
| 218 revert = self.m.hacky_tryserver_detection.is_tryserver |
| 219 |
| 220 if inject_parent_got_revision: |
| 221 self.inject_parent_got_revision(cfg, override=True) |
| 222 |
| 223 spec_string = jsonish_to_python(cfg.as_jsonish(), True) |
| 224 |
| 225 self('setup', ['config', '--spec', spec_string], **kwargs) |
| 226 |
| 227 sync_step = None |
| 228 try: |
| 229 if not cfg.GIT_MODE: |
| 230 try: |
| 231 if revert: |
| 232 self.revert(**kwargs) |
| 233 finally: |
| 234 sync_step = self.sync(cfg, with_branch_heads=with_branch_heads, |
| 235 **kwargs) |
| 236 else: |
| 237 sync_step = self.sync(cfg, with_branch_heads=with_branch_heads, |
| 238 **kwargs) |
| 239 |
| 240 cfg_cmds = [ |
| 241 ('user.name', 'local_bot'), |
| 242 ('user.email', 'local_bot@example.com'), |
| 243 ] |
| 244 for var, val in cfg_cmds: |
| 245 name = 'recurse (git config %s)' % var |
| 246 self(name, ['recurse', 'git', 'config', var, val], **kwargs) |
| 247 |
| 248 finally: |
| 249 cwd = kwargs.get('cwd', self.m.path['slave_build']) |
| 250 if 'checkout' not in self.m.path: |
| 251 self.m.path['checkout'] = cwd.join( |
| 252 *cfg.solutions[0].name.split(self.m.path.sep)) |
| 253 |
| 254 return sync_step |
| 255 |
| 256 def revert(self, **kwargs): |
| 257 """Return a gclient_safe_revert step.""" |
| 258 # Not directly calling gclient, so don't use self(). |
| 259 alias = self.spec_alias |
| 260 prefix = '%sgclient ' % (('[spec: %s] ' % alias) if alias else '') |
| 261 |
| 262 return self.m.python(prefix + 'revert', |
| 263 self.m.path['build'].join('scripts', 'slave', 'gclient_safe_revert.py'), |
| 264 ['.', self.m.path['depot_tools'].join('gclient', |
| 265 platform_ext={'win': '.bat'})], |
| 266 infra_step=True, |
| 267 **kwargs |
| 268 ) |
| 269 |
| 270 def runhooks(self, args=None, name='runhooks', **kwargs): |
| 271 args = args or [] |
| 272 assert isinstance(args, (list, tuple)) |
| 273 return self( |
| 274 name, ['runhooks'] + list(args), infra_step=False, **kwargs) |
| 275 |
| 276 @property |
| 277 def is_blink_mode(self): |
| 278 """ Indicates wether the caller is to use the Blink config rather than the |
| 279 Chromium config. This may happen for one of two reasons: |
| 280 1. The builder is configured to always use TOT Blink. (factory property |
| 281 top_of_tree_blink=True) |
| 282 2. A try job comes in that applies to the Blink tree. (patch_project is |
| 283 blink) |
| 284 """ |
| 285 return ( |
| 286 self.m.properties.get('top_of_tree_blink') or |
| 287 self.m.properties.get('patch_project') == 'blink') |
| 288 |
| 289 def break_locks(self): |
| 290 """Remove all index.lock files. If a previous run of git crashed, bot was |
| 291 reset, etc... we might end up with leftover index.lock files. |
| 292 """ |
| 293 self.m.python.inline( |
| 294 'cleanup index.lock', |
| 295 """ |
| 296 import os, sys |
| 297 |
| 298 build_path = sys.argv[1] |
| 299 if os.path.exists(build_path): |
| 300 for (path, dir, files) in os.walk(build_path): |
| 301 for cur_file in files: |
| 302 if cur_file.endswith('index.lock'): |
| 303 path_to_file = os.path.join(path, cur_file) |
| 304 print 'deleting %s' % path_to_file |
| 305 os.remove(path_to_file) |
| 306 """, |
| 307 args=[self.m.path['slave_build']], |
| 308 infra_step=True, |
| 309 ) |
OLD | NEW |