File size: 8,193 Bytes
72268ee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# (c) Anaconda, Inc. / https://anaconda.com
# All Rights Reserved
# This file is under the BSD license
#
# Helper script for adding and removing entries in the
# Windows system path from the NSIS installer.

__all__ = ['remove_from_system_path', 'add_to_system_path',
           'broadcast_environment_settings_change']

import ctypes
import os
import re
import sys
from ctypes import wintypes
from os import path

if sys.version_info[0] >= 3:
    import winreg as reg
else:
    import _winreg as reg

# If pythonw is being run, there may be no write function
if sys.stdout and sys.stdout.write:
    out = sys.stdout.write
    err = sys.stderr.write
else:
    OutputDebugString = ctypes.windll.kernel32.OutputDebugStringW
    OutputDebugString.argtypes = [ctypes.c_wchar_p]

    def out(x):
        OutputDebugString('_nsis.py: ' + x)

    def err(x):
        OutputDebugString('_nsis.py: Error: ' + x)

HWND_BROADCAST = 0xffff
WM_SETTINGCHANGE = 0x001A
SMTO_ABORTIFHUNG = 0x0002
SendMessageTimeout = ctypes.windll.user32.SendMessageTimeoutW
SendMessageTimeout.restype = None  # wintypes.LRESULT
SendMessageTimeout.argtypes = [wintypes.HWND, wintypes.UINT, wintypes.WPARAM,
                               wintypes.LPCWSTR, wintypes.UINT, wintypes.UINT,
                               ctypes.POINTER(wintypes.DWORD)]


def sz_expand(value, value_type):
    if value_type == reg.REG_EXPAND_SZ:
        return reg.ExpandEnvironmentStrings(value)
    else:
        return value


def remove_from_system_path(pathname, allusers=True, path_env_var='PATH'):
    """Removes all entries from the path which match the value in 'pathname'

       You must call broadcast_environment_settings_change() after you are finished
       manipulating the environment with this and other functions.

       For example,
         # Remove Anaconda from PATH
         remove_from_system_path('C:\\Anaconda')
         broadcast_environment_settings_change()
    """
    pathname = path.normcase(path.normpath(pathname))

    envkeys = [(reg.HKEY_CURRENT_USER, r'Environment')]
    if allusers:
        envkeys.append((reg.HKEY_LOCAL_MACHINE,
                        r'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'))
    for root, keyname in envkeys:
        key = reg.OpenKey(root, keyname, 0,
                          reg.KEY_QUERY_VALUE | reg.KEY_SET_VALUE)
        reg_value = None
        try:
            reg_value = reg.QueryValueEx(key, path_env_var)
        except WindowsError:
            # This will happen if we're a non-admin install and the user has
            # no PATH variable.
            reg.CloseKey(key)
            continue

        try:
            any_change = False
            results = []
            for v in reg_value[0].split(os.pathsep):
                vexp = sz_expand(v, reg_value[1])
                # Check if the expanded path matches the
                # requested path in a normalized way
                if path.normcase(path.normpath(vexp)) == pathname:
                    any_change = True
                else:
                    # Append the original unexpanded version to the results
                    results.append(v)

            modified_path = os.pathsep.join(results)
            if any_change:
                reg.SetValueEx(key, path_env_var, 0, reg_value[1], modified_path)
        except Exception:
            # If there's an error (e.g. when there is no PATH for the current
            # user), continue on to try the next root/keyname pair
            reg.CloseKey(key)


def add_to_system_path(paths, allusers=True, path_env_var='PATH'):
    """Adds the requested paths to the system PATH variable.

       You must call broadcast_environment_settings_change() after you are finished
       manipulating the environment with this and other functions.

    """
    # Make sure it's a list
    if not issubclass(type(paths), list):
        paths = [paths]

    # Ensure all the paths are valid before we start messing with the
    # registry.
    new_paths = None
    for p in paths:
        p = path.abspath(p)
        if new_paths:
            new_paths = new_paths + os.pathsep + p
        else:
            new_paths = p

    if allusers:
        # All Users
        root, keyname = (reg.HKEY_LOCAL_MACHINE,
                         r'SYSTEM\CurrentControlSet\Control\Session Manager\Environment')
    else:
        # Just Me
        root, keyname = (reg.HKEY_CURRENT_USER, r'Environment')

    key = reg.OpenKey(root, keyname, 0,
                      reg.KEY_QUERY_VALUE | reg.KEY_SET_VALUE)

    reg_type = None
    reg_value = None
    try:
        try:
            reg_value = reg.QueryValueEx(key, path_env_var)
        except WindowsError:
            # This will happen if we're a non-admin install and the user has
            # no PATH variable; in which case, we can write our new paths
            # directly.
            reg_type = reg.REG_EXPAND_SZ
            final_value = new_paths
        else:
            # Put to the front of PATH irrespective of allusers. The old
            # behaviour was asking for trouble and did not, contrary to what
            # this comment used to say, mirror what happens on *nix.
            reg_type = reg_value[1]
            final_value = new_paths + os.pathsep + reg_value[0]
        # Replace coincident ';' with a single ';'
        final_value = re.sub(r'([\;])+', r'\1', final_value)
        # Remove trailing ';'
        final_value = re.sub(r'\;$', '', final_value)
        # Remove any '"', they are not needed and break conda.
        final_value = final_value.replace('"', '')
        # Warn about directories that do not exist.
        directories = final_value.split(';')
        for directory in directories:
            if '%' not in directory and not os.path.exists(directory):
                out("WARNING: Old PATH entry '%s' does not exist\n" % (directory))
        reg.SetValueEx(key, path_env_var, 0, reg_type, final_value)

    finally:
        reg.CloseKey(key)


def _reg_query_sub_keys(handle, key, keylist=[]):
    reghandle = reg.OpenKey(handle, key, 0, reg.KEY_READ)
    try:
        i = 0
        while True:
            subkey = reg.EnumKey(reghandle, i)
            i += 1
            _reg_query_sub_keys(handle, key + subkey + "\\", keylist)
    except WindowsError as ex:
        if ex.winerror == 259:
            keylist.append(key)
    finally:
        reg.CloseKey(reghandle)


def get_previous_install_prefixes(pyversion, arch, allusers=True):
    """Returns a list of prefixes for all old installations of this arch so that
       they can be removed from PATH if present. Note, it would be preferable to
       uninstall them properly instead.
    """
    if allusers:
        # All Users
        key, subkey = (reg.HKEY_LOCAL_MACHINE,
                       r'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\')
    else:
        # Just Me
        key, subkey = (reg.HKEY_CURRENT_USER,
                       r'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\')

    keylist = []
    # We ignore pyversion and instead look for any *conda installations.
    regex = re.compile(r'Python \S+ \(\S+conda[0-9]+ \S+ '+arch+r'\)')
    _reg_query_sub_keys(key, subkey, keylist)
    results = []
    for uninstsubkey in keylist:
        final_part = os.path.basename(uninstsubkey.rstrip('\\'))
        if regex.match(final_part):
            try:
                with reg.OpenKeyEx(key, uninstsubkey, 0,
                                   reg.KEY_QUERY_VALUE) as keyhandle:
                    reg_value = reg.QueryValueEx(keyhandle, 'UninstallString')
                    results.append(os.path.dirname(re.sub(r'^"|"$', '', reg_value[0])))
            except Exception:
                pass
    return results


def broadcast_environment_settings_change():
    """Broadcasts to the system indicating that master environment variables have changed.

    This must be called after using the other functions in this module to
    manipulate environment variables.
    """
    SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, u'Environment',
                       SMTO_ABORTIFHUNG, 5000, ctypes.pointer(wintypes.DWORD()))