OLD | NEW |
1 # Copyright 2015 The LUCI Authors. All rights reserved. | 1 # Copyright 2015 The LUCI Authors. All rights reserved. |
2 # Use of this source code is governed under the Apache License, Version 2.0 | 2 # Use of this source code is governed under the Apache License, Version 2.0 |
3 # that can be found in the LICENSE file. | 3 # that can be found in the LICENSE file. |
4 | 4 |
5 import copy | 5 import copy |
6 import difflib | 6 import difflib |
7 import logging | 7 import logging |
8 import operator | 8 import operator |
9 import os | 9 import os |
10 import subprocess | 10 import subprocess |
(...skipping 18 matching lines...) Expand all Loading... |
29 | 29 |
30 class CyclicDependencyError(Exception): | 30 class CyclicDependencyError(Exception): |
31 pass | 31 pass |
32 | 32 |
33 | 33 |
34 def cleanup_pyc(path): | 34 def cleanup_pyc(path): |
35 """Removes any .pyc files from |path|'s directory tree. | 35 """Removes any .pyc files from |path|'s directory tree. |
36 | 36 |
37 This ensures we always use the fresh code. | 37 This ensures we always use the fresh code. |
38 """ | 38 """ |
39 for root, dirs, files in os.walk(path): | 39 for root, _dirs, files in os.walk(path): |
40 for f in files: | 40 for f in files: |
41 if f.endswith('.pyc'): | 41 if f.endswith('.pyc'): |
42 os.unlink(os.path.join(root, f)) | 42 os.unlink(os.path.join(root, f)) |
43 | 43 |
44 | 44 |
45 class InfraRepoConfig(object): | 45 class InfraRepoConfig(object): |
46 def to_recipes_cfg(self, repo_root): | 46 def to_recipes_cfg(self, repo_root): |
47 # TODO(luqui): This is not always correct. It can be configured in | 47 # TODO(luqui): This is not always correct. It can be configured in |
48 # infra/config:refs.cfg. | 48 # infra/config:refs.cfg. |
49 return os.path.join(repo_root, 'infra', 'config', 'recipes.cfg') | 49 return os.path.join(repo_root, 'infra', 'config', 'recipes.cfg') |
(...skipping 304 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
354 return False | 354 return False |
355 return self._proto_file == other._proto_file | 355 return self._proto_file == other._proto_file |
356 | 356 |
357 | 357 |
358 class Package(object): | 358 class Package(object): |
359 """Package represents a loaded package, and contains path and dependency | 359 """Package represents a loaded package, and contains path and dependency |
360 information. | 360 information. |
361 | 361 |
362 This is accessed by loader.py through RecipeDeps.get_package. | 362 This is accessed by loader.py through RecipeDeps.get_package. |
363 """ | 363 """ |
364 def __init__(self, name, repo_spec, deps, repo_root, relative_recipes_dir): | 364 def __init__(self, name, repo_spec, deps, repo_root, relative_recipes_dir, |
| 365 canonical_base_url): |
365 self.name = name | 366 self.name = name |
366 self.repo_spec = repo_spec | 367 self.repo_spec = repo_spec |
367 self.deps = deps | 368 self.deps = deps |
368 self.repo_root = repo_root | 369 self.repo_root = repo_root |
369 self.relative_recipes_dir = relative_recipes_dir | 370 self.relative_recipes_dir = relative_recipes_dir |
| 371 self.canonical_base_url = canonical_base_url |
370 | 372 |
371 @property | 373 @property |
372 def recipes_dir(self): | 374 def recipes_dir(self): |
373 return os.path.join(self.repo_root, self.relative_recipes_dir) | 375 return os.path.join(self.repo_root, self.relative_recipes_dir) |
374 | 376 |
375 @property | 377 @property |
376 def recipe_dir(self): | 378 def recipe_dir(self): |
377 return os.path.join(self.recipes_dir, 'recipes') | 379 return os.path.join(self.recipes_dir, 'recipes') |
378 | 380 |
379 @property | 381 @property |
380 def module_dir(self): | 382 def module_dir(self): |
381 return os.path.join(self.recipes_dir, 'recipe_modules') | 383 return os.path.join(self.recipes_dir, 'recipe_modules') |
382 | 384 |
383 def find_dep(self, dep_name): | 385 def find_dep(self, dep_name): |
384 if dep_name == self.name: | 386 if dep_name == self.name: |
385 return self | 387 return self |
386 | 388 |
387 assert dep_name in self.deps, ( | 389 assert dep_name in self.deps, ( |
388 '%s does not exist or is not declared as a dependency of %s' % ( | 390 '%s does not exist or is not declared as a dependency of %s' % ( |
389 dep_name, self.name)) | 391 dep_name, self.name)) |
390 return self.deps[dep_name] | 392 return self.deps[dep_name] |
391 | 393 |
392 def module_path(self, module_name): | 394 def module_path(self, module_name): |
393 return os.path.join(self.recipes_dir, 'recipe_modules', module_name) | 395 return os.path.join(self.recipes_dir, 'recipe_modules', module_name) |
394 | 396 |
395 def __repr__(self): | 397 def __repr__(self): |
396 return 'Package(%r, %r, %r, %r)' % ( | 398 return 'Package(%r, %r, %r, %r, %r)' % ( |
397 self.name, self.repo_spec, self.deps, self.recipe_dir) | 399 self.name, self.repo_spec, self.deps, self.recipe_dir, |
| 400 self.canonical_base_url) |
398 | 401 |
399 def __str__(self): | 402 def __str__(self): |
400 return 'Package %s, with dependencies %s' % (self.name, self.deps.keys()) | 403 return 'Package %s, with dependencies %s' % (self.name, self.deps.keys()) |
401 | 404 |
402 | 405 |
403 class RollCandidate(object): | 406 class RollCandidate(object): |
404 """RollCandidate represents a recipe roll candidate, i.e. updates | 407 """RollCandidate represents a recipe roll candidate, i.e. updates |
405 to pinned revisions of recipe dependencies. | 408 to pinned revisions of recipe dependencies. |
406 | 409 |
407 This is mostly used by recipes.py autoroll command. | 410 This is mostly used by recipes.py autoroll command. |
(...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
459 def get_rolled_spec(self): | 462 def get_rolled_spec(self): |
460 """Returns a PackageSpec with all the deps updates from this roll.""" | 463 """Returns a PackageSpec with all the deps updates from this roll.""" |
461 # TODO(phajdan.jr): does this preserve comments? should it? | 464 # TODO(phajdan.jr): does this preserve comments? should it? |
462 new_deps = _updated( | 465 new_deps = _updated( |
463 self._package_spec.deps, | 466 self._package_spec.deps, |
464 { project_id: spec for project_id, spec in | 467 { project_id: spec for project_id, spec in |
465 self._updates.iteritems() }) | 468 self._updates.iteritems() }) |
466 return PackageSpec( | 469 return PackageSpec( |
467 self._package_spec.project_id, | 470 self._package_spec.project_id, |
468 self._package_spec.recipes_path, | 471 self._package_spec.recipes_path, |
469 new_deps) | 472 new_deps, |
| 473 self._package_spec.canonical_base_url) |
470 | 474 |
471 def get_commit_infos(self): | 475 def get_commit_infos(self): |
472 """Returns a mapping project_id -> list of commits from that repo | 476 """Returns a mapping project_id -> list of commits from that repo |
473 that are getting pulled by this roll. | 477 that are getting pulled by this roll. |
474 """ | 478 """ |
475 commit_infos = {} | 479 commit_infos = {} |
476 | 480 |
477 for project_id, update in self._updates.iteritems(): | 481 for project_id, update in self._updates.iteritems(): |
478 commit_infos[project_id] = self._package_spec.deps[ | 482 commit_infos[project_id] = self._package_spec.deps[ |
479 project_id].commit_infos(self._context, update.revision) | 483 project_id].commit_infos(self._context, update.revision) |
(...skipping 10 matching lines...) Expand all Loading... |
490 """Returns a unified diff between original package spec and one after roll. | 494 """Returns a unified diff between original package spec and one after roll. |
491 """ | 495 """ |
492 orig = str(self._package_spec.dump()).splitlines() | 496 orig = str(self._package_spec.dump()).splitlines() |
493 new = str(self.get_rolled_spec().dump()).splitlines() | 497 new = str(self.get_rolled_spec().dump()).splitlines() |
494 return '\n'.join(difflib.unified_diff(orig, new, lineterm='')) | 498 return '\n'.join(difflib.unified_diff(orig, new, lineterm='')) |
495 | 499 |
496 | 500 |
497 class PackageSpec(object): | 501 class PackageSpec(object): |
498 API_VERSION = 1 | 502 API_VERSION = 1 |
499 | 503 |
500 def __init__(self, project_id, recipes_path, deps): | 504 def __init__(self, project_id, recipes_path, deps, canonical_base_url): |
501 self._project_id = project_id | 505 self._project_id = project_id |
502 self._recipes_path = recipes_path | 506 self._recipes_path = recipes_path |
503 self._deps = deps | 507 self._deps = deps |
| 508 self._canonical_base_url = canonical_base_url |
504 | 509 |
505 @classmethod | 510 @classmethod |
506 def load_proto(cls, proto_file): | 511 def load_proto(cls, proto_file): |
507 buf = proto_file.read() | 512 buf = proto_file.read() |
508 assert buf.api_version == cls.API_VERSION | 513 assert buf.api_version == cls.API_VERSION |
509 | 514 |
510 deps = { str(dep.project_id): cls.spec_for_dep(dep) | 515 deps = { str(dep.project_id): cls.spec_for_dep(dep) |
511 for dep in buf.deps } | 516 for dep in buf.deps } |
512 return cls(str(buf.project_id), str(buf.recipes_path), deps) | 517 return cls(str(buf.project_id), str(buf.recipes_path), deps, |
| 518 buf.canonical_base_url) |
513 | 519 |
514 @classmethod | 520 @classmethod |
515 def spec_for_dep(cls, dep): | 521 def spec_for_dep(cls, dep): |
516 """Returns a RepoSpec for the given dependency protobuf.""" | 522 """Returns a RepoSpec for the given dependency protobuf.""" |
517 url = str(dep.url) | 523 url = str(dep.url) |
518 if url.startswith("file://"): | 524 if url.startswith("file://"): |
519 return PathRepoSpec(str(dep.project_id), url[len("file://"):]) | 525 return PathRepoSpec(str(dep.project_id), url[len("file://"):]) |
520 | 526 |
521 if dep.repo_type in (package_pb2.DepSpec.GIT, package_pb2.DepSpec.GITILES): | 527 if dep.repo_type in (package_pb2.DepSpec.GIT, package_pb2.DepSpec.GITILES): |
522 if dep.repo_type == package_pb2.DepSpec.GIT: | 528 if dep.repo_type == package_pb2.DepSpec.GIT: |
(...skipping 14 matching lines...) Expand all Loading... |
537 return self._project_id | 543 return self._project_id |
538 | 544 |
539 @property | 545 @property |
540 def recipes_path(self): | 546 def recipes_path(self): |
541 return self._recipes_path | 547 return self._recipes_path |
542 | 548 |
543 @property | 549 @property |
544 def deps(self): | 550 def deps(self): |
545 return self._deps | 551 return self._deps |
546 | 552 |
| 553 @property |
| 554 def canonical_base_url(self): |
| 555 return self._canonical_base_url |
| 556 |
547 def dump(self): | 557 def dump(self): |
548 return package_pb2.Package( | 558 return package_pb2.Package( |
549 api_version=self.API_VERSION, | 559 api_version=self.API_VERSION, |
550 project_id=self._project_id, | 560 project_id=self._project_id, |
551 recipes_path=self._recipes_path, | 561 recipes_path=self._recipes_path, |
552 deps=[ self._deps[dep].dump() for dep in sorted(self._deps.keys()) ]) | 562 deps=[ self._deps[dep].dump() for dep in sorted(self._deps.keys()) ], |
| 563 canonical_base_url=self._canonical_base_url) |
553 | 564 |
554 def roll_candidates(self, root_spec, context): | 565 def roll_candidates(self, root_spec, context): |
555 """Returns list of consistent roll candidates, and rejected roll candidates. | 566 """Returns list of consistent roll candidates, and rejected roll candidates. |
556 | 567 |
557 The first one is sorted by score, descending. The more commits are pulled by | 568 The first one is sorted by score, descending. The more commits are pulled by |
558 the roll, the higher score. | 569 the roll, the higher score. |
559 | 570 |
560 Second list is included to distinguish between a situation where there are | 571 Second list is included to distinguish between a situation where there are |
561 no roll candidates from one where there are updates but they're not | 572 no roll candidates from one where there are updates but they're not |
562 consistent. | 573 consistent. |
(...skipping 54 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
617 are the override path. | 628 are the override path. |
618 """ | 629 """ |
619 context = PackageContext.from_proto_file(repo_root, proto_file, allow_fetch, | 630 context = PackageContext.from_proto_file(repo_root, proto_file, allow_fetch, |
620 deps_path=deps_path) | 631 deps_path=deps_path) |
621 | 632 |
622 if overrides: | 633 if overrides: |
623 overrides = {project_id: PathRepoSpec(project_id, path) | 634 overrides = {project_id: PathRepoSpec(project_id, path) |
624 for project_id, path in overrides.iteritems()} | 635 for project_id, path in overrides.iteritems()} |
625 package_deps = cls(context, overrides=overrides) | 636 package_deps = cls(context, overrides=overrides) |
626 | 637 |
627 package_deps._root_package = package_deps._create_package(RootRepoSpec(proto
_file)) | 638 package_deps._root_package = package_deps._create_package( |
| 639 RootRepoSpec(proto_file)) |
628 | 640 |
629 return package_deps | 641 return package_deps |
630 | 642 |
631 def _create_package(self, repo_spec): | 643 def _create_package(self, repo_spec): |
632 repo_spec.checkout(self._context) | 644 repo_spec.checkout(self._context) |
633 package_spec = PackageSpec.load_proto(repo_spec.proto_file(self._context)) | 645 package_spec = PackageSpec.load_proto(repo_spec.proto_file(self._context)) |
634 return self._create_from_spec(repo_spec, package_spec) | 646 return self._create_from_spec(repo_spec, package_spec) |
635 | 647 |
636 def _create_from_spec(self, repo_spec, package_spec): | 648 def _create_from_spec(self, repo_spec, package_spec): |
637 project_id = package_spec.project_id | 649 project_id = package_spec.project_id |
(...skipping 12 matching lines...) Expand all Loading... |
650 self._packages[project_id] = None | 662 self._packages[project_id] = None |
651 | 663 |
652 deps = {} | 664 deps = {} |
653 for dep, dep_repo in sorted(package_spec.deps.items()): | 665 for dep, dep_repo in sorted(package_spec.deps.items()): |
654 dep_repo = self._overrides.get(dep, dep_repo) | 666 dep_repo = self._overrides.get(dep, dep_repo) |
655 deps[dep] = self._create_package(dep_repo) | 667 deps[dep] = self._create_package(dep_repo) |
656 | 668 |
657 package = Package( | 669 package = Package( |
658 project_id, repo_spec, deps, | 670 project_id, repo_spec, deps, |
659 repo_spec.repo_root(self._context), | 671 repo_spec.repo_root(self._context), |
660 package_spec.recipes_path) | 672 package_spec.recipes_path, |
| 673 package_spec.canonical_base_url) |
661 | 674 |
662 self._packages[project_id] = package | 675 self._packages[project_id] = package |
663 return package | 676 return package |
664 | 677 |
665 # TODO(luqui): Remove this, so all accesses to packages are done | 678 # TODO(luqui): Remove this, so all accesses to packages are done |
666 # via other packages with properly scoped deps. | 679 # via other packages with properly scoped deps. |
667 def get_package(self, package_id): | 680 def get_package(self, package_id): |
668 return self._packages[package_id] | 681 return self._packages[package_id] |
669 | 682 |
670 @property | 683 @property |
(...skipping 12 matching lines...) Expand all Loading... |
683 >>> d = { 'x': 1, 'y': 2 } | 696 >>> d = { 'x': 1, 'y': 2 } |
684 >>> sorted(_updated(d, { 'y': 3, 'z': 4 }).items()) | 697 >>> sorted(_updated(d, { 'y': 3, 'z': 4 }).items()) |
685 [('x', 1), ('y', 3), ('z', 4)] | 698 [('x', 1), ('y', 3), ('z', 4)] |
686 >>> sorted(d.items()) | 699 >>> sorted(d.items()) |
687 [('x', 1), ('y', 2)] | 700 [('x', 1), ('y', 2)] |
688 """ | 701 """ |
689 | 702 |
690 d = copy.copy(d) | 703 d = copy.copy(d) |
691 d.update(updates) | 704 d.update(updates) |
692 return d | 705 return d |
OLD | NEW |