-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
Copy pathversion.py
223 lines (196 loc) · 8.94 KB
/
version.py
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
import sys
import os
import subprocess
import tokenize
import re
from buildtools.helper import print_exception_indented
TROVE = re.compile(r"Programming Language\s+::\s+Python\s+::\s+(\d)")
if sys.version_info > (3,):
import collections.abc as collections
file_open = tokenize.open
else:
import collections
file_open = open
WIN = sys.platform == "win32"
if WIN and "CODEQL_EXTRACTOR_PYTHON_OPTION_PYTHON_EXECUTABLE_NAME" not in os.environ:
# installing `py` launcher is optional when installing Python on windows, so it's
# possible that the user did not install it, see
# https://github.com/github/codeql-cli-binaries/issues/125#issuecomment-1157429430
# so we check whether it has been installed, and we check only if the "python_executable_name"
# extractor option has not been specified. Newer versions have a `--list` option,
# but that has only been mentioned in the docs since 3.9, so to not risk it not
# working on potential older versions, we'll just use `py --version` which forwards
# the `--version` argument to the default python executable.
try:
subprocess.check_call(["py", "--version"])
except (subprocess.CalledProcessError, Exception):
sys.stderr.write("The `py` launcher is required for CodeQL to work on Windows.")
sys.stderr.write("Please include it when installing Python for Windows.")
sys.stderr.write("see https://docs.python.org/3/using/windows.html#python-launcher-for-windows")
sys.stderr.flush()
sys.exit(4) # 4 was a unique exit code at the time of writing
AVAILABLE_VERSIONS = []
def set_available_versions():
"""Sets the global `AVAILABLE_VERSIONS` to a list of available (major) Python versions."""
global AVAILABLE_VERSIONS
if AVAILABLE_VERSIONS:
return # already set
for version in [3, 2]:
try:
subprocess.check_call(" ".join(executable_name(version) + ["-c", "pass"]), shell=True)
AVAILABLE_VERSIONS.append(version)
except Exception:
pass # If not available, we simply don't add it to the list
if not AVAILABLE_VERSIONS:
# If neither 'python3' nor 'python2' is available, we'll just try 'python' and hope for the best
AVAILABLE_VERSIONS = ['']
def executable(version):
"""Returns the executable to use for the given Python version."""
global AVAILABLE_VERSIONS
set_available_versions()
if version not in AVAILABLE_VERSIONS:
available_version = AVAILABLE_VERSIONS[0]
print("Wanted to run Python %s, but it is not available. Using Python %s instead" % (version, available_version))
version = available_version
return executable_name(version)
def executable_name(version):
if WIN:
return ["py", "-%s" % version]
else:
return ["python%s" % version]
PREFERRED_PYTHON_VERSION = None
def extractor_executable():
'''
Returns the executable to use for the extractor.
If a Python executable name is specified using the extractor option, returns that name.
In the absence of a user-specified executable name, returns the executable name for
Python 3 if it is available, and Python 2 if not.
'''
executable_name = os.environ.get("CODEQL_EXTRACTOR_PYTHON_OPTION_PYTHON_EXECUTABLE_NAME", None)
if executable_name is not None:
print("Using Python executable name provided via the python_executable_name extractor option: {}"
.format(executable_name)
)
return [executable_name]
# Call machine_version() to ensure we've set PREFERRED_PYTHON_VERSION
if PREFERRED_PYTHON_VERSION is None:
machine_version()
return executable(PREFERRED_PYTHON_VERSION)
def machine_version():
"""If only Python 2 or Python 3 is installed, will return that version"""
global PREFERRED_PYTHON_VERSION
print("Trying to guess Python version based on installed versions")
if sys.version_info > (3,):
this, other = 3, 2
else:
this, other = 2, 3
try:
exe = executable(other)
# We need `shell=True` here in order for the test framework to function correctly. For
# whatever reason, the `PATH` variable is ignored if `shell=False`.
# Also, this in turn forces us to give the whole command as a string, rather than a list.
# Otherwise, the effect is that the Python interpreter is invoked _as a REPL_, rather than
# with the given piece of code.
subprocess.check_call(" ".join(exe + [ "-c", "pass" ]), shell=True)
print("This script is running Python {}, but Python {} is also available (as '{}')"
.format(this, other, ' '.join(exe))
)
# If both versions are available, our preferred version is Python 3
PREFERRED_PYTHON_VERSION = 3
return None
except Exception:
print("Only Python {} installed -- will use that version".format(this))
PREFERRED_PYTHON_VERSION = this
return this
def trove_version(root):
print("Trying to guess Python version based on Trove classifiers in setup.py")
try:
full_path = os.path.join(root, "setup.py")
if not os.path.exists(full_path):
print("Did not find setup.py (expected it to be at {})".format(full_path))
return None
versions = set()
with file_open(full_path) as fd:
contents = fd.read()
for match in TROVE.finditer(contents):
versions.add(int(match.group(1)))
if 2 in versions and 3 in versions:
print("Found Trove classifiers for both Python 2 and Python 3 in setup.py -- will use Python 3")
return 3
elif len(versions) == 1:
result = versions.pop()
print("Found Trove classifier for Python {} in setup.py -- will use that version".format(result))
return result
else:
print("Found no Trove classifiers for Python in setup.py")
except Exception:
print("Skipping due to exception:")
print_exception_indented()
return None
def wrap_with_list(x):
if isinstance(x, collections.Iterable) and not isinstance(x, str):
return x
else:
return [x]
def travis_version(root):
print("Trying to guess Python version based on travis file")
try:
full_paths = [os.path.join(root, filename) for filename in [".travis.yml", "travis.yml"]]
travis_file_paths = [path for path in full_paths if os.path.exists(path)]
if not travis_file_paths:
print("Did not find any travis files (expected them at either {})".format(full_paths))
return None
try:
import yaml
except ImportError:
print("Found a travis file, but yaml library not available")
return None
with open(travis_file_paths[0]) as travis_file:
travis_yaml = yaml.safe_load(travis_file)
if "python" in travis_yaml:
versions = wrap_with_list(travis_yaml["python"])
else:
versions = []
# 'matrix' is an alias for 'jobs' now (https://github.com/travis-ci/docs-travis-ci-com/issues/1500)
# If both are defined, only the last defined will be used.
if "matrix" in travis_yaml and "jobs" in travis_yaml:
print("Ignoring 'matrix' and 'jobs' in Travis file, since they are both defined (only one of them should be).")
else:
matrix = travis_yaml.get("matrix") or travis_yaml.get("jobs") or dict()
includes = matrix.get("include") or []
for include in includes:
if "python" in include:
versions.extend(wrap_with_list(include["python"]))
found = set()
for version in versions:
# Yaml may convert version strings to numbers, convert them back.
version = str(version)
if version.startswith("2"):
found.add(2)
if version.startswith("3"):
found.add(3)
if len(found) == 1:
result = found.pop()
print("Only found Python {} in travis file -- will use that version".format(result))
return result
elif len(found) == 2:
print("Found both Python 2 and Python 3 being used in travis file -- ignoring")
else:
print("Found no Python being used in travis file")
except Exception:
print("Skipping due to exception:")
print_exception_indented()
return None
VERSION_TAG = "LGTM_PYTHON_SETUP_VERSION"
def best_version(root, default):
if VERSION_TAG in os.environ:
try:
return int(os.environ[VERSION_TAG])
except ValueError:
raise SyntaxError("Illegal value for " + VERSION_TAG)
print("Will try to guess Python version, as it was not specified in `lgtm.yml`")
version = trove_version(root) or travis_version(root) or machine_version()
if version is None:
version = default
print("Could not guess Python version, will use default: Python {}".format(version))
return version