aboutsummaryrefslogtreecommitdiff
path: root/scripts/vswhere.py
blob: 82b0723f90750e55500c579a47b5591fe6a47755 (plain) (blame)
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
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
r"""
Interface to Microsoft's Visual Studio locator tool, vswhere.

If Visual Studio 15.2 or later has been installed, this will use the vswhere
binary installed with Visual Studio. Otherwise, it will download the latest
release of vswhere from https://github.com/Microsoft/vswhere the first time a
function is called.
"""

import json
import os
import shutil
import subprocess

__version__ = '1.3.0'
__author__ = 'Joel Spadin'
__license__ = 'MIT'

LATEST_RELEASE_ENDPOINT = 'https://api.github.com/repos/Microsoft/vswhere/releases/latest'
DOWNLOAD_PATH = os.path.join(os.path.dirname(__file__), 'vswhere.exe')

if 'ProgramFiles(x86)' in os.environ:
    DEFAULT_PATH = os.path.join(os.environ['ProgramFiles(x86)'], 'Microsoft Visual Studio', 'Installer', 'vswhere.exe')
else:
    DEFAULT_PATH = None

alternate_path = None
download_mirror_url = None


def execute(args):
    """
    Call vswhere with the given arguments and return an array of results.

    `args` is a list of command line arguments to pass to vswhere.

    If the argument list contains '-property', this returns an array with the
    property value for each result. Otherwise, this returns an array of
    dictionaries containing the results.
    """
    is_property = '-property' in args

    args = [get_vswhere_path(), '-utf8'] + args

    if not is_property:
        args.extend(['-format', 'json'])

    output = subprocess.check_output(args).decode('utf-8')

    if is_property:
        return output.splitlines()
    else:
        return json.loads(output)


def find(
    find=None,
    find_all=False,
    latest=False,
    legacy=False,
    path=None,
    prerelease=False,
    products=None,
    prop=None,
    requires=None,
    requires_any=False,
    sort=False,
    version=None,
):
    """
    Call vswhere and return an array of the results.

    Selection Options:
        find_all: If True, finds all instances even if they are incomplete and
            may not launch.
        prerelease: If True, also searches prereleases. By default, only
            releases are searched.
        products: a product ID or list of one or more product IDs to find.
            Defaults to Community, Professional, and Enterprise if not specified.
            Specify '*' by itself to search all product instances installed.
            See https://aka.ms/vs/workloads for a list of product IDs.
        requires: a workload component ID or list of one or more IDs required
            when finding instances. All specified IDs must be installed unless
            `requires_any` is True. See https://aka.ms/vs/workloads for a list
            of workload and component IDs.
        requires_any: If True, find instances with any one or more workload or
            component IDs passed to `requires`.
        version: A version range for instances to find. Example: '[15.0,16.0)'
            will find versions 15.*.
        latest: If True, returns only the newest version and last installed.
        legacy: If True, also searches Visual Studio 2015 and older products.
            Information is limited. This option cannot be used with either
            `products` or `requires`.
        path: Gets the instance for the given file path. Not compatible with any
            other selection option.

    Output Options:
        sort: If True, sorts the instances from newest version and last installed
            to oldest. When used with `find`, first instances are sorted, then
            files are sorted lexigraphically.
        prop: The name of a property to return instead of the full installation
            details. Use delimiters '.', '/', or '_' to separate object and
            property names. Example: 'properties.nickname' will return the
            'nickname' property under 'properties'.
        find: Returns the file paths matching this glob pattern under the
            installation path. The following patterns are supported:
            ?  Matches any one character except "\\"
            *  Matches zero or more characters except "\\"
            ** Searches the current directory and subdirectories for the
               remaining search pattern.
    """
    args = []

    if find:
        args.append('-find')
        args.append(find)

    if find_all:
        args.append('-all')

    if latest:
        args.append('-latest')

    if legacy:
        args.append('-legacy')

    if path:
        args.append('-path')
        args.append(path)

    if prerelease:
        args.append('-prerelease')

    if products:
        args.append('-products')
        _extend_or_append(args, products)

    if prop:
        args.append('-property')
        args.append(prop)

    if requires:
        args.append('-requires')
        _extend_or_append(args, requires)

    if requires_any:
        args.append('-requiresAny')

    if sort:
        args.append('-sort')

    if version:
        args.append('-version')
        args.append(version)

    return execute(args)


def find_first(**kwargs):
    """
    Call vswhere and returns only the first result, or None if there are no results.

    See find() for keyword arguments.
    """
    return next(iter(find(**kwargs)), None)


def get_latest(legacy=None, **kwargs):
    """
    Get the information for the latest installed version of Visual Studio.

    Also supports the same selection options as find(), for example to select
    different products. If the `legacy` argument is not set, it defaults to
    `True` unless either `products` or `requires` arguments are set.
    """
    legacy = _get_legacy_arg(legacy, **kwargs)
    return find_first(latest=True, legacy=legacy, **kwargs)


def get_latest_path(legacy=None, **kwargs):
    """
    Get the file path to the latest installed version of Visual Studio.

    Returns None if no installations could be found.

    Also supports the same selection options as find(), for example to select
    different products. If the `legacy` argument is not set, it defaults to
    `True` unless either `products` or `requires` arguments are set.
    """
    legacy = _get_legacy_arg(legacy, **kwargs)
    return find_first(latest=True, legacy=legacy, prop='installationPath', **kwargs)


def get_latest_version(legacy=None, **kwargs):
    """
    Get the version string of the latest installed version of Visual Studio.

    For Visual Studio 2017 and newer, this is the full version number, for
    example: '15.8.28010.2003'.

    For Visual Studio 2015 and older, this only contains the major version, with
    the minor version set to 0, for example: '14.0'.

    Returns None if no installations could be found.

    Also supports the same selection options as find(), for example to select
    different products. If the `legacy` argument is not set, it defaults to
    `True` unless either `products` or `requires` arguments are set.
    """
    legacy = _get_legacy_arg(legacy, **kwargs)
    return find_first(latest=True, legacy=legacy, prop='installationVersion', **kwargs)

def get_latest_major_version(**kwargs):
    """
    Get the major version of the latest installed version of Visual Studio as an int.

    Returns 0 if no installations could be found.

    Also supports the same selection options as find(), for example to select
    different products. If the `legacy` argument is not set, it defaults to
    `True` unless either `products` or `requires` arguments are set.
    """
    return int(next(iter(get_latest_version(**kwargs).split('.')), '0'))


def get_vswhere_path():
    """
    Get the path to vshwere.exe.

    If vswhere is not already installed as part of Visual Studio, and no
    alternate path is given using `set_vswhere_path()`, the latest release will
    be downloaded and stored alongside this script.
    """
    if alternate_path and os.path.exists(alternate_path):
        return alternate_path

    if DEFAULT_PATH and os.path.exists(DEFAULT_PATH):
        return DEFAULT_PATH

    if os.path.exists(DOWNLOAD_PATH):
        return DOWNLOAD_PATH

    _download_vswhere()
    return DOWNLOAD_PATH


def set_vswhere_path(path):
    """
    Set the path to vswhere.exe.

    If this is set, it overrides any version installed as part of Visual Studio.
    """
    global alternate_path
    alternate_path = path


def set_download_mirror(url):
    """
    Set a URL from which vswhere.exe should be downloaded if it is not already
    installed as part of Visual Studio and no alternate path is given using
    `set_vswhere_path()`.
    """
    global download_mirror_url
    download_mirror_url = url


def _extend_or_append(lst, value):
    if isinstance(value, str):
        lst.append(value)
    else:
        lst.extend(value)


def _get_legacy_arg(legacy, **kwargs):
    if legacy is None:
        return 'products' not in kwargs and 'requires' not in kwargs
    else:
        return legacy


def _download_vswhere():
    """
    Download vswhere to DOWNLOAD_PATH.
    """
    print('downloading from', _get_latest_release_url())
    try:
        from urllib.request import urlopen
        with urlopen(_get_latest_release_url()) as response, open(DOWNLOAD_PATH, 'wb') as outfile:
            shutil.copyfileobj(response, outfile)
    except ImportError:
        # Python 2
        import urllib
        urllib.urlretrieve(_get_latest_release_url(), DOWNLOAD_PATH)


def _get_latest_release_url():
    """
    The the URL of the latest release of vswhere.
    """
    if download_mirror_url:
        return download_mirror_url

    try:
        from urllib.request import urlopen
        with urlopen(LATEST_RELEASE_ENDPOINT) as response:
            release = json.loads(response.read(), encoding=response.headers.get_content_charset() or 'utf-8')
    except ImportError:
        # Python 2
        import urllib2
        response = urllib2.urlopen(LATEST_RELEASE_ENDPOINT)
        release = json.loads(response.read(), encoding=response.headers.getparam('charset') or 'utf-8')

    for asset in release['assets']:
        if asset['name'] == 'vswhere.exe':
            return asset['browser_download_url']

    raise Exception('Could not locate the latest release of vswhere.')