blob: fccdf90159d41057154977003d966d9f25c23888 [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
8import optparse
9import os
Ryan Cui7d7c88b2011-11-01 22:11:55 -070010import re
Ryan Cuib603c542011-07-27 16:00:11 -070011
Brian Harring503f3ab2012-03-09 21:39:41 -080012from chromite.buildbot import repository
13from chromite.lib import cros_build_lib as cros_lib
Ryan Cuib603c542011-07-27 16:00:11 -070014
15
Ryan Cui44360de2011-08-22 16:50:20 -070016_CHROMIUM_ROOT = 'chromium'
17_CHROMIUM_SRC_ROOT = os.path.join(_CHROMIUM_ROOT, 'src')
Ryan Cuid1024a82011-08-26 17:58:36 -070018_CHROMIUM_SRC_INTERNAL = os.path.join(_CHROMIUM_ROOT, 'src-internal')
Ryan Cui44360de2011-08-22 16:50:20 -070019_CHROMIUM_CROS_DEPS = os.path.join(_CHROMIUM_SRC_ROOT, 'tools/cros.DEPS/DEPS')
Ryan Cuib603c542011-07-27 16:00:11 -070020
21
22def _LoadDEPS(deps_content):
23 """Load contents of DEPS file into a dictionary.
24
25 Arguments:
26 deps_content: The contents of the .DEPS.git file.
27
28 Returns:
29 A dictionary indexed by the top level items in the structure - i.e.,
30 'hooks', 'deps', 'os_deps'.
31 """
32 class FromImpl:
33 """From syntax is not supported."""
34 def __init__(self, module_name, var_name):
35 raise NotImplementedError('The From() syntax is not supported in'
36 'chrome_set_ver.')
37
38 class _VarImpl:
Ryan Cui446e8e72011-09-23 18:02:41 -070039 def __init__(self, custom_vars, local_scope):
Ryan Cuib603c542011-07-27 16:00:11 -070040 self._custom_vars = custom_vars
41 self._local_scope = local_scope
42
43 def Lookup(self, var_name):
44 """Implements the Var syntax."""
45 if var_name in self._custom_vars:
46 return self._custom_vars[var_name]
47 elif var_name in self._local_scope.get('vars', {}):
48 return self._local_scope['vars'][var_name]
49 raise Exception('Var is not defined: %s' % var_name)
50
51 locals = {}
52 var = _VarImpl({}, locals)
53 globals = {'From': FromImpl, 'Var': var.Lookup, 'deps_os': {}}
54 exec(deps_content) in globals, locals
55 return locals
56
57
Ryan Cui446e8e72011-09-23 18:02:41 -070058def _CreateCrosSymlink(repo_root):
59 """Create symlinks to CrOS projects specified in the cros_DEPS/DEPS file."""
60 cros_deps_file = os.path.join(repo_root, _CHROMIUM_CROS_DEPS)
61 _, merged_deps = GetParsedDeps(cros_deps_file)
62 chromium_root = os.path.join(repo_root, _CHROMIUM_ROOT)
Ryan Cuiaf3e2eb2011-09-30 16:43:03 -070063
Ryan Cui446e8e72011-09-23 18:02:41 -070064 mappings = GetPathToProjectMappings(merged_deps)
65 for rel_path, project in mappings.iteritems():
66 link_dir = os.path.join(chromium_root, rel_path)
67 target_dir = os.path.join(repo_root,
68 cros_lib.GetProjectDir(repo_root, project))
69 path_to_target = os.path.relpath(target_dir, os.path.dirname(link_dir))
70 if not os.path.exists(link_dir):
71 os.symlink(path_to_target, link_dir)
72
73
74def _ExtractProjectFromUrl(repo_url):
75 """Get the Gerrit project name from an url."""
Ryan Cui7d7c88b2011-11-01 22:11:55 -070076 # Example: 'ssh://gerrit-int.chromium.org:12121/my/project.git' would parse to
77 # 'my/project'. See unit test for more examples. For 'URL's like
78 # '../abc/efg' leave as-is to support unit tests.
79 mo = re.match(r'(?:[\w\+]+://[^/]+/)?(.*)', repo_url)
80 return os.path.splitext(mo.group(1))[0]
Ryan Cuib603c542011-07-27 16:00:11 -070081
82
Ryan Cuid1024a82011-08-26 17:58:36 -070083def _ExtractProjectFromEntry(entry):
84 """From a deps entry extract the Gerrit project name.
85
86 Arguments:
Ryan Cui65e1f9a2011-09-21 17:06:43 -070087 entry: The deps entry in the format ${url_prefix}/${project_name}@${hash}.
Ryan Cuid1024a82011-08-26 17:58:36 -070088 """
89 # We only support Gerrit urls, where the path is the project name.
90 repo_url = entry.partition('@')[0]
Ryan Cui446e8e72011-09-23 18:02:41 -070091 return _ExtractProjectFromUrl(repo_url)
Ryan Cuid1024a82011-08-26 17:58:36 -070092
93
94def GetPathToProjectMappings(deps):
95 """Get dictionary relating path to Gerrit project names.
96
97 Arguments:
98 deps: a dictionary indexed by repo paths. The same format as the 'deps'
99 entry in the '.DEPS.git' file.
100 """
101 mappings = {}
102 for rel_path, entry in deps.iteritems():
103 mappings[rel_path] = _ExtractProjectFromEntry(entry)
104
105 return mappings
106
107
Ryan Cui446e8e72011-09-23 18:02:41 -0700108class ProjectException(Exception):
109 """Raised by Project class when a Pin error """
110 pass
Ryan Cuib75c3032011-08-10 16:19:39 -0700111
112
Ryan Cui446e8e72011-09-23 18:02:41 -0700113class Project(object):
114 """Encapsulates functionality to pin a project to a specific commit."""
115 def __init__(self, repo_root, project_url, rel_path):
116 """Construct the class.
117
118 Arguments:
119 repo_root: The root of the repo checkout.
120 project_url: The Gerrit url of the project.
121 rel_path: The path the project is expected to be checked out to. Relative
122 to <repo_root>/chromium.
123 """
124 self.repo_root = repo_root
125 self.chromium_root = os.path.join(self.repo_root, _CHROMIUM_ROOT)
126 self.project_url = project_url
127 self.rel_path = rel_path
128 self.abs_path = os.path.join(self.chromium_root, self.rel_path)
129 self.manifest_rel_path = os.path.join(_CHROMIUM_ROOT, self.rel_path)
130 self.project_name = _ExtractProjectFromUrl(self.project_url)
131
132 def _ResetProject(self, commit_hash):
133 """Actually pin project to the specified commit hash."""
134 if not cros_lib.DoesCommitExistInRepo(self.abs_path, commit_hash):
135 cros_lib.Die('Commit %s not found in %s.\n'
136 "You probably need to run 'repo sync --jobs=<jobs>' "
137 'to update your checkout.'
138 % (commit_hash, self.abs_path))
139
140 result = cros_lib.RunCommand(['git', 'checkout', commit_hash],
141 error_code_ok=True, cwd=self.abs_path)
142 if result.returncode != 0:
143 cros_lib.Warning('Failed to pin project %s.\n'
144 'You probably have uncommited local changes.'
145 % self.abs_path)
146
147 def _PrepareProject(self):
148 """Make sure the project is synced properly and is ready for pinning."""
David James1bd7fae2012-04-22 10:14:38 -0700149 handler = cros_lib.ParseFullManifest(self.repo_root)
150 path_to_project_dict = dict(([attrs['path'], project]) for project, attrs
151 in handler.projects.iteritems())
Ryan Cui446e8e72011-09-23 18:02:41 -0700152
153 # TODO(rcui): Handle case where a dependency never makes it to the manifest
154 # (i.e., dep path added as double checkout, and then gets deleted). We need
155 # to delete those. crosbug/22123.
156 if not cros_lib.IsDirectoryAGitRepoRoot(self.abs_path):
157 if self.manifest_rel_path in path_to_project_dict:
158 raise ProjectException('%s in full layout manifest but not in working '
159 "tree. Please run 'repo sync %s'"
160 % (self.manifest_rel_path,
161 path_to_project_dict[self.manifest_rel_path]))
162 else:
163 cros_lib.Warning('Project %s is not in the manifest. Automatically '
164 'checking out to %s.\n'
165 % (self.project_url, self.abs_path))
166 repository.CloneGitRepo(self.abs_path, self.project_url)
167 cros_lib.RunCommand(['git', 'checkout',
168 cros_lib.GetGitRepoRevision(self.abs_path)],
169 cwd=self.abs_path)
170 elif not cros_lib.IsProjectManagedByRepo(self.abs_path):
171 if self.manifest_rel_path in path_to_project_dict:
172 # If path is now in the manifest, tell user to manually delete our
173 # managed checkout and re-sync.
174 raise ProjectException('%s needs to be replaced. Please remove the '
175 "directory and run 'repo sync %s'"
176 % (self.manifest_rel_path,
177 path_to_project_dict[self.manifest_rel_path]))
178 else:
179 # If not managed by Repo we need to perform sync.
180 cros_lib.RunCommand(['git', 'pull', '--rebase', self.project_url],
181 cwd=self.abs_path)
182 elif not os.path.islink(self.abs_path):
183 # Skip symlinks - we don't want to error out for the cros.DEPS projects.
184 if self.manifest_rel_path not in path_to_project_dict:
185 # If it is 'managed by repo' but not in the manifest, repo tried
186 # deleting it but failed because of local changes.
187 raise ProjectException('%s is no longer in the manifest but has local '
188 'changes. Please remove and try again.'
189 % self.manifest_rel_path)
190 elif self.project_name != path_to_project_dict[self.manifest_rel_path]:
191 cros_lib.Die('.DEPS.git for %s conflicts with manifest.xml! Running '
192 'with older .DEPS.git files are not yet supported. '
193 "Please run'repo sync --jobs=<jobs>' to sync everything "
194 'up.' % self.manifest_rel_path)
195
196 def Pin(self, commit_hash):
197 """Attempt to pin the project to the specified commit hash.
198
199 Arguments:
200 commit_hash: The commit to pin the project to.
201
202 Raises:
203 ProjectException when an error occurs.
204 """
205 self._PrepareProject()
206 if cros_lib.GetCurrentBranch(self.abs_path):
207 cros_lib.Warning("Not pinning project %s that's checked out to a "
208 'development branch.' % self.rel_path)
209 elif (commit_hash and
210 (commit_hash != cros_lib.GetGitRepoRevision(self.abs_path))):
211 print 'Pinning project %s' % self.rel_path
212 self._ResetProject(commit_hash)
213 else:
214 cros_lib.Debug('Skipping project %s, already pinned' % self.rel_path)
215
216
217def _ResetGitCheckout(repo_root, deps):
Ryan Cuib603c542011-07-27 16:00:11 -0700218 """Reset chrome repos to hashes specified in the DEPS file.
219
220 Arguments:
221 chromium_root: directory where chromium is checked out - level above 'src'.
222 deps: a dictionary indexed by repo paths. The same format as the 'deps'
223 entry in the '.DEPS.git' file.
224 """
Ryan Cui446e8e72011-09-23 18:02:41 -0700225 errors = []
Ryan Cuib603c542011-07-27 16:00:11 -0700226 for rel_path, project_hash in deps.iteritems():
Ryan Cuid1024a82011-08-26 17:58:36 -0700227 repo_url, _, commit_hash = project_hash.partition('@')
Ryan Cui446e8e72011-09-23 18:02:41 -0700228 project = Project(repo_root, repo_url, rel_path)
229 try:
230 project.Pin(commit_hash)
231 except ProjectException as e:
232 errors.append(str(e))
Ryan Cui65e1f9a2011-09-21 17:06:43 -0700233
Ryan Cui446e8e72011-09-23 18:02:41 -0700234 if errors:
235 cros_lib.Die('Errors encountered:\n* ' + '\n* '.join(errors))
Ryan Cuib603c542011-07-27 16:00:11 -0700236
237
238def _RunHooks(chromium_root, hooks):
239 """Run the hooks contained in the DEPS file.
240
241 Arguments:
242 chromium_root: directory where chromium is checked out - level above 'src'.
243 hooks: a list of hook dictionaries. The same format as the 'hooks' entry
244 in the '.DEPS.git' file.
245 """
246 for hook in hooks:
Ryan Cuiaf3e2eb2011-09-30 16:43:03 -0700247 print '[running hook] %s' % ' '.join(hook['action'])
Ryan Cuib603c542011-07-27 16:00:11 -0700248 cros_lib.RunCommand(hook['action'], cwd=chromium_root)
249
250
Ryan Cui44360de2011-08-22 16:50:20 -0700251def _MergeDeps(dest, update):
252 """Merge the dependencies specified in two dictionaries.
253
254 Arguments:
255 dest: The dictionary that will be merged into.
256 update: The dictionary whose elements will be merged into dest.
257 """
258 assert(not set(dest.keys()).intersection(set(update.keys())))
259 dest.update(update)
260 return dest
261
262
Ryan Cui97b2f302011-10-24 17:34:05 -0700263def GetParsedDeps(deps_file):
Ryan Cui6c851112011-08-11 21:51:37 -0700264 """Returns the full parsed DEPS file dictionary, and merged deps.
265
266 Arguments:
Ryan Cui65e1f9a2011-09-21 17:06:43 -0700267 deps_file: Path to the .DEPS.git file.
Ryan Cui6c851112011-08-11 21:51:37 -0700268
269 Returns:
270 An (x,y) tuple. x is a dictionary containing the contents of the DEPS file,
271 and y is a dictionary containing the result of merging unix and common deps.
272 """
273 with open(deps_file, 'rU') as f:
274 deps = _LoadDEPS(f.read())
275
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:
301 cros_lib.DebugLevel.SetDebugLevel(cros_lib.DebugLevel.DEBUG)
302 else:
303 cros_lib.DebugLevel.SetDebugLevel(cros_lib.DebugLevel.WARNING)
304
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)