blob: 51489bea48b6e383d5c88877f24b04875172360e [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
Brian Harring609dc4e2012-05-07 02:17:44 -0700115def _IsGitStoreInRepo(path):
116 """Checks if the git repo rooted at a directory is in repo's storage.
117
118 Note that just because a pathway is in .repo, does *not* mean that
119 repo can actually use it (the git repo must be in the manifest for
120 that to be true).
121 """
122 repo_dir = os.path.realpath(cros_lib.FindRepoDir(path))
123 git_objects_dir = os.path.realpath(os.path.join(path, '.git/objects'))
124 return git_objects_dir.startswith(repo_dir)
125
126
Ryan Cui446e8e72011-09-23 18:02:41 -0700127class Project(object):
128 """Encapsulates functionality to pin a project to a specific commit."""
129 def __init__(self, repo_root, project_url, rel_path):
130 """Construct the class.
131
132 Arguments:
133 repo_root: The root of the repo checkout.
134 project_url: The Gerrit url of the project.
135 rel_path: The path the project is expected to be checked out to. Relative
136 to <repo_root>/chromium.
137 """
138 self.repo_root = repo_root
139 self.chromium_root = os.path.join(self.repo_root, _CHROMIUM_ROOT)
140 self.project_url = project_url
141 self.rel_path = rel_path
142 self.abs_path = os.path.join(self.chromium_root, self.rel_path)
143 self.manifest_rel_path = os.path.join(_CHROMIUM_ROOT, self.rel_path)
144 self.project_name = _ExtractProjectFromUrl(self.project_url)
145
146 def _ResetProject(self, commit_hash):
147 """Actually pin project to the specified commit hash."""
148 if not cros_lib.DoesCommitExistInRepo(self.abs_path, commit_hash):
149 cros_lib.Die('Commit %s not found in %s.\n'
150 "You probably need to run 'repo sync --jobs=<jobs>' "
151 'to update your checkout.'
152 % (commit_hash, self.abs_path))
153
154 result = cros_lib.RunCommand(['git', 'checkout', commit_hash],
155 error_code_ok=True, cwd=self.abs_path)
156 if result.returncode != 0:
157 cros_lib.Warning('Failed to pin project %s.\n'
158 'You probably have uncommited local changes.'
159 % self.abs_path)
160
161 def _PrepareProject(self):
162 """Make sure the project is synced properly and is ready for pinning."""
Brian Harring609dc4e2012-05-07 02:17:44 -0700163 handler = cros_lib.ManifestCheckout.Cached(self.repo_root)
David James1bd7fae2012-04-22 10:14:38 -0700164 path_to_project_dict = dict(([attrs['path'], project]) for project, attrs
165 in handler.projects.iteritems())
Ryan Cui446e8e72011-09-23 18:02:41 -0700166
167 # TODO(rcui): Handle case where a dependency never makes it to the manifest
168 # (i.e., dep path added as double checkout, and then gets deleted). We need
169 # to delete those. crosbug/22123.
170 if not cros_lib.IsDirectoryAGitRepoRoot(self.abs_path):
171 if self.manifest_rel_path in path_to_project_dict:
172 raise ProjectException('%s in full layout manifest but not in working '
173 "tree. Please run 'repo sync %s'"
174 % (self.manifest_rel_path,
175 path_to_project_dict[self.manifest_rel_path]))
176 else:
177 cros_lib.Warning('Project %s is not in the manifest. Automatically '
178 'checking out to %s.\n'
179 % (self.project_url, self.abs_path))
180 repository.CloneGitRepo(self.abs_path, self.project_url)
181 cros_lib.RunCommand(['git', 'checkout',
182 cros_lib.GetGitRepoRevision(self.abs_path)],
183 cwd=self.abs_path)
Brian Harring609dc4e2012-05-07 02:17:44 -0700184 elif not _IsGitStoreInRepo(self.abs_path):
Ryan Cui446e8e72011-09-23 18:02:41 -0700185 if self.manifest_rel_path in path_to_project_dict:
186 # If path is now in the manifest, tell user to manually delete our
187 # managed checkout and re-sync.
188 raise ProjectException('%s needs to be replaced. Please remove the '
189 "directory and run 'repo sync %s'"
190 % (self.manifest_rel_path,
191 path_to_project_dict[self.manifest_rel_path]))
192 else:
193 # If not managed by Repo we need to perform sync.
194 cros_lib.RunCommand(['git', 'pull', '--rebase', self.project_url],
195 cwd=self.abs_path)
196 elif not os.path.islink(self.abs_path):
197 # Skip symlinks - we don't want to error out for the cros.DEPS projects.
198 if self.manifest_rel_path not in path_to_project_dict:
199 # If it is 'managed by repo' but not in the manifest, repo tried
200 # deleting it but failed because of local changes.
201 raise ProjectException('%s is no longer in the manifest but has local '
202 'changes. Please remove and try again.'
203 % self.manifest_rel_path)
204 elif self.project_name != path_to_project_dict[self.manifest_rel_path]:
205 cros_lib.Die('.DEPS.git for %s conflicts with manifest.xml! Running '
206 'with older .DEPS.git files are not yet supported. '
207 "Please run'repo sync --jobs=<jobs>' to sync everything "
208 'up.' % self.manifest_rel_path)
209
210 def Pin(self, commit_hash):
211 """Attempt to pin the project to the specified commit hash.
212
213 Arguments:
214 commit_hash: The commit to pin the project to.
215
216 Raises:
217 ProjectException when an error occurs.
218 """
219 self._PrepareProject()
220 if cros_lib.GetCurrentBranch(self.abs_path):
221 cros_lib.Warning("Not pinning project %s that's checked out to a "
222 'development branch.' % self.rel_path)
223 elif (commit_hash and
224 (commit_hash != cros_lib.GetGitRepoRevision(self.abs_path))):
225 print 'Pinning project %s' % self.rel_path
226 self._ResetProject(commit_hash)
227 else:
228 cros_lib.Debug('Skipping project %s, already pinned' % self.rel_path)
229
230
231def _ResetGitCheckout(repo_root, deps):
Ryan Cuib603c542011-07-27 16:00:11 -0700232 """Reset chrome repos to hashes specified in the DEPS file.
233
234 Arguments:
235 chromium_root: directory where chromium is checked out - level above 'src'.
236 deps: a dictionary indexed by repo paths. The same format as the 'deps'
237 entry in the '.DEPS.git' file.
238 """
Ryan Cui446e8e72011-09-23 18:02:41 -0700239 errors = []
Ryan Cuib603c542011-07-27 16:00:11 -0700240 for rel_path, project_hash in deps.iteritems():
Ryan Cuid1024a82011-08-26 17:58:36 -0700241 repo_url, _, commit_hash = project_hash.partition('@')
Ryan Cui446e8e72011-09-23 18:02:41 -0700242 project = Project(repo_root, repo_url, rel_path)
243 try:
244 project.Pin(commit_hash)
245 except ProjectException as e:
246 errors.append(str(e))
Ryan Cui65e1f9a2011-09-21 17:06:43 -0700247
Ryan Cui446e8e72011-09-23 18:02:41 -0700248 if errors:
249 cros_lib.Die('Errors encountered:\n* ' + '\n* '.join(errors))
Ryan Cuib603c542011-07-27 16:00:11 -0700250
251
252def _RunHooks(chromium_root, hooks):
253 """Run the hooks contained in the DEPS file.
254
255 Arguments:
256 chromium_root: directory where chromium is checked out - level above 'src'.
257 hooks: a list of hook dictionaries. The same format as the 'hooks' entry
258 in the '.DEPS.git' file.
259 """
260 for hook in hooks:
Ryan Cuiaf3e2eb2011-09-30 16:43:03 -0700261 print '[running hook] %s' % ' '.join(hook['action'])
Ryan Cuib603c542011-07-27 16:00:11 -0700262 cros_lib.RunCommand(hook['action'], cwd=chromium_root)
263
264
Ryan Cui44360de2011-08-22 16:50:20 -0700265def _MergeDeps(dest, update):
266 """Merge the dependencies specified in two dictionaries.
267
268 Arguments:
269 dest: The dictionary that will be merged into.
270 update: The dictionary whose elements will be merged into dest.
271 """
272 assert(not set(dest.keys()).intersection(set(update.keys())))
273 dest.update(update)
274 return dest
275
276
Ryan Cui97b2f302011-10-24 17:34:05 -0700277def GetParsedDeps(deps_file):
Ryan Cui6c851112011-08-11 21:51:37 -0700278 """Returns the full parsed DEPS file dictionary, and merged deps.
279
280 Arguments:
Ryan Cui65e1f9a2011-09-21 17:06:43 -0700281 deps_file: Path to the .DEPS.git file.
Ryan Cui6c851112011-08-11 21:51:37 -0700282
283 Returns:
284 An (x,y) tuple. x is a dictionary containing the contents of the DEPS file,
285 and y is a dictionary containing the result of merging unix and common deps.
286 """
Brian Harringaf019fb2012-05-10 15:06:13 -0700287 deps = _LoadDEPS(osutils.ReadFile(deps_file, 'rU'))
Ryan Cui44360de2011-08-22 16:50:20 -0700288 merged_deps = deps.get('deps', {})
Ryan Cuid1024a82011-08-26 17:58:36 -0700289 unix_deps = deps.get('deps_os', {}).get('unix', {})
Ryan Cui44360de2011-08-22 16:50:20 -0700290 merged_deps = _MergeDeps(merged_deps, unix_deps)
Ryan Cui6c851112011-08-11 21:51:37 -0700291 return deps, merged_deps
292
293
Brian Harring30675052012-02-29 12:18:22 -0800294def main(argv):
Ryan Cuib603c542011-07-27 16:00:11 -0700295
Ryan Cui9ef7c922011-08-17 11:48:02 -0700296 usage = 'usage: %prog [-d <DEPS.git file>] [command]'
297 parser = optparse.OptionParser(usage=usage)
Ryan Cui6c851112011-08-11 21:51:37 -0700298
Ryan Cui65e1f9a2011-09-21 17:06:43 -0700299 # TODO(rcui): have -d accept a URL.
Ryan Cuib603c542011-07-27 16:00:11 -0700300 parser.add_option('-d', '--deps', default=None,
301 help=('DEPS file to use. Defaults to '
302 '<chrome_src_root>/.DEPS.git'))
303 parser.add_option('--internal', action='store_false', dest='internal',
304 default=True, help='Allow chrome-internal URLs')
Ryan Cui9ef7c922011-08-17 11:48:02 -0700305 parser.add_option('--runhooks', action='store_true', dest='runhooks',
306 default=False, help="Run hooks as well.")
Ryan Cui65e1f9a2011-09-21 17:06:43 -0700307 parser.add_option('-v', '--verbose', default=False, action='store_true',
308 help='Run with debug output.')
Ryan Cui446e8e72011-09-23 18:02:41 -0700309 (options, _inputs) = parser.parse_args(argv)
Ryan Cuib603c542011-07-27 16:00:11 -0700310
Ryan Cui65e1f9a2011-09-21 17:06:43 -0700311 # Set cros_build_lib debug level to hide RunCommand spew.
312 if options.verbose:
Chris Sosa4f6ffaf2012-05-01 17:05:44 -0700313 cros_lib.logger.setLevel(logging.DEBUG)
Ryan Cui65e1f9a2011-09-21 17:06:43 -0700314 else:
Chris Sosa4f6ffaf2012-05-01 17:05:44 -0700315 cros_lib.logger.setLevel(logging.WARNING)
Ryan Cui65e1f9a2011-09-21 17:06:43 -0700316
Ryan Cuie52602e2011-11-03 15:54:35 -0700317 if cros_lib.IsInsideChroot():
318 ssh_path = '/usr/bin/ssh_no_update'
319 if os.path.isfile(ssh_path):
320 os.environ['GIT_SSH'] = ssh_path
321 else:
322 cros_lib.Warning("Can't find %s. Run build_packages or setup_board to "
323 'update your chooot.' % ssh_path)
324
Ryan Cuib75c3032011-08-10 16:19:39 -0700325 repo_root = cros_lib.FindRepoCheckoutRoot()
326 chromium_src_root = os.path.join(repo_root, _CHROMIUM_SRC_ROOT)
Ryan Cuib603c542011-07-27 16:00:11 -0700327 if not os.path.isdir(chromium_src_root):
Ryan Cui6c851112011-08-11 21:51:37 -0700328 cros_lib.Die('chromium src/ dir not found')
Ryan Cuib603c542011-07-27 16:00:11 -0700329
Ryan Cui65e1f9a2011-09-21 17:06:43 -0700330 # Add DEPS files to parse.
Ryan Cui44360de2011-08-22 16:50:20 -0700331 deps_files_to_parse = []
Ryan Cuib603c542011-07-27 16:00:11 -0700332 if options.deps:
Ryan Cui44360de2011-08-22 16:50:20 -0700333 deps_files_to_parse.append(options.deps)
334 else:
335 deps_files_to_parse.append(os.path.join(chromium_src_root, '.DEPS.git'))
Ryan Cuib603c542011-07-27 16:00:11 -0700336
Ryan Cuid1024a82011-08-26 17:58:36 -0700337 internal_deps = os.path.join(repo_root, _CHROMIUM_SRC_INTERNAL, '.DEPS.git')
338 if os.path.exists(internal_deps):
339 deps_files_to_parse.append(internal_deps)
340
Ryan Cui44360de2011-08-22 16:50:20 -0700341 deps_files_to_parse.append(os.path.join(repo_root, _CHROMIUM_CROS_DEPS))
Ryan Cuib603c542011-07-27 16:00:11 -0700342
Ryan Cui65e1f9a2011-09-21 17:06:43 -0700343 # Prepare source tree for resetting.
Ryan Cui44360de2011-08-22 16:50:20 -0700344 chromium_root = os.path.join(repo_root, _CHROMIUM_ROOT)
Ryan Cuib75c3032011-08-10 16:19:39 -0700345 _CreateCrosSymlink(repo_root)
Ryan Cui44360de2011-08-22 16:50:20 -0700346
Ryan Cui65e1f9a2011-09-21 17:06:43 -0700347 # Parse DEPS files and store hooks.
Ryan Cui44360de2011-08-22 16:50:20 -0700348 hook_dicts = []
349 for deps_file in deps_files_to_parse:
Ryan Cui97b2f302011-10-24 17:34:05 -0700350 deps, merged_deps = GetParsedDeps(deps_file)
Ryan Cui446e8e72011-09-23 18:02:41 -0700351 _ResetGitCheckout(repo_root, merged_deps)
Ryan Cui44360de2011-08-22 16:50:20 -0700352 hook_dicts.append(deps.get('hooks', {}))
353
Ryan Cui65e1f9a2011-09-21 17:06:43 -0700354 # Run hooks after checkout has been reset properly.
Ryan Cui44360de2011-08-22 16:50:20 -0700355 if options.runhooks:
356 for hooks in hook_dicts:
357 _RunHooks(chromium_root, hooks)