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

Side by Side Diff: pipa/build/gitdeps.py

Issue 2693343002: Pull Chromium dependencies to Pipa checkout via DEPS and GITDEPS. (Closed)
Patch Set: One code block in Readme Created 3 years, 10 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 | « README.md ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # Copyright 2014 Google Inc. All Rights Reserved.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 #
15 # This file was originally copied from syzygy project available at
16 # https://github.com/google/syzygy.
17 """A utility script for checking out subdirectories of many GIT repositories
18 to specified locations, like is possible with SVN and gclient. This uses a
19 combination of GIT, sparse-checkout, shallow-clone and filesystem junctions.
20
21 For each dependency in a 'gitdeps' file this script will checkout one
22 subdirectory of one repository into a specified location. The input is as
23 follows:
24
25 - The user specifies a local destination for the checkout.
26 - The user specifies a source repository.
27 - The user specifies a list of subdirectories of the repository to get.
28 - The user specifies a revision.
29
30 The checkout works as follows:
31
32 - An empty git checkout is initialized in the cache directory. This will be
33 in a subfolder with an essentially random name.
34 - The specified repository is added as a remote to that repo.
35 - A sparse-checkout directive is added to select only the desired
36 subdirectories.
37 - The repository is cloned using a depth of 1 (no history, only the actual
38 contents of the desired revision).
39 - The destination directories are created as junctions pointing to the
40 desired subdirectory of the checkout in the cache directory.
41
42 The script maintains its state in the root of the cache directory, allowing it
43 to reuse checkout directories when possible.
44 """
45
46 import ast
47 import glob
48 import hashlib
49 import logging
50 import optparse
51 import os
52 import random
53 import re
54 import subprocess
55 import threading
56
57
58 _LOGGER = logging.getLogger(os.path.basename(__file__))
59
60
61 # Matches a SHA1 hash used as a git revision.
62 _GIT_SHA1_RE = re.compile('^[A-Fa-f0-9]{40}$')
63
64
65 def _ParseCommandLine():
66 """Parses the command-line and returns an options structure."""
67 option_parser = optparse.OptionParser()
68 option_parser.add_option('--cache-dir', type='string',
69 default='.gitdeps-cache',
70 help='The directory to be used for storing cache files. Defaults to '
71 '.gitdeps-cache in the current working directory.')
72 option_parser.add_option('--output-dir', type='string', default='.',
73 help='The directory to be used as the root of all output. Defaults to '
74 'the current working directory.')
75 option_parser.add_option('--dry-run', action='store_true', default=False,
76 help='If true then will simply list actions that would be performed.')
77 option_parser.add_option('--force', action='store_true', default=False,
78 help='If true then will force the checkout to be completely rebuilt.')
79 option_parser.add_option('--verbose', dest='log_level', action='store_const',
80 default=logging.INFO, const=logging.DEBUG,
81 help='Enables verbose logging.')
82 option_parser.add_option('--quiet', dest='log_level', action='store_const',
83 default=logging.INFO, const=logging.ERROR,
84 help='Disables all output except for errors.')
85
86 options, args = option_parser.parse_args()
87
88 # Configure logging.
89 logging.basicConfig(level=options.log_level)
90
91 # Set default values.
92 if not args:
93 # Default to checking for a file in the current working directory.
94 _LOGGER.info('Defaulting to using GITDEPS in current working directory.')
95 args = ['GITDEPS']
96
97 # Validate arguments and options.
98 if not os.path.isdir(options.output_dir):
99 option_parser.error('Output directory does not exist: %s' %
100 options.output_dir)
101 for path in args:
102 if not os.path.exists(path):
103 option_parser.error('Missing dependency file: %s' % path)
104
105 # Normalize local paths for prettier output.
106 options.cache_dir = os.path.normpath(os.path.abspath(options.cache_dir))
107 options.output_dir = os.path.normpath(os.path.abspath(options.output_dir))
108
109 return options, args
110
111
112 class RepoOptions(object):
113 """Light object used for shuttling around information about a dependency."""
114
115 def __init__(self):
116 self.repository = None
117 self.revision = None
118 self.output_dir = None
119 self.remote_dirs = []
120 self.deps_file = None
121 self.checkout_dir = None
122 self.recurse = False
123
124 def __str__(self):
125 """Stringifies this object for debugging."""
126 return ('RepoOptions(repository=%s, revision=%s, output_dir=%s, '
127 'remote_dirs=%s, deps_file=%s, checkout_dir=%s, recurse=%s)') % (
128 self.repository.__repr__(),
129 self.revision.__repr__(),
130 self.output_dir.__repr__(),
131 self.remote_dirs.__repr__(),
132 self.deps_file.__repr__(),
133 self.checkout_dir.__repr__(),
134 self.recurse.__repr__())
135
136
137 def _ParseRepoOptions(cache_dir, root_output_dir, deps_file_path, key, value):
138 """Given the |root_output_dir| specified on the command line, a |key| and
139 |value| pair from a GITDEPS file, and the path of the deps file, generates
140 a corresponding RepoOptions object. The |key| is the output path of the
141 checkout relative to |root_output_dir|, and |value| consists of a
142 (repository URL, remote directory, revision hash) tuple. This can raise an
143 Exception on failure.
144 """
145 bad = False
146 if ((type(value) != list and type(value) != tuple) or len(value) < 3 or
147 len(value) > 4 or (type(value[1]) != list and type(value[1]) != tuple)):
148 bad = True
149 if len(value) == 4 and type(value[3]) != dict:
150 bad = True
151 if bad:
152 _LOGGER.error('Invalid dependency tuple: %s', value)
153 raise Exception()
154
155 # Always use lowercase SHA1 hashes for consistency.
156 refspec = value[2]
157 if _GIT_SHA1_RE.match(refspec):
158 refspec = refspec.lower()
159
160 repo_options = RepoOptions()
161 repo_options.output_dir = os.path.normpath(os.path.abspath(os.path.join(
162 root_output_dir, key)))
163 repo_options.repository = value[0]
164 repo_options.remote_dirs = value[1]
165 repo_options.revision = refspec
166 repo_options.deps_file = deps_file_path
167
168 # Parse additional options.
169 if len(value) > 3:
170 repo_options.recurse = value[3].get('recurse', False) == True
171
172 # Create a unique name for the checkout in the cache directory. Make the
173 # output directory relative to the cache directory so that they can be
174 # moved around together.
175 output_dir_rel = os.path.relpath(repo_options.output_dir,
176 root_output_dir).lower()
177 if output_dir_rel.startswith('..'):
178 raise Exception('Invalid output directory: %s' % key)
179 n = hashlib.md5(output_dir_rel).hexdigest()
180 repo_options.checkout_dir = os.path.abspath(os.path.join(cache_dir, n, 'src'))
181
182 return repo_options
183
184
185 def _EnsureDirectoryExists(path, comment_name, dry_run):
186 """Ensures that the given |path| exists. Only actually creates the directory
187 if |dry_run| is False. |comment_name| is used during logging of this
188 operation.
189 """
190 if not comment_name:
191 comment_name += ' '
192 else:
193 comment_name = ''
194 if not os.path.exists(path):
195 _LOGGER.debug('Creating %sdirectory: %s', comment_name, path)
196 if not dry_run:
197 os.makedirs(path)
198
199
200 def _GetCasedFilename(filename):
201 """Returns the full case-sensitive filename for the given |filename|. If the
202 path does not exist, returns the original |filename| as is.
203 """
204 pattern = '%s[%s]' % (filename[:-1], filename[-1])
205 filenames = glob.glob(pattern)
206 if not filenames:
207 return filename
208 return filenames[0]
209
210
211 def _Shell(*cmd, **kw):
212 """Runs |cmd|, returns the results from Popen(cmd).communicate(). Additional
213 keyword arguments are passed on to subprocess.Popen. If |stdout| and |stderr|
214 are not specified, they default to subprocess.PIPE. If |dry_run| is not
215 specified it defaults to True. The command is only actually run if |dry_run|
216 is False. This can raise a RuntimeError on failure.
217 """
218 if 'cwd' in kw:
219 _LOGGER.debug('Executing %s in "%s".', cmd, kw['cwd'])
220 else:
221 _LOGGER.debug('Executing %s.', cmd)
222 if kw.get('dry_run', True):
223 return ('', '')
224 kw.pop('dry_run', None)
225 dump_on_error = kw.pop('dump_on_error', False)
226
227 kw['shell'] = True
228 kw.setdefault('stdout', subprocess.PIPE)
229 kw.setdefault('stderr', subprocess.PIPE)
230 prog = subprocess.Popen(cmd, **kw)
231
232 stdout, stderr = prog.communicate()
233 if prog.returncode != 0:
234 if dump_on_error:
235 print stdout
236 print stderr
237 raise RuntimeError('Command "%s" returned %d.' % (cmd, prog.returncode))
238 return (stdout, stderr)
239
240
241 def _IsGitCheckoutRoot(path):
242 """Return true if the given |path| is the root of a git checkout."""
243 return os.path.exists(os.path.join(path, '.git'))
244
245
246 # Matches a GIT config file section header, and grabs the name of the section
247 # in the first group. Used by _GetGitOrigin.
248 _GIT_CONFIG_SECTION_RE = re.compile(r'^\s*\[(.*?)\]\s*$')
249 # Matches the URL line from a 'remote' section of a GIT config. Used by
250 # _GetGitOrigin.
251 _GIT_CONFIG_REMOTE_URL_RE = re.compile(r'^\s*url\s*=\s*(.*?)\s*$')
252
253
254 def _GetGitOrigin(path):
255 """Returns the URL of the 'origin' remote for the git repo in |path|. Returns
256 None if the 'origin' remote doesn't exist. Raises an IOError if |path| doesn't
257 exist or is not a git repo.
258 """
259 section = None
260 for line in open(os.path.join(path, '.git', 'config'), 'rb'):
261 m = _GIT_CONFIG_SECTION_RE.match(line)
262 if m:
263 section = m.group(1)
264 continue
265
266 # We only care about the 'origin' configuration.
267 if section != 'remote "origin"':
268 continue
269
270 m = _GIT_CONFIG_REMOTE_URL_RE.match(line)
271 if m:
272 return m.group(1).strip()
273
274 return None
275
276
277 def _GetGitHead(path):
278 """Returns the hash of the head of the git repo in |path|. Raises an IOError
279 if |path| doesn't exist or is not a git repo.
280 """
281 return open(os.path.join(path, '.git', 'HEAD'), 'rb').read().strip()
282
283
284 def _NormalizeGitPath(path):
285 """Given a |path| in a GIT repository (relative to its root), normalizes it so
286 it will match only that exact path in a sparse checkout.
287 """
288 path = path.strip()
289 if not path.startswith('/'):
290 path = '/' + path
291 if not path.endswith('/'):
292 path += '/'
293 return path
294
295
296 def _RenameCheckout(path, dry_run):
297 """Renames the checkout in |path| so that it can be subsequently deleted.
298 Only actually does the work if |dry_run| is False. Returns the path of the
299 renamed checkout directory. Raises an Exception on failure.
300 """
301
302 def _RenameCheckoutImpl(path, dry_run):
303 if dry_run:
304 return path + '-old-dryrun'
305 attempts = 0
306 while attempts < 10:
307 newpath = '%s-old-%04d' % (path, random.randint(0, 999))
308 try:
309 os.rename(path, newpath)
310 return newpath
311 except WindowsError:
312 attempts += 1
313 raise Exception('Unable to rename checkout directory: %s' % path)
314
315 newpath = _RenameCheckoutImpl(path, dry_run)
316 _LOGGER.debug('Renamed checkout directory: %s', newpath)
317 return newpath
318
319
320 def _DeleteCheckout(path, dry_run):
321 """Deletes the checkout in |path|. Only actually deletes the checkout if
322 |dry_run| is False.
323 """
324 _LOGGER.info('Deleting checkout directory: %s', path)
325 if dry_run:
326 return
327 _Shell('rmdir', '/S', '/Q', path, dry_run=False)
328
329
330 def _GenerateSparseCheckoutPathAndContents(repo):
331 """Generates the path to the sparse checkout file, and the desired
332 contents. Returns a tuple of (path, contents). |repo| is a RepoOptions object.
333 """
334 sparse_file = os.path.join(repo.checkout_dir, '.git', 'info',
335 'sparse-checkout')
336 if not repo.remote_dirs:
337 contents = '*\n'
338 else:
339 contents = ''.join(_NormalizeGitPath(dir) + '\n'
340 for dir in repo.remote_dirs)
341 return (sparse_file, contents)
342
343
344 def _HasValidSparseCheckoutConfig(repo):
345 """Determines if the GIT repo in |path| has a valid sparse-checkout
346 configuration as configured by the RepoOptions |repo|. Returns True or False.
347 """
348 (sparse_file, contents) = _GenerateSparseCheckoutPathAndContents(repo)
349 try:
350 if open(sparse_file, 'rb').read() == contents:
351 return True
352 return False
353 except IOError:
354 return False
355
356
357 def _CreateCheckout(path, repo, dry_run):
358 """Creates a checkout in the provided |path|. The |path| must not already
359 exist. Uses the repository configuration from the provided |repo| RepoOptions
360 object. Only actually creates the checkout if |dry_run| is false.
361 """
362 # We expect the directory not to exist, as this is a fresh checkout we are
363 # creating.
364 if not dry_run:
365 if os.path.exists(path):
366 raise Exception('Checkout directory already exists: %s' % path)
367
368 _LOGGER.info('Creating checkout directory: %s', path)
369 if not dry_run:
370 os.makedirs(path)
371
372 _LOGGER.debug('Initializing the checkout.')
373 _Shell('git', 'init', cwd=path, dry_run=dry_run)
374 _Shell('git', 'remote', 'add', 'origin', repo.repository, cwd=path,
375 dry_run=dry_run)
376 _Shell('git', 'config', 'core.sparsecheckout', 'true', cwd=path,
377 dry_run=dry_run)
378 if not dry_run:
379 _LOGGER.debug('Creating sparse checkout configuration file for '
380 'directory: %s', repo.remote_dirs)
381 if not dry_run:
382 (path, contents) = _GenerateSparseCheckoutPathAndContents(repo)
383 with open(path, 'wb') as io:
384 io.write(contents)
385
386
387 def _UpdateCheckout(path, repo, dry_run):
388 """Updates a GIT checkout in |path| by pulling down a specific revision
389 from it, as configured by RepoOptions |repo|. Only actually runs if
390 |dry_run| is False.
391 """
392 try:
393 # Try a checkout first. If this fails then we'll actually need to fetch
394 # the revision.
395 _LOGGER.info('Trying to checkout revision %s.', repo.revision)
396 _Shell('git', 'checkout', repo.revision, cwd=path,
397 dry_run=dry_run)
398 return
399 except RuntimeError:
400 pass
401
402 # Fetch the revision and then check it out. Let output go to screen rather
403 # than be buffered.
404 _LOGGER.info('Fetching and checking out revision %s.', repo.revision)
405 _Shell('git', 'fetch', '--depth=1', 'origin', repo.revision,
406 cwd=path, dry_run=dry_run, stdout=None, stderr=None)
407 _Shell('git', 'checkout', repo.revision, cwd=path,
408 dry_run=dry_run, stdout=None, stderr=None)
409
410
411 # Used by _GetJunctionInfo to extract information about junctions.
412 _DIR_JUNCTION_RE = re.compile(r'^.*<JUNCTION>\s+(.+)\s+\[(.+)\]$')
413
414
415 # TODO(chrisha): This is ugly, and there has to be a better way!
416 def _GetJunctionInfo(junction):
417 """Returns the target of a junction, if it exists, None otherwise."""
418 dirname = os.path.dirname(junction)
419 basename = os.path.basename(junction)
420 try:
421 stdout, dummy_stderr = _Shell('dir', '/AL', '/N', dirname, dry_run=False)
422 except RuntimeError:
423 return
424
425 lines = stdout.splitlines(False)
426 for line in stdout.splitlines(False):
427 m = _DIR_JUNCTION_RE.match(line)
428 if not m:
429 continue
430 if m.group(1).lower() == basename.lower():
431 return m.group(2)
432
433 return None
434
435
436 def _EnsureJunction(cache_dir, target_dir, options, repo):
437 """Ensures that the appropriate junction exists from the configured output
438 directory to the specified sub-directory of the GIT checkout.
439 """
440 # Ensure that the target directory was created.
441 target_cache_dir = _GetCasedFilename(os.path.normpath(
442 os.path.join(cache_dir, target_dir)))
443 if not options.dry_run and not os.path.isdir(target_cache_dir):
444 raise Exception('Checkout does not contain the desired remote folder.')
445
446 # Ensure the parent directory exists before checking if the junction needs to
447 # be created.
448 output_dir = os.path.normpath(os.path.join(repo.output_dir, target_dir))
449 _EnsureDirectoryExists(
450 os.path.dirname(output_dir), 'junction', options.dry_run)
451
452 # Determine if the link needs to be created.
453 create_link = True
454 if os.path.exists(output_dir):
455 dest = _GetJunctionInfo(output_dir)
456
457 # If the junction is valid nothing needs to be done. If it points to the
458 # wrong place or isn't a junction then delete it and let it be remade.
459 if dest == target_cache_dir:
460 _LOGGER.debug('Junction is up to date.')
461 create_link = False
462 else:
463 if dest:
464 _LOGGER.info('Erasing existing junction: %s', output_dir)
465 else:
466 _LOGGER.info('Deleting existing directory: %s', output_dir)
467 _Shell('rmdir', '/S', '/Q', output_dir, dry_run=options.dry_run)
468
469 if create_link:
470 _LOGGER.info('Creating output junction: %s', output_dir)
471 _Shell('mklink', '/J', output_dir, target_cache_dir,
472 dry_run=options.dry_run)
473
474
475 def _InstallRepository(options, repo):
476 """Installs a repository as configured by the options. Assumes that the
477 specified cache directory already exists.
478
479 Returns True if the checkout was modified, False otherwise.
480 """
481
482 _LOGGER.debug('Processing directories "%s" from repository "%s".',
483 repo.remote_dirs, repo.repository)
484
485 # Ensure the output directory's *parent* exists.
486 output_dirname = os.path.dirname(repo.output_dir)
487 output_basename = os.path.basename(repo.output_dir)
488 _EnsureDirectoryExists(output_dirname, 'output', options.dry_run)
489
490 # Get the properly cased names for the output directories.
491 output_dirname = _GetCasedFilename(output_dirname)
492 repo.output_dir = os.path.join(output_dirname, output_basename)
493
494 # These are the 3 basic steps that need to occur. Depending on the state of
495 # the checkout we may not need to perform all of them. We assume initially
496 # that everything needs to be done, unless proven otherwise.
497 create_checkout = True
498 update_checkout = True
499
500 # If the cache directory exists then lookup the repo and the revision and see
501 # what needs to be updated.
502 threads = []
503 if os.path.exists(repo.checkout_dir):
504 keep_cache_dir = False
505
506 # Only run these checks if we're not in 'force' mode. Otherwise, we
507 # deliberately turf the cache directory and start from scratch.
508 if not options.force and _IsGitCheckoutRoot(repo.checkout_dir):
509 # Get the repo origin.
510 repo_url = _GetGitOrigin(repo.checkout_dir)
511 if (repo_url == repo.repository and
512 _HasValidSparseCheckoutConfig(repo)):
513 _LOGGER.debug('Checkout is for correct repository and subdirectory.')
514 keep_cache_dir = True
515 create_checkout = False
516
517 # Get the checked out revision.
518 revhash = _GetGitHead(repo.checkout_dir)
519 if revhash == repo.revision:
520 _LOGGER.debug('Checkout is already up to date.')
521 update_checkout = False
522
523 if not keep_cache_dir:
524 # The old checkout directory is renamed and erased in a separate thread
525 # so that the new checkout can start immediately.
526 _LOGGER.info('Erasing stale checkout directory: %s', repo.checkout_dir)
527
528 # Any existing junctions to this repo must be removed otherwise the
529 # rename may fail.
530 for d in repo.remote_dirs:
531 j = os.path.abspath(os.path.join(repo.output_dir, d))
532 _RemoveOrphanedJunction(options, j)
533
534 newpath = _RenameCheckout(repo.checkout_dir, options.dry_run)
535 thread = threading.Thread(target=_DeleteCheckout,
536 args=(newpath, options.dry_run))
537 threads.append(thread)
538 thread.start()
539
540 # Create and update the checkout as necessary.
541 if create_checkout:
542 _CreateCheckout(repo.checkout_dir, repo, options.dry_run)
543 else:
544 _LOGGER.debug('Reusing checkout directory: %s', repo.checkout_dir)
545 if update_checkout:
546 _UpdateCheckout(repo.checkout_dir, repo, options.dry_run)
547
548 # Ensure the junctions exists.
549 if repo.remote_dirs:
550 for remote_dir in repo.remote_dirs:
551 _EnsureJunction(repo.checkout_dir, remote_dir, options, repo)
552 else:
553 _EnsureJunction(repo.checkout_dir, '', options, repo)
554
555 # Join any worker threads that are ongoing.
556 for thread in threads:
557 thread.join()
558
559 # Return True if any modifications were made.
560 return create_checkout or update_checkout
561
562
563 def _WriteIfChanged(path, contents, dry_run):
564 if os.path.exists(path):
565 d = open(path, 'rb').read()
566 if d == contents:
567 _LOGGER.debug('Contents unchanged, not writing file: %s', path)
568 return
569
570 _LOGGER.info('Writing file: %s', path)
571 if not dry_run:
572 open(path, 'wb').write(contents)
573
574
575 def _RecurseRepository(options, repo):
576 """Recursively follows dependencies in the given repository."""
577 # Only run if there's an appropriate DEPS file.
578 deps = os.path.isfile(os.path.join(repo.checkout_dir, 'DEPS'))
579 gitdeps = os.path.isfile(os.path.join(repo.checkout_dir, '.DEPS.git'))
580 if not deps and not gitdeps:
581 _LOGGER.debug('No deps file found in repository: %s', repo.repository)
582 return
583
584 # Generate the .gclient solution file.
585 cache_dir = os.path.dirname(os.path.abspath(repo.checkout_dir))
586 gclient_file = os.path.join(cache_dir, '.gclient')
587 deps_file = 'DEPS'
588 if gitdeps:
589 deps_file = '.DEPS.git'
590 solutions = [
591 {
592 'name': 'src',
593 'url': repo.repository,
594 'managed': False,
595 'custom_deps': [],
596 'deps_file': deps_file,
597 'safesync_url': '',
598 }
599 ]
600 solutions = 'solutions=%s' % solutions.__repr__()
601 _WriteIfChanged(gclient_file, solutions, options.dry_run)
602
603 # Invoke 'gclient' on the sub-repository.
604 _Shell('gclient', 'sync', cwd=repo.checkout_dir, dry_run=options.dry_run)
605
606
607 def _FindGlobalVariableInAstTree(tree, name, functions=None):
608 """Finds and evaluates to global assignment of the variables |name| in the
609 AST |tree|. Will allow the evaluations of some functions as defined in
610 |functions|.
611 """
612 if functions is None:
613 functions = {}
614
615 class FunctionEvaluator(ast.NodeTransformer):
616 """A tree transformer that evaluates permitted functions."""
617
618 def visit_BinOp(self, binop_node):
619 """Is called for BinOp nodes. We only support string additions."""
620 if type(binop_node.op) != ast.Add:
621 return binop_node
622 left = ast.literal_eval(self.visit(binop_node.left))
623 right = ast.literal_eval(self.visit(binop_node.right))
624 value = left + right
625 new_node = ast.Str(s=value)
626 new_node = ast.copy_location(new_node, binop_node)
627 return new_node
628
629 def visit_Call(self, call_node):
630 """Evaluates function calls that return a single string as output."""
631 func_name = call_node.func.id
632 if func_name not in functions:
633 return call_node
634 func = functions[func_name]
635
636 # Evaluate the arguments. We don't care about starargs, keywords or
637 # kwargs.
638 args = [ast.literal_eval(self.visit(arg)) for arg in
639 call_node.args]
640
641 # Now evaluate the function.
642 value = func(*args)
643 new_node = ast.Str(s=value)
644 new_node = ast.copy_location(new_node, call_node)
645 return new_node
646
647 # Look for assignment nodes.
648 for node in tree.body:
649 if type(node) != ast.Assign:
650 continue
651 # Look for assignment in the 'store' context, to a variable with
652 # the given name.
653 for target in node.targets:
654 if type(target) != ast.Name:
655 continue
656 if type(target.ctx) != ast.Store:
657 continue
658 if target.id == name:
659 value = FunctionEvaluator().visit(node.value)
660 value = ast.fix_missing_locations(value)
661 value = ast.literal_eval(value)
662 return value
663
664
665 def _ParseDepsFile(path):
666 """Parsed a DEPS-like file at the given |path|."""
667 # Utility function for performing variable expansions.
668 vars_dict = {}
669 def _Var(s):
670 return vars_dict[s]
671
672 contents = open(path, 'rb').read()
673 tree = ast.parse(contents, path)
674 vars_dict = _FindGlobalVariableInAstTree(tree, 'vars')
675 deps_dict = _FindGlobalVariableInAstTree(
676 tree, 'deps', functions={'Var': _Var})
677 return deps_dict
678
679
680 def _RemoveFile(options, path):
681 """Removes the provided file. If it doesn't exist, raises an Exception."""
682 _LOGGER.debug('Removing file: %s', path)
683 if not os.path.isfile(path):
684 raise Exception('Path does not exist: %s' % path)
685
686 if not options.dry_run:
687 os.remove(path)
688
689
690 def _RemoveOrphanedJunction(options, junction):
691 """Removes an orphaned junction at the path |junction|. If the path doesn't
692 exist or is not a junction, raises an Exception.
693 """
694 _LOGGER.debug('Removing orphaned junction: %s', junction)
695 absdir = os.path.join(options.output_dir, junction)
696 if not os.path.exists(absdir):
697 _LOGGER.debug('Junction path does not exist, ignoring.')
698 return
699 if not _GetJunctionInfo(absdir):
700 _LOGGER.error('Path is not a junction: %s', absdir)
701 raise Exception()
702 _Shell('rmdir', '/S', '/Q', absdir, dry_run=options.dry_run)
703
704 reldir = os.path.dirname(junction)
705 while reldir:
706 absdir = os.path.join(options.output_dir, reldir)
707 if os.listdir(absdir):
708 return
709 _LOGGER.debug('Removing empty parent directory of junction: %s', absdir)
710 _Shell('rmdir', '/S', '/Q', absdir, dry_run=options.dry_run)
711 reldir = os.path.dirname(reldir)
712
713
714 def _GetCacheDirEntryVersion(path):
715 """Returns the version of the cache directory entry, -1 if invalid."""
716
717 git = os.path.join(path, '.git')
718 src = os.path.join(path, 'src')
719 gclient = os.path.join(path, '.gclient')
720
721 # Version 0 contains a '.git' directory and no '.gclient' entry.
722 if os.path.isdir(git):
723 if os.path.exists(gclient):
724 return -1
725 return 0
726
727 # Version 1 contains a 'src' directory and no '.git' entry.
728 if os.path.isdir(src):
729 if os.path.exists(git):
730 return -1
731 return 1
732
733
734 def _GetCacheDirEntries(cache_dir):
735 """Returns the list of entries in the given |cache_dir|."""
736 entries = []
737 for path in os.listdir(cache_dir):
738 if not re.match('^[a-z0-9]{32}$', path):
739 continue
740 entries.append(path)
741 return entries
742
743
744 def _GetCacheDirVersion(cache_dir):
745 """Returns the version of the cache directory."""
746 # If it doesn't exist then it's clearly the latest version.
747 if not os.path.exists(cache_dir):
748 return 1
749
750 cache_version = None
751 for path in _GetCacheDirEntries(cache_dir):
752 repo = os.path.join(cache_dir, path)
753 if not os.path.isdir(repo):
754 return -1
755
756 entry_version = _GetCacheDirEntryVersion(repo)
757 if entry_version == -1:
758 return -1
759
760 if cache_version == None:
761 cache_version = entry_version
762 else:
763 if cache_version != entry_version:
764 return -1
765
766 # If there are no entries in the cache it may as well be the latest version.
767 if cache_version is None:
768 return 1
769
770 return cache_version
771
772
773 def _GetJunctionStatePath(options):
774 """Returns the junction state file path."""
775 return os.path.join(options.cache_dir, '.gitdeps_junctions')
776
777
778 def _ReadJunctions(options):
779 """Reads the list of junctions as a dictionary."""
780 state_path = _GetJunctionStatePath(options)
781 old_junctions = {}
782 if os.path.exists(state_path):
783 _LOGGER.debug('Loading list of existing junctions.')
784 for j in open(state_path, 'rb'):
785 old_junctions[j.strip()] = True
786
787 return old_junctions
788
789
790 def _Rename(src, dst, dry_run):
791 _LOGGER.debug('Renaming "%s" to "%s".', src, dst)
792 if not dry_run:
793 os.rename(src, dst)
794
795
796 def _UpgradeCacheDir(options):
797 """Upgrades the cache directory format to the most modern layout.
798
799 Returns true on success, false otherwise.
800 """
801 cache_version = _GetCacheDirVersion(options.cache_dir)
802 if cache_version == 1:
803 _LOGGER.debug('No cache directory upgrade required.')
804 return
805
806 _LOGGER.debug('Upgrading cache directory from version 0 to 1.')
807
808 _LOGGER.debug('Removing all junctions.')
809 junctions = _ReadJunctions(options).keys()
810 junctions = sorted(junctions, key=lambda j: len(j), reverse=True)
811 for junction in junctions:
812 _RemoveOrphanedJunction(options, junction)
813 _RemoveFile(options, _GetJunctionStatePath(options))
814
815 for entry in _GetCacheDirEntries(options.cache_dir):
816 _LOGGER.debug('Upgrading cache entry "%s".', entry)
817 tmp_entry = os.path.abspath(os.path.join(
818 options.cache_dir,
819 'TMP%d-%04d' % (os.getpid(), random.randint(0, 999))))
820 abs_entry = os.path.abspath(os.path.join(options.cache_dir, entry))
821 src = os.path.join(abs_entry, 'src')
822 _Rename(abs_entry, tmp_entry, options.dry_run)
823 _EnsureDirectoryExists(abs_entry, 'cache entry', options.dry_run)
824 _Rename(tmp_entry, src, options.dry_run)
825
826 if options.dry_run:
827 _LOGGER.debug('Cache needs upgrading, unable to further simulate dry-run.')
828 raise Exception("")
829
830
831 def main():
832 options, args = _ParseCommandLine()
833
834 # Upgrade the cache directory if necessary.
835 _UpgradeCacheDir(options)
836
837 # Ensure the cache directory exists and get the full properly cased path to
838 # it.
839 _EnsureDirectoryExists(options.cache_dir, 'cache', options.dry_run)
840 options.cache_dir = _GetCasedFilename(options.cache_dir)
841
842 # Read junctions that have been written in previous runs.
843 state_path = _GetJunctionStatePath(options)
844 old_junctions = _ReadJunctions(options)
845
846 # Parse each deps file in order, and extract the dependencies, looking for
847 # conflicts in the output directories.
848 output_dirs = {}
849 all_deps = []
850 for deps_file in args:
851 deps = _ParseDepsFile(deps_file)
852 for key, value in deps.iteritems():
853 repo_options = _ParseRepoOptions(
854 options.cache_dir, options.output_dir, deps_file, key, value)
855 if repo_options.output_dir in output_dirs:
856 other_repo_options = output_dirs[repo_options.output_dir]
857 _LOGGER.error('Conflicting output directory: %s',
858 repo_options.output_dir)
859 _LOGGER.error('First specified in file: %s',
860 other_repo_options.deps_file)
861 _LOGGER.error('And then specified in file: %s', repo_options.deps_file)
862 output_dirs[repo_options.output_dir] = repo_options
863 all_deps.append(repo_options)
864 output_dirs = {}
865
866 # Handle each dependency, in order of shortest path names first. This ensures
867 # that nested dependencies are handled properly.
868 checkout_dirs = {}
869 deps = sorted(all_deps, key=lambda x: len(x.deps_file))
870 junctions = []
871 for repo in all_deps:
872 changes_made = _InstallRepository(options, repo)
873 checkout_dirs[repo.checkout_dir] = changes_made
874
875 new_junction_dirs = repo.remote_dirs if repo.remote_dirs else ['']
876 for new_junction_dir in new_junction_dirs:
877 junction = os.path.relpath(
878 os.path.join(repo.output_dir, new_junction_dir),
879 options.output_dir)
880 old_junctions.pop(junction, None)
881 # Write each junction as we create it. This allows for recovery from
882 # partial runs.
883 if not options.dry_run:
884 open(state_path, 'ab').write(junction + '\n')
885 junctions.append(junction)
886
887 # Clean up orphaned junctions if there are any.
888 if old_junctions:
889 _LOGGER.debug('Removing orphaned junctions.')
890 for j in old_junctions.iterkeys():
891 _RemoveOrphanedJunction(options, j)
892
893 # Output the final list of junctions.
894 _LOGGER.debug('Writing final list of junctions.')
895 if not options.dry_run:
896 with open(state_path, 'wb') as io:
897 for j in sorted(junctions):
898 io.write(j)
899 io.write('\n')
900
901 # Iterate all directories in the cache directory. Any that we didn't
902 # specifically create or update should be cleaned up. Do this in parallel
903 # so things are cleaned up as soon as possible.
904 threads = []
905 for path in glob.glob(os.path.join(options.cache_dir, '*')):
906 if os.path.join(path, 'src') not in checkout_dirs:
907 _LOGGER.debug('Erasing orphaned checkout directory: %s', path)
908 thread = threading.Thread(target=_DeleteCheckout,
909 args=(path, options.dry_run))
910 threads.append(thread)
911 thread.start()
912 for thread in threads:
913 thread.join()
914
915 # Recursively process other dependencies.
916 for repo in all_deps:
917 if not repo.recurse:
918 continue
919 if not checkout_dirs[repo.checkout_dir] and not options.force:
920 continue
921 _RecurseRepository(options, repo)
922
923 return
924
925
926 if __name__ == '__main__':
927 main()
OLDNEW
« no previous file with comments | « README.md ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698