comparison wdev.py @ 1:1d09e1dec1d9 upstream

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