diff options
Diffstat (limited to 'project.py')
| -rwxr-xr-x | project.py | 147 |
1 files changed, 126 insertions, 21 deletions
| @@ -18,6 +18,7 @@ from __future__ import print_function | |||
| 18 | import errno | 18 | import errno |
| 19 | import filecmp | 19 | import filecmp |
| 20 | import glob | 20 | import glob |
| 21 | import json | ||
| 21 | import os | 22 | import os |
| 22 | import random | 23 | import random |
| 23 | import re | 24 | import re |
| @@ -544,6 +545,105 @@ class RepoHook(object): | |||
| 544 | prompt % (self._GetMustVerb(), self._script_fullpath), | 545 | prompt % (self._GetMustVerb(), self._script_fullpath), |
| 545 | 'Scripts have changed since %s was allowed.' % (self._hook_type,)) | 546 | 'Scripts have changed since %s was allowed.' % (self._hook_type,)) |
| 546 | 547 | ||
| 548 | @staticmethod | ||
| 549 | def _ExtractInterpFromShebang(data): | ||
| 550 | """Extract the interpreter used in the shebang. | ||
| 551 | |||
| 552 | Try to locate the interpreter the script is using (ignoring `env`). | ||
| 553 | |||
| 554 | Args: | ||
| 555 | data: The file content of the script. | ||
| 556 | |||
| 557 | Returns: | ||
| 558 | The basename of the main script interpreter, or None if a shebang is not | ||
| 559 | used or could not be parsed out. | ||
| 560 | """ | ||
| 561 | firstline = data.splitlines()[:1] | ||
| 562 | if not firstline: | ||
| 563 | return None | ||
| 564 | |||
| 565 | # The format here can be tricky. | ||
| 566 | shebang = firstline[0].strip() | ||
| 567 | m = re.match(r'^#!\s*([^\s]+)(?:\s+([^\s]+))?', shebang) | ||
| 568 | if not m: | ||
| 569 | return None | ||
| 570 | |||
| 571 | # If the using `env`, find the target program. | ||
| 572 | interp = m.group(1) | ||
| 573 | if os.path.basename(interp) == 'env': | ||
| 574 | interp = m.group(2) | ||
| 575 | |||
| 576 | return interp | ||
| 577 | |||
| 578 | def _ExecuteHookViaReexec(self, interp, context, **kwargs): | ||
| 579 | """Execute the hook script through |interp|. | ||
| 580 | |||
| 581 | Note: Support for this feature should be dropped ~Jun 2021. | ||
| 582 | |||
| 583 | Args: | ||
| 584 | interp: The Python program to run. | ||
| 585 | context: Basic Python context to execute the hook inside. | ||
| 586 | kwargs: Arbitrary arguments to pass to the hook script. | ||
| 587 | |||
| 588 | Raises: | ||
| 589 | HookError: When the hooks failed for any reason. | ||
| 590 | """ | ||
| 591 | # This logic needs to be kept in sync with _ExecuteHookViaImport below. | ||
| 592 | script = """ | ||
| 593 | import json, os, sys | ||
| 594 | path = '''%(path)s''' | ||
| 595 | kwargs = json.loads('''%(kwargs)s''') | ||
| 596 | context = json.loads('''%(context)s''') | ||
| 597 | sys.path.insert(0, os.path.dirname(path)) | ||
| 598 | data = open(path).read() | ||
| 599 | exec(compile(data, path, 'exec'), context) | ||
| 600 | context['main'](**kwargs) | ||
| 601 | """ % { | ||
| 602 | 'path': self._script_fullpath, | ||
| 603 | 'kwargs': json.dumps(kwargs), | ||
| 604 | 'context': json.dumps(context), | ||
| 605 | } | ||
| 606 | |||
| 607 | # We pass the script via stdin to avoid OS argv limits. It also makes | ||
| 608 | # unhandled exception tracebacks less verbose/confusing for users. | ||
| 609 | cmd = [interp, '-c', 'import sys; exec(sys.stdin.read())'] | ||
| 610 | proc = subprocess.Popen(cmd, stdin=subprocess.PIPE) | ||
| 611 | proc.communicate(input=script.encode('utf-8')) | ||
| 612 | if proc.returncode: | ||
| 613 | raise HookError('Failed to run %s hook.' % (self._hook_type,)) | ||
| 614 | |||
| 615 | def _ExecuteHookViaImport(self, data, context, **kwargs): | ||
| 616 | """Execute the hook code in |data| directly. | ||
| 617 | |||
| 618 | Args: | ||
| 619 | data: The code of the hook to execute. | ||
| 620 | context: Basic Python context to execute the hook inside. | ||
| 621 | kwargs: Arbitrary arguments to pass to the hook script. | ||
| 622 | |||
| 623 | Raises: | ||
| 624 | HookError: When the hooks failed for any reason. | ||
| 625 | """ | ||
| 626 | # Exec, storing global context in the context dict. We catch exceptions | ||
| 627 | # and convert to a HookError w/ just the failing traceback. | ||
| 628 | try: | ||
| 629 | exec(compile(data, self._script_fullpath, 'exec'), context) | ||
| 630 | except Exception: | ||
| 631 | raise HookError('%s\nFailed to import %s hook; see traceback above.' % | ||
| 632 | (traceback.format_exc(), self._hook_type)) | ||
| 633 | |||
| 634 | # Running the script should have defined a main() function. | ||
| 635 | if 'main' not in context: | ||
| 636 | raise HookError('Missing main() in: "%s"' % self._script_fullpath) | ||
| 637 | |||
| 638 | # Call the main function in the hook. If the hook should cause the | ||
| 639 | # build to fail, it will raise an Exception. We'll catch that convert | ||
| 640 | # to a HookError w/ just the failing traceback. | ||
| 641 | try: | ||
| 642 | context['main'](**kwargs) | ||
| 643 | except Exception: | ||
| 644 | raise HookError('%s\nFailed to run main() for %s hook; see traceback ' | ||
| 645 | 'above.' % (traceback.format_exc(), self._hook_type)) | ||
| 646 | |||
| 547 | def _ExecuteHook(self, **kwargs): | 647 | def _ExecuteHook(self, **kwargs): |
| 548 | """Actually execute the given hook. | 648 | """Actually execute the given hook. |
| 549 | 649 | ||
| @@ -568,19 +668,8 @@ class RepoHook(object): | |||
| 568 | # hooks can't import repo files. | 668 | # hooks can't import repo files. |
| 569 | sys.path = [os.path.dirname(self._script_fullpath)] + sys.path[1:] | 669 | sys.path = [os.path.dirname(self._script_fullpath)] + sys.path[1:] |
| 570 | 670 | ||
| 571 | # Exec, storing global context in the context dict. We catch exceptions | 671 | # Initial global context for the hook to run within. |
| 572 | # and convert to a HookError w/ just the failing traceback. | ||
| 573 | context = {'__file__': self._script_fullpath} | 672 | context = {'__file__': self._script_fullpath} |
| 574 | try: | ||
| 575 | exec(compile(open(self._script_fullpath).read(), | ||
| 576 | self._script_fullpath, 'exec'), context) | ||
| 577 | except Exception: | ||
| 578 | raise HookError('%s\nFailed to import %s hook; see traceback above.' % | ||
| 579 | (traceback.format_exc(), self._hook_type)) | ||
| 580 | |||
| 581 | # Running the script should have defined a main() function. | ||
| 582 | if 'main' not in context: | ||
| 583 | raise HookError('Missing main() in: "%s"' % self._script_fullpath) | ||
| 584 | 673 | ||
| 585 | # Add 'hook_should_take_kwargs' to the arguments to be passed to main. | 674 | # Add 'hook_should_take_kwargs' to the arguments to be passed to main. |
| 586 | # We don't actually want hooks to define their main with this argument-- | 675 | # We don't actually want hooks to define their main with this argument-- |
| @@ -592,15 +681,31 @@ class RepoHook(object): | |||
| 592 | kwargs = kwargs.copy() | 681 | kwargs = kwargs.copy() |
| 593 | kwargs['hook_should_take_kwargs'] = True | 682 | kwargs['hook_should_take_kwargs'] = True |
| 594 | 683 | ||
| 595 | # Call the main function in the hook. If the hook should cause the | 684 | # See what version of python the hook has been written against. |
| 596 | # build to fail, it will raise an Exception. We'll catch that convert | 685 | data = open(self._script_fullpath).read() |
| 597 | # to a HookError w/ just the failing traceback. | 686 | interp = self._ExtractInterpFromShebang(data) |
| 598 | try: | 687 | reexec = False |
| 599 | context['main'](**kwargs) | 688 | if interp: |
| 600 | except Exception: | 689 | prog = os.path.basename(interp) |
| 601 | raise HookError('%s\nFailed to run main() for %s hook; see traceback ' | 690 | if prog.startswith('python2') and sys.version_info.major != 2: |
| 602 | 'above.' % (traceback.format_exc(), | 691 | reexec = True |
| 603 | self._hook_type)) | 692 | elif prog.startswith('python3') and sys.version_info.major == 2: |
| 693 | reexec = True | ||
| 694 | |||
| 695 | # Attempt to execute the hooks through the requested version of Python. | ||
| 696 | if reexec: | ||
| 697 | try: | ||
| 698 | self._ExecuteHookViaReexec(interp, context, **kwargs) | ||
| 699 | except OSError as e: | ||
| 700 | if e.errno == errno.ENOENT: | ||
| 701 | # We couldn't find the interpreter, so fallback to importing. | ||
| 702 | reexec = False | ||
| 703 | else: | ||
| 704 | raise | ||
| 705 | |||
| 706 | # Run the hook by importing directly. | ||
| 707 | if not reexec: | ||
| 708 | self._ExecuteHookViaImport(data, context, **kwargs) | ||
| 604 | finally: | 709 | finally: |
| 605 | # Restore sys.path and CWD. | 710 | # Restore sys.path and CWD. |
| 606 | sys.path = orig_syspath | 711 | sys.path = orig_syspath |
