| 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 |