comparison mupdf-source/scripts/wdev.py @ 2:b50eed0cc0ef upstream

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