summaryrefslogtreecommitdiffstats
path: root/project.py
diff options
context:
space:
mode:
Diffstat (limited to 'project.py')
-rwxr-xr-xproject.py147
1 files changed, 126 insertions, 21 deletions
diff --git a/project.py b/project.py
index 9702e9da..58942514 100755
--- a/project.py
+++ b/project.py
@@ -18,6 +18,7 @@ from __future__ import print_function
18import errno 18import errno
19import filecmp 19import filecmp
20import glob 20import glob
21import json
21import os 22import os
22import random 23import random
23import re 24import 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 = """
593import json, os, sys
594path = '''%(path)s'''
595kwargs = json.loads('''%(kwargs)s''')
596context = json.loads('''%(context)s''')
597sys.path.insert(0, os.path.dirname(path))
598data = open(path).read()
599exec(compile(data, path, 'exec'), context)
600context['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