wdev.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. '''
  2. Finds locations of Windows command-line development tools.
  3. '''
  4. import os
  5. import platform
  6. import glob
  7. import re
  8. import subprocess
  9. import sys
  10. import sysconfig
  11. import textwrap
  12. class WindowsVS:
  13. r'''
  14. Windows only. Finds locations of Visual Studio command-line tools. Assumes
  15. VS2019-style paths.
  16. Members and example values::
  17. .year: 2019
  18. .grade: Community
  19. .version: 14.28.29910
  20. .directory: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community
  21. .vcvars: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat
  22. .cl: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.28.29910\bin\Hostx64\x64\cl.exe
  23. .link: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.28.29910\bin\Hostx64\x64\link.exe
  24. .csc: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\Roslyn\csc.exe
  25. .msbuild: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe
  26. .devenv: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE\devenv.com
  27. `.csc` is C# compiler; will be None if not found.
  28. '''
  29. def __init__( self, year=None, grade=None, version=None, cpu=None, verbose=False):
  30. '''
  31. Args:
  32. year:
  33. None or, for example, `2019`. If None we use environment
  34. variable WDEV_VS_YEAR if set.
  35. grade:
  36. None or, for example, one of:
  37. * `Community`
  38. * `Professional`
  39. * `Enterprise`
  40. If None we use environment variable WDEV_VS_GRADE if set.
  41. version:
  42. None or, for example: `14.28.29910`. If None we use environment
  43. variable WDEV_VS_VERSION if set.
  44. cpu:
  45. None or a `WindowsCpu` instance.
  46. '''
  47. def default(value, name):
  48. if value is None:
  49. name2 = f'WDEV_VS_{name.upper()}'
  50. value = os.environ.get(name2)
  51. if value is not None:
  52. _log(f'Setting {name} from environment variable {name2}: {value!r}')
  53. return value
  54. try:
  55. year = default(year, 'year')
  56. grade = default(grade, 'grade')
  57. version = default(version, 'version')
  58. if not cpu:
  59. cpu = WindowsCpu()
  60. # Find `directory`.
  61. #
  62. pattern = f'C:\\Program Files*\\Microsoft Visual Studio\\{year if year else "2*"}\\{grade if grade else "*"}'
  63. directories = glob.glob( pattern)
  64. if verbose:
  65. _log( f'Matches for: {pattern=}')
  66. _log( f'{directories=}')
  67. assert directories, f'No match found for: {pattern}'
  68. directories.sort()
  69. directory = directories[-1]
  70. # Find `devenv`.
  71. #
  72. devenv = f'{directory}\\Common7\\IDE\\devenv.com'
  73. assert os.path.isfile( devenv), f'Does not exist: {devenv}'
  74. # Extract `year` and `grade` from `directory`.
  75. #
  76. # We use r'...' for regex strings because an extra level of escaping is
  77. # required for backslashes.
  78. #
  79. regex = rf'^C:\\Program Files.*\\Microsoft Visual Studio\\([^\\]+)\\([^\\]+)'
  80. m = re.match( regex, directory)
  81. assert m, f'No match: {regex=} {directory=}'
  82. year2 = m.group(1)
  83. grade2 = m.group(2)
  84. if year:
  85. assert year2 == year
  86. else:
  87. year = year2
  88. if grade:
  89. assert grade2 == grade
  90. else:
  91. grade = grade2
  92. # Find vcvars.bat.
  93. #
  94. vcvars = f'{directory}\\VC\\Auxiliary\\Build\\vcvars{cpu.bits}.bat'
  95. assert os.path.isfile( vcvars), f'No match for: {vcvars}'
  96. # Find cl.exe.
  97. #
  98. cl_pattern = f'{directory}\\VC\\Tools\\MSVC\\{version if version else "*"}\\bin\\Host{cpu.windows_name}\\{cpu.windows_name}\\cl.exe'
  99. cl_s = glob.glob( cl_pattern)
  100. assert cl_s, f'No match for: {cl_pattern}'
  101. cl_s.sort()
  102. cl = cl_s[ -1]
  103. # Extract `version` from cl.exe's path.
  104. #
  105. m = re.search( rf'\\VC\\Tools\\MSVC\\([^\\]+)\\bin\\Host{cpu.windows_name}\\{cpu.windows_name}\\cl.exe$', cl)
  106. assert m
  107. version2 = m.group(1)
  108. if version:
  109. assert version2 == version
  110. else:
  111. version = version2
  112. assert version
  113. # Find link.exe.
  114. #
  115. link_pattern = f'{directory}\\VC\\Tools\\MSVC\\{version}\\bin\\Host{cpu.windows_name}\\{cpu.windows_name}\\link.exe'
  116. link_s = glob.glob( link_pattern)
  117. assert link_s, f'No match for: {link_pattern}'
  118. link_s.sort()
  119. link = link_s[ -1]
  120. # Find csc.exe.
  121. #
  122. csc = None
  123. for dirpath, dirnames, filenames in os.walk(directory):
  124. for filename in filenames:
  125. if filename == 'csc.exe':
  126. csc = os.path.join(dirpath, filename)
  127. #_log(f'{csc=}')
  128. #break
  129. # Find MSBuild.exe.
  130. #
  131. msbuild = None
  132. for dirpath, dirnames, filenames in os.walk(directory):
  133. for filename in filenames:
  134. if filename == 'MSBuild.exe':
  135. msbuild = os.path.join(dirpath, filename)
  136. #_log(f'{csc=}')
  137. #break
  138. self.cl = cl
  139. self.devenv = devenv
  140. self.directory = directory
  141. self.grade = grade
  142. self.link = link
  143. self.csc = csc
  144. self.msbuild = msbuild
  145. self.vcvars = vcvars
  146. self.version = version
  147. self.year = year
  148. except Exception as e:
  149. raise Exception( f'Unable to find Visual Studio') from e
  150. def description_ml( self, indent=''):
  151. '''
  152. Return multiline description of `self`.
  153. '''
  154. ret = textwrap.dedent(f'''
  155. year: {self.year}
  156. grade: {self.grade}
  157. version: {self.version}
  158. directory: {self.directory}
  159. vcvars: {self.vcvars}
  160. cl: {self.cl}
  161. link: {self.link}
  162. csc: {self.csc}
  163. msbuild: {self.msbuild}
  164. devenv: {self.devenv}
  165. ''')
  166. return textwrap.indent( ret, indent)
  167. def __str__( self):
  168. return ' '.join( self._description())
  169. class WindowsCpu:
  170. '''
  171. For Windows only. Paths and names that depend on cpu.
  172. Members:
  173. .bits
  174. 32 or 64.
  175. .windows_subdir
  176. Empty string or `x64/`.
  177. .windows_name
  178. `x86` or `x64`.
  179. .windows_config
  180. `x64` or `Win32`, e.g. for use in `/Build Release|x64`.
  181. .windows_suffix
  182. `64` or empty string.
  183. '''
  184. def __init__(self, name=None):
  185. if not name:
  186. name = _cpu_name()
  187. self.name = name
  188. if name == 'x32':
  189. self.bits = 32
  190. self.windows_subdir = ''
  191. self.windows_name = 'x86'
  192. self.windows_config = 'Win32'
  193. self.windows_suffix = ''
  194. elif name == 'x64':
  195. self.bits = 64
  196. self.windows_subdir = 'x64/'
  197. self.windows_name = 'x64'
  198. self.windows_config = 'x64'
  199. self.windows_suffix = '64'
  200. else:
  201. assert 0, f'Unrecognised cpu name: {name}'
  202. def __str__(self):
  203. return self.name
  204. class WindowsPython:
  205. '''
  206. Windows only. Information about installed Python with specific word size
  207. and version. Defaults to the currently-running Python.
  208. Members:
  209. .path:
  210. Path of python binary.
  211. .version:
  212. `{major}.{minor}`, e.g. `3.9` or `3.11`. Same as `version` passed
  213. to `__init__()` if not None, otherwise the inferred version.
  214. .include:
  215. Python include path.
  216. .cpu:
  217. A `WindowsCpu` instance, same as `cpu` passed to `__init__()` if
  218. not None, otherwise the inferred cpu.
  219. .libs:
  220. Python libs directory.
  221. We parse the output from `py -0p` to find all available python
  222. installations.
  223. '''
  224. def __init__( self, cpu=None, version=None, verbose=True):
  225. '''
  226. Args:
  227. cpu:
  228. A WindowsCpu instance. If None, we use whatever we are running
  229. on.
  230. version:
  231. Two-digit Python version as a string such as `3.8`. If None we
  232. use current Python's version.
  233. verbose:
  234. If true we show diagnostics.
  235. '''
  236. if cpu is None:
  237. cpu = WindowsCpu(_cpu_name())
  238. if version is None:
  239. version = '.'.join(platform.python_version().split('.')[:2])
  240. _log(f'Looking for Python {version=} {cpu.bits=}.')
  241. if '.'.join(platform.python_version().split('.')[:2]) == version:
  242. # Current python matches, so use it directly. This avoids problems
  243. # on Github where experimental python-3.13 is not available via
  244. # `py`.
  245. _log(f'{cpu=} {version=}: using {sys.executable=}.')
  246. self.path = sys.executable
  247. self.version = version
  248. self.cpu = cpu
  249. self.include = sysconfig.get_path('include')
  250. else:
  251. command = 'py -0p'
  252. if verbose:
  253. _log(f'{cpu=} {version=}: Running: {command}')
  254. text = subprocess.check_output( command, shell=True, text=True)
  255. for line in text.split('\n'):
  256. #_log( f' {line}')
  257. if m := re.match( '^ *-V:([0-9.]+)(-32)? ([*])? +(.+)$', line):
  258. version2 = m.group(1)
  259. bits = 32 if m.group(2) else 64
  260. current = m.group(3)
  261. path = m.group(4).strip()
  262. elif m := re.match( '^ *-([0-9.]+)-((32)|(64)) +(.+)$', line):
  263. version2 = m.group(1)
  264. bits = int(m.group(2))
  265. path = m.group(5).strip()
  266. else:
  267. if verbose:
  268. _log( f'No match for {line=}')
  269. continue
  270. if verbose:
  271. _log( f'{version2=} {bits=} {path=} from {line=}.')
  272. if bits != cpu.bits or version2 != version:
  273. continue
  274. root = os.path.dirname(path)
  275. if not os.path.exists(path):
  276. # Sometimes it seems that the specified .../python.exe does not exist,
  277. # and we have to change it to .../python<version>.exe.
  278. #
  279. assert path.endswith('.exe'), f'path={path!r}'
  280. path2 = f'{path[:-4]}{version}.exe'
  281. _log( f'Python {path!r} does not exist; changed to: {path2!r}')
  282. assert os.path.exists( path2)
  283. path = path2
  284. self.path = path
  285. self.version = version
  286. self.cpu = cpu
  287. command = f'{self.path} -c "import sysconfig; print(sysconfig.get_path(\'include\'))"'
  288. _log(f'Finding Python include path by running {command=}.')
  289. self.include = subprocess.check_output(command, shell=True, text=True).strip()
  290. _log(f'Python include path is {self.include=}.')
  291. #_log( f'pipcl.py:WindowsPython():\n{self.description_ml(" ")}')
  292. break
  293. else:
  294. _log(f'Failed to find python matching cpu={cpu}.')
  295. _log(f'Output from {command!r} was:\n{text}')
  296. raise Exception( f'Failed to find python matching cpu={cpu} {version=}.')
  297. # Oddly there doesn't seem to be a
  298. # `sysconfig.get_path('libs')`, but it seems to be next
  299. # to `includes`:
  300. self.libs = os.path.abspath(f'{self.include}/../libs')
  301. _log( f'WindowsPython:\n{self.description_ml(" ")}')
  302. def description_ml(self, indent=''):
  303. ret = textwrap.dedent(f'''
  304. path: {self.path}
  305. version: {self.version}
  306. cpu: {self.cpu}
  307. include: {self.include}
  308. libs: {self.libs}
  309. ''')
  310. return textwrap.indent( ret, indent)
  311. def __repr__(self):
  312. return f'path={self.path!r} version={self.version!r} cpu={self.cpu!r} include={self.include!r} libs={self.libs!r}'
  313. # Internal helpers.
  314. #
  315. def _cpu_name():
  316. '''
  317. Returns `x32` or `x64` depending on Python build.
  318. '''
  319. #log(f'sys.maxsize={hex(sys.maxsize)}')
  320. return f'x{32 if sys.maxsize == 2**31 - 1 else 64}'
  321. def _log(text=''):
  322. '''
  323. Logs lines with prefix.
  324. '''
  325. for line in text.split('\n'):
  326. print(f'{__file__}: {line}')
  327. sys.stdout.flush()