1# -*- coding: utf-8 -*- 2 3"""Functions for discovering and executing various cookiecutter hooks.""" 4 5import errno 6import io 7import logging 8import os 9import subprocess 10import sys 11import tempfile 12 13from cookiecutter import utils 14from cookiecutter.environment import StrictEnvironment 15from cookiecutter.exceptions import FailedHookException 16 17logger = logging.getLogger(__name__) 18 19_HOOKS = [ 20 'pre_gen_project', 21 'post_gen_project', 22] 23EXIT_SUCCESS = 0 24 25 26def valid_hook(hook_file, hook_name): 27 """Determine if a hook file is valid. 28 29 :param hook_file: The hook file to consider for validity 30 :param hook_name: The hook to find 31 :return: The hook file validity 32 """ 33 filename = os.path.basename(hook_file) 34 basename = os.path.splitext(filename)[0] 35 36 matching_hook = basename == hook_name 37 supported_hook = basename in _HOOKS 38 backup_file = filename.endswith('~') 39 40 return matching_hook and supported_hook and not backup_file 41 42 43def find_hook(hook_name, hooks_dir='hooks'): 44 """Return a dict of all hook scripts provided. 45 46 Must be called with the project template as the current working directory. 47 Dict's key will be the hook/script's name, without extension, while values 48 will be the absolute path to the script. Missing scripts will not be 49 included in the returned dict. 50 51 :param hook_name: The hook to find 52 :param hooks_dir: The hook directory in the template 53 :return: The absolute path to the hook script or None 54 """ 55 logger.debug('hooks_dir is %s', os.path.abspath(hooks_dir)) 56 57 if not os.path.isdir(hooks_dir): 58 logger.debug('No hooks/dir in template_dir') 59 return None 60 61 for hook_file in os.listdir(hooks_dir): 62 if valid_hook(hook_file, hook_name): 63 return os.path.abspath(os.path.join(hooks_dir, hook_file)) 64 65 return None 66 67 68def run_script(script_path, cwd='.'): 69 """Execute a script from a working directory. 70 71 :param script_path: Absolute path to the script to run. 72 :param cwd: The directory to run the script from. 73 """ 74 run_thru_shell = sys.platform.startswith('win') 75 if script_path.endswith('.py'): 76 script_command = [sys.executable, script_path] 77 else: 78 script_command = [script_path] 79 80 utils.make_executable(script_path) 81 82 try: 83 proc = subprocess.Popen(script_command, shell=run_thru_shell, cwd=cwd) 84 exit_status = proc.wait() 85 if exit_status != EXIT_SUCCESS: 86 raise FailedHookException( 87 'Hook script failed (exit status: {})'.format(exit_status) 88 ) 89 except OSError as os_error: 90 if os_error.errno == errno.ENOEXEC: 91 raise FailedHookException( 92 'Hook script failed, might be an ' 'empty file or missing a shebang' 93 ) 94 raise FailedHookException('Hook script failed (error: {})'.format(os_error)) 95 96 97def run_script_with_context(script_path, cwd, context): 98 """Execute a script after rendering it with Jinja. 99 100 :param script_path: Absolute path to the script to run. 101 :param cwd: The directory to run the script from. 102 :param context: Cookiecutter project template context. 103 """ 104 _, extension = os.path.splitext(script_path) 105 106 with io.open(script_path, 'r', encoding='utf-8') as file: 107 contents = file.read() 108 109 with tempfile.NamedTemporaryFile(delete=False, mode='wb', suffix=extension) as temp: 110 env = StrictEnvironment(context=context, keep_trailing_newline=True) 111 template = env.from_string(contents) 112 output = template.render(**context) 113 temp.write(output.encode('utf-8')) 114 115 run_script(temp.name, cwd) 116 117 118def run_hook(hook_name, project_dir, context): 119 """ 120 Try to find and execute a hook from the specified project directory. 121 122 :param hook_name: The hook to execute. 123 :param project_dir: The directory to execute the script from. 124 :param context: Cookiecutter project context. 125 """ 126 script = find_hook(hook_name) 127 if script is None: 128 logger.debug('No %s hook found', hook_name) 129 return 130 logger.debug('Running hook %s', hook_name) 131 run_script_with_context(script, project_dir, context) 132