| 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 |