Mercurial > hgrepos > Python2 > PyMuPDF
view wdev.py @ 22:d77477b4e151
Let _int_rc() also handle (i.e. ignore) a local version suffix
| author | Franz Glasner <fzglas.hg@dom66.de> |
|---|---|
| date | Fri, 19 Sep 2025 12:05:57 +0200 |
| parents | 1d09e1dec1d9 |
| children |
line wrap: on
line source
''' 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)
