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 |