summaryrefslogtreecommitdiff
path: root/src/virtualenv/discovery/builtin.py
blob: 58a912ddd0d97d1bb3f77d9697158e29cc3c60fa (plain)
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
from __future__ import annotations

import logging
import os
import sys

from virtualenv.info import IS_WIN

from .discover import Discover
from .py_info import PythonInfo
from .py_spec import PythonSpec


class Builtin(Discover):
    def __init__(self, options):
        super().__init__(options)
        self.python_spec = options.python if options.python else [sys.executable]
        self.app_data = options.app_data
        self.try_first_with = options.try_first_with

    @classmethod
    def add_parser_arguments(cls, parser):
        parser.add_argument(
            "-p",
            "--python",
            dest="python",
            metavar="py",
            type=str,
            action="append",
            default=[],
            help="interpreter based on what to create environment (path/identifier) "
            "- by default use the interpreter where the tool is installed - first found wins",
        )
        parser.add_argument(
            "--try-first-with",
            dest="try_first_with",
            metavar="py_exe",
            type=str,
            action="append",
            default=[],
            help="try first these interpreters before starting the discovery",
        )

    def run(self):
        for python_spec in self.python_spec:
            result = get_interpreter(python_spec, self.try_first_with, self.app_data, self._env)
            if result is not None:
                return result
        return None

    def __repr__(self):
        spec = self.python_spec[0] if len(self.python_spec) == 1 else self.python_spec
        return f"{self.__class__.__name__} discover of python_spec={spec!r}"


def get_interpreter(key, try_first_with, app_data=None, env=None):
    spec = PythonSpec.from_string_spec(key)
    logging.info("find interpreter for spec %r", spec)
    proposed_paths = set()
    env = os.environ if env is None else env
    for interpreter, impl_must_match in propose_interpreters(spec, try_first_with, app_data, env):
        key = interpreter.system_executable, impl_must_match
        if key in proposed_paths:
            continue
        logging.info("proposed %s", interpreter)
        if interpreter.satisfies(spec, impl_must_match):
            logging.debug("accepted %s", interpreter)
            return interpreter
        proposed_paths.add(key)


def propose_interpreters(spec, try_first_with, app_data, env=None):
    # 0. try with first
    env = os.environ if env is None else env
    for py_exe in try_first_with:
        path = os.path.abspath(py_exe)
        try:
            os.lstat(path)  # Windows Store Python does not work with os.path.exists, but does for os.lstat
        except OSError:
            pass
        else:
            yield PythonInfo.from_exe(os.path.abspath(path), app_data, env=env), True

    # 1. if it's a path and exists
    if spec.path is not None:
        try:
            os.lstat(spec.path)  # Windows Store Python does not work with os.path.exists, but does for os.lstat
        except OSError:
            if spec.is_abs:
                raise
        else:
            yield PythonInfo.from_exe(os.path.abspath(spec.path), app_data, env=env), True
        if spec.is_abs:
            return
    else:
        # 2. otherwise try with the current
        yield PythonInfo.current_system(app_data), True

        # 3. otherwise fallback to platform default logic
        if IS_WIN:
            from .windows import propose_interpreters

            for interpreter in propose_interpreters(spec, app_data, env):
                yield interpreter, True
    # finally just find on path, the path order matters (as the candidates are less easy to control by end user)
    paths = get_paths(env)
    tested_exes = set()
    for pos, path in enumerate(paths):
        path_str = str(path)
        logging.debug(LazyPathDump(pos, path_str, env))
        for candidate, match in possible_specs(spec):
            found = check_path(candidate, path_str)
            if found is not None:
                exe = os.path.abspath(found)
                if exe not in tested_exes:
                    tested_exes.add(exe)
                    interpreter = PathPythonInfo.from_exe(exe, app_data, raise_on_error=False, env=env)
                    if interpreter is not None:
                        yield interpreter, match


def get_paths(env):
    path = env.get("PATH", None)
    if path is None:
        try:
            path = os.confstr("CS_PATH")
        except (AttributeError, ValueError):
            path = os.defpath
    if not path:
        paths = []
    else:
        paths = [p for p in path.split(os.pathsep) if os.path.exists(p)]
    return paths


class LazyPathDump:
    def __init__(self, pos, path, env):
        self.pos = pos
        self.path = path
        self.env = env

    def __repr__(self):
        content = f"discover PATH[{self.pos}]={self.path}"
        if self.env.get("_VIRTUALENV_DEBUG"):  # this is the over the board debug
            content += " with =>"
            for file_name in os.listdir(self.path):
                try:
                    file_path = os.path.join(self.path, file_name)
                    if os.path.isdir(file_path) or not os.access(file_path, os.X_OK):
                        continue
                except OSError:
                    pass
                content += " "
                content += file_name
        return content


def check_path(candidate, path):
    _, ext = os.path.splitext(candidate)
    if sys.platform == "win32" and ext != ".exe":
        candidate = candidate + ".exe"
    if os.path.isfile(candidate):
        return candidate
    candidate = os.path.join(path, candidate)
    if os.path.isfile(candidate):
        return candidate
    return None


def possible_specs(spec):
    # 4. then maybe it's something exact on PATH - if it was direct lookup implementation no longer counts
    yield spec.str_spec, False
    # 5. or from the spec we can deduce a name on path  that matches
    yield from spec.generate_names()


class PathPythonInfo(PythonInfo):
    """python info from path"""


__all__ = [
    "get_interpreter",
    "Builtin",
    "PathPythonInfo",
]