Mercurial > hgrepos > Python2 > PyMuPDF
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wdev.py Mon Sep 15 11:37:51 2025 +0200 @@ -0,0 +1,424 @@ +''' +Finds locations of Windows command-line development tools. +''' + +import os +import platform +import glob +import re +import subprocess +import sys +import sysconfig +import textwrap + +import pipcl + + +class WindowsVS: + r''' + 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 + .msbuild: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.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, + directory=None, + verbose=False, + ): + ''' + Args: + year: + None or, for example, `2019`. If None we use environment + variable WDEV_VS_YEAR if set. + grade: + None or, for example, one of: + + * `Community` + * `Professional` + * `Enterprise` + + If None we use environment variable WDEV_VS_GRADE if set. + version: + None or, for example: `14.28.29910`. If None we use environment + variable WDEV_VS_VERSION if set. + cpu: + None or a `WindowsCpu` instance. + directory: + Ignore year, grade, version and cpu and use this directory + directly. + verbose: + . + + ''' + if year is not None: + year = str(year) # Allow specification as a number. + def default(value, name): + if value is None: + name2 = f'WDEV_VS_{name.upper()}' + value = os.environ.get(name2) + if value is not None: + _log(f'Setting {name} from environment variable {name2}: {value!r}') + return value + try: + year = default(year, 'year') + grade = default(grade, 'grade') + version = default(version, 'version') + + if not cpu: + cpu = WindowsCpu() + + if not directory: + # Find `directory`. + # + pattern = _vs_pattern(year, grade) + 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 + + # Find MSBuild.exe. + # + msbuild = None + for dirpath, dirnames, filenames in os.walk(directory): + for filename in filenames: + if filename == 'MSBuild.exe': + msbuild = os.path.join(dirpath, filename) + #_log(f'{csc=}') + #break + + self.cl = cl + self.devenv = devenv + self.directory = directory + self.grade = grade + self.link = link + self.csc = csc + self.msbuild = msbuild + self.vcvars = vcvars + self.version = version + self.year = year + self.cpu = cpu + except Exception as e: + raise Exception( f'Unable to find Visual Studio {year=} {grade=} {version=} {cpu=} {directory=}') from e + + 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} + msbuild: {self.msbuild} + devenv: {self.devenv} + cpu: {self.cpu} + ''') + return textwrap.indent( ret, indent) + + def __repr__( self): + items = list() + for name in ( + 'year', + 'grade', + 'version', + 'directory', + 'vcvars', + 'cl', + 'link', + 'csc', + 'msbuild', + 'devenv', + 'cpu', + ): + items.append(f'{name}={getattr(self, name)!r}') + return ' '.join(items) + + +def _vs_pattern(year=None, grade=None): + return f'C:\\Program Files*\\Microsoft Visual Studio\\{year if year else "2*"}\\{grade if grade else "*"}' + + +def windows_vs_multiple(year=None, grade=None, verbose=0): + ''' + Returns list of WindowsVS instances. + ''' + ret = list() + directories = glob.glob(_vs_pattern(year, grade)) + for directory in directories: + vs = WindowsVS(directory=directory) + if verbose: + _log(vs.description_ml()) + ret.append(vs) + return ret + + +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 __repr__(self): + return self.name + + +class WindowsPython: + ''' + 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. + .include: + Python include path. + .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=True): + ''' + 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]) + _log(f'Looking for Python {version=} {cpu.bits=}.') + + if '.'.join(platform.python_version().split('.')[:2]) == version: + # Current python matches, so use it directly. This avoids problems + # on Github where experimental python-3.13 was not available via + # `py`, and is kept here in case a similar problems happens with + # future Python versions. + _log(f'{cpu=} {version=}: using {sys.executable=}.') + self.path = sys.executable + self.version = version + self.cpu = cpu + self.include = sysconfig.get_path('include') + + else: + command = 'py -0p' + if verbose: + _log(f'{cpu=} {version=}: Running: {command}') + text = subprocess.check_output( command, shell=True, text=True) + for line in text.split('\n'): + #_log( f' {line}') + if m := re.match( '^ *-V:([0-9.]+)(-32)? ([*])? +(.+)$', line): + version2 = m.group(1) + bits = 32 if m.group(2) else 64 + current = m.group(3) + path = m.group(4).strip() + elif m := re.match( '^ *-([0-9.]+)-((32)|(64)) +(.+)$', line): + version2 = m.group(1) + bits = int(m.group(2)) + path = m.group(5).strip() + else: + if verbose: + _log( f'No match for {line=}') + continue + if verbose: + _log( f'{version2=} {bits=} {path=} from {line=}.') + if bits != cpu.bits or version2 != version: + continue + root = os.path.dirname(path) + 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.cpu = cpu + command = f'{self.path} -c "import sysconfig; print(sysconfig.get_path(\'include\'))"' + _log(f'Finding Python include path by running {command=}.') + self.include = subprocess.check_output(command, shell=True, text=True).strip() + _log(f'Python include path is {self.include=}.') + #_log( f'pipcl.py:WindowsPython():\n{self.description_ml(" ")}') + break + else: + _log(f'Failed to find python matching cpu={cpu}.') + _log(f'Output from {command!r} was:\n{text}') + raise Exception( f'Failed to find python matching cpu={cpu} {version=}.') + + # Oddly there doesn't seem to be a + # `sysconfig.get_path('libs')`, but it seems to be next + # to `includes`: + self.libs = os.path.abspath(f'{self.include}/../libs') + + _log( f'WindowsPython:\n{self.description_ml(" ")}') + + def description_ml(self, indent=''): + ret = textwrap.dedent(f''' + path: {self.path} + version: {self.version} + cpu: {self.cpu} + include: {self.include} + libs: {self.libs} + ''') + return textwrap.indent( ret, indent) + + def __repr__(self): + return f'path={self.path!r} version={self.version!r} cpu={self.cpu!r} include={self.include!r} libs={self.libs!r}' + + +# 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 - 1 else 64}' + + + +def _log(text='', caller=1): + ''' + Logs lines with prefix. + ''' + pipcl.log1(text, caller+1)
