1# encoding: utf-8
2#
3# Copyright (c) 2010 Doug Hellmann.  All rights reserved.
4#
5"""Plugin to handle hooks in user-defined scripts.
6"""
7
8import logging
9import os
10import re
11import stat
12import subprocess
13import sys
14
15
16log = logging.getLogger(__name__)
17
18# Are we running under msys
19if sys.platform == 'win32' and \
20   os.environ.get('OS') == 'Windows_NT' and \
21   os.environ.get('MSYSTEM') in ('MINGW32', 'MINGW64'):
22    is_msys = True
23    script_folder = 'Scripts'
24else:
25    is_msys = False
26    script_folder = 'bin'
27
28
29def _get_msys_shell():
30    if 'MSYS_HOME' in os.environ:
31        return [get_path(os.environ['MSYS_HOME'], 'bin', 'sh.exe')]
32    else:
33        for path in os.environ['PATH'].split(';'):
34            if os.path.exists(os.path.join(path, 'sh.exe')):
35                return [get_path(path, 'sh.exe')]
36    raise Exception('Could not find sh.exe')
37
38
39def run_script(script_path, *args):
40    """Execute a script in a subshell.
41    """
42    if os.path.exists(script_path):
43        cmd = [script_path] + list(args)
44        if is_msys:
45            cmd = _get_msys_shell() + cmd
46        log.debug('running %s', str(cmd))
47        try:
48            subprocess.call(cmd)
49        except OSError:
50            _, msg, _ = sys.exc_info()
51            log.error('could not run "%s": %s', script_path, str(msg))
52        # log.debug('Returned %s', return_code)
53    return
54
55
56def run_global(script_name, *args):
57    """Run a script from $VIRTUALENVWRAPPER_HOOK_DIR.
58    """
59    script_path = get_path('$VIRTUALENVWRAPPER_HOOK_DIR', script_name)
60    run_script(script_path, *args)
61    return
62
63
64PERMISSIONS = (
65    stat.S_IRWXU    # read/write/execute, user
66    | stat.S_IRGRP  # read, group
67    | stat.S_IXGRP  # execute, group
68    | stat.S_IROTH  # read, others
69    | stat.S_IXOTH  # execute, others
70)
71PERMISSIONS_SOURCED = PERMISSIONS & ~(
72    # remove executable bits for
73    stat.S_IXUSR     # ... user
74    | stat.S_IXGRP   # ... group
75    | stat.S_IXOTH   # ... others
76)
77
78
79GLOBAL_HOOKS = [
80    # initialize
81    ("initialize",
82     "This hook is sourced during the startup phase "
83     "when loading virtualenvwrapper.sh.",
84     PERMISSIONS_SOURCED),
85
86    # mkvirtualenv
87    ("premkvirtualenv",
88     "This hook is run after a new virtualenv is created "
89     "and before it is activated.\n"
90     "# argument: name of new environment",
91     PERMISSIONS),
92    ("postmkvirtualenv",
93     "This hook is sourced after a new virtualenv is activated.",
94     PERMISSIONS_SOURCED),
95
96    # cpvirtualenv:
97    # precpvirtualenv <old> <new> (run),
98    # postcpvirtualenv (sourced)
99
100    # rmvirtualenv
101    ("prermvirtualenv",
102     "This hook is run before a virtualenv is deleted.\n"
103     "# argument: full path to environment directory",
104     PERMISSIONS),
105    ("postrmvirtualenv",
106     "This hook is run after a virtualenv is deleted.\n"
107     "# argument: full path to environment directory",
108     PERMISSIONS),
109
110    # deactivate
111    ("predeactivate",
112     "This hook is sourced before every virtualenv is deactivated.",
113     PERMISSIONS_SOURCED),
114    ("postdeactivate",
115     "This hook is sourced after every virtualenv is deactivated.",
116     PERMISSIONS_SOURCED),
117
118    # activate
119    ("preactivate",
120     "This hook is run before every virtualenv is activated.\n"
121     "# argument: environment name",
122     PERMISSIONS),
123    ("postactivate",
124     "This hook is sourced after every virtualenv is activated.",
125     PERMISSIONS_SOURCED),
126
127    # mkproject:
128    # premkproject <new project name> (run),
129    # postmkproject (sourced)
130
131    # get_env_details
132    ("get_env_details",
133     "This hook is run when the list of virtualenvs is printed "
134     "so each name can include details.\n"
135     "# argument: environment name",
136     PERMISSIONS),
137]
138
139
140LOCAL_HOOKS = [
141    # deactivate
142    ("predeactivate",
143     "This hook is sourced before this virtualenv is deactivated.",
144     PERMISSIONS_SOURCED),
145    ("postdeactivate",
146     "This hook is sourced after this virtualenv is deactivated.",
147     PERMISSIONS_SOURCED),
148
149    # activate
150    ("preactivate",
151     "This hook is run before this virtualenv is activated.",
152     PERMISSIONS),
153    ("postactivate",
154     "This hook is sourced after this virtualenv is activated.",
155     PERMISSIONS_SOURCED),
156
157    # get_env_details
158    ("get_env_details",
159     "This hook is run when the list of virtualenvs is printed "
160     "in 'long' mode so each name can include details.\n"
161     "# argument: environment name",
162     PERMISSIONS),
163]
164
165
166def make_hook(filename, comment, permissions):
167    """Create a hook script.
168
169    :param filename: The name of the file to write.
170    :param comment: The comment to insert into the file.
171    """
172    filename = get_path(filename)
173    if not os.path.exists(filename):
174        log.info('creating %s', filename)
175        f = open(filename, 'w')
176        try:
177            # for sourced scripts, the shebang line won't be used;
178            # it is useful for editors to recognize the file type, though
179            f.write("#!%(shell)s\n# %(comment)s\n\n" % {
180                'comment': comment,
181                'shell': os.environ.get('SHELL', '/bin/sh'),
182            })
183        finally:
184            f.close()
185        os.chmod(filename, permissions)
186    return
187
188
189# HOOKS
190
191
192def initialize(args):
193    for filename, comment, permissions in GLOBAL_HOOKS:
194        make_hook(get_path('$VIRTUALENVWRAPPER_HOOK_DIR', filename),
195                  comment, permissions)
196    return
197
198
199def initialize_source(args):
200    return """
201#
202# Run user-provided scripts
203#
204[ -f "$VIRTUALENVWRAPPER_HOOK_DIR/initialize" ] && \
205    source "$VIRTUALENVWRAPPER_HOOK_DIR/initialize"
206"""
207
208
209def pre_mkvirtualenv(args):
210    log.debug('pre_mkvirtualenv %s', str(args))
211    envname = args[0]
212    for filename, comment, permissions in LOCAL_HOOKS:
213        make_hook(get_path('$WORKON_HOME', envname, script_folder, filename),
214                  comment, permissions)
215    run_global('premkvirtualenv', *args)
216    return
217
218
219def post_mkvirtualenv_source(args):
220    log.debug('post_mkvirtualenv_source %s', str(args))
221    return """
222#
223# Run user-provided scripts
224#
225[ -f "$VIRTUALENVWRAPPER_HOOK_DIR/postmkvirtualenv" ] && \
226    source "$VIRTUALENVWRAPPER_HOOK_DIR/postmkvirtualenv"
227"""
228
229
230def pre_cpvirtualenv(args):
231    log.debug('pre_cpvirtualenv %s', str(args))
232    envname = args[0]
233    for filename, comment, permissions in LOCAL_HOOKS:
234        make_hook(get_path('$WORKON_HOME', envname, script_folder, filename),
235                  comment, permissions)
236    run_global('precpvirtualenv', *args)
237    return
238
239
240def post_cpvirtualenv_source(args):
241    log.debug('post_cpvirtualenv_source %s', str(args))
242    return """
243#
244# Run user-provided scripts
245#
246[ -f "$VIRTUALENVWRAPPER_HOOK_DIR/postcpvirtualenv" ] && \
247    source "$VIRTUALENVWRAPPER_HOOK_DIR/postcpvirtualenv"
248"""
249
250
251def pre_rmvirtualenv(args):
252    log.debug('pre_rmvirtualenv')
253    run_global('prermvirtualenv', *args)
254    return
255
256
257def post_rmvirtualenv(args):
258    log.debug('post_rmvirtualenv')
259    run_global('postrmvirtualenv', *args)
260    return
261
262
263def pre_activate(args):
264    log.debug('pre_activate')
265    run_global('preactivate', *args)
266    script_path = get_path('$WORKON_HOME', args[0],
267                           script_folder, 'preactivate')
268    run_script(script_path, *args)
269    return
270
271
272def post_activate_source(args):
273    log.debug('post_activate_source')
274    return """
275#
276# Run user-provided scripts
277#
278[ -f "$VIRTUALENVWRAPPER_HOOK_DIR/postactivate" ] && \
279    source "$VIRTUALENVWRAPPER_HOOK_DIR/postactivate"
280[ -f "$VIRTUAL_ENV/$VIRTUALENVWRAPPER_ENV_BIN_DIR/postactivate" ] && \
281    source "$VIRTUAL_ENV/$VIRTUALENVWRAPPER_ENV_BIN_DIR/postactivate"
282"""
283
284
285def pre_deactivate_source(args):
286    log.debug('pre_deactivate_source')
287    return """
288#
289# Run user-provided scripts
290#
291[ -f "$VIRTUAL_ENV/$VIRTUALENVWRAPPER_ENV_BIN_DIR/predeactivate" ] && \
292    source "$VIRTUAL_ENV/$VIRTUALENVWRAPPER_ENV_BIN_DIR/predeactivate"
293[ -f "$VIRTUALENVWRAPPER_HOOK_DIR/predeactivate" ] && \
294    source "$VIRTUALENVWRAPPER_HOOK_DIR/predeactivate"
295"""
296
297
298def post_deactivate_source(args):
299    log.debug('post_deactivate_source')
300    return """
301#
302# Run user-provided scripts
303#
304VIRTUALENVWRAPPER_LAST_VIRTUAL_ENV="$WORKON_HOME/%(env_name)s"
305[ -f "$WORKON_HOME/%(env_name)s/bin/postdeactivate" ] && \
306    source "$WORKON_HOME/%(env_name)s/bin/postdeactivate"
307[ -f "$VIRTUALENVWRAPPER_HOOK_DIR/postdeactivate" ] && \
308    source "$VIRTUALENVWRAPPER_HOOK_DIR/postdeactivate"
309unset VIRTUALENVWRAPPER_LAST_VIRTUAL_ENV
310""" % {'env_name': args[0]}
311
312
313def get_env_details(args):
314    log.debug('get_env_details')
315    run_global('get_env_details', *args)
316    script_path = get_path('$WORKON_HOME', args[0],
317                           script_folder, 'get_env_details')
318    run_script(script_path, *args)
319    return
320
321
322def get_path(*args):
323    '''
324    Get a full path from args.
325
326    Path separator is determined according to the os and the shell and
327    allow to use is_msys.
328
329    Variables and user are expanded during the process.
330    '''
331    path = os.path.expanduser(os.path.expandvars(os.path.join(*args)))
332    if is_msys:
333        # MSYS accept unix or Win32 and sometimes
334        # it drives to mixed style paths
335        if re.match(r'^/[a-zA-Z](/|^)', path):
336            # msys path could starts with '/c/'-form drive letter
337            path = ''.join((path[1], ':', path[2:]))
338        path = path.replace('/', os.sep)
339
340    return os.path.abspath(path)
341