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