File size: 7,076 Bytes
a65138c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# -*- coding: utf-8 -*-

import logging
import os
import os.path
import sys
import re


logger = logging.getLogger(__name__)


EXPOSE_NAMES = ('executable', 'version', 'prefix', 'exec_prefix', 'through')


def wellknown_locations():
    if sys.platform == 'win32':
        base = 'c:\\'
        for name in os.listdir(base):
            name = name.lower()
            if name.startswith('python'):
                shortversion = name[len('python'):]
                m = re.match('[23][0-9]', shortversion)
                if m:
                    yield base + name
    elif 'PYTHONZ_ROOT' in os.environ:
        pythonz_root = os.environ['PYTHONZ_ROOT']
        pythons = os.path.join(pythonz_root, 'pythons')
        for item in os.listdir(pythons):
            yield os.path.join(pythons, item)


def discover_python(in_wellknown=True, in_path=True):

    if in_wellknown:
        for found in search_in_wellknown_locations():
            yield found

    if in_path:
        for found in search_in_path():
            yield found


def search_in_wellknown_locations():
    for location in wellknown_locations():
        if sys.platform == 'win32':
            founds = contains_python(location)
        else:
            founds = contains_python_in_bin(location)

        for found in founds:
            found['through'] = 'WELLKNOWN_LOCATION'
            yield found


def search_in_path():
    if 'PATH' in os.environ:
        path = os.environ['PATH']
        path = path.split(os.pathsep)
        for dir in path:
            for found in contains_python(dir):
                found['through'] = 'PATH'

                # resolve symlinks
                resolved = os.path.realpath(found['executable'])
                found['executable'] = resolved
                yield found


def contains_python_in_bin(dir):
    bindir = os.path.join(dir, 'bin')
    return contains_python(bindir)


def contains_python(dir):
    vers = {
        2: [3, 4, 5, 6, 7],
        3: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
    }
    names = (('python%d.%d' % (major, minor))
             for major in reversed(sorted(vers))
             for minor in reversed(sorted(vers[major])))
    names = list(names) + ['python']
    for name in names:
        executable = executable_in_dir(name, dir)
        if executable:
            found = executable_is_python(executable)
            if found:
                yield found


def executable_in_dir(name, dir):
    assert name == os.path.basename(name)
    if sys.platform == 'win32':
        name += '.exe'
    path = os.path.join(dir, name)
    if not os.path.exists(path):
        return
    return path


def executable_is_python(executable):
    from subprocess import Popen
    from subprocess import PIPE
    cmd = '''
    import os, sys
    print(sys.hexversion)
    print(os.pathsep.join([sys.prefix, sys.exec_prefix]))
    '''.strip().replace('\n', ';')
    args = [executable, '-c', cmd]
    env = dict(os.environ)
    for k in ('PYTHONPATH', 'PYTHONHOME'):
        if k in env:
            del env[k]
    try:
        p = Popen(args, stdout=PIPE, env=env)
        lines = p.stdout.read().split('\n')
        p.wait()
        ver = int(lines[0])
        ver_major = str(ver >> 24 & 0xff)
        ver_minor = str(ver >> 16 & 0xff)
        ver_patch = str(ver >> 8  & 0xff)
        ver = ver_major, ver_minor, ver_patch
        version = '.'.join(ver)
        prefix, exec_prefix = lines[1].split(os.pathsep)
        return dict(executable=executable, version=version,
                    prefix=prefix, exec_prefix=exec_prefix)
    except Exception, e:
        logger.error('popen failed: %s', args)
        logger.exception(e)


def log_discovered(qualify, found, names, log=logger.info):
    for item in found:
        msg = qualify + ':'
        for name in names:
            if name in item:
                msg += ' %s=%s' % (name, item[name])
        log(msg)
        yield item


def expose_options(options, names, found, not_found, logger=logger):
    for name in names:
        if name in found:
            value = found[name]
            if name in options:
                if value != options[name]:
                    logger.info('(updating) %s = %s', name, value)
                    options[name] = value
                else:
                    logger.info('(preserving) %s = %s', name, value)
            else:
                logger.info('(exposing) %s = %s', name, value)
                options[name] = value
        else:
            if name not in options:
                options[name] = value = not_found
                logger.info('(exposing) %s = %s', name, value)


class Discover(object):
    ''' Discover Python and provide its location.
    '''

    def __init__(self, buildout, name, options):
        from zc.buildout import UserError
        self.__logger = logger = logging.getLogger(name)
        for k, v in options.items():
            logger.info('%s: %r', k, v)

        self.__recipe = options['recipe']

        not_found = options.get('not-found', 'not-found')
        version = options.get('version', '').strip()

        if 'location' in options:
            # if location is explicitly specified, it must contains java
            # executable.
            for found in contains_python_in_bin(options['location']):
                if not version or found['version'].startswith(version):
                    # Python found, no further discovery required.
                    options['executable'] = found['executable']
                    return
            raise UserError('Python not found at %s' % options['location'])

        in_wellknown = options.get('search-in-wellknown-places',
                                   'true').lower().strip()
        in_wellknown = in_wellknown in ('true', 'yes', '1') 
        in_path = options.get('search-in-path', 'true').lower().strip()
        in_path = in_path in ('true', 'yes', '1')

        founds = discover_python(in_wellknown=in_wellknown,
                                 in_path=in_path)
        founds = log_discovered('candidates', founds, EXPOSE_NAMES,
                                log=logger.debug)
        if version:
            # filter with version
            founds = (found for found in founds
                      if found['version'].startswith(version))
        founds = log_discovered('matching', founds, EXPOSE_NAMES,
                                log=logger.info)
        founds = list(founds)

        # location is not specified: try to discover a Python installation
        if founds:
            found = founds[0]
            logger.info('the first-matching one will be used:')
            expose_options(options, EXPOSE_NAMES, found,
                           not_found=not_found, logger=logger)
            return

        # ensure executable publishes not-found marker
        expose_options(options, ['executable'], dict(), not_found=not_found,
                       logger=logger)
        logger.warning('Python not found')
        return

    def install(self):
        return []

    update = install