Mercurial > hgrepos > Python2 > PyMuPDF
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) |
