| OLD | NEW |
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 # Copyright (c) 2010 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2010 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Meta checkout manager supporting both Subversion and GIT. | 6 """Meta checkout manager supporting both Subversion and GIT. |
| 7 | 7 |
| 8 Files | 8 Files |
| 9 .gclient : Current client configuration, written by 'config' command. | 9 .gclient : Current client configuration, written by 'config' command. |
| 10 Format is a Python script defining 'solutions', a list whose | 10 Format is a Python script defining 'solutions', a list whose |
| (...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 42 it will be removed from the list and the list will be extended | 42 it will be removed from the list and the list will be extended |
| 43 by the list of matching files. | 43 by the list of matching files. |
| 44 | 44 |
| 45 Example: | 45 Example: |
| 46 hooks = [ | 46 hooks = [ |
| 47 { "pattern": "\\.(gif|jpe?g|pr0n|png)$", | 47 { "pattern": "\\.(gif|jpe?g|pr0n|png)$", |
| 48 "action": ["python", "image_indexer.py", "--all"]}, | 48 "action": ["python", "image_indexer.py", "--all"]}, |
| 49 ] | 49 ] |
| 50 """ | 50 """ |
| 51 | 51 |
| 52 __version__ = "0.4.1" | 52 __version__ = "0.5" |
| 53 | 53 |
| 54 import errno | |
| 55 import logging | 54 import logging |
| 56 import optparse | 55 import optparse |
| 57 import os | 56 import os |
| 58 import pprint | 57 import pprint |
| 59 import re | 58 import re |
| 60 import sys | 59 import sys |
| 61 import urlparse | 60 import urlparse |
| 62 import urllib | 61 import urllib |
| 63 | 62 |
| 64 import breakpad | 63 import breakpad |
| (...skipping 24 matching lines...) Expand all Loading... |
| 89 | 88 |
| 90 sub_target_name is an optional parameter if the module name in the other | 89 sub_target_name is an optional parameter if the module name in the other |
| 91 DEPS file is different. E.g., you might want to map src/net to net.""" | 90 DEPS file is different. E.g., you might want to map src/net to net.""" |
| 92 self.module_name = module_name | 91 self.module_name = module_name |
| 93 self.sub_target_name = sub_target_name | 92 self.sub_target_name = sub_target_name |
| 94 | 93 |
| 95 def __str__(self): | 94 def __str__(self): |
| 96 return 'From(%s, %s)' % (repr(self.module_name), | 95 return 'From(%s, %s)' % (repr(self.module_name), |
| 97 repr(self.sub_target_name)) | 96 repr(self.sub_target_name)) |
| 98 | 97 |
| 99 def GetUrl(self, target_name, sub_deps_base_url, root_dir, sub_deps): | |
| 100 """Resolve the URL for this From entry.""" | |
| 101 sub_deps_target_name = target_name | |
| 102 if self.sub_target_name: | |
| 103 sub_deps_target_name = self.sub_target_name | |
| 104 url = sub_deps[sub_deps_target_name] | |
| 105 if url.startswith('/'): | |
| 106 # If it's a relative URL, we need to resolve the URL relative to the | |
| 107 # sub deps base URL. | |
| 108 if not isinstance(sub_deps_base_url, basestring): | |
| 109 sub_deps_base_url = sub_deps_base_url.GetPath() | |
| 110 scm = gclient_scm.CreateSCM(sub_deps_base_url, root_dir, | |
| 111 None) | |
| 112 url = scm.FullUrlForRelativeUrl(url) | |
| 113 return url | |
| 114 | |
| 115 class FileImpl(object): | 98 class FileImpl(object): |
| 116 """Used to implement the File('') syntax which lets you sync a single file | 99 """Used to implement the File('') syntax which lets you sync a single file |
| 117 from an SVN repo.""" | 100 from an SVN repo.""" |
| 118 | 101 |
| 119 def __init__(self, file_location): | 102 def __init__(self, file_location): |
| 120 self.file_location = file_location | 103 self.file_location = file_location |
| 121 | 104 |
| 122 def __str__(self): | 105 def __str__(self): |
| 123 return 'File("%s")' % self.file_location | 106 return 'File("%s")' % self.file_location |
| 124 | 107 |
| (...skipping 27 matching lines...) Expand all Loading... |
| 152 class Dependency(GClientKeywords): | 135 class Dependency(GClientKeywords): |
| 153 """Object that represents a dependency checkout.""" | 136 """Object that represents a dependency checkout.""" |
| 154 DEPS_FILE = 'DEPS' | 137 DEPS_FILE = 'DEPS' |
| 155 | 138 |
| 156 def __init__(self, parent, name, url, safesync_url=None, custom_deps=None, | 139 def __init__(self, parent, name, url, safesync_url=None, custom_deps=None, |
| 157 custom_vars=None, deps_file=None): | 140 custom_vars=None, deps_file=None): |
| 158 GClientKeywords.__init__(self) | 141 GClientKeywords.__init__(self) |
| 159 self.parent = parent | 142 self.parent = parent |
| 160 self.name = name | 143 self.name = name |
| 161 self.url = url | 144 self.url = url |
| 145 self.parsed_url = None |
| 162 # These 2 are only set in .gclient and not in DEPS files. | 146 # These 2 are only set in .gclient and not in DEPS files. |
| 163 self.safesync_url = safesync_url | 147 self.safesync_url = safesync_url |
| 164 self.custom_vars = custom_vars or {} | 148 self.custom_vars = custom_vars or {} |
| 165 self.custom_deps = custom_deps or {} | 149 self.custom_deps = custom_deps or {} |
| 166 self.deps_hooks = [] | 150 self.deps_hooks = [] |
| 167 self.dependencies = [] | 151 self.dependencies = [] |
| 168 self.deps_file = deps_file or self.DEPS_FILE | 152 self.deps_file = deps_file or self.DEPS_FILE |
| 153 # A cache of the files affected by the current operation, necessary for |
| 154 # hooks. |
| 155 self.file_list = [] |
| 169 self.deps_parsed = False | 156 self.deps_parsed = False |
| 170 self.direct_reference = False | 157 self.direct_reference = False |
| 171 | 158 |
| 172 # Sanity checks | 159 # Sanity checks |
| 173 if not self.name and self.parent: | 160 if not self.name and self.parent: |
| 174 raise gclient_utils.Error('Dependency without name') | 161 raise gclient_utils.Error('Dependency without name') |
| 162 if self.name in [d.name for d in self.tree(False)]: |
| 163 raise gclient_utils.Error('Dependency %s specified more than once' % |
| 164 self.name) |
| 175 if not isinstance(self.url, | 165 if not isinstance(self.url, |
| 176 (basestring, self.FromImpl, self.FileImpl, None.__class__)): | 166 (basestring, self.FromImpl, self.FileImpl, None.__class__)): |
| 177 raise gclient_utils.Error('dependency url must be either a string, None, ' | 167 raise gclient_utils.Error('dependency url must be either a string, None, ' |
| 178 'File() or From() instead of %s' % | 168 'File() or From() instead of %s' % |
| 179 self.url.__class__.__name__) | 169 self.url.__class__.__name__) |
| 180 if '/' in self.deps_file or '\\' in self.deps_file: | 170 if '/' in self.deps_file or '\\' in self.deps_file: |
| 181 raise gclient_utils.Error('deps_file name must not be a path, just a ' | 171 raise gclient_utils.Error('deps_file name must not be a path, just a ' |
| 182 'filename. %s' % self.deps_file) | 172 'filename. %s' % self.deps_file) |
| 183 | 173 |
| 174 def LateOverride(self, url): |
| 175 overriden_url = self.get_custom_deps(self.name, url) |
| 176 if overriden_url != url: |
| 177 self.parsed_url = overriden_url |
| 178 logging.debug('%s, %s was overriden to %s' % (self.name, url, |
| 179 self.parsed_url)) |
| 180 elif isinstance(url, self.FromImpl): |
| 181 ref = [dep for dep in self.tree(True) if url.module_name == dep.name] |
| 182 if not len(ref) == 1: |
| 183 raise Exception('Failed to find one reference to %s. %s' % ( |
| 184 url.module_name, ref)) |
| 185 ref = ref[0] |
| 186 sub_target = url.sub_target_name or url |
| 187 # Make sure the referenced dependency DEPS file is loaded and file the |
| 188 # inner referenced dependency. |
| 189 ref.ParseDepsFile(False) |
| 190 found_dep = None |
| 191 for d in ref.dependencies: |
| 192 if d.name == sub_target: |
| 193 found_dep = d |
| 194 break |
| 195 if not found_dep: |
| 196 raise Exception('Couldn\'t find %s in %s, referenced by %s' % ( |
| 197 sub_target, ref.name, self.name)) |
| 198 # Call LateOverride() again. |
| 199 self.parsed_url = found_dep.LateOverride(found_dep.url) |
| 200 logging.debug('%s, %s to %s' % (self.name, url, self.parsed_url)) |
| 201 elif isinstance(url, basestring): |
| 202 parsed_url = urlparse.urlparse(url) |
| 203 if not parsed_url[0]: |
| 204 # A relative url. Fetch the real base. |
| 205 path = parsed_url[2] |
| 206 if not path.startswith('/'): |
| 207 raise gclient_utils.Error( |
| 208 'relative DEPS entry \'%s\' must begin with a slash' % url) |
| 209 # Create a scm just to query the full url. |
| 210 scm = gclient_scm.CreateSCM(self.parent.parsed_url, self.root_dir(), |
| 211 None) |
| 212 self.parsed_url = scm.FullUrlForRelativeUrl(url) |
| 213 else: |
| 214 self.parsed_url = url |
| 215 logging.debug('%s, %s -> %s' % (self.name, url, self.parsed_url)) |
| 216 elif isinstance(url, self.FileImpl): |
| 217 self.parsed_url = url |
| 218 logging.debug('%s, %s -> %s (File)' % (self.name, url, self.parsed_url)) |
| 219 return self.parsed_url |
| 220 |
| 184 def ParseDepsFile(self, direct_reference): | 221 def ParseDepsFile(self, direct_reference): |
| 185 """Parses the DEPS file for this dependency.""" | 222 """Parses the DEPS file for this dependency.""" |
| 186 if direct_reference: | 223 if direct_reference: |
| 187 # Maybe it was referenced earlier by a From() keyword but it's now | 224 # Maybe it was referenced earlier by a From() keyword but it's now |
| 188 # directly referenced. | 225 # directly referenced. |
| 189 self.direct_reference = direct_reference | 226 self.direct_reference = direct_reference |
| 227 if self.deps_parsed: |
| 228 return |
| 190 self.deps_parsed = True | 229 self.deps_parsed = True |
| 191 filepath = os.path.join(self.root_dir(), self.name, self.deps_file) | 230 filepath = os.path.join(self.root_dir(), self.name, self.deps_file) |
| 192 if not os.path.isfile(filepath): | 231 if not os.path.isfile(filepath): |
| 193 return {} | 232 return |
| 194 deps_content = gclient_utils.FileRead(filepath) | 233 deps_content = gclient_utils.FileRead(filepath) |
| 195 | 234 |
| 196 # Eval the content. | 235 # Eval the content. |
| 197 # One thing is unintuitive, vars= {} must happen before Var() use. | 236 # One thing is unintuitive, vars= {} must happen before Var() use. |
| 198 local_scope = {} | 237 local_scope = {} |
| 199 var = self.VarImpl(self.custom_vars, local_scope) | 238 var = self.VarImpl(self.custom_vars, local_scope) |
| 200 global_scope = { | 239 global_scope = { |
| 201 'File': self.FileImpl, | 240 'File': self.FileImpl, |
| 202 'From': self.FromImpl, | 241 'From': self.FromImpl, |
| 203 'Var': var.Lookup, | 242 'Var': var.Lookup, |
| (...skipping 27 matching lines...) Expand all Loading... |
| 231 # the dictionary using paths relative to the directory containing | 270 # the dictionary using paths relative to the directory containing |
| 232 # the DEPS file. | 271 # the DEPS file. |
| 233 use_relative_paths = local_scope.get('use_relative_paths', False) | 272 use_relative_paths = local_scope.get('use_relative_paths', False) |
| 234 if use_relative_paths: | 273 if use_relative_paths: |
| 235 rel_deps = {} | 274 rel_deps = {} |
| 236 for d, url in deps.items(): | 275 for d, url in deps.items(): |
| 237 # normpath is required to allow DEPS to use .. in their | 276 # normpath is required to allow DEPS to use .. in their |
| 238 # dependency local path. | 277 # dependency local path. |
| 239 rel_deps[os.path.normpath(os.path.join(self.name, d))] = url | 278 rel_deps[os.path.normpath(os.path.join(self.name, d))] = url |
| 240 deps = rel_deps | 279 deps = rel_deps |
| 241 # TODO(maruel): Add these dependencies into self.dependencies. | |
| 242 return deps | |
| 243 | 280 |
| 244 def _ParseAllDeps(self, solution_urls): | 281 # Convert the deps into real Dependency. |
| 245 """Parse the complete list of dependencies for the client. | 282 for name, url in deps.iteritems(): |
| 283 if name in [s.name for s in self.dependencies]: |
| 284 raise |
| 285 self.dependencies.append(Dependency(self, name, url)) |
| 286 # Sort by name. |
| 287 self.dependencies.sort(key=lambda x: x.name) |
| 288 logging.info('Loaded: %s' % str(self)) |
| 246 | 289 |
| 247 Args: | 290 def RunCommandRecursively(self, options, revision_overrides, |
| 248 solution_urls: A dict mapping module names (as relative paths) to URLs | 291 command, args, pm): |
| 249 corresponding to the solutions specified by the client. This parameter | 292 """Runs 'command' before parsing the DEPS in case it's a initial checkout |
| 250 is passed as an optimization. | 293 or a revert.""" |
| 294 assert self.file_list == [] |
| 295 # When running runhooks, there's no need to consult the SCM. |
| 296 # All known hooks are expected to run unconditionally regardless of working |
| 297 # copy state, so skip the SCM status check. |
| 298 run_scm = command not in ('runhooks', None) |
| 299 self.LateOverride(self.url) |
| 300 if run_scm and self.parsed_url: |
| 301 if isinstance(self.parsed_url, self.FileImpl): |
| 302 # Special support for single-file checkout. |
| 303 options.revision = self.parsed_url.GetRevision() |
| 304 scm = gclient_scm.CreateSCM(self.parsed_url.GetPath(), self.root_dir(), |
| 305 self.name) |
| 306 scm.RunCommand("updatesingle", options, |
| 307 args + [self.parsed_url.GetFilename()], self.file_list) |
| 308 else: |
| 309 options.revision = revision_overrides.get(self.name) |
| 310 scm = gclient_scm.CreateSCM(self.parsed_url, self.root_dir(), self.name) |
| 311 scm.RunCommand(command, options, args, self.file_list) |
| 312 self.file_list = [os.path.join(self.name, f.strip()) |
| 313 for f in self.file_list] |
| 314 options.revision = None |
| 315 if pm: |
| 316 # The + 1 comes from the fact that .gclient is considered a step in |
| 317 # itself, .i.e. this code is called one time for the .gclient. This is not |
| 318 # conceptually correct but it simplifies code. |
| 319 pm._total = len(self.tree(False)) + 1 |
| 320 pm.update() |
| 321 if self.recursion_limit(): |
| 322 # Then we can parse the DEPS file. |
| 323 self.ParseDepsFile(True) |
| 324 if pm: |
| 325 pm._total = len(self.tree(False)) + 1 |
| 326 pm.update(0) |
| 327 # Parse the dependencies of this dependency. |
| 328 for s in self.dependencies: |
| 329 # TODO(maruel): All these can run concurrently! No need for threads, |
| 330 # just buffer stdout&stderr on pipes and flush as they complete. |
| 331 # Watch out for stdin. |
| 332 s.RunCommandRecursively(options, revision_overrides, command, args, pm) |
| 251 | 333 |
| 252 Returns: | 334 def RunHooksRecursively(self, options): |
| 253 A dict mapping module names (as relative paths) to URLs corresponding | 335 """Evaluates all hooks, running actions as needed. RunCommandRecursively() |
| 254 to the entire set of dependencies to checkout for the given client. | 336 must have been called before to load the DEPS.""" |
| 337 # If "--force" was specified, run all hooks regardless of what files have |
| 338 # changed. |
| 339 if self.deps_hooks: |
| 340 # TODO(maruel): If the user is using git or git-svn, then we don't know |
| 341 # what files have changed so we always run all hooks. It'd be nice to fix |
| 342 # that. |
| 343 if (options.force or |
| 344 gclient_scm.GetScmName(self.parsed_url) in ('git', None) or |
| 345 os.path.isdir(os.path.join(self.root_dir(), self.name, '.git'))): |
| 346 for hook_dict in self.deps_hooks: |
| 347 self._RunHookAction(hook_dict, []) |
| 348 else: |
| 349 # TODO(phajdan.jr): We should know exactly when the paths are absolute. |
| 350 # Convert all absolute paths to relative. |
| 351 for i in range(len(self.file_list)): |
| 352 # It depends on the command being executed (like runhooks vs sync). |
| 353 if not os.path.isabs(self.file_list[i]): |
| 354 continue |
| 255 | 355 |
| 256 Raises: | 356 prefix = os.path.commonprefix([self.root_dir().lower(), |
| 257 Error: If a dependency conflicts with another dependency or of a solution. | 357 self.file_list[i].lower()]) |
| 258 """ | 358 self.file_list[i] = self.file_list[i][len(prefix):] |
| 259 deps = {} | |
| 260 for solution in self.dependencies: | |
| 261 solution_deps = solution.ParseDepsFile(True) | |
| 262 | 359 |
| 263 # If a line is in custom_deps, but not in the solution, we want to append | 360 # Strip any leading path separators. |
| 264 # this line to the solution. | 361 while self.file_list[i][0] in ('\\', '/'): |
| 265 for d in solution.custom_deps: | 362 self.file_list[i] = self.file_list[i][1:] |
| 266 if d not in solution_deps: | |
| 267 solution_deps[d] = solution.custom_deps[d] | |
| 268 | 363 |
| 269 for d in solution_deps: | 364 # Run hooks on the basis of whether the files from the gclient operation |
| 270 if d in solution.custom_deps: | 365 # match each hook's pattern. |
| 271 # Dependency is overriden. | 366 for hook_dict in self.deps_hooks: |
| 272 url = solution.custom_deps[d] | 367 pattern = re.compile(hook_dict['pattern']) |
| 273 if url is None: | 368 matching_file_list = [f for f in self.file_list if pattern.search(f)] |
| 274 continue | 369 if matching_file_list: |
| 275 else: | 370 self._RunHookAction(hook_dict, matching_file_list) |
| 276 url = solution_deps[d] | 371 if self.recursion_limit(): |
| 277 # if we have a From reference dependent on another solution, then | 372 for s in self.dependencies: |
| 278 # just skip the From reference. When we pull deps for the solution, | 373 s.RunHooksRecursively(options) |
| 279 # we will take care of this dependency. | |
| 280 # | |
| 281 # If multiple solutions all have the same From reference, then we | |
| 282 # should only add one to our list of dependencies. | |
| 283 if isinstance(url, self.FromImpl): | |
| 284 if url.module_name in solution_urls: | |
| 285 # Already parsed. | |
| 286 continue | |
| 287 if d in deps and type(deps[d]) != str: | |
| 288 if url.module_name == deps[d].module_name: | |
| 289 continue | |
| 290 elif isinstance(url, str): | |
| 291 parsed_url = urlparse.urlparse(url) | |
| 292 scheme = parsed_url[0] | |
| 293 if not scheme: | |
| 294 # A relative url. Fetch the real base. | |
| 295 path = parsed_url[2] | |
| 296 if path[0] != "/": | |
| 297 raise gclient_utils.Error( | |
| 298 "relative DEPS entry \"%s\" must begin with a slash" % d) | |
| 299 # Create a scm just to query the full url. | |
| 300 scm = gclient_scm.CreateSCM(solution.url, self.root_dir(), | |
| 301 None) | |
| 302 url = scm.FullUrlForRelativeUrl(url) | |
| 303 if d in deps and deps[d] != url: | |
| 304 raise gclient_utils.Error( | |
| 305 "Solutions have conflicting versions of dependency \"%s\"" % d) | |
| 306 if d in solution_urls and solution_urls[d] != url: | |
| 307 raise gclient_utils.Error( | |
| 308 "Dependency \"%s\" conflicts with specified solution" % d) | |
| 309 # Grab the dependency. | |
| 310 deps[d] = url | |
| 311 return deps | |
| 312 | 374 |
| 313 def _RunHookAction(self, hook_dict, matching_file_list): | 375 def _RunHookAction(self, hook_dict, matching_file_list): |
| 314 """Runs the action from a single hook.""" | 376 """Runs the action from a single hook.""" |
| 315 logging.info(hook_dict) | 377 logging.info(hook_dict) |
| 316 logging.info(matching_file_list) | 378 logging.info(matching_file_list) |
| 317 command = hook_dict['action'][:] | 379 command = hook_dict['action'][:] |
| 318 if command[0] == 'python': | 380 if command[0] == 'python': |
| 319 # If the hook specified "python" as the first item, the action is a | 381 # If the hook specified "python" as the first item, the action is a |
| 320 # Python script. Run it by starting a new copy of the same | 382 # Python script. Run it by starting a new copy of the same |
| 321 # interpreter. | 383 # interpreter. |
| 322 command[0] = sys.executable | 384 command[0] = sys.executable |
| 323 | 385 |
| 324 if '$matching_files' in command: | 386 if '$matching_files' in command: |
| 325 splice_index = command.index('$matching_files') | 387 splice_index = command.index('$matching_files') |
| 326 command[splice_index:splice_index + 1] = matching_file_list | 388 command[splice_index:splice_index + 1] = matching_file_list |
| 327 | 389 |
| 328 # Use a discrete exit status code of 2 to indicate that a hook action | 390 # Use a discrete exit status code of 2 to indicate that a hook action |
| 329 # failed. Users of this script may wish to treat hook action failures | 391 # failed. Users of this script may wish to treat hook action failures |
| 330 # differently from VC failures. | 392 # differently from VC failures. |
| 331 return gclient_utils.SubprocessCall(command, self.root_dir(), fail_status=2) | 393 return gclient_utils.SubprocessCall(command, self.root_dir(), fail_status=2) |
| 332 | 394 |
| 333 def _RunHooks(self, command, file_list, is_using_git): | |
| 334 """Evaluates all hooks, running actions as needed. | |
| 335 """ | |
| 336 # Hooks only run for these command types. | |
| 337 if not command in ('update', 'revert', 'runhooks'): | |
| 338 return | |
| 339 | |
| 340 # Hooks only run when --nohooks is not specified | |
| 341 if self._options.nohooks: | |
| 342 return | |
| 343 | |
| 344 # Get any hooks from the .gclient file. | |
| 345 hooks = self.deps_hooks[:] | |
| 346 # Add any hooks found in DEPS files. | |
| 347 for d in self.dependencies: | |
| 348 hooks.extend(d.deps_hooks) | |
| 349 | |
| 350 # If "--force" was specified, run all hooks regardless of what files have | |
| 351 # changed. If the user is using git, then we don't know what files have | |
| 352 # changed so we always run all hooks. | |
| 353 if self._options.force or is_using_git: | |
| 354 for hook_dict in hooks: | |
| 355 self._RunHookAction(hook_dict, []) | |
| 356 return | |
| 357 | |
| 358 # Run hooks on the basis of whether the files from the gclient operation | |
| 359 # match each hook's pattern. | |
| 360 for hook_dict in hooks: | |
| 361 pattern = re.compile(hook_dict['pattern']) | |
| 362 matching_file_list = [f for f in file_list if pattern.search(f)] | |
| 363 if matching_file_list: | |
| 364 self._RunHookAction(hook_dict, matching_file_list) | |
| 365 | |
| 366 def root_dir(self): | 395 def root_dir(self): |
| 367 return self.parent.root_dir() | 396 return self.parent.root_dir() |
| 368 | 397 |
| 369 def enforced_os(self): | 398 def enforced_os(self): |
| 370 return self.parent.enforced_os() | 399 return self.parent.enforced_os() |
| 371 | 400 |
| 372 def recursion_limit(self): | 401 def recursion_limit(self): |
| 373 return self.parent.recursion_limit() - 1 | 402 return self.parent.recursion_limit() - 1 |
| 374 | 403 |
| 375 def tree(self, force_all): | 404 def tree(self, force_all): |
| 376 return self.parent.tree(force_all) | 405 return self.parent.tree(force_all) |
| 377 | 406 |
| 378 def get_custom_deps(self, name, url): | 407 def get_custom_deps(self, name, url): |
| 379 """Returns a custom deps if applicable.""" | 408 """Returns a custom deps if applicable.""" |
| 380 if self.parent: | 409 if self.parent: |
| 381 url = self.parent.get_custom_deps(name, url) | 410 url = self.parent.get_custom_deps(name, url) |
| 382 # None is a valid return value to disable a dependency. | 411 # None is a valid return value to disable a dependency. |
| 383 return self.custom_deps.get(name, url) | 412 return self.custom_deps.get(name, url) |
| 384 | 413 |
| 385 def __str__(self): | 414 def __str__(self): |
| 386 out = [] | 415 out = [] |
| 387 for i in ('name', 'url', 'safesync_url', 'custom_deps', 'custom_vars', | 416 for i in ('name', 'url', 'safesync_url', 'custom_deps', 'custom_vars', |
| 388 'deps_hooks'): | 417 'deps_hooks', 'file_list'): |
| 389 # 'deps_file' | 418 # 'deps_file' |
| 390 if self.__dict__[i]: | 419 if self.__dict__[i]: |
| 391 out.append('%s: %s' % (i, self.__dict__[i])) | 420 out.append('%s: %s' % (i, self.__dict__[i])) |
| 392 | 421 |
| 393 for d in self.dependencies: | 422 for d in self.dependencies: |
| 394 out.extend([' ' + x for x in str(d).splitlines()]) | 423 out.extend([' ' + x for x in str(d).splitlines()]) |
| 395 out.append('') | 424 out.append('') |
| 396 return '\n'.join(out) | 425 return '\n'.join(out) |
| 397 | 426 |
| 398 def __repr__(self): | 427 def __repr__(self): |
| (...skipping 106 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 505 os.path.join(path, options.config_filename))) | 534 os.path.join(path, options.config_filename))) |
| 506 return client | 535 return client |
| 507 | 536 |
| 508 def SetDefaultConfig(self, solution_name, solution_url, safesync_url): | 537 def SetDefaultConfig(self, solution_name, solution_url, safesync_url): |
| 509 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % { | 538 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % { |
| 510 'solution_name': solution_name, | 539 'solution_name': solution_name, |
| 511 'solution_url': solution_url, | 540 'solution_url': solution_url, |
| 512 'safesync_url' : safesync_url, | 541 'safesync_url' : safesync_url, |
| 513 }) | 542 }) |
| 514 | 543 |
| 515 def _SaveEntries(self, entries): | 544 def _SaveEntries(self): |
| 516 """Creates a .gclient_entries file to record the list of unique checkouts. | 545 """Creates a .gclient_entries file to record the list of unique checkouts. |
| 517 | 546 |
| 518 The .gclient_entries file lives in the same directory as .gclient. | 547 The .gclient_entries file lives in the same directory as .gclient. |
| 519 """ | 548 """ |
| 520 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It | 549 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It |
| 521 # makes testing a bit too fun. | 550 # makes testing a bit too fun. |
| 551 entries = dict([(i.name, i.parsed_url) for i in self.tree(False)]) |
| 522 result = pprint.pformat(entries, 2) | 552 result = pprint.pformat(entries, 2) |
| 523 if result.startswith('{\''): | 553 if result.startswith('{\''): |
| 524 result = '{ \'' + result[2:] | 554 result = '{ \'' + result[2:] |
| 525 text = 'entries = \\\n' + result + '\n' | 555 text = 'entries = \\\n' + result + '\n' |
| 526 file_path = os.path.join(self.root_dir(), self._options.entries_filename) | 556 file_path = os.path.join(self.root_dir(), self._options.entries_filename) |
| 527 gclient_utils.FileWrite(file_path, text) | 557 gclient_utils.FileWrite(file_path, text) |
| 528 | 558 |
| 529 def _ReadEntries(self): | 559 def _ReadEntries(self): |
| 530 """Read the .gclient_entries file for the given client. | 560 """Read the .gclient_entries file for the given client. |
| 531 | 561 |
| (...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 575 def RunOnDeps(self, command, args): | 605 def RunOnDeps(self, command, args): |
| 576 """Runs a command on each dependency in a client and its dependencies. | 606 """Runs a command on each dependency in a client and its dependencies. |
| 577 | 607 |
| 578 Args: | 608 Args: |
| 579 command: The command to use (e.g., 'status' or 'diff') | 609 command: The command to use (e.g., 'status' or 'diff') |
| 580 args: list of str - extra arguments to add to the command line. | 610 args: list of str - extra arguments to add to the command line. |
| 581 """ | 611 """ |
| 582 if not self.dependencies: | 612 if not self.dependencies: |
| 583 raise gclient_utils.Error('No solution specified') | 613 raise gclient_utils.Error('No solution specified') |
| 584 revision_overrides = self._EnforceRevisions() | 614 revision_overrides = self._EnforceRevisions() |
| 585 | 615 pm = None |
| 586 # When running runhooks --force, there's no need to consult the SCM. | |
| 587 # All known hooks are expected to run unconditionally regardless of working | |
| 588 # copy state, so skip the SCM status check. | |
| 589 run_scm = not (command == 'runhooks' and self._options.force) | |
| 590 | |
| 591 entries = {} | |
| 592 file_list = [] | |
| 593 # Run on the base solutions first. | |
| 594 for solution in self.dependencies: | |
| 595 name = solution.name | |
| 596 if name in entries: | |
| 597 raise gclient_utils.Error("solution %s specified more than once" % name) | |
| 598 url = solution.url | |
| 599 entries[name] = url | |
| 600 if run_scm and url: | |
| 601 self._options.revision = revision_overrides.get(name) | |
| 602 scm = gclient_scm.CreateSCM(url, self.root_dir(), name) | |
| 603 scm.RunCommand(command, self._options, args, file_list) | |
| 604 file_list = [os.path.join(name, f.strip()) for f in file_list] | |
| 605 self._options.revision = None | |
| 606 | |
| 607 # Process the dependencies next (sort alphanumerically to ensure that | |
| 608 # containing directories get populated first and for readability) | |
| 609 deps = self._ParseAllDeps(entries) | |
| 610 deps_to_process = deps.keys() | |
| 611 deps_to_process.sort() | |
| 612 | |
| 613 # First pass for direct dependencies. | |
| 614 if command == 'update' and not self._options.verbose: | 616 if command == 'update' and not self._options.verbose: |
| 615 pm = Progress('Syncing projects', len(deps_to_process)) | 617 pm = Progress('Syncing projects', len(self.tree(False)) + 1) |
| 616 for d in deps_to_process: | 618 self.RunCommandRecursively(self._options, revision_overrides, |
| 617 if command == 'update' and not self._options.verbose: | 619 command, args, pm) |
| 618 pm.update() | 620 if pm: |
| 619 if type(deps[d]) == str: | |
| 620 url = deps[d] | |
| 621 entries[d] = url | |
| 622 if run_scm: | |
| 623 self._options.revision = revision_overrides.get(d) | |
| 624 scm = gclient_scm.CreateSCM(url, self.root_dir(), d) | |
| 625 scm.RunCommand(command, self._options, args, file_list) | |
| 626 self._options.revision = None | |
| 627 elif isinstance(deps[d], self.FileImpl): | |
| 628 file_dep = deps[d] | |
| 629 self._options.revision = file_dep.GetRevision() | |
| 630 if run_scm: | |
| 631 scm = gclient_scm.CreateSCM(file_dep.GetPath(), self.root_dir(), d) | |
| 632 scm.RunCommand("updatesingle", self._options, | |
| 633 args + [file_dep.GetFilename()], file_list) | |
| 634 | |
| 635 if command == 'update' and not self._options.verbose: | |
| 636 pm.end() | 621 pm.end() |
| 637 | 622 |
| 638 # Second pass for inherited deps (via the From keyword) | 623 # Once all the dependencies have been processed, it's now safe to run the |
| 639 for d in deps_to_process: | 624 # hooks. |
| 640 if isinstance(deps[d], self.FromImpl): | 625 if not self._options.nohooks: |
| 641 # Getting the URL from the sub_deps file can involve having to resolve | 626 self.RunHooksRecursively(self._options) |
| 642 # a File() or having to resolve a relative URL. To resolve relative | |
| 643 # URLs, we need to pass in the orignal sub deps URL. | |
| 644 sub_deps_base_url = deps[deps[d].module_name] | |
| 645 sub_deps = Dependency(self, deps[d].module_name, sub_deps_base_url | |
| 646 ).ParseDepsFile(False) | |
| 647 url = deps[d].GetUrl(d, sub_deps_base_url, self.root_dir(), sub_deps) | |
| 648 entries[d] = url | |
| 649 if run_scm: | |
| 650 self._options.revision = revision_overrides.get(d) | |
| 651 scm = gclient_scm.CreateSCM(url, self.root_dir(), d) | |
| 652 scm.RunCommand(command, self._options, args, file_list) | |
| 653 self._options.revision = None | |
| 654 | |
| 655 # Convert all absolute paths to relative. | |
| 656 for i in range(len(file_list)): | |
| 657 # TODO(phajdan.jr): We should know exactly when the paths are absolute. | |
| 658 # It depends on the command being executed (like runhooks vs sync). | |
| 659 if not os.path.isabs(file_list[i]): | |
| 660 continue | |
| 661 | |
| 662 prefix = os.path.commonprefix([self.root_dir().lower(), | |
| 663 file_list[i].lower()]) | |
| 664 file_list[i] = file_list[i][len(prefix):] | |
| 665 | |
| 666 # Strip any leading path separators. | |
| 667 while file_list[i].startswith('\\') or file_list[i].startswith('/'): | |
| 668 file_list[i] = file_list[i][1:] | |
| 669 | |
| 670 is_using_git = gclient_utils.IsUsingGit(self.root_dir(), entries.keys()) | |
| 671 self._RunHooks(command, file_list, is_using_git) | |
| 672 | 627 |
| 673 if command == 'update': | 628 if command == 'update': |
| 674 # Notify the user if there is an orphaned entry in their working copy. | 629 # Notify the user if there is an orphaned entry in their working copy. |
| 675 # Only delete the directory if there are no changes in it, and | 630 # Only delete the directory if there are no changes in it, and |
| 676 # delete_unversioned_trees is set to true. | 631 # delete_unversioned_trees is set to true. |
| 677 prev_entries = self._ReadEntries() | 632 entries = [i.name for i in self.tree(False)] |
| 678 for entry in prev_entries: | 633 for entry, prev_url in self._ReadEntries().iteritems(): |
| 679 # Fix path separator on Windows. | 634 # Fix path separator on Windows. |
| 680 entry_fixed = entry.replace('/', os.path.sep) | 635 entry_fixed = entry.replace('/', os.path.sep) |
| 681 e_dir = os.path.join(self.root_dir(), entry_fixed) | 636 e_dir = os.path.join(self.root_dir(), entry_fixed) |
| 682 # Use entry and not entry_fixed there. | 637 # Use entry and not entry_fixed there. |
| 683 if entry not in entries and os.path.exists(e_dir): | 638 if entry not in entries and os.path.exists(e_dir): |
| 684 modified_files = False | 639 file_list = [] |
| 685 if isinstance(prev_entries, list): | 640 scm = gclient_scm.CreateSCM(prev_url, self.root_dir(), entry_fixed) |
| 686 # old .gclient_entries format was list, now dict | 641 scm.status(self._options, [], file_list) |
| 687 modified_files = gclient_scm.scm.SVN.CaptureStatus(e_dir) | 642 modified_files = file_list != [] |
| 688 else: | |
| 689 file_list = [] | |
| 690 scm = gclient_scm.CreateSCM(prev_entries[entry], self.root_dir(), | |
| 691 entry_fixed) | |
| 692 scm.status(self._options, [], file_list) | |
| 693 modified_files = file_list != [] | |
| 694 if not self._options.delete_unversioned_trees or modified_files: | 643 if not self._options.delete_unversioned_trees or modified_files: |
| 695 # There are modified files in this entry. Keep warning until | 644 # There are modified files in this entry. Keep warning until |
| 696 # removed. | 645 # removed. |
| 697 print(('\nWARNING: \'%s\' is no longer part of this client. ' | 646 print(('\nWARNING: \'%s\' is no longer part of this client. ' |
| 698 'It is recommended that you manually remove it.\n') % | 647 'It is recommended that you manually remove it.\n') % |
| 699 entry_fixed) | 648 entry_fixed) |
| 700 else: | 649 else: |
| 701 # Delete the entry | 650 # Delete the entry |
| 702 print('\n________ deleting \'%s\' in \'%s\'' % ( | 651 print('\n________ deleting \'%s\' in \'%s\'' % ( |
| 703 entry_fixed, self.root_dir())) | 652 entry_fixed, self.root_dir())) |
| 704 gclient_utils.RemoveDirectory(e_dir) | 653 gclient_utils.RemoveDirectory(e_dir) |
| 705 # record the current list of entries for next time | 654 # record the current list of entries for next time |
| 706 self._SaveEntries(entries) | 655 self._SaveEntries() |
| 707 return 0 | 656 return 0 |
| 708 | 657 |
| 709 def PrintRevInfo(self): | 658 def PrintRevInfo(self): |
| 710 """Output revision info mapping for the client and its dependencies. | |
| 711 | |
| 712 This allows the capture of an overall "revision" for the source tree that | |
| 713 can be used to reproduce the same tree in the future. It is only useful for | |
| 714 "unpinned dependencies", i.e. DEPS/deps references without a svn revision | |
| 715 number or a git hash. A git branch name isn't "pinned" since the actual | |
| 716 commit can change. | |
| 717 | |
| 718 The --snapshot option allows creating a .gclient file to reproduce the tree. | |
| 719 """ | |
| 720 if not self.dependencies: | 659 if not self.dependencies: |
| 721 raise gclient_utils.Error('No solution specified') | 660 raise gclient_utils.Error('No solution specified') |
| 722 | 661 self.RunCommandRecursively(self._options, {}, None, [], None) |
| 723 # Inner helper to generate base url and rev tuple | 662 def GetActualRevision(i): |
| 724 def GetURLAndRev(name, original_url): | 663 url, _ = gclient_utils.SplitUrlRevision(i.parsed_url) |
| 725 url, _ = gclient_utils.SplitUrlRevision(original_url) | 664 scm = gclient_scm.CreateSCM(url, self.root_dir(), i.name) |
| 726 scm = gclient_scm.CreateSCM(original_url, self.root_dir(), name) | 665 if not os.path.isdir(scm.checkout_path): |
| 727 return (url, scm.revinfo(self._options, [], None)) | 666 entries[i.name] = None |
| 728 | 667 else: |
| 729 # text of the snapshot gclient file | 668 entries[i.name] = '%s@%s' % (url, scm.revinfo(self._options, [], None)) |
| 730 new_gclient = "" | |
| 731 # Dictionary of { path : SCM url } to ensure no duplicate solutions | |
| 732 solution_names = {} | |
| 733 entries = {} | |
| 734 # Run on the base solutions first. | |
| 735 for solution in self.dependencies: | |
| 736 # Dictionary of { path : SCM url } to describe the gclient checkout | |
| 737 name = solution.name | |
| 738 if name in solution_names: | |
| 739 raise gclient_utils.Error("solution %s specified more than once" % name) | |
| 740 (url, rev) = GetURLAndRev(name, solution.url) | |
| 741 entries[name] = "%s@%s" % (url, rev) | |
| 742 solution_names[name] = "%s@%s" % (url, rev) | |
| 743 | |
| 744 # Process the dependencies next (sort alphanumerically to ensure that | |
| 745 # containing directories get populated first and for readability) | |
| 746 deps = self._ParseAllDeps(entries) | |
| 747 deps_to_process = deps.keys() | |
| 748 deps_to_process.sort() | |
| 749 | |
| 750 # First pass for direct dependencies. | |
| 751 for d in deps_to_process: | |
| 752 if type(deps[d]) == str: | |
| 753 (url, rev) = GetURLAndRev(d, deps[d]) | |
| 754 entries[d] = "%s@%s" % (url, rev) | |
| 755 | |
| 756 # Second pass for inherited deps (via the From keyword) | |
| 757 for d in deps_to_process: | |
| 758 if isinstance(deps[d], self.FromImpl): | |
| 759 deps_parent_url = entries[deps[d].module_name] | |
| 760 if deps_parent_url.find("@") < 0: | |
| 761 raise gclient_utils.Error("From %s missing revisioned url" % | |
| 762 deps[d].module_name) | |
| 763 sub_deps_base_url = deps[deps[d].module_name] | |
| 764 sub_deps = Dependency(self, deps[d].module_name, sub_deps_base_url | |
| 765 ).ParseDepsFile(False) | |
| 766 url = deps[d].GetUrl(d, sub_deps_base_url, self.root_dir(), sub_deps) | |
| 767 (url, rev) = GetURLAndRev(d, url) | |
| 768 entries[d] = "%s@%s" % (url, rev) | |
| 769 | |
| 770 # Build the snapshot configuration string | |
| 771 if self._options.snapshot: | 669 if self._options.snapshot: |
| 772 url = entries.pop(name) | 670 new_gclient = '' |
| 773 custom_deps = ''.join([' \"%s\": \"%s\",\n' % (x, entries[x]) | 671 # First level at .gclient |
| 774 for x in sorted(entries.keys())]) | 672 for d in self.dependencies: |
| 775 | 673 entries = {} |
| 776 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % { | 674 # Lower level at DEPS |
| 777 'solution_name': name, | 675 def GrabDeps(sol): |
| 778 'solution_url': url, | 676 for i in sol.dependencies: |
| 779 'safesync_url' : '', | 677 GetActualRevision(i) |
| 780 'solution_deps': custom_deps, | 678 GrabDeps(i) |
| 781 } | 679 GrabDeps(d) |
| 680 custom_deps = [] |
| 681 for k in sorted(entries.keys()): |
| 682 if entries[k]: |
| 683 # Quotes aren't escaped... |
| 684 custom_deps.append(' \"%s\": \'%s\',\n' % (k, entries[k])) |
| 685 else: |
| 686 custom_deps.append(' \"%s\": None,\n' % k) |
| 687 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % { |
| 688 'solution_name': d.name, |
| 689 'solution_url': d.url, |
| 690 'safesync_url' : d.safesync_url or '', |
| 691 'solution_deps': ''.join(custom_deps), |
| 692 } |
| 693 # Print the snapshot configuration file |
| 694 print(self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient}) |
| 782 else: | 695 else: |
| 783 print(';\n'.join(['%s: %s' % (x, entries[x]) | 696 entries = {} |
| 784 for x in sorted(entries.keys())])) | 697 for i in self.tree(False): |
| 785 | 698 if self._options.actual: |
| 786 # Print the snapshot configuration file | 699 GetActualRevision(i) |
| 787 if self._options.snapshot: | 700 else: |
| 788 config = self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient} | 701 entries[i.name] = i.parsed_url |
| 789 snapclient = GClient(self.root_dir(), self._options) | 702 for x in sorted(entries.keys()): |
| 790 snapclient.SetConfig(config) | 703 print '%s: %s' % (x, entries[x]) |
| 791 print(snapclient.config_content) | |
| 792 | 704 |
| 793 def ParseDepsFile(self, direct_reference): | 705 def ParseDepsFile(self, direct_reference): |
| 794 """No DEPS to parse for a .gclient file.""" | 706 """No DEPS to parse for a .gclient file.""" |
| 795 self.direct_reference = direct_reference | 707 self.direct_reference = direct_reference |
| 796 self.deps_parsed = True | 708 self.deps_parsed = True |
| 797 | 709 |
| 798 def root_dir(self): | 710 def root_dir(self): |
| 799 """Root directory of gclient checkout.""" | 711 """Root directory of gclient checkout.""" |
| 800 return self._root_dir | 712 return self._root_dir |
| 801 | 713 |
| (...skipping 269 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1071 This allows the capture of an overall 'revision' for the source tree that | 983 This allows the capture of an overall 'revision' for the source tree that |
| 1072 can be used to reproduce the same tree in the future. It is only useful for | 984 can be used to reproduce the same tree in the future. It is only useful for |
| 1073 'unpinned dependencies', i.e. DEPS/deps references without a svn revision | 985 'unpinned dependencies', i.e. DEPS/deps references without a svn revision |
| 1074 number or a git hash. A git branch name isn't 'pinned' since the actual | 986 number or a git hash. A git branch name isn't 'pinned' since the actual |
| 1075 commit can change. | 987 commit can change. |
| 1076 """ | 988 """ |
| 1077 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST', | 989 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST', |
| 1078 help='override deps for the specified (comma-separated) ' | 990 help='override deps for the specified (comma-separated) ' |
| 1079 'platform(s); \'all\' will process all deps_os ' | 991 'platform(s); \'all\' will process all deps_os ' |
| 1080 'references') | 992 'references') |
| 993 parser.add_option('-a', '--actual', action='store_true', |
| 994 help='gets the actual checked out revisions instead of the ' |
| 995 'ones specified in the DEPS and .gclient files') |
| 1081 parser.add_option('-s', '--snapshot', action='store_true', | 996 parser.add_option('-s', '--snapshot', action='store_true', |
| 1082 help='creates a snapshot .gclient file of the current ' | 997 help='creates a snapshot .gclient file of the current ' |
| 1083 'version of all repositories to reproduce the tree, ' | 998 'version of all repositories to reproduce the tree, ' |
| 1084 'implies -a') | 999 'implies -a') |
| 1085 (options, args) = parser.parse_args(args) | 1000 (options, args) = parser.parse_args(args) |
| 1086 client = GClient.LoadCurrentConfig(options) | 1001 client = GClient.LoadCurrentConfig(options) |
| 1087 if not client: | 1002 if not client: |
| 1088 raise gclient_utils.Error('client not configured; see \'gclient config\'') | 1003 raise gclient_utils.Error('client not configured; see \'gclient config\'') |
| 1089 client.PrintRevInfo() | 1004 client.PrintRevInfo() |
| 1090 return 0 | 1005 return 0 |
| (...skipping 75 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1166 return CMDhelp(parser, argv) | 1081 return CMDhelp(parser, argv) |
| 1167 except gclient_utils.Error, e: | 1082 except gclient_utils.Error, e: |
| 1168 print >> sys.stderr, 'Error: %s' % str(e) | 1083 print >> sys.stderr, 'Error: %s' % str(e) |
| 1169 return 1 | 1084 return 1 |
| 1170 | 1085 |
| 1171 | 1086 |
| 1172 if '__main__' == __name__: | 1087 if '__main__' == __name__: |
| 1173 sys.exit(Main(sys.argv[1:])) | 1088 sys.exit(Main(sys.argv[1:])) |
| 1174 | 1089 |
| 1175 # vim: ts=2:sw=2:tw=80:et: | 1090 # vim: ts=2:sw=2:tw=80:et: |
| OLD | NEW |