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 |