Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(370)

Side by Side Diff: gclient.py

Issue 3112002: Add a new class to take care of out-of-order execution. (Closed)
Patch Set: . Created 10 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | tests/gclient_smoketest.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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.5" 52 __version__ = "0.5.1"
53 53
54 import logging 54 import logging
55 import optparse 55 import optparse
56 import os 56 import os
57 import posixpath 57 import posixpath
58 import pprint 58 import pprint
59 import re 59 import re
60 import subprocess 60 import subprocess
61 import sys 61 import sys
62 import threading
62 import urlparse 63 import urlparse
63 import urllib 64 import urllib
64 65
65 import breakpad 66 import breakpad
66 67
67 import gclient_scm 68 import gclient_scm
68 import gclient_utils 69 import gclient_utils
69 from third_party.repo.progress import Progress 70 from third_party.repo.progress import Progress
70 71
71 72
72 def attr(attr, data): 73 def attr(attr, data):
73 """Sets an attribute on a function.""" 74 """Sets an attribute on a function."""
74 def hook(fn): 75 def hook(fn):
75 setattr(fn, attr, data) 76 setattr(fn, attr, data)
76 return fn 77 return fn
77 return hook 78 return hook
78 79
79 80
80 ## GClient implementation. 81 ## GClient implementation.
81 82
83 class WorkItem(object):
84 """One work item."""
85 requirements = []
86 name = None
87
88 def run(self):
89 pass
90
91
92 class ExecutionQueue(object):
93 """Dependencies sometime needs to be run out of order due to From() keyword.
94
95 This class manages that all the required dependencies are run before running
96 each one.
97
98 Methods of this class are multithread safe.
99 """
100 def __init__(self, progress):
101 self.lock = threading.Lock()
102 # List of Dependency.
103 self.queued = []
104 # List of strings representing each Dependency.name that was run.
105 self.ran = []
106 # List of items currently running.
107 self.running = []
108 self.progress = progress
109 if self.progress:
110 self.progress.update()
111
112 def enqueue(self, d):
113 """Enqueue one Dependency to be executed later once its requirements are
114 satisfied.
115 """
116 assert isinstance(d, WorkItem)
117 try:
118 self.lock.acquire()
119 self.queued.append(d)
120 total = len(self.queued) + len(self.ran) + len(self.running)
121 finally:
122 self.lock.release()
123 if self.progress:
124 self.progress._total = total + 1
125 self.progress.update(0)
126
127 def flush(self, *args, **kwargs):
128 """Runs all enqueued items until all are executed."""
129 while self._run_one_item(*args, **kwargs):
130 pass
131 queued = []
132 running = []
133 try:
134 self.lock.acquire()
135 if self.queued:
136 queued = self.queued
137 self.queued = []
138 if self.running:
139 running = self.running
140 self.running = []
141 finally:
142 self.lock.release()
143 if self.progress:
144 self.progress.end()
145 if queued:
146 raise gclient_utils.Error('Entries still queued: %s' % str(queued))
147 if running:
148 raise gclient_utils.Error('Entries still queued: %s' % str(running))
149
150 def _run_one_item(self, *args, **kwargs):
151 """Removes one item from the queue that has all its requirements completed
152 and execute it.
153
154 Returns False if no item could be run.
155 """
156 i = 0
157 d = None
158 try:
159 self.lock.acquire()
160 while i != len(self.queued) and not d:
161 d = self.queued.pop(i)
162 for r in d.requirements:
163 if not r in self.ran:
164 self.queued.insert(i, d)
165 d = None
166 break
167 i += 1
168 if not d:
169 return False
170 self.running.append(d)
171 finally:
172 self.lock.release()
173 d.run(*args, **kwargs)
174 try:
175 self.lock.acquire()
176 # TODO(maruel): http://crbug.com/51711
177 #assert not d.name in self.ran
178 if not d.name in self.ran:
179 self.ran.append(d.name)
180 self.running.remove(d)
181 if self.progress:
182 self.progress.update(1)
183 finally:
184 self.lock.release()
185 return True
186
82 187
83 class GClientKeywords(object): 188 class GClientKeywords(object):
84 class FromImpl(object): 189 class FromImpl(object):
85 """Used to implement the From() syntax.""" 190 """Used to implement the From() syntax."""
86 191
87 def __init__(self, module_name, sub_target_name=None): 192 def __init__(self, module_name, sub_target_name=None):
88 """module_name is the dep module we want to include from. It can also be 193 """module_name is the dep module we want to include from. It can also be
89 the name of a subdirectory to include from. 194 the name of a subdirectory to include from.
90 195
91 sub_target_name is an optional parameter if the module name in the other 196 sub_target_name is an optional parameter if the module name in the other
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after
127 232
128 def Lookup(self, var_name): 233 def Lookup(self, var_name):
129 """Implements the Var syntax.""" 234 """Implements the Var syntax."""
130 if var_name in self._custom_vars: 235 if var_name in self._custom_vars:
131 return self._custom_vars[var_name] 236 return self._custom_vars[var_name]
132 elif var_name in self._local_scope.get("vars", {}): 237 elif var_name in self._local_scope.get("vars", {}):
133 return self._local_scope["vars"][var_name] 238 return self._local_scope["vars"][var_name]
134 raise gclient_utils.Error("Var is not defined: %s" % var_name) 239 raise gclient_utils.Error("Var is not defined: %s" % var_name)
135 240
136 241
137 class Dependency(GClientKeywords): 242 class Dependency(GClientKeywords, WorkItem):
138 """Object that represents a dependency checkout.""" 243 """Object that represents a dependency checkout."""
139 DEPS_FILE = 'DEPS' 244 DEPS_FILE = 'DEPS'
140 245
141 def __init__(self, parent, name, url, safesync_url, custom_deps, 246 def __init__(self, parent, name, url, safesync_url, custom_deps,
142 custom_vars, deps_file): 247 custom_vars, deps_file):
143 GClientKeywords.__init__(self) 248 GClientKeywords.__init__(self)
144 self.parent = parent 249 self.parent = parent
145 self.name = name 250 self.name = name
146 self.url = url 251 self.url = url
147 self.parsed_url = None 252 self.parsed_url = None
(...skipping 165 matching lines...) Expand 10 before | Expand all | Expand 10 after
313 deps = rel_deps 418 deps = rel_deps
314 419
315 # Convert the deps into real Dependency. 420 # Convert the deps into real Dependency.
316 for name, url in deps.iteritems(): 421 for name, url in deps.iteritems():
317 if name in [s.name for s in self.dependencies]: 422 if name in [s.name for s in self.dependencies]:
318 raise gclient_utils.Error( 423 raise gclient_utils.Error(
319 'The same name "%s" appears multiple times in the deps section' % 424 'The same name "%s" appears multiple times in the deps section' %
320 name) 425 name)
321 self.dependencies.append(Dependency(self, name, url, None, None, None, 426 self.dependencies.append(Dependency(self, name, url, None, None, None,
322 None)) 427 None))
323 # Sorting by name would in theory make the whole thing coherent, since
324 # subdirectories will be sorted after the parent directory, but that doens't
325 # work with From() that fetch from a dependency with a name being sorted
326 # later. But if this would be removed right now, many projects wouldn't be
327 # able to sync anymore.
328 self.dependencies.sort(key=lambda x: x.name)
329 logging.debug('Loaded: %s' % str(self)) 428 logging.debug('Loaded: %s' % str(self))
330 429
331 def RunCommandRecursively(self, options, revision_overrides, 430 def run(self, options, revision_overrides, command, args, work_queue):
332 command, args, pm):
333 """Runs 'command' before parsing the DEPS in case it's a initial checkout 431 """Runs 'command' before parsing the DEPS in case it's a initial checkout
334 or a revert.""" 432 or a revert."""
335 assert self._file_list == [] 433 assert self._file_list == []
336 # When running runhooks, there's no need to consult the SCM. 434 # When running runhooks, there's no need to consult the SCM.
337 # All known hooks are expected to run unconditionally regardless of working 435 # All known hooks are expected to run unconditionally regardless of working
338 # copy state, so skip the SCM status check. 436 # copy state, so skip the SCM status check.
339 run_scm = command not in ('runhooks', None) 437 run_scm = command not in ('runhooks', None)
340 self.parsed_url = self.LateOverride(self.url) 438 self.parsed_url = self.LateOverride(self.url)
341 if run_scm and self.parsed_url: 439 if run_scm and self.parsed_url:
342 if isinstance(self.parsed_url, self.FileImpl): 440 if isinstance(self.parsed_url, self.FileImpl):
343 # Special support for single-file checkout. 441 # Special support for single-file checkout.
344 if not command in (None, 'cleanup', 'diff', 'pack', 'status'): 442 if not command in (None, 'cleanup', 'diff', 'pack', 'status'):
345 options.revision = self.parsed_url.GetRevision() 443 options.revision = self.parsed_url.GetRevision()
346 scm = gclient_scm.SVNWrapper(self.parsed_url.GetPath(), 444 scm = gclient_scm.SVNWrapper(self.parsed_url.GetPath(),
347 self.root_dir(), 445 self.root_dir(),
348 self.name) 446 self.name)
349 scm.RunCommand('updatesingle', options, 447 scm.RunCommand('updatesingle', options,
350 args + [self.parsed_url.GetFilename()], 448 args + [self.parsed_url.GetFilename()],
351 self._file_list) 449 self._file_list)
352 else: 450 else:
353 options.revision = revision_overrides.get(self.name) 451 options.revision = revision_overrides.get(self.name)
354 scm = gclient_scm.CreateSCM(self.parsed_url, self.root_dir(), self.name) 452 scm = gclient_scm.CreateSCM(self.parsed_url, self.root_dir(), self.name)
355 scm.RunCommand(command, options, args, self._file_list) 453 scm.RunCommand(command, options, args, self._file_list)
356 self._file_list = [os.path.join(self.name, f.strip()) 454 self._file_list = [os.path.join(self.name, f.strip())
357 for f in self._file_list] 455 for f in self._file_list]
358 options.revision = None 456 options.revision = None
359 self.processed = True 457 self.processed = True
360 if pm:
361 # The + 1 comes from the fact that .gclient is considered a step in
362 # itself, .i.e. this code is called one time for the .gclient. This is not
363 # conceptually correct but it simplifies code.
364 pm._total = len(self.tree(False)) + 1
365 pm.update()
366 if self.recursion_limit(): 458 if self.recursion_limit():
367 # Then we can parse the DEPS file. 459 # Then we can parse the DEPS file.
368 self.ParseDepsFile(True) 460 self.ParseDepsFile(True)
369 if pm:
370 pm._total = len(self.tree(False)) + 1
371 pm.update(0)
372 # Adjust the implicit dependency requirement; e.g. if a DEPS file contains 461 # Adjust the implicit dependency requirement; e.g. if a DEPS file contains
373 # both src/foo and src/foo/bar, src/foo/bar is implicitly dependent of 462 # both src/foo and src/foo/bar, src/foo/bar is implicitly dependent of
374 # src/foo. Yes, it's O(n^2)... 463 # src/foo. Yes, it's O(n^2)... It's important to do that before
464 # enqueueing them.
375 for s in self.dependencies: 465 for s in self.dependencies:
376 for s2 in self.dependencies: 466 for s2 in self.dependencies:
377 if s is s2: 467 if s is s2:
378 continue 468 continue
379 if s.name.startswith(posixpath.join(s2.name, '')): 469 if s.name.startswith(posixpath.join(s2.name, '')):
380 s.requirements.append(s2.name) 470 s.requirements.append(s2.name)
381 471
382 # Parse the dependencies of this dependency. 472 # Parse the dependencies of this dependency.
383 for s in self.dependencies: 473 for s in self.dependencies:
384 # TODO(maruel): All these can run concurrently! No need for threads, 474 work_queue.enqueue(s)
385 # just buffer stdout&stderr on pipes and flush as they complete.
386 # Watch out for stdin.
387 s.RunCommandRecursively(options, revision_overrides, command, args, pm)
388 475
389 def RunHooksRecursively(self, options): 476 def RunHooksRecursively(self, options):
390 """Evaluates all hooks, running actions as needed. RunCommandRecursively() 477 """Evaluates all hooks, running actions as needed. run()
391 must have been called before to load the DEPS.""" 478 must have been called before to load the DEPS."""
392 # If "--force" was specified, run all hooks regardless of what files have 479 # If "--force" was specified, run all hooks regardless of what files have
393 # changed. 480 # changed.
394 if self.deps_hooks and self.direct_reference: 481 if self.deps_hooks and self.direct_reference:
395 # TODO(maruel): If the user is using git or git-svn, then we don't know 482 # TODO(maruel): If the user is using git or git-svn, then we don't know
396 # what files have changed so we always run all hooks. It'd be nice to fix 483 # what files have changed so we always run all hooks. It'd be nice to fix
397 # that. 484 # that.
398 if (options.force or 485 if (options.force or
399 isinstance(self.parsed_url, self.FileImpl) or 486 isinstance(self.parsed_url, self.FileImpl) or
400 gclient_scm.GetScmName(self.parsed_url) in ('git', None) or 487 gclient_scm.GetScmName(self.parsed_url) in ('git', None) or
(...skipping 85 matching lines...) Expand 10 before | Expand all | Expand 10 after
486 def file_list(self): 573 def file_list(self):
487 result = self._file_list[:] 574 result = self._file_list[:]
488 for d in self.dependencies: 575 for d in self.dependencies:
489 result.extend(d.file_list()) 576 result.extend(d.file_list())
490 return result 577 return result
491 578
492 def __str__(self): 579 def __str__(self):
493 out = [] 580 out = []
494 for i in ('name', 'url', 'parsed_url', 'safesync_url', 'custom_deps', 581 for i in ('name', 'url', 'parsed_url', 'safesync_url', 'custom_deps',
495 'custom_vars', 'deps_hooks', '_file_list', 'processed', 582 'custom_vars', 'deps_hooks', '_file_list', 'processed',
496 'hooks_ran', 'deps_parsed', 'requirements'): 583 'hooks_ran', 'deps_parsed', 'requirements', 'direct_reference'):
497 # 'deps_file' 584 # 'deps_file'
498 if self.__dict__[i]: 585 if self.__dict__[i]:
499 out.append('%s: %s' % (i, self.__dict__[i])) 586 out.append('%s: %s' % (i, self.__dict__[i]))
500 587
501 for d in self.dependencies: 588 for d in self.dependencies:
502 out.extend([' ' + x for x in str(d).splitlines()]) 589 out.extend([' ' + x for x in str(d).splitlines()])
503 out.append('') 590 out.append('')
504 return '\n'.join(out) 591 return '\n'.join(out)
505 592
506 def __repr__(self): 593 def __repr__(self):
(...skipping 87 matching lines...) Expand 10 before | Expand all | Expand 10 after
594 self, s['name'], s['url'], 681 self, s['name'], s['url'],
595 s.get('safesync_url', None), 682 s.get('safesync_url', None),
596 s.get('custom_deps', {}), 683 s.get('custom_deps', {}),
597 s.get('custom_vars', {}), 684 s.get('custom_vars', {}),
598 None)) 685 None))
599 except KeyError: 686 except KeyError:
600 raise gclient_utils.Error('Invalid .gclient file. Solution is ' 687 raise gclient_utils.Error('Invalid .gclient file. Solution is '
601 'incomplete: %s' % s) 688 'incomplete: %s' % s)
602 # .gclient can have hooks. 689 # .gclient can have hooks.
603 self.deps_hooks = config_dict.get('hooks', []) 690 self.deps_hooks = config_dict.get('hooks', [])
691 self.direct_reference = True
692 self.deps_parsed = True
604 693
605 def SaveConfig(self): 694 def SaveConfig(self):
606 gclient_utils.FileWrite(os.path.join(self.root_dir(), 695 gclient_utils.FileWrite(os.path.join(self.root_dir(),
607 self._options.config_filename), 696 self._options.config_filename),
608 self.config_content) 697 self.config_content)
609 698
610 @staticmethod 699 @staticmethod
611 def LoadCurrentConfig(options): 700 def LoadCurrentConfig(options):
612 """Searches for and loads a .gclient file relative to the current working 701 """Searches for and loads a .gclient file relative to the current working
613 dir. Returns a GClient object.""" 702 dir. Returns a GClient object."""
(...skipping 84 matching lines...) Expand 10 before | Expand all | Expand 10 after
698 787
699 Args: 788 Args:
700 command: The command to use (e.g., 'status' or 'diff') 789 command: The command to use (e.g., 'status' or 'diff')
701 args: list of str - extra arguments to add to the command line. 790 args: list of str - extra arguments to add to the command line.
702 """ 791 """
703 if not self.dependencies: 792 if not self.dependencies:
704 raise gclient_utils.Error('No solution specified') 793 raise gclient_utils.Error('No solution specified')
705 revision_overrides = self._EnforceRevisions() 794 revision_overrides = self._EnforceRevisions()
706 pm = None 795 pm = None
707 if command == 'update' and not self._options.verbose: 796 if command == 'update' and not self._options.verbose:
708 pm = Progress('Syncing projects', len(self.tree(False)) + 1) 797 pm = Progress('Syncing projects', 1)
709 self.RunCommandRecursively(self._options, revision_overrides, 798 work_queue = ExecutionQueue(pm)
710 command, args, pm) 799 for s in self.dependencies:
711 if pm: 800 work_queue.enqueue(s)
712 pm.end() 801 work_queue.flush(self._options, revision_overrides, command, args,
802 work_queue)
713 803
714 # Once all the dependencies have been processed, it's now safe to run the 804 # Once all the dependencies have been processed, it's now safe to run the
715 # hooks. 805 # hooks.
716 if not self._options.nohooks: 806 if not self._options.nohooks:
717 self.RunHooksRecursively(self._options) 807 self.RunHooksRecursively(self._options)
718 808
719 if command == 'update': 809 if command == 'update':
720 # Notify the user if there is an orphaned entry in their working copy. 810 # Notify the user if there is an orphaned entry in their working copy.
721 # Only delete the directory if there are no changes in it, and 811 # Only delete the directory if there are no changes in it, and
722 # delete_unversioned_trees is set to true. 812 # delete_unversioned_trees is set to true.
(...skipping 20 matching lines...) Expand all
743 entry_fixed, self.root_dir())) 833 entry_fixed, self.root_dir()))
744 gclient_utils.RemoveDirectory(e_dir) 834 gclient_utils.RemoveDirectory(e_dir)
745 # record the current list of entries for next time 835 # record the current list of entries for next time
746 self._SaveEntries() 836 self._SaveEntries()
747 return 0 837 return 0
748 838
749 def PrintRevInfo(self): 839 def PrintRevInfo(self):
750 if not self.dependencies: 840 if not self.dependencies:
751 raise gclient_utils.Error('No solution specified') 841 raise gclient_utils.Error('No solution specified')
752 # Load all the settings. 842 # Load all the settings.
753 self.RunCommandRecursively(self._options, {}, None, [], None) 843 work_queue = ExecutionQueue(None)
844 for s in self.dependencies:
845 work_queue.enqueue(s)
846 work_queue.flush(self._options, {}, None, [], work_queue)
754 847
755 def GetURLAndRev(dep): 848 def GetURLAndRev(dep):
756 """Returns the revision-qualified SCM url for a Dependency.""" 849 """Returns the revision-qualified SCM url for a Dependency."""
757 if dep.parsed_url is None: 850 if dep.parsed_url is None:
758 return None 851 return None
759 if isinstance(dep.parsed_url, self.FileImpl): 852 if isinstance(dep.parsed_url, self.FileImpl):
760 original_url = dep.parsed_url.file_location 853 original_url = dep.parsed_url.file_location
761 else: 854 else:
762 original_url = dep.parsed_url 855 original_url = dep.parsed_url
763 url, _ = gclient_utils.SplitUrlRevision(original_url) 856 url, _ = gclient_utils.SplitUrlRevision(original_url)
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after
802 keys = sorted(entries.keys()) 895 keys = sorted(entries.keys())
803 for x in keys: 896 for x in keys:
804 line = '%s: %s' % (x, entries[x]) 897 line = '%s: %s' % (x, entries[x])
805 if x is not keys[-1]: 898 if x is not keys[-1]:
806 line += ';' 899 line += ';'
807 print line 900 print line
808 logging.info(str(self)) 901 logging.info(str(self))
809 902
810 def ParseDepsFile(self, direct_reference): 903 def ParseDepsFile(self, direct_reference):
811 """No DEPS to parse for a .gclient file.""" 904 """No DEPS to parse for a .gclient file."""
812 self.direct_reference = True 905 raise gclient_utils.Error('Internal error')
813 self.deps_parsed = True
814 906
815 def root_dir(self): 907 def root_dir(self):
816 """Root directory of gclient checkout.""" 908 """Root directory of gclient checkout."""
817 return self._root_dir 909 return self._root_dir
818 910
819 def enforced_os(self): 911 def enforced_os(self):
820 """What deps_os entries that are to be parsed.""" 912 """What deps_os entries that are to be parsed."""
821 return self._enforced_os 913 return self._enforced_os
822 914
823 def recursion_limit(self): 915 def recursion_limit(self):
(...skipping 388 matching lines...) Expand 10 before | Expand all | Expand 10 after
1212 return CMDhelp(parser, argv) 1304 return CMDhelp(parser, argv)
1213 except gclient_utils.Error, e: 1305 except gclient_utils.Error, e:
1214 print >> sys.stderr, 'Error: %s' % str(e) 1306 print >> sys.stderr, 'Error: %s' % str(e)
1215 return 1 1307 return 1
1216 1308
1217 1309
1218 if '__main__' == __name__: 1310 if '__main__' == __name__:
1219 sys.exit(Main(sys.argv[1:])) 1311 sys.exit(Main(sys.argv[1:]))
1220 1312
1221 # vim: ts=2:sw=2:tw=80:et: 1313 # vim: ts=2:sw=2:tw=80:et:
OLDNEW
« no previous file with comments | « no previous file | tests/gclient_smoketest.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698