blob: 47542634ec98d9d44634c0fd92e422850058a796 [file] [log] [blame]
Ryan Cuib603c542011-07-27 16:00:11 -07001#!/usr/bin/python
2# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Script that resets your Chrome GIT checkout."""
7
Chris Sosa4f6ffaf2012-05-01 17:05:44 -07008import logging
Ryan Cuib603c542011-07-27 16:00:11 -07009import optparse
10import os
Ryan Cui7d7c88b2011-11-01 22:11:55 -070011import re
Ryan Cuib603c542011-07-27 16:00:11 -070012
Brian Harring503f3ab2012-03-09 21:39:41 -080013from chromite.buildbot import repository
14from chromite.lib import cros_build_lib as cros_lib
Brian Harringaf019fb2012-05-10 15:06:13 -070015from chromite.lib import osutils
Ryan Cuib603c542011-07-27 16:00:11 -070016
17
Ryan Cui44360de2011-08-22 16:50:20 -070018_CHROMIUM_ROOT = 'chromium'
19_CHROMIUM_SRC_ROOT = os.path.join(_CHROMIUM_ROOT, 'src')
Ryan Cuid1024a82011-08-26 17:58:36 -070020_CHROMIUM_SRC_INTERNAL = os.path.join(_CHROMIUM_ROOT, 'src-internal')
Ryan Cui44360de2011-08-22 16:50:20 -070021_CHROMIUM_CROS_DEPS = os.path.join(_CHROMIUM_SRC_ROOT, 'tools/cros.DEPS/DEPS')
Ryan Cuib603c542011-07-27 16:00:11 -070022
23
24def _LoadDEPS(deps_content):
25 """Load contents of DEPS file into a dictionary.
26
27 Arguments:
28 deps_content: The contents of the .DEPS.git file.
29
30 Returns:
31 A dictionary indexed by the top level items in the structure - i.e.,
32 'hooks', 'deps', 'os_deps'.
33 """
34 class FromImpl:
35 """From syntax is not supported."""
36 def __init__(self, module_name, var_name):
37 raise NotImplementedError('The From() syntax is not supported in'
38 'chrome_set_ver.')
39
40 class _VarImpl:
Ryan Cui446e8e72011-09-23 18:02:41 -070041 def __init__(self, custom_vars, local_scope):
Ryan Cuib603c542011-07-27 16:00:11 -070042 self._custom_vars = custom_vars
43 self._local_scope = local_scope
44
45 def Lookup(self, var_name):
46 """Implements the Var syntax."""
47 if var_name in self._custom_vars:
48 return self._custom_vars[var_name]
49 elif var_name in self._local_scope.get('vars', {}):
50 return self._local_scope['vars'][var_name]
51 raise Exception('Var is not defined: %s' % var_name)
52
53 locals = {}
54 var = _VarImpl({}, locals)
55 globals = {'From': FromImpl, 'Var': var.Lookup, 'deps_os': {}}
56 exec(deps_content) in globals, locals
57 return locals
58
59
Ryan Cui446e8e72011-09-23 18:02:41 -070060def _CreateCrosSymlink(repo_root):
61 """Create symlinks to CrOS projects specified in the cros_DEPS/DEPS file."""
62 cros_deps_file = os.path.join(repo_root, _CHROMIUM_CROS_DEPS)
63 _, merged_deps = GetParsedDeps(cros_deps_file)
64 chromium_root = os.path.join(repo_root, _CHROMIUM_ROOT)
Ryan Cuiaf3e2eb2011-09-30 16:43:03 -070065
Ryan Cui446e8e72011-09-23 18:02:41 -070066 mappings = GetPathToProjectMappings(merged_deps)
67 for rel_path, project in mappings.iteritems():
68 link_dir = os.path.join(chromium_root, rel_path)
69 target_dir = os.path.join(repo_root,
70 cros_lib.GetProjectDir(repo_root, project))
71 path_to_target = os.path.relpath(target_dir, os.path.dirname(link_dir))
72 if not os.path.exists(link_dir):
73 os.symlink(path_to_target, link_dir)
74
75
76def _ExtractProjectFromUrl(repo_url):
77 """Get the Gerrit project name from an url."""
Ryan Cui7d7c88b2011-11-01 22:11:55 -070078 # Example: 'ssh://gerrit-int.chromium.org:12121/my/project.git' would parse to
79 # 'my/project'. See unit test for more examples. For 'URL's like
80 # '../abc/efg' leave as-is to support unit tests.
81 mo = re.match(r'(?:[\w\+]+://[^/]+/)?(.*)', repo_url)
82 return os.path.splitext(mo.group(1))[0]
Ryan Cuib603c542011-07-27 16:00:11 -070083
84
Ryan Cuid1024a82011-08-26 17:58:36 -070085def _ExtractProjectFromEntry(entry):
86 """From a deps entry extract the Gerrit project name.
87
88 Arguments:
Ryan Cui65e1f9a2011-09-21 17:06:43 -070089 entry: The deps entry in the format ${url_prefix}/${project_name}@${hash}.
Ryan Cuid1024a82011-08-26 17:58:36 -070090 """
91 # We only support Gerrit urls, where the path is the project name.
92 repo_url = entry.partition('@')[0]
Ryan Cui446e8e72011-09-23 18:02:41 -070093 return _ExtractProjectFromUrl(repo_url)
Ryan Cuid1024a82011-08-26 17:58:36 -070094
95
96def GetPathToProjectMappings(deps):
97 """Get dictionary relating path to Gerrit project names.
98
99 Arguments:
100 deps: a dictionary indexed by repo paths. The same format as the 'deps'
101 entry in the '.DEPS.git' file.
102 """
103 mappings = {}
104 for rel_path, entry in deps.iteritems():
105 mappings[rel_path] = _ExtractProjectFromEntry(entry)
106
107 return mappings
108
109
Ryan Cui446e8e72011-09-23 18:02:41 -0700110class ProjectException(Exception):
111 """Raised by Project class when a Pin error """
112 pass
Ryan Cuib75c3032011-08-10 16:19:39 -0700113
114
Ryan Cui446e8e72011-09-23 18:02:41 -0700115class Project(object):
116 """Encapsulates functionality to pin a project to a specific commit."""
117 def __init__(self, repo_root, project_url, rel_path):
118 """Construct the class.
119
120 Arguments:
121 repo_root: The root of the repo checkout.
122 project_url: The Gerrit url of the project.
123 rel_path: The path the project is expected to be checked out to. Relative
124 to <repo_root>/chromium.
125 """
126 self.repo_root = repo_root
127 self.chromium_root = os.path.join(self.repo_root, _CHROMIUM_ROOT)
128 self.project_url = project_url
129 self.rel_path = rel_path
130 self.abs_path = os.path.join(self.chromium_root, self.rel_path)
131 self.manifest_rel_path = os.path.join(_CHROMIUM_ROOT, self.rel_path)
132 self.project_name = _ExtractProjectFromUrl(self.project_url)
133
134 def _ResetProject(self, commit_hash):
135 """Actually pin project to the specified commit hash."""
136 if not cros_lib.DoesCommitExistInRepo(self.abs_path, commit_hash):
137 cros_lib.Die('Commit %s not found in %s.\n'
138 "You probably need to run 'repo sync --jobs=<jobs>' "
139 'to update your checkout.'
140 % (commit_hash, self.abs_path))
141
142 result = cros_lib.RunCommand(['git', 'checkout', commit_hash],
143 error_code_ok=True, cwd=self.abs_path)
144 if result.returncode != 0:
145 cros_lib.Warning('Failed to pin project %s.\n'
146 'You probably have uncommited local changes.'
147 % self.abs_path)
148
149 def _PrepareProject(self):
150 """Make sure the project is synced properly and is ready for pinning."""
David James1bd7fae2012-04-22 10:14:38 -0700151 handler = cros_lib.ParseFullManifest(self.repo_root)
152 path_to_project_dict = dict(([attrs['path'], project]) for project, attrs
153 in handler.projects.iteritems())
Ryan Cui446e8e72011-09-23 18:02:41 -0700154
155 # TODO(rcui): Handle case where a dependency never makes it to the manifest
156 # (i.e., dep path added as double checkout, and then gets deleted). We need
157 # to delete those. crosbug/22123.
158 if not cros_lib.IsDirectoryAGitRepoRoot(self.abs_path):
159 if self.manifest_rel_path in path_to_project_dict:
160 raise ProjectException('%s in full layout manifest but not in working '
161 "tree. Please run 'repo sync %s'"
162 % (self.manifest_rel_path,
163 path_to_project_dict[self.manifest_rel_path]))
164 else:
165 cros_lib.Warning('Project %s is not in the manifest. Automatically '
166 'checking out to %s.\n'
167 % (self.project_url, self.abs_path))
168 repository.CloneGitRepo(self.abs_path, self.project_url)
169 cros_lib.RunCommand(['git', 'checkout',
170 cros_lib.GetGitRepoRevision(self.abs_path)],
171 cwd=self.abs_path)
172 elif not cros_lib.IsProjectManagedByRepo(self.abs_path):
173 if self.manifest_rel_path in path_to_project_dict:
174 # If path is now in the manifest, tell user to manually delete our
175 # managed checkout and re-sync.
176 raise ProjectException('%s needs to be replaced. Please remove the '
177 "directory and run 'repo sync %s'"
178 % (self.manifest_rel_path,
179 path_to_project_dict[self.manifest_rel_path]))
180 else:
181 # If not managed by Repo we need to perform sync.
182 cros_lib.RunCommand(['git', 'pull', '--rebase', self.project_url],
183 cwd=self.abs_path)
184 elif not os.path.islink(self.abs_path):
185 # Skip symlinks - we don't want to error out for the cros.DEPS projects.
186 if self.manifest_rel_path not in path_to_project_dict:
187 # If it is 'managed by repo' but not in the manifest, repo tried
188 # deleting it but failed because of local changes.
189 raise ProjectException('%s is no longer in the manifest but has local '
190 'changes. Please remove and try again.'
191 % self.manifest_rel_path)
192 elif self.project_name != path_to_project_dict[self.manifest_rel_path]:
193 cros_lib.Die('.DEPS.git for %s conflicts with manifest.xml! Running '
194 'with older .DEPS.git files are not yet supported. '
195 "Please run'repo sync --jobs=<jobs>' to sync everything "
196 'up.' % self.manifest_rel_path)
197
198 def Pin(self, commit_hash):
199 """Attempt to pin the project to the specified commit hash.
200
201 Arguments:
202 commit_hash: The commit to pin the project to.
203
204 Raises:
205 ProjectException when an error occurs.
206 """
207 self._PrepareProject()
208 if cros_lib.GetCurrentBranch(self.abs_path):
209 cros_lib.Warning("Not pinning project %s that's checked out to a "
210 'development branch.' % self.rel_path)
211 elif (commit_hash and
212 (commit_hash != cros_lib.GetGitRepoRevision(self.abs_path))):
213 print 'Pinning project %s' % self.rel_path
214 self._ResetProject(commit_hash)
215 else:
216 cros_lib.Debug('Skipping project %s, already pinned' % self.rel_path)
217
218
219def _ResetGitCheckout(repo_root, deps):
Ryan Cuib603c542011-07-27 16:00:11 -0700220 """Reset chrome repos to hashes specified in the DEPS file.
221
222 Arguments:
223 chromium_root: directory where chromium is checked out - level above 'src'.
224 deps: a dictionary indexed by repo paths. The same format as the 'deps'
225 entry in the '.DEPS.git' file.
226 """
Ryan Cui446e8e72011-09-23 18:02:41 -0700227 errors = []
Ryan Cuib603c542011-07-27 16:00:11 -0700228 for rel_path, project_hash in deps.iteritems():
Ryan Cuid1024a82011-08-26 17:58:36 -0700229 repo_url, _, commit_hash = project_hash.partition('@')
Ryan Cui446e8e72011-09-23 18:02:41 -0700230 project = Project(repo_root, repo_url, rel_path)
231 try:
232 project.Pin(commit_hash)
233 except ProjectException as e:
234 errors.append(str(e))
Ryan Cui65e1f9a2011-09-21 17:06:43 -0700235
Ryan Cui446e8e72011-09-23 18:02:41 -0700236 if errors:
237 cros_lib.Die('Errors encountered:\n* ' + '\n* '.join(errors))
Ryan Cuib603c542011-07-27 16:00:11 -0700238
239
240def _RunHooks(chromium_root, hooks):
241 """Run the hooks contained in the DEPS file.
242
243 Arguments:
244 chromium_root: directory where chromium is checked out - level above 'src'.
245 hooks: a list of hook dictionaries. The same format as the 'hooks' entry
246 in the '.DEPS.git' file.
247 """
248 for hook in hooks:
Ryan Cuiaf3e2eb2011-09-30 16:43:03 -0700249 print '[running hook] %s' % ' '.join(hook['action'])
Ryan Cuib603c542011-07-27 16:00:11 -0700250 cros_lib.RunCommand(hook['action'], cwd=chromium_root)
251
252
Ryan Cui44360de2011-08-22 16:50:20 -0700253def _MergeDeps(dest, update):
254 """Merge the dependencies specified in two dictionaries.
255
256 Arguments:
257 dest: The dictionary that will be merged into.
258 update: The dictionary whose elements will be merged into dest.
259 """
260 assert(not set(dest.keys()).intersection(set(update.keys())))
261 dest.update(update)
262 return dest
263
264
Ryan Cui97b2f302011-10-24 17:34:05 -0700265def GetParsedDeps(deps_file):
Ryan Cui6c851112011-08-11 21:51:37 -0700266 """Returns the full parsed DEPS file dictionary, and merged deps.
267
268 Arguments:
Ryan Cui65e1f9a2011-09-21 17:06:43 -0700269 deps_file: Path to the .DEPS.git file.
Ryan Cui6c851112011-08-11 21:51:37 -0700270
271 Returns:
272 An (x,y) tuple. x is a dictionary containing the contents of the DEPS file,
273 and y is a dictionary containing the result of merging unix and common deps.
274 """
Brian Harringaf019fb2012-05-10 15:06:13 -0700275 deps = _LoadDEPS(osutils.ReadFile(deps_file, 'rU'))
Ryan Cui44360de2011-08-22 16:50:20 -0700276 merged_deps = deps.get('deps', {})
Ryan Cuid1024a82011-08-26 17:58:36 -0700277 unix_deps = deps.get('deps_os', {}).get('unix', {})
Ryan Cui44360de2011-08-22 16:50:20 -0700278 merged_deps = _MergeDeps(merged_deps, unix_deps)
Ryan Cui6c851112011-08-11 21:51:37 -0700279 return deps, merged_deps
280
281
Brian Harring30675052012-02-29 12:18:22 -0800282def main(argv):
Ryan Cuib603c542011-07-27 16:00:11 -0700283
Ryan Cui9ef7c922011-08-17 11:48:02 -0700284 usage = 'usage: %prog [-d <DEPS.git file>] [command]'
285 parser = optparse.OptionParser(usage=usage)
Ryan Cui6c851112011-08-11 21:51:37 -0700286
Ryan Cui65e1f9a2011-09-21 17:06:43 -0700287 # TODO(rcui): have -d accept a URL.
Ryan Cuib603c542011-07-27 16:00:11 -0700288 parser.add_option('-d', '--deps', default=None,
289 help=('DEPS file to use. Defaults to '
290 '<chrome_src_root>/.DEPS.git'))
291 parser.add_option('--internal', action='store_false', dest='internal',
292 default=True, help='Allow chrome-internal URLs')
Ryan Cui9ef7c922011-08-17 11:48:02 -0700293 parser.add_option('--runhooks', action='store_true', dest='runhooks',
294 default=False, help="Run hooks as well.")
Ryan Cui65e1f9a2011-09-21 17:06:43 -0700295 parser.add_option('-v', '--verbose', default=False, action='store_true',
296 help='Run with debug output.')
Ryan Cui446e8e72011-09-23 18:02:41 -0700297 (options, _inputs) = parser.parse_args(argv)
Ryan Cuib603c542011-07-27 16:00:11 -0700298
Ryan Cui65e1f9a2011-09-21 17:06:43 -0700299 # Set cros_build_lib debug level to hide RunCommand spew.
300 if options.verbose:
Chris Sosa4f6ffaf2012-05-01 17:05:44 -0700301 cros_lib.logger.setLevel(logging.DEBUG)
Ryan Cui65e1f9a2011-09-21 17:06:43 -0700302 else:
Chris Sosa4f6ffaf2012-05-01 17:05:44 -0700303 cros_lib.logger.setLevel(logging.WARNING)
Ryan Cui65e1f9a2011-09-21 17:06:43 -0700304
Ryan Cuie52602e2011-11-03 15:54:35 -0700305 if cros_lib.IsInsideChroot():
306 ssh_path = '/usr/bin/ssh_no_update'
307 if os.path.isfile(ssh_path):
308 os.environ['GIT_SSH'] = ssh_path
309 else:
310 cros_lib.Warning("Can't find %s. Run build_packages or setup_board to "
311 'update your chooot.' % ssh_path)
312
Ryan Cuib75c3032011-08-10 16:19:39 -0700313 repo_root = cros_lib.FindRepoCheckoutRoot()
314 chromium_src_root = os.path.join(repo_root, _CHROMIUM_SRC_ROOT)
Ryan Cuib603c542011-07-27 16:00:11 -0700315 if not os.path.isdir(chromium_src_root):
Ryan Cui6c851112011-08-11 21:51:37 -0700316 cros_lib.Die('chromium src/ dir not found')
Ryan Cuib603c542011-07-27 16:00:11 -0700317
Ryan Cui65e1f9a2011-09-21 17:06:43 -0700318 # Add DEPS files to parse.
Ryan Cui44360de2011-08-22 16:50:20 -0700319 deps_files_to_parse = []
Ryan Cuib603c542011-07-27 16:00:11 -0700320 if options.deps:
Ryan Cui44360de2011-08-22 16:50:20 -0700321 deps_files_to_parse.append(options.deps)
322 else:
323 deps_files_to_parse.append(os.path.join(chromium_src_root, '.DEPS.git'))
Ryan Cuib603c542011-07-27 16:00:11 -0700324
Ryan Cuid1024a82011-08-26 17:58:36 -0700325 internal_deps = os.path.join(repo_root, _CHROMIUM_SRC_INTERNAL, '.DEPS.git')
326 if os.path.exists(internal_deps):
327 deps_files_to_parse.append(internal_deps)
328
Ryan Cui44360de2011-08-22 16:50:20 -0700329 deps_files_to_parse.append(os.path.join(repo_root, _CHROMIUM_CROS_DEPS))
Ryan Cuib603c542011-07-27 16:00:11 -0700330
Ryan Cui65e1f9a2011-09-21 17:06:43 -0700331 # Prepare source tree for resetting.
Ryan Cui44360de2011-08-22 16:50:20 -0700332 chromium_root = os.path.join(repo_root, _CHROMIUM_ROOT)
Ryan Cuib75c3032011-08-10 16:19:39 -0700333 _CreateCrosSymlink(repo_root)
Ryan Cui44360de2011-08-22 16:50:20 -0700334
Ryan Cui65e1f9a2011-09-21 17:06:43 -0700335 # Parse DEPS files and store hooks.
Ryan Cui44360de2011-08-22 16:50:20 -0700336 hook_dicts = []
337 for deps_file in deps_files_to_parse:
Ryan Cui97b2f302011-10-24 17:34:05 -0700338 deps, merged_deps = GetParsedDeps(deps_file)
Ryan Cui446e8e72011-09-23 18:02:41 -0700339 _ResetGitCheckout(repo_root, merged_deps)
Ryan Cui44360de2011-08-22 16:50:20 -0700340 hook_dicts.append(deps.get('hooks', {}))
341
Ryan Cui65e1f9a2011-09-21 17:06:43 -0700342 # Run hooks after checkout has been reset properly.
Ryan Cui44360de2011-08-22 16:50:20 -0700343 if options.runhooks:
344 for hooks in hook_dicts:
345 _RunHooks(chromium_root, hooks)