--- /dev/null
+'''
+Finds locations of Windows command-line development tools.
+'''
+
+import os
+import platform
+import glob
+import re
+import subprocess
+import sys
+import textwrap
+
+
+class WindowsVS:
+ '''
+ Windows only. Finds locations of Visual Studio command-line tools. Assumes
+ VS2019-style paths.
+
+ Members and example values::
+
+ .year: 2019
+ .grade: Community
+ .version: 14.28.29910
+ .directory: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community
+ .vcvars: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat
+ .cl: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.28.29910\bin\Hostx64\x64\cl.exe
+ .link: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.28.29910\bin\Hostx64\x64\link.exe
+ .csc: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\Roslyn\csc.exe
+ .devenv: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE\devenv.com
+
+ `.csc` is C# compiler; will be None if not found.
+ '''
+ def __init__( self, year=None, grade=None, version=None, cpu=None, verbose=False):
+ '''
+ Args:
+ year:
+ None or, for example, `2019`.
+ grade:
+ None or, for example, one of:
+
+ * `Community`
+ * `Professional`
+ * `Enterprise`
+
+ version:
+ None or, for example: `14.28.29910`
+ cpu:
+ None or a `WindowsCpu` instance.
+ '''
+ if not cpu:
+ cpu = WindowsCpu()
+
+ # Find `directory`.
+ #
+ pattern = f'C:\\Program Files*\\Microsoft Visual Studio\\{year if year else "2*"}\\{grade if grade else "*"}'
+ directories = glob.glob( pattern)
+ if verbose:
+ _log( f'Matches for: {pattern=}')
+ _log( f'{directories=}')
+ assert directories, f'No match found for: {pattern}'
+ directories.sort()
+ directory = directories[-1]
+
+ # Find `devenv`.
+ #
+ devenv = f'{directory}\\Common7\\IDE\\devenv.com'
+ assert os.path.isfile( devenv), f'Does not exist: {devenv}'
+
+ # Extract `year` and `grade` from `directory`.
+ #
+ # We use r'...' for regex strings because an extra level of escaping is
+ # required for backslashes.
+ #
+ regex = rf'^C:\\Program Files.*\\Microsoft Visual Studio\\([^\\]+)\\([^\\]+)'
+ m = re.match( regex, directory)
+ assert m, f'No match: {regex=} {directory=}'
+ year2 = m.group(1)
+ grade2 = m.group(2)
+ if year:
+ assert year2 == year
+ else:
+ year = year2
+ if grade:
+ assert grade2 == grade
+ else:
+ grade = grade2
+
+ # Find vcvars.bat.
+ #
+ vcvars = f'{directory}\\VC\Auxiliary\\Build\\vcvars{cpu.bits}.bat'
+ assert os.path.isfile( vcvars), f'No match for: {vcvars}'
+
+ # Find cl.exe.
+ #
+ cl_pattern = f'{directory}\\VC\\Tools\\MSVC\\{version if version else "*"}\\bin\\Host{cpu.windows_name}\\{cpu.windows_name}\\cl.exe'
+ cl_s = glob.glob( cl_pattern)
+ assert cl_s, f'No match for: {cl_pattern}'
+ cl_s.sort()
+ cl = cl_s[ -1]
+
+ # Extract `version` from cl.exe's path.
+ #
+ m = re.search( rf'\\VC\\Tools\\MSVC\\([^\\]+)\\bin\\Host{cpu.windows_name}\\{cpu.windows_name}\\cl.exe$', cl)
+ assert m
+ version2 = m.group(1)
+ if version:
+ assert version2 == version
+ else:
+ version = version2
+ assert version
+
+ # Find link.exe.
+ #
+ link_pattern = f'{directory}\\VC\\Tools\\MSVC\\{version}\\bin\\Host{cpu.windows_name}\\{cpu.windows_name}\\link.exe'
+ link_s = glob.glob( link_pattern)
+ assert link_s, f'No match for: {link_pattern}'
+ link_s.sort()
+ link = link_s[ -1]
+
+ # Find csc.exe.
+ #
+ csc = None
+ for dirpath, dirnames, filenames in os.walk(directory):
+ for filename in filenames:
+ if filename == 'csc.exe':
+ csc = os.path.join(dirpath, filename)
+ _log(f'{csc=}')
+ #break
+ #if csc:
+ # break
+
+ self.cl = cl
+ self.devenv = devenv
+ self.directory = directory
+ self.grade = grade
+ self.link = link
+ self.csc = csc
+ self.vcvars = vcvars
+ self.version = version
+ self.year = year
+
+ def description_ml( self, indent=''):
+ '''
+ Return multiline description of `self`.
+ '''
+ ret = textwrap.dedent(f'''
+ year: {self.year}
+ grade: {self.grade}
+ version: {self.version}
+ directory: {self.directory}
+ vcvars: {self.vcvars}
+ cl: {self.cl}
+ link: {self.link}
+ csc: {self.csc}
+ devenv: {self.devenv}
+ ''')
+ return textwrap.indent( ret, indent)
+
+ def __str__( self):
+ return ' '.join( self._description())
+
+
+class WindowsCpu:
+ '''
+ For Windows only. Paths and names that depend on cpu.
+
+ Members:
+ .bits
+ 32 or 64.
+ .windows_subdir
+ Empty string or `x64/`.
+ .windows_name
+ `x86` or `x64`.
+ .windows_config
+ `x64` or `Win32`, e.g. for use in `/Build Release|x64`.
+ .windows_suffix
+ `64` or empty string.
+ '''
+ def __init__(self, name=None):
+ if not name:
+ name = _cpu_name()
+ self.name = name
+ if name == 'x32':
+ self.bits = 32
+ self.windows_subdir = ''
+ self.windows_name = 'x86'
+ self.windows_config = 'Win32'
+ self.windows_suffix = ''
+ elif name == 'x64':
+ self.bits = 64
+ self.windows_subdir = 'x64/'
+ self.windows_name = 'x64'
+ self.windows_config = 'x64'
+ self.windows_suffix = '64'
+ else:
+ assert 0, f'Unrecognised cpu name: {name}'
+
+ def __str__(self):
+ return self.name
+
+
+class WindowsPython:
+ '''
+ Experimental. Windows only. Information about installed Python with
+ specific word size and version. Defaults to the currently-running Python.
+
+ Members:
+
+ .path:
+ Path of python binary.
+ .version:
+ `{major}.{minor}`, e.g. `3.9` or `3.11`. Same as `version` passed
+ to `__init__()` if not None, otherwise the inferred version.
+ .root:
+ The parent directory of `.path`; allows Python headers to be found,
+ for example `{root}/include/Python.h`.
+ .cpu:
+ A `WindowsCpu` instance, same as `cpu` passed to `__init__()` if
+ not None, otherwise the inferred cpu.
+
+ We parse the output from `py -0p` to find all available python
+ installations.
+ '''
+
+ def __init__( self, cpu=None, version=None, verbose=False):
+ '''
+ Args:
+
+ cpu:
+ A WindowsCpu instance. If None, we use whatever we are running
+ on.
+ version:
+ Two-digit Python version as a string such as `3.8`. If None we
+ use current Python's version.
+ verbose:
+ If true we show diagnostics.
+ '''
+ if cpu is None:
+ cpu = WindowsCpu(_cpu_name())
+ if version is None:
+ version = '.'.join(platform.python_version().split('.')[:2])
+ command = 'py -0p'
+ if verbose:
+ _log(f'Running: {command}')
+ text = subprocess.check_output( command, shell=True, text=True)
+ for line in text.split('\n'):
+ #_log( f' {line}')
+ m = re.match( '^ *-V:([0-9.]+)(-32)? ([*])? +(.+)$', line)
+ if not m:
+ if verbose:
+ _log( f'No match for {line=}')
+ continue
+ version2 = m.group(1)
+ bits = 32 if m.group(2) else 64
+ current = m.group(3)
+ if verbose:
+ _log( f'{version2=} {bits=}')
+ if bits != cpu.bits or version2 != version:
+ continue
+ path = m.group(4).strip()
+ root = path[ :path.rfind('\\')]
+ if not os.path.exists(path):
+ # Sometimes it seems that the specified .../python.exe does not exist,
+ # and we have to change it to .../python<version>.exe.
+ #
+ assert path.endswith('.exe'), f'path={path!r}'
+ path2 = f'{path[:-4]}{version}.exe'
+ _log( f'Python {path!r} does not exist; changed to: {path2!r}')
+ assert os.path.exists( path2)
+ path = path2
+
+ self.path = path
+ self.version = version
+ self.root = root
+ self.cpu = cpu
+ #_log( f'pipcl.py:WindowsPython():\n{self.description_ml(" ")}')
+ return
+
+ raise Exception( f'Failed to find python matching cpu={cpu}. Run "py -0p" to see available pythons')
+
+ def description_ml(self, indent=''):
+ ret = textwrap.dedent(f'''
+ root: {self.root}
+ path: {self.path}
+ version: {self.version}
+ cpu: {self.cpu}
+ ''')
+ return textwrap.indent( ret, indent)
+
+
+# Internal helpers.
+#
+
+def _cpu_name():
+ '''
+ Returns `x32` or `x64` depending on Python build.
+ '''
+ #log(f'sys.maxsize={hex(sys.maxsize)}')
+ return f'x{32 if sys.maxsize == 2**31 else 64}'
+
+
+
+def _log(text=''):
+ '''
+ Logs lines with prefix.
+ '''
+ for line in text.split('\n'):
+ print(f'{__file__}: {line}')
+ sys.stdout.flush()