PKV09f,,&meson_python-0.13.0.dist-info/METADATAMetadata-Version: 2.1 Name: meson-python Version: 0.13.0 Summary: Meson Python build backend (PEP 517) Keywords: meson build backend pep517 package Home-page: https://github.com/mesonbuild/meson-python Author-Email: Filipe Laíns License: Copyright © 2022 the meson-python contributors Copyright © 2021 Quansight Labs and Filipe Laíns Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Classifier: Development Status :: 4 - Beta Classifier: Programming Language :: Python Project-URL: Homepage, https://github.com/mesonbuild/meson-python Project-URL: Repository, https://github.com/mesonbuild/meson-python Project-URL: Documentation, https://meson-python.readthedocs.io/ Project-URL: Changelog, https://meson-python.readthedocs.io/en/latest/changelog.html Requires-Python: >=3.7 Requires-Dist: colorama; os_name == "nt" Requires-Dist: meson>=0.63.3 Requires-Dist: pyproject-metadata>=0.7.1 Requires-Dist: tomli>=1.0.0; python_version < "3.11" Requires-Dist: setuptools>=60.0; python_version >= "3.12" Requires-Dist: build; extra == "test" Requires-Dist: pytest>=6.0; extra == "test" Requires-Dist: pytest-cov[toml]; extra == "test" Requires-Dist: pytest-mock; extra == "test" Requires-Dist: auditwheel; extra == "test" Requires-Dist: cython>=0.29.34; extra == "test" Requires-Dist: wheel; extra == "test" Requires-Dist: typing-extensions>=3.7.4; python_version < "3.10" and extra == "test" Requires-Dist: furo>=2021.08.31; extra == "docs" Requires-Dist: sphinx~=4.0; extra == "docs" Requires-Dist: sphinx-autodoc-typehints>=1.10; extra == "docs" Requires-Dist: sphinx-contributors>=0.2.2; extra == "docs" Requires-Dist: sphinx-copybutton>=0.5.0; extra == "docs" Requires-Dist: sphinx-design>=0.1.0; extra == "docs" Requires-Dist: sphinx-autobuild; extra == "docs" Requires-Dist: sphinxcontrib-spelling>=7.0.0; extra == "docs" Requires-Dist: sphinxext-opengraph>=0.7.0; extra == "docs" Provides-Extra: test Provides-Extra: docs Description-Content-Type: text/x-rst .. SPDX-FileCopyrightText: 2021 The meson-python developers .. SPDX-License-Identifier: MIT meson-python ============ ``meson-python`` is a Python build backend built on top of the Meson__ build system. It enables to use Meson for the configuration and build steps of Python packages. Meson is an open source build system meant to be both extremely fast, and, even more importantly, as user friendly as possible. ``meson-python`` is best suited for building Python packages containing extension modules implemented in languages such as C, C++, Cython, Fortran, Pythran, or Rust. Consult the documentation__ for more details. Questions regarding the use of ``meson-python`` can be directed to the discussions__ space on GitHub. Bug reports and feature requests can be filed as GitHub issues__. __ https://mesonbuild.com/ __ https://meson-python.readthedocs.io/en/latest/ __ https://github.com/mesonbuild/meson-python/discussions/ __ https://github.com/mesonbuild/meson-python/issues/ PKVx>KK#meson_python-0.13.0.dist-info/WHEELWheel-Version: 1.0 Generator: meson Root-Is-Purelib: true Tag: py3-none-anyPKV%meson_python-0.13.0.dist-info/MIT.txtCopyright © 2022 the meson-python contributors Copyright © 2021 Quansight Labs and Filipe Laíns Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PKV7'mesonpy/__init__.py# SPDX-FileCopyrightText: 2021 Filipe Laíns # SPDX-FileCopyrightText: 2021 Quansight, LLC # SPDX-FileCopyrightText: 2022 The meson-python developers # # SPDX-License-Identifier: MIT """Meson Python build backend Implements PEP 517 hooks. """ from __future__ import annotations import argparse import collections import contextlib import difflib import functools import importlib.machinery import io import itertools import json import os import pathlib import platform import re import shutil import subprocess import sys import sysconfig import tarfile import tempfile import textwrap import typing import warnings from typing import Dict if sys.version_info < (3, 11): import tomli as tomllib else: import tomllib import packaging.version import pyproject_metadata import mesonpy._compat import mesonpy._dylib import mesonpy._elf import mesonpy._tags import mesonpy._util import mesonpy._wheelfile from mesonpy._compat import Collection, Mapping, cached_property, read_binary if typing.TYPE_CHECKING: # pragma: no cover from typing import ( Any, Callable, ClassVar, DefaultDict, List, Literal, Optional, Sequence, TextIO, Tuple, Type, TypeVar, Union ) from mesonpy._compat import Iterator, ParamSpec, Path P = ParamSpec('P') T = TypeVar('T') __version__ = '0.13.0' # XXX: Once Python 3.8 is our minimum supported version, get rid of # meson_args_keys and use typing.get_args(MesonArgsKeys) instead. # Keep both definitions in sync! _MESON_ARGS_KEYS = ['dist', 'setup', 'compile', 'install'] if typing.TYPE_CHECKING: MesonArgsKeys = Literal['dist', 'setup', 'compile', 'install'] MesonArgs = Mapping[MesonArgsKeys, List[str]] else: MesonArgs = dict _COLORS = { 'red': '\33[31m', 'cyan': '\33[36m', 'yellow': '\33[93m', 'light_blue': '\33[94m', 'bold': '\33[1m', 'dim': '\33[2m', 'underline': '\33[4m', 'reset': '\33[0m', } _NO_COLORS = {color: '' for color in _COLORS} _NINJA_REQUIRED_VERSION = '1.8.2' _MESON_REQUIRED_VERSION = '0.63.3' # keep in sync with the version requirement in pyproject.toml class _depstr: """Namespace that holds the requirement strings for dependencies we *might* need at runtime. Having them in one place makes it easier to update. """ patchelf = 'patchelf >= 0.11.0' ninja = f'ninja >= {_NINJA_REQUIRED_VERSION}' def _init_colors() -> Dict[str, str]: """Detect if we should be using colors in the output. We will enable colors if running in a TTY, and no environment variable overrides it. Setting the NO_COLOR (https://no-color.org/) environment variable force-disables colors, and FORCE_COLOR forces color to be used, which is useful for thing like Github actions. """ if 'NO_COLOR' in os.environ: if 'FORCE_COLOR' in os.environ: warnings.warn( 'Both NO_COLOR and FORCE_COLOR environment variables are set, disabling color', stacklevel=1, ) return _NO_COLORS elif 'FORCE_COLOR' in os.environ or sys.stdout.isatty(): return _COLORS return _NO_COLORS _STYLES = _init_colors() # holds the color values, should be _COLORS or _NO_COLORS _SUFFIXES = importlib.machinery.all_suffixes() _EXTENSION_SUFFIXES = importlib.machinery.EXTENSION_SUFFIXES _EXTENSION_SUFFIX_REGEX = re.compile(r'^\.(?:(?P[^.]+)\.)?(?:so|pyd|dll)$') assert all(re.match(_EXTENSION_SUFFIX_REGEX, x) for x in _EXTENSION_SUFFIXES) # Map Meson installation path placeholders to wheel installation paths. # See https://docs.python.org/3/library/sysconfig.html#installation-paths _INSTALLATION_PATH_MAP = { '{bindir}': 'scripts', '{py_purelib}': 'purelib', '{py_platlib}': 'platlib', '{moduledir_shared}': 'platlib', '{includedir}': 'headers', '{datadir}': 'data', # custom location '{libdir}': 'mesonpy-libs', '{libdir_shared}': 'mesonpy-libs', } def _map_to_wheel(sources: Dict[str, Dict[str, Any]]) -> DefaultDict[str, List[Tuple[pathlib.Path, str]]]: """Map files to the wheel, organized by wheel installation directrory.""" wheel_files: DefaultDict[str, List[Tuple[pathlib.Path, str]]] = collections.defaultdict(list) packages: Dict[str, str] = {} for group in sources.values(): for src, target in group.items(): destination = pathlib.Path(target['destination']) anchor = destination.parts[0] path = _INSTALLATION_PATH_MAP.get(anchor) if path is None: raise BuildError(f'Could not map installation path to an equivalent wheel directory: {str(destination)!r}') if path == 'purelib' or path == 'platlib': package = destination.parts[1] other = packages.setdefault(package, path) if other != path: this = os.fspath(pathlib.Path(path, *destination.parts[1:])) that = os.fspath(other / next(d for d, s in wheel_files[other] if d.parts[0] == destination.parts[1])) raise BuildError( f'The {package} package is split between {path} and {other}: ' f'{this!r} and {that!r}, a "pure: false" argument may be missing in meson.build') wheel_files[path].append((pathlib.Path(*destination.parts[1:]), src)) return wheel_files def _showwarning( message: Union[Warning, str], category: Type[Warning], filename: str, lineno: int, file: Optional[TextIO] = None, line: Optional[str] = None, ) -> None: # pragma: no cover """Callable to override the default warning handler, to have colored output.""" print('{yellow}meson-python: warning:{reset} {}'.format(message, **_STYLES)) def _setup_cli() -> None: """Setup CLI stuff (eg. handlers, hooks, etc.). Should only be called when actually we are in control of the CLI, not on a normal import. """ warnings.showwarning = _showwarning try: # pragma: no cover import colorama except ModuleNotFoundError: # pragma: no cover pass else: # pragma: no cover colorama.init() # fix colors on windows class Error(RuntimeError): def __str__(self) -> str: return str(self.args[0]) class ConfigError(Error): """Error in the backend configuration.""" class BuildError(Error): """Error when building the wheel.""" class MesonBuilderError(Error): """Error when building the Meson package.""" class _WheelBuilder(): """Helper class to build wheels from projects.""" def __init__( self, project: Project, metadata: Optional[pyproject_metadata.StandardMetadata], source_dir: pathlib.Path, build_dir: pathlib.Path, sources: Dict[str, Dict[str, Any]], ) -> None: self._project = project self._metadata = metadata self._source_dir = source_dir self._build_dir = build_dir self._sources = sources self._libs_build_dir = self._build_dir / 'mesonpy-wheel-libs' @cached_property def _wheel_files(self) -> DefaultDict[str, List[Tuple[pathlib.Path, str]]]: return _map_to_wheel(self._sources) @property def _has_internal_libs(self) -> bool: return bool(self._wheel_files['mesonpy-libs']) @property def _has_extension_modules(self) -> bool: # Assume that all code installed in {platlib} is Python ABI dependent. return bool(self._wheel_files['platlib']) @property def normalized_name(self) -> str: return self._project.name.replace('-', '_') @property def basename(self) -> str: """Normalized wheel name and version (eg. meson_python-1.0.0).""" return '{distribution}-{version}'.format( distribution=self.normalized_name, version=self._project.version, ) @property def tag(self) -> mesonpy._tags.Tag: """Wheel tags.""" if self.is_pure: return mesonpy._tags.Tag('py3', 'none', 'any') if not self._has_extension_modules: # The wheel has platform dependent code (is not pure) but # does not contain any extension module (does not # distribute any file in {platlib}) thus use generic # implementation and ABI tags. return mesonpy._tags.Tag('py3', 'none', None) return mesonpy._tags.Tag(None, self._stable_abi, None) @property def name(self) -> str: """Wheel name, this includes the basename and tag.""" return '{basename}-{tag}'.format( basename=self.basename, tag=self.tag, ) @property def distinfo_dir(self) -> str: return f'{self.basename}.dist-info' @property def data_dir(self) -> str: return f'{self.basename}.data' @cached_property def is_pure(self) -> bool: """Is the wheel "pure" (architecture independent)?""" # XXX: I imagine some users might want to force the package to be # non-pure, but I think it's better that we evaluate use-cases as they # arise and make sure allowing the user to override this is indeed the # best option for the use-case. if self._wheel_files['platlib']: return False for _, file in self._wheel_files['scripts']: if self._is_native(file): return False return True @property def wheel(self) -> bytes: """Return WHEEL file for dist-info.""" return textwrap.dedent(''' Wheel-Version: 1.0 Generator: meson Root-Is-Purelib: {is_purelib} Tag: {tag} ''').strip().format( is_purelib='true' if self.is_pure else 'false', tag=self.tag, ).encode() @property def entrypoints_txt(self) -> bytes: """dist-info entry_points.txt.""" if not self._metadata: return b'' data = self._metadata.entrypoints.copy() data.update({ 'console_scripts': self._metadata.scripts, 'gui_scripts': self._metadata.gui_scripts, }) text = '' for entrypoint in data: if data[entrypoint]: text += f'[{entrypoint}]\n' for name, target in data[entrypoint].items(): text += f'{name} = {target}\n' text += '\n' return text.encode() @cached_property def _stable_abi(self) -> Optional[str]: """Determine stabe ABI compatibility. Examine all files installed in {platlib} that look like extension modules (extension .pyd on Windows, .dll on Cygwin, and .so on other platforms) and, if they all share the same PEP 3149 filename stable ABI tag, return it. Other files are ignored. """ soext = sorted(_EXTENSION_SUFFIXES, key=len)[0] abis = [] for path, _ in self._wheel_files['platlib']: # NOTE: When searching for shared objects files, we assume the host # and build machines have the same soext, even though that we might # be cross compiling. if path.suffix == soext: match = re.match(r'^[^.]+(.*)$', path.name) assert match is not None suffix = match.group(1) match = _EXTENSION_SUFFIX_REGEX.match(suffix) if match: abis.append(match.group('abi')) stable = [x for x in abis if x and re.match(r'abi\d+', x)] if len(stable) > 0 and len(stable) == len(abis) and all(x == stable[0] for x in stable[1:]): return stable[0] return None @property def top_level_modules(self) -> Collection[str]: modules = set() for type_ in self._wheel_files: for path, _ in self._wheel_files[type_]: name, dot, ext = path.parts[0].partition('.') if dot: # module suffix = dot + ext if suffix in _SUFFIXES: modules.add(name) else: # package modules.add(name) return modules def _is_native(self, file: Union[str, pathlib.Path]) -> bool: """Check if file is a native file.""" self._project.build() # the project needs to be built for this :/ with open(file, 'rb') as f: if platform.system() == 'Linux': return f.read(4) == b'\x7fELF' # ELF elif platform.system() == 'Darwin': return f.read(4) in ( b'\xfe\xed\xfa\xce', # 32-bit b'\xfe\xed\xfa\xcf', # 64-bit b'\xcf\xfa\xed\xfe', # arm64 b'\xca\xfe\xba\xbe', # universal / fat (same as java class so beware!) ) elif platform.system() == 'Windows': return f.read(2) == b'MZ' # For unknown platforms, check for file extensions. _, ext = os.path.splitext(file) if ext in ('.so', '.a', '.out', '.exe', '.dll', '.dylib', '.pyd'): return True return False def _install_path( # noqa: C901 self, wheel_file: mesonpy._wheelfile.WheelFile, counter: mesonpy._util.CLICounter, origin: Path, destination: pathlib.Path, ) -> None: """"Install" file or directory into the wheel and do the necessary processing before doing so. Some files might need to be fixed up to set the RPATH to the internal library directory on Linux wheels for eg. """ location = destination.as_posix() counter.update(location) # fix file if os.path.isdir(origin): for root, dirnames, filenames in os.walk(str(origin)): # Sort the directory names so that `os.walk` will walk them in a # defined order on the next iteration. dirnames.sort() for name in sorted(filenames): path = os.path.normpath(os.path.join(root, name)) if os.path.isfile(path): arcname = os.path.join(destination, os.path.relpath(path, origin).replace(os.path.sep, '/')) wheel_file.write(path, arcname) else: if self._has_internal_libs: if platform.system() == 'Linux' or platform.system() == 'Darwin': # add .mesonpy.libs to the RPATH of ELF files if self._is_native(os.fspath(origin)): # copy ELF to our working directory to avoid Meson having to regenerate the file new_origin = self._libs_build_dir / pathlib.Path(origin).relative_to(self._build_dir) os.makedirs(new_origin.parent, exist_ok=True) shutil.copy2(origin, new_origin) origin = new_origin # add our in-wheel libs folder to the RPATH if platform.system() == 'Linux': elf = mesonpy._elf.ELF(origin) libdir_path = \ f'$ORIGIN/{os.path.relpath(f".{self._project.name}.mesonpy.libs", destination.parent)}' if libdir_path not in elf.rpath: elf.rpath = [*elf.rpath, libdir_path] elif platform.system() == 'Darwin': dylib = mesonpy._dylib.Dylib(origin) libdir_path = \ f'@loader_path/{os.path.relpath(f".{self._project.name}.mesonpy.libs", destination.parent)}' if libdir_path not in dylib.rpath: dylib.rpath = [*dylib.rpath, libdir_path] else: # Internal libraries are currently unsupported on this platform raise NotImplementedError("Bundling libraries in wheel is not supported on platform '{}'" .format(platform.system())) try: wheel_file.write(origin, location) except FileNotFoundError: # work around for Meson bug, see https://github.com/mesonbuild/meson/pull/11655 if not os.fspath(origin).endswith('.pdb'): raise def _wheel_write_metadata(self, whl: mesonpy._wheelfile.WheelFile) -> None: # add metadata whl.writestr(f'{self.distinfo_dir}/METADATA', self._project.metadata) whl.writestr(f'{self.distinfo_dir}/WHEEL', self.wheel) if self.entrypoints_txt: whl.writestr(f'{self.distinfo_dir}/entry_points.txt', self.entrypoints_txt) # add license (see https://github.com/mesonbuild/meson-python/issues/88) if self._project.license_file: whl.write( self._source_dir / self._project.license_file, f'{self.distinfo_dir}/{os.path.basename(self._project.license_file)}', ) def build(self, directory: Path) -> pathlib.Path: # ensure project is built self._project.build() # install the project self._project.install() wheel_file = pathlib.Path(directory, f'{self.name}.whl') with mesonpy._wheelfile.WheelFile(wheel_file, 'w') as whl: self._wheel_write_metadata(whl) print('{light_blue}{bold}Copying files to wheel...{reset}'.format(**_STYLES)) with mesonpy._util.cli_counter( len(list(itertools.chain.from_iterable(self._wheel_files.values()))), ) as counter: # install root scheme files root_scheme = 'purelib' if self.is_pure else 'platlib' for destination, origin in self._wheel_files[root_scheme]: self._install_path(whl, counter, origin, destination) # install bundled libraries for destination, origin in self._wheel_files['mesonpy-libs']: destination = pathlib.Path(f'.{self._project.name}.mesonpy.libs', destination) self._install_path(whl, counter, origin, destination) # install the other schemes for scheme in self._wheel_files.keys(): if scheme in (root_scheme, 'mesonpy-libs'): continue for destination, origin in self._wheel_files[scheme]: destination = pathlib.Path(self.data_dir, scheme, destination) self._install_path(whl, counter, origin, destination) return wheel_file def build_editable(self, directory: Path, verbose: bool = False) -> pathlib.Path: # ensure project is built self._project.build() wheel_file = pathlib.Path(directory, f'{self.name}.whl') with mesonpy._wheelfile.WheelFile(wheel_file, 'w') as whl: self._wheel_write_metadata(whl) whl.writestr( f'{self.distinfo_dir}/direct_url.json', self._source_dir.as_uri().encode('utf-8'), ) # install loader module loader_module_name = f'_{self.normalized_name.replace(".", "_")}_editable_loader' whl.writestr( f'{loader_module_name}.py', read_binary('mesonpy', '_editable.py') + textwrap.dedent(f''' install( {self.top_level_modules!r}, {os.fspath(self._build_dir)!r}, {self._project._build_command!r}, {verbose!r}, )''').encode('utf-8')) # install .pth file whl.writestr( f'{self.normalized_name}-editable.pth', f'import {loader_module_name}'.encode('utf-8')) return wheel_file def _validate_pyproject_config(pyproject: Dict[str, Any]) -> Dict[str, Any]: def _table(scheme: Dict[str, Callable[[Any, str], Any]]) -> Callable[[Any, str], Dict[str, Any]]: def func(value: Any, name: str) -> Dict[str, Any]: if not isinstance(value, dict): raise ConfigError(f'Configuration entry "{name}" must be a table') table = {} for key, val in value.items(): check = scheme.get(key) if check is None: raise ConfigError(f'Unknown configuration entry "{name}.{key}"') table[key] = check(val, f'{name}.{key}') return table return func def _strings(value: Any, name: str) -> List[str]: if not isinstance(value, list) or not all(isinstance(x, str) for x in value): raise ConfigError(f'Configuration entry "{name}" must be a list of strings') return value scheme = _table({ 'args': _table({ name: _strings for name in _MESON_ARGS_KEYS }) }) table = pyproject.get('tool', {}).get('meson-python', {}) return scheme(table, 'tool.meson-python') def _validate_config_settings(config_settings: Dict[str, Any]) -> Dict[str, Any]: """Validate options received from build frontend.""" def _string(value: Any, name: str) -> str: if not isinstance(value, str): raise ConfigError(f'Only one value for "{name}" can be specified') return value def _bool(value: Any, name: str) -> bool: return True def _string_or_strings(value: Any, name: str) -> List[str]: return list([value,] if isinstance(value, str) else value) options = { 'builddir': _string, 'editable-verbose': _bool, 'dist-args': _string_or_strings, 'setup-args': _string_or_strings, 'compile-args': _string_or_strings, 'install-args': _string_or_strings, } assert all(f'{name}-args' in options for name in _MESON_ARGS_KEYS) config = {} for key, value in config_settings.items(): parser = options.get(key) if parser is None: matches = difflib.get_close_matches(key, options.keys(), n=2) if matches: alternatives = ' or '.join(f'"{match}"' for match in matches) raise ConfigError(f'Unknown option "{key}". Did you mean {alternatives}?') else: raise ConfigError(f'Unknown option "{key}"') config[key] = parser(value, key) return config class Project(): """Meson project wrapper to generate Python artifacts.""" _ALLOWED_DYNAMIC_FIELDS: ClassVar[List[str]] = [ 'version', ] _metadata: pyproject_metadata.StandardMetadata def __init__( self, source_dir: Path, working_dir: Path, build_dir: Optional[Path] = None, meson_args: Optional[MesonArgs] = None, editable_verbose: bool = False, ) -> None: self._source_dir = pathlib.Path(source_dir).absolute() self._working_dir = pathlib.Path(working_dir).absolute() self._build_dir = pathlib.Path(build_dir).absolute() if build_dir else (self._working_dir / 'build') self._editable_verbose = editable_verbose self._install_dir = self._working_dir / 'install' self._meson_native_file = self._build_dir / 'meson-python-native-file.ini' self._meson_cross_file = self._build_dir / 'meson-python-cross-file.ini' self._meson_args: MesonArgs = collections.defaultdict(list) self._env = os.environ.copy() _check_meson_version() self._ninja = _env_ninja_command() if self._ninja is None: raise ConfigError(f'Could not find ninja version {_NINJA_REQUIRED_VERSION} or newer.') self._env.setdefault('NINJA', self._ninja) # setuptools-like ARCHFLAGS environment variable support if sysconfig.get_platform().startswith('macosx-'): archflags = self._env.get('ARCHFLAGS', '').strip() if archflags: arch, *other = filter(None, (x.strip() for x in archflags.split('-arch'))) if other: raise ConfigError(f'Multi-architecture builds are not supported but $ARCHFLAGS={archflags!r}') macver, _, nativearch = platform.mac_ver() if arch != nativearch: x = self._env.setdefault('_PYTHON_HOST_PLATFORM', f'macosx-{macver}-{arch}') if not x.endswith(arch): raise ConfigError(f'$ARCHFLAGS={archflags!r} and $_PYTHON_HOST_PLATFORM={x!r} do not agree') family = 'aarch64' if arch == 'arm64' else arch cross_file_data = textwrap.dedent(f''' [binaries] c = ['cc', '-arch', {arch!r}] cpp = ['c++', '-arch', {arch!r}] [host_machine] system = 'Darwin' cpu = {arch!r} cpu_family = {family!r} endian = 'little' ''') self._meson_cross_file.write_text(cross_file_data) self._meson_args['setup'].extend(('--cross-file', os.fspath(self._meson_cross_file))) # load pyproject.toml pyproject = tomllib.loads(self._source_dir.joinpath('pyproject.toml').read_text()) # load meson args from pyproject.toml pyproject_config = _validate_pyproject_config(pyproject) for key, value in pyproject_config.get('args', {}).items(): self._meson_args[key].extend(value) # meson arguments from the command line take precedence over # arguments from the configuration file thus are added later if meson_args: for key, value in meson_args.items(): self._meson_args[key].extend(value) # make sure the build dir exists self._build_dir.mkdir(exist_ok=True, parents=True) self._install_dir.mkdir(exist_ok=True, parents=True) # write the native file native_file_data = textwrap.dedent(f''' [binaries] python = '{sys.executable}' ''') self._meson_native_file.write_text(native_file_data) # reconfigure if we have a valid Meson build directory. Meson # uses the presence of the 'meson-private/coredata.dat' file # in the build directory as indication that the build # directory has already been configured and arranges this file # to be created as late as possible or deleted if something # goes wrong during setup. reconfigure = self._build_dir.joinpath('meson-private/coredata.dat').is_file() # run meson setup self._configure(reconfigure=reconfigure) # package metadata if 'project' in pyproject: self._metadata = pyproject_metadata.StandardMetadata.from_pyproject(pyproject, self._source_dir) else: self._metadata = pyproject_metadata.StandardMetadata( name=self._meson_name, version=packaging.version.Version(self._meson_version)) self._validate_metadata() # set version from meson.build if dynamic if 'version' in self._metadata.dynamic: self._metadata.version = packaging.version.Version(self._meson_version) def _run(self, cmd: Sequence[str]) -> None: """Invoke a subprocess.""" print('{cyan}{bold}+ {}{reset}'.format(' '.join(cmd), **_STYLES)) r = subprocess.run(cmd, env=self._env, cwd=self._build_dir) if r.returncode != 0: raise SystemExit(r.returncode) def _configure(self, reconfigure: bool = False) -> None: """Configure Meson project.""" setup_args = [ os.fspath(self._source_dir), os.fspath(self._build_dir), # default build options '-Dbuildtype=release', '-Db_ndebug=if-release', '-Db_vscrt=md', # user build options *self._meson_args['setup'], # pass native file last to have it override the python # interpreter path that may have been specified in user # provided native files f'--native-file={os.fspath(self._meson_native_file)}', ] if reconfigure: setup_args.insert(0, '--reconfigure') self._run(['meson', 'setup', *setup_args]) def _validate_metadata(self) -> None: """Check the pyproject.toml metadata and see if there are any issues.""" # check for unsupported dynamic fields unsupported_dynamic = { key for key in self._metadata.dynamic if key not in self._ALLOWED_DYNAMIC_FIELDS } if unsupported_dynamic: s = ', '.join(f'"{x}"' for x in unsupported_dynamic) raise MesonBuilderError(f'Unsupported dynamic fields: {s}') # check if we are running on an unsupported interpreter if self._metadata.requires_python: self._metadata.requires_python.prereleases = True if platform.python_version().rstrip('+') not in self._metadata.requires_python: raise MesonBuilderError( f'Unsupported Python version {platform.python_version()}, ' f'expected {self._metadata.requires_python}' ) @cached_property def _wheel_builder(self) -> _WheelBuilder: return _WheelBuilder( self, self._metadata, self._source_dir, self._build_dir, self._install_plan, ) @property def _build_command(self) -> List[str]: assert self._ninja is not None # help mypy out if platform.system() == 'Windows': # On Windows use 'meson compile' to setup the MSVC compiler # environment. Using the --ninja-args option allows to # provide the exact same semantics for the compile arguments # provided by the users. cmd = ['meson', 'compile'] args = list(self._meson_args['compile']) if args: cmd.append(f'--ninja-args={args!r}') return cmd return [self._ninja, *self._meson_args['compile']] @functools.lru_cache(maxsize=None) def build(self) -> None: """Build the Meson project.""" self._run(self._build_command) def install(self) -> None: """Install the Meson project.""" destdir = os.fspath(self._install_dir) self._run(['meson', 'install', '--no-rebuild', '--destdir', destdir, *self._meson_args['install']]) @classmethod @contextlib.contextmanager def with_temp_working_dir( cls, source_dir: Path = os.path.curdir, build_dir: Optional[Path] = None, meson_args: Optional[MesonArgs] = None, editable_verbose: bool = False, ) -> Iterator[Project]: """Creates a project instance pointing to a temporary working directory.""" with tempfile.TemporaryDirectory(prefix='.mesonpy-', dir=os.fspath(source_dir)) as tmpdir: yield cls(source_dir, tmpdir, build_dir, meson_args, editable_verbose) @functools.lru_cache() def _info(self, name: str) -> Dict[str, Any]: """Read info from meson-info directory.""" file = self._build_dir.joinpath('meson-info', f'{name}.json') return typing.cast( Dict[str, str], json.loads(file.read_text()) ) @property def _install_plan(self) -> Dict[str, Dict[str, Dict[str, str]]]: """Meson install_plan metadata.""" # copy the install plan so we can modify it install_plan = self._info('intro-install_plan').copy() # parse install args for install tags (--tags) parser = argparse.ArgumentParser() parser.add_argument('--tags') args, _ = parser.parse_known_args(self._meson_args['install']) # filter the install_plan for files that do not fit the install tags if args.tags: install_tags = args.tags.split(',') for files in install_plan.values(): for file, details in list(files.items()): if details['tag'].strip() not in install_tags: del files[file] return install_plan @property def _meson_name(self) -> str: """Name in meson.build.""" name = self._info('intro-projectinfo')['descriptive_name'] assert isinstance(name, str) return name @property def _meson_version(self) -> str: """Version in meson.build.""" name = self._info('intro-projectinfo')['version'] assert isinstance(name, str) return name @property def name(self) -> str: """Project name.""" return str(self._metadata.name).replace('-', '_') @property def version(self) -> str: """Project version.""" return str(self._metadata.version) @cached_property def metadata(self) -> bytes: """Project metadata as an RFC822 message.""" return bytes(self._metadata.as_rfc822()) @property def license_file(self) -> Optional[pathlib.Path]: if self._metadata: license_ = self._metadata.license if license_ and license_.file: return pathlib.Path(license_.file) return None @property def is_pure(self) -> bool: """Is the wheel "pure" (architecture independent)?""" return bool(self._wheel_builder.is_pure) def sdist(self, directory: Path) -> pathlib.Path: """Generates a sdist (source distribution) in the specified directory.""" # generate meson dist file self._run(['meson', 'dist', '--allow-dirty', '--no-tests', '--formats', 'gztar', *self._meson_args['dist']]) # move meson dist file to output path dist_name = f'{self.name}-{self.version}' meson_dist_name = f'{self._meson_name}-{self._meson_version}' meson_dist_path = pathlib.Path(self._build_dir, 'meson-dist', f'{meson_dist_name}.tar.gz') sdist = pathlib.Path(directory, f'{dist_name}.tar.gz') with tarfile.open(meson_dist_path, 'r:gz') as meson_dist, mesonpy._util.create_targz(sdist) as (tar, mtime): for member in meson_dist.getmembers(): # calculate the file path in the source directory assert member.name, member.name member_parts = member.name.split('/') if len(member_parts) <= 1: continue path = self._source_dir.joinpath(*member_parts[1:]) if not path.exists() and member.isfile(): # File doesn't exists on the source directory but exists on # the Meson dist, so it is generated file, which we need to # include. # See https://mesonbuild.com/Reference-manual_builtin_meson.html#mesonadd_dist_script # MESON_DIST_ROOT could have a different base name # than the actual sdist basename, so we need to rename here file = meson_dist.extractfile(member.name) member.name = str(pathlib.Path(dist_name, *member_parts[1:]).as_posix()) tar.addfile(member, file) continue if not path.is_file(): continue info = tarfile.TarInfo(member.name) file_stat = os.stat(path) info.size = file_stat.st_size info.mode = int(oct(file_stat.st_mode)[-3:], 8) # rewrite the path if necessary, to match the sdist distribution name if dist_name != meson_dist_name: info.name = pathlib.Path( dist_name, path.relative_to(self._source_dir) ).as_posix() with path.open('rb') as f: tar.addfile(info, fileobj=f) # add PKG-INFO to dist file to make it a sdist pkginfo_info = tarfile.TarInfo(f'{dist_name}/PKG-INFO') if mtime: pkginfo_info.mtime = mtime pkginfo_info.size = len(self.metadata) tar.addfile(pkginfo_info, fileobj=io.BytesIO(self.metadata)) return sdist def wheel(self, directory: Path) -> pathlib.Path: """Generates a wheel (binary distribution) in the specified directory.""" file = self._wheel_builder.build(directory) assert isinstance(file, pathlib.Path) return file def editable(self, directory: Path) -> pathlib.Path: file = self._wheel_builder.build_editable(directory, self._editable_verbose) assert isinstance(file, pathlib.Path) return file @contextlib.contextmanager def _project(config_settings: Optional[Dict[Any, Any]]) -> Iterator[Project]: """Create the project given the given config settings.""" settings = _validate_config_settings(config_settings or {}) meson_args = {name: settings.get(f'{name}-args', []) for name in _MESON_ARGS_KEYS} with Project.with_temp_working_dir( build_dir=settings.get('builddir'), meson_args=typing.cast(MesonArgs, meson_args), editable_verbose=bool(settings.get('editable-verbose')) ) as project: yield project def _parse_version_string(string: str) -> Tuple[int, ...]: """Parse version string.""" try: return tuple(map(int, string.split('.')[:3])) except ValueError: return (0, ) def _env_ninja_command(*, version: str = _NINJA_REQUIRED_VERSION) -> Optional[str]: """Returns the path to ninja, or None if no ninja found.""" required_version = _parse_version_string(version) env_ninja = os.environ.get('NINJA') ninja_candidates = [env_ninja] if env_ninja else ['ninja', 'ninja-build', 'samu'] for ninja in ninja_candidates: ninja_path = shutil.which(ninja) if ninja_path is not None: version = subprocess.run([ninja_path, '--version'], check=False, text=True, capture_output=True).stdout if _parse_version_string(version) >= required_version: return ninja_path return None def _check_meson_version(*, version: str = _MESON_REQUIRED_VERSION) -> None: """Check that the meson executable in the path has an appropriate version. The meson Python package is a dependency of the meson-python Python package, however, it may occur that the meson Python package is installed but the corresponding meson command is not available in $PATH. Implement a runtime check to verify that the build environment is setup correcly. """ required_version = _parse_version_string(version) meson_version = subprocess.run(['meson', '--version'], check=False, text=True, capture_output=True).stdout if _parse_version_string(meson_version) < required_version: raise ConfigError(f'Could not find meson version {version} or newer, found {meson_version}.') def _add_ignore_files(directory: pathlib.Path) -> None: directory.joinpath('.gitignore').write_text(textwrap.dedent(''' # This file is generated by meson-python. It will not be recreated if deleted or modified. * '''), encoding='utf-8') directory.joinpath('.hgignore').write_text(textwrap.dedent(''' # This file is generated by meson-python. It will not be recreated if deleted or modified. syntax: glob **/* '''), encoding='utf-8') def _pyproject_hook(func: Callable[P, T]) -> Callable[P, T]: @functools.wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: try: return func(*args, **kwargs) except (Error, pyproject_metadata.ConfigurationError) as exc: prefix = '{red}meson-python: error:{reset} '.format(**_STYLES) print('\n' + textwrap.indent(str(exc), prefix)) raise SystemExit(1) from exc return wrapper @_pyproject_hook def get_requires_for_build_sdist( config_settings: Optional[Dict[str, str]] = None, ) -> List[str]: if os.environ.get('NINJA') is None and _env_ninja_command() is None: return [_depstr.ninja] return [] @_pyproject_hook def build_sdist( sdist_directory: str, config_settings: Optional[Dict[Any, Any]] = None, ) -> str: _setup_cli() out = pathlib.Path(sdist_directory) with _project(config_settings) as project: return project.sdist(out).name @_pyproject_hook def get_requires_for_build_wheel( config_settings: Optional[Dict[str, str]] = None, ) -> List[str]: dependencies = [] if os.environ.get('NINJA') is None and _env_ninja_command() is None: dependencies.append(_depstr.ninja) if sys.platform.startswith('linux'): # we may need patchelf if not shutil.which('patchelf'): # patchelf not already accessible on the system if _env_ninja_command() is not None: # we have ninja available, so we can run Meson and check if the project needs patchelf with _project(config_settings) as project: if not project.is_pure: dependencies.append(_depstr.patchelf) else: # we can't check if the project needs patchelf, so always add it # XXX: wait for https://github.com/mesonbuild/meson/pull/10779 dependencies.append(_depstr.patchelf) return dependencies @_pyproject_hook def build_wheel( wheel_directory: str, config_settings: Optional[Dict[Any, Any]] = None, metadata_directory: Optional[str] = None, ) -> str: _setup_cli() out = pathlib.Path(wheel_directory) with _project(config_settings) as project: return project.wheel(out).name @_pyproject_hook def build_editable( wheel_directory: str, config_settings: Optional[Dict[Any, Any]] = None, metadata_directory: Optional[str] = None, ) -> str: _setup_cli() # force set a permanent builddir if not config_settings: config_settings = {} if 'builddir' not in config_settings: builddir = pathlib.Path('build') builddir.mkdir(exist_ok=True) if not next(builddir.iterdir(), None): _add_ignore_files(builddir) config_settings['builddir'] = os.fspath(builddir / str(mesonpy._tags.get_abi_tag())) out = pathlib.Path(wheel_directory) with _project(config_settings) as project: return project.editable(out).name @_pyproject_hook def get_requires_for_build_editable( config_settings: Optional[Dict[str, str]] = None, ) -> List[str]: return get_requires_for_build_wheel() PKVw9C##mesonpy/_compat.py# SPDX-FileCopyrightText: 2021 Filipe Laíns # SPDX-FileCopyrightText: 2021 Quansight, LLC # SPDX-FileCopyrightText: 2022 The meson-python developers # # SPDX-License-Identifier: MIT from __future__ import annotations import functools import importlib.resources import os import pathlib import sys import typing if sys.version_info >= (3, 9): from collections.abc import Collection, Iterable, Iterator, Mapping, Sequence else: from typing import Collection, Iterable, Iterator, Mapping, Sequence if sys.version_info >= (3, 8): from functools import cached_property else: cached_property = lambda x: property(functools.lru_cache(maxsize=None)(x)) # noqa: E731 if sys.version_info >= (3, 9): def read_binary(package: str, resource: str) -> bytes: return importlib.resources.files(package).joinpath(resource).read_bytes() else: read_binary = importlib.resources.read_binary if typing.TYPE_CHECKING: from typing import Union if sys.version_info >= (3, 10): from typing import ParamSpec else: from typing_extensions import ParamSpec Path = Union[str, os.PathLike] # backport og pathlib.Path.is_relative_to def is_relative_to(path: pathlib.Path, other: Union[pathlib.Path, str]) -> bool: try: path.relative_to(other) except ValueError: return False return True __all__ = [ 'cached_property', 'is_relative_to', 'read_binary', 'Collection', 'Iterable', 'Iterator', 'Mapping', 'Path', 'ParamSpec', 'Sequence', ] PKV,Amesonpy/_dylib.py# SPDX-License-Identifier: MIT # SPDX-FileCopyrightText: 2023 Lars Pastewka from __future__ import annotations import os import subprocess import typing if typing.TYPE_CHECKING: from typing import Optional from mesonpy._compat import Collection, Path # This class is modeled after the ELF class in _elf.py class Dylib: def __init__(self, path: Path) -> None: self._path = os.fspath(path) self._rpath: Optional[Collection[str]] = None self._needed: Optional[Collection[str]] = None def _otool(self, *args: str) -> str: return subprocess.check_output(['otool', *args, self._path], stderr=subprocess.STDOUT).decode() def _install_name_tool(self, *args: str) -> str: return subprocess.check_output(['install_name_tool', *args, self._path], stderr=subprocess.STDOUT).decode() @property def rpath(self) -> Collection[str]: if self._rpath is None: self._rpath = [] # Run otool -l to get the load commands otool_output = self._otool('-l').strip() # Manually parse the output for LC_RPATH rpath_tag = False for line in [x.split() for x in otool_output.split('\n')]: if line == ['cmd', 'LC_RPATH']: rpath_tag = True elif len(line) >= 2 and line[0] == 'path' and rpath_tag: self._rpath += [line[1]] rpath_tag = False return frozenset(self._rpath) @rpath.setter def rpath(self, value: Collection[str]) -> None: # We clear all LC_RPATH load commands if self._rpath: for rpath in self._rpath: self._install_name_tool('-delete_rpath', rpath) # We then rewrite the new load commands for rpath in value: self._install_name_tool('-add_rpath', rpath) self._rpath = value PKV1((mesonpy/_editable.py# SPDX-FileCopyrightText: 2022 The meson-python developers # # SPDX-License-Identifier: MIT # This file should be standalone! It is copied during the editable hook installation. from __future__ import annotations import functools import importlib.abc import importlib.machinery import importlib.util import json import os import pathlib import subprocess import sys import typing if typing.TYPE_CHECKING: from collections.abc import Sequence, Set from types import ModuleType from typing import Any, Dict, Iterator, List, Optional, Tuple, Union NodeBase = Dict[str, Union[Node, str]] PathStr = Union[str, os.PathLike[str]] else: NodeBase = dict if sys.version_info >= (3, 12): from importlib.resources.abc import Traversable, TraversableResources elif sys.version_info >= (3, 9): from importlib.abc import Traversable, TraversableResources else: class Traversable: pass class TraversableResources: pass MARKER = 'MESONPY_EDITABLE_SKIP' VERBOSE = 'MESONPY_EDITABLE_VERBOSE' class MesonpyOrphan(Traversable): def __init__(self, name: str): self._name = name @property def name(self) -> str: return self._name def is_dir(self) -> bool: return False def is_file(self) -> bool: return False def iterdir(self) -> Iterator[Traversable]: raise FileNotFoundError() def open(self, *args, **kwargs): # type: ignore raise FileNotFoundError() def joinpath(self, *descendants: PathStr) -> Traversable: if not descendants: return self name = os.fspath(descendants[-1]).split('/')[-1] return MesonpyOrphan(name) def __truediv__(self, child: PathStr) -> Traversable: return self.joinpath(child) def read_bytes(self) -> bytes: raise FileNotFoundError() def read_text(self, encoding: Optional[str] = None) -> str: raise FileNotFoundError() class MesonpyTraversable(Traversable): def __init__(self, name: str, tree: Node): self._name = name self._tree = tree @property def name(self) -> str: return self._name def is_dir(self) -> bool: return True def is_file(self) -> bool: return False def iterdir(self) -> Iterator[Traversable]: for name, node in self._tree.items(): yield MesonpyTraversable(name, node) if isinstance(node, dict) else pathlib.Path(node) # type: ignore def open(self, *args, **kwargs): # type: ignore raise IsADirectoryError() @staticmethod def _flatten(names: Tuple[PathStr, ...]) -> Iterator[str]: for name in names: yield from os.fspath(name).split('/') def joinpath(self, *descendants: PathStr) -> Traversable: if not descendants: return self names = self._flatten(descendants) name = next(names) node = self._tree.get(name) if isinstance(node, dict): return MesonpyTraversable(name, node).joinpath(*names) if isinstance(node, str): return pathlib.Path(node).joinpath(*names) return MesonpyOrphan(name).joinpath(*names) def __truediv__(self, child: PathStr) -> Traversable: return self.joinpath(child) def read_bytes(self) -> bytes: raise IsADirectoryError() def read_text(self, encoding: Optional[str] = None) -> str: raise IsADirectoryError() class MesonpyReader(TraversableResources): def __init__(self, name: str, tree: Node): self._name = name self._tree = tree def files(self) -> Traversable: return MesonpyTraversable(self._name, self._tree) class ExtensionFileLoader(importlib.machinery.ExtensionFileLoader): def __init__(self, name: str, path: str, tree: Node): super().__init__(name, path) self._tree = tree def get_resource_reader(self, name: str) -> TraversableResources: return MesonpyReader(name, self._tree) class SourceFileLoader(importlib.machinery.SourceFileLoader): def __init__(self, name: str, path: str, tree: Node): super().__init__(name, path) self._tree = tree def set_data(self, path: Union[bytes, str], data: bytes, *, _mode: int = ...) -> None: # disable saving bytecode pass def get_resource_reader(self, name: str) -> TraversableResources: return MesonpyReader(name, self._tree) class SourcelessFileLoader(importlib.machinery.SourcelessFileLoader): def __init__(self, name: str, path: str, tree: Node): super().__init__(name, path) self._tree = tree def get_resource_reader(self, name: str) -> TraversableResources: return MesonpyReader(name, self._tree) LOADERS = [ (ExtensionFileLoader, tuple(importlib.machinery.EXTENSION_SUFFIXES)), (SourceFileLoader, tuple(importlib.machinery.SOURCE_SUFFIXES)), (SourcelessFileLoader, tuple(importlib.machinery.BYTECODE_SUFFIXES)), ] def build_module_spec(cls: type, name: str, path: str, tree: Optional[Node]) -> importlib.machinery.ModuleSpec: loader = cls(name, path, tree) spec = importlib.machinery.ModuleSpec(name, loader, origin=path) spec.has_location = True if loader.is_package(name): spec.submodule_search_locations = [] return spec class Node(NodeBase): """Tree structure to store a virtual filesystem view.""" def __missing__(self, key: str) -> Node: value = self[key] = Node() return value def __setitem__(self, key: Union[str, Tuple[str, ...]], value: Union[Node, str]) -> None: node = self if isinstance(key, tuple): for k in key[:-1]: node = typing.cast(Node, node[k]) key = key[-1] dict.__setitem__(node, key, value) def __getitem__(self, key: Union[str, Tuple[str, ...]]) -> Union[Node, str]: node = self if isinstance(key, tuple): for k in key[:-1]: node = typing.cast(Node, node[k]) key = key[-1] return dict.__getitem__(node, key) def get(self, key: Union[str, Tuple[str, ...]]) -> Optional[Union[Node, str]]: # type: ignore[override] node = self if isinstance(key, tuple): for k in key[:-1]: v = dict.get(node, k) if v is None: return None node = typing.cast(Node, v) key = key[-1] return dict.get(node, key) def walk(root: str, path: str = '') -> Iterator[pathlib.Path]: with os.scandir(os.path.join(root, path)) as entries: for entry in entries: if entry.is_dir(): yield from walk(root, os.path.join(path, entry.name)) else: yield pathlib.Path(path, entry.name) def collect(install_plan: Dict[str, Dict[str, Any]]) -> Node: tree = Node() for key, data in install_plan.items(): for src, target in data.items(): path = pathlib.Path(target['destination']) if path.parts[0] in {'{py_platlib}', '{py_purelib}'}: if key == 'install_subdirs' and os.path.isdir(src): for entry in walk(src): tree[(*path.parts[1:], *entry.parts)] = os.path.join(src, *entry.parts) else: tree[path.parts[1:]] = src return tree class MesonpyMetaFinder(importlib.abc.MetaPathFinder): def __init__(self, names: Set[str], path: str, cmd: List[str], verbose: bool = False): self._top_level_modules = names self._build_path = path self._build_cmd = cmd self._verbose = verbose self._loaders: List[Tuple[type, str]] = [] for loader, suffixes in LOADERS: self._loaders.extend((loader, suffix) for suffix in suffixes) def __repr__(self) -> str: return f'{self.__class__.__name__}({self._build_path!r})' def find_spec( self, fullname: str, path: Optional[Sequence[Union[bytes, str]]] = None, target: Optional[ModuleType] = None ) -> Optional[importlib.machinery.ModuleSpec]: if fullname.split('.', maxsplit=1)[0] in self._top_level_modules: if self._build_path in os.environ.get(MARKER, '').split(os.pathsep): return None namespace = False tree = self.rebuild() parts = fullname.split('.') # look for a package package = tree.get(tuple(parts)) if isinstance(package, Node): for loader, suffix in self._loaders: src = package.get('__init__' + suffix) if isinstance(src, str): return build_module_spec(loader, fullname, src, package) else: namespace = True # look for a module for loader, suffix in self._loaders: src = tree.get((*parts[:-1], parts[-1] + suffix)) if isinstance(src, str): return build_module_spec(loader, fullname, src, None) # namespace if namespace: spec = importlib.machinery.ModuleSpec(fullname, None) spec.submodule_search_locations = [] return spec return None @functools.lru_cache(maxsize=1) def rebuild(self) -> Node: # skip editable wheel lookup during rebuild: during the build # the module we are rebuilding might be imported causing a # rebuild loop. env = os.environ.copy() env[MARKER] = os.pathsep.join((env.get(MARKER, ''), self._build_path)) if self._verbose or bool(env.get(VERBOSE, '')): print('+ ' + ' '.join(self._build_cmd)) stdout = None else: stdout = subprocess.DEVNULL subprocess.run(self._build_cmd, cwd=self._build_path, env=env, stdout=stdout, check=True) install_plan_path = os.path.join(self._build_path, 'meson-info', 'intro-install_plan.json') with open(install_plan_path, 'r', encoding='utf8') as f: install_plan = json.load(f) return collect(install_plan) def install(names: Set[str], path: str, cmd: List[str], verbose: bool) -> None: sys.meta_path.insert(0, MesonpyMetaFinder(names, path, cmd, verbose)) PKV%dmesonpy/_elf.py# SPDX-FileCopyrightText: 2021 Filipe Laíns # SPDX-FileCopyrightText: 2021 Quansight, LLC # SPDX-FileCopyrightText: 2022 The meson-python developers # # SPDX-License-Identifier: MIT from __future__ import annotations import os import subprocess import typing if typing.TYPE_CHECKING: # pragma: no cover from typing import Optional from mesonpy._compat import Collection, Path class ELF: def __init__(self, path: Path) -> None: self._path = os.fspath(path) self._rpath: Optional[Collection[str]] = None self._needed: Optional[Collection[str]] = None def _patchelf(self, *args: str) -> str: return subprocess.check_output(['patchelf', *args, self._path], stderr=subprocess.STDOUT).decode() @property def rpath(self) -> Collection[str]: if self._rpath is None: rpath = self._patchelf('--print-rpath').strip() self._rpath = rpath.split(':') if rpath else [] return frozenset(self._rpath) @rpath.setter def rpath(self, value: Collection[str]) -> None: self._patchelf('--set-rpath', ':'.join(value)) self._rpath = value @property def needed(self) -> Collection[str]: if self._needed is None: self._needed = frozenset(self._patchelf('--print-needed').splitlines()) return self._needed @needed.setter def needed(self, value: Collection[str]) -> None: value = frozenset(value) for entry in self.needed: if entry not in value: self._patchelf('--remove-needed', entry) for entry in value: if entry not in self.needed: self._patchelf('--add-needed', entry) self._needed = value PKV str: name = sys.implementation.name name = INTERPRETERS.get(name, name) version = sys.version_info return f'{name}{version[0]}{version[1]}' def _get_config_var(name: str, default: Union[str, int, None] = None) -> Union[str, int, None]: value: Union[str, int, None] = sysconfig.get_config_var(name) if value is None: return default return value def _get_cpython_abi() -> str: version = sys.version_info debug = pymalloc = '' if _get_config_var('Py_DEBUG', hasattr(sys, 'gettotalrefcount')): debug = 'd' if version < (3, 8) and _get_config_var('WITH_PYMALLOC', True): pymalloc = 'm' return f'cp{version[0]}{version[1]}{debug}{pymalloc}' def get_abi_tag() -> str: # The best solution to obtain the Python ABI is to parse the # $SOABI or $EXT_SUFFIX sysconfig variables as defined in PEP-314. # PyPy reports a $SOABI that does not agree with $EXT_SUFFIX. # Using $EXT_SUFFIX will not break when PyPy will fix this. # See https://foss.heptapod.net/pypy/pypy/-/issues/3816 and # https://github.com/pypa/packaging/pull/607. try: empty, abi, ext = str(sysconfig.get_config_var('EXT_SUFFIX')).split('.') except ValueError as exc: # CPython <= 3.8.7 on Windows does not implement PEP3149 and # uses '.pyd' as $EXT_SUFFIX, which does not allow to extract # the interpreter ABI. Check that the fallback is not hit for # any other Python implementation. if sys.implementation.name != 'cpython': raise NotImplementedError from exc return _get_cpython_abi() # The packaging module initially based his understanding of the # $SOABI variable on the inconsistent value reported by PyPy, and # did not strip architecture information from it. Therefore the # ABI tag for later Python implementations (all the ones not # explicitly handled below) contains architecture information too. # Unfortunately, fixing this now would break compatibility. if abi.startswith('cpython'): abi = 'cp' + abi.split('-')[1] elif abi.startswith('cp'): abi = abi.split('-')[0] elif abi.startswith('pypy'): abi = '_'.join(abi.split('-')[:2]) elif abi.startswith('graalpy'): abi = '_'.join(abi.split('-')[:3]) return abi.replace('.', '_').replace('-', '_') def _get_macosx_platform_tag() -> str: ver, _, arch = platform.mac_ver() # Override the architecture with the one provided in the # _PYTHON_HOST_PLATFORM environment variable. This environment # variable affects the sysconfig.get_platform() return value and # is used to cross-compile python extensions on macOS for a # different architecture. We base the platform tag computation on # platform.mac_ver() but respect the content of the environment # variable. try: arch = os.environ.get('_PYTHON_HOST_PLATFORM', '').split('-')[2] except IndexError: pass # Override the macOS version if one is provided via the # MACOSX_DEPLOYMENT_TARGET environment variable. try: version = tuple(map(int, os.environ.get('MACOSX_DEPLOYMENT_TARGET', '').split('.')))[:2] except ValueError: version = tuple(map(int, ver.split('.')))[:2] # Python built with older macOS SDK on macOS 11, reports an # unexising macOS 10.16 version instead of the real version. # # The packaging module introduced a workaround # https://github.com/pypa/packaging/commit/67c4a2820c549070bbfc4bfbf5e2a250075048da # # This results in packaging versions up to 21.3 generating # platform tags like "macosx_10_16_x86_64" and later versions # generating "macosx_11_0_x86_64". Using the latter would be more # correct but prevents the resulting wheel from being installed on # systems using packaging 21.3 or earlier (pip 22.3 or earlier). # # Fortunately packaging versions carrying the workaround still # accepts "macosx_10_16_x86_64" as a compatible platform tag. We # can therefore ignore the issue and generate the slightly # incorrect tag. major, minor = version if major >= 11: # For macOS reelases up to 10.15, the major version number is # actually part of the OS name and the minor version is the # actual OS release. Starting with macOS 11, the major # version number is the OS release and the minor version is # the patch level. Reset the patch level to zero. minor = 0 if _32_BIT_INTERPRETER: # 32-bit Python running on a 64-bit kernel. if arch == 'ppc64': arch = 'ppc' if arch == 'x86_64': arch = 'i386' return f'macosx_{major}_{minor}_{arch}' def get_platform_tag() -> str: platform = sysconfig.get_platform() if platform.startswith('macosx'): return _get_macosx_platform_tag() if _32_BIT_INTERPRETER: # 32-bit Python running on a 64-bit kernel. if platform == 'linux-x86_64': return 'linux_i686' if platform == 'linux-aarch64': return 'linux_armv7l' return platform.replace('-', '_').replace('.', '_') class Tag: def __init__(self, interpreter: Optional[str] = None, abi: Optional[str] = None, platform: Optional[str] = None): self.interpreter = interpreter or get_interpreter_tag() self.abi = abi or get_abi_tag() self.platform = platform or get_platform_tag() def __str__(self) -> str: return f'{self.interpreter}-{self.abi}-{self.platform}' PKV|4x mesonpy/_util.py# SPDX-FileCopyrightText: 2021 Filipe Laíns # SPDX-FileCopyrightText: 2021 Quansight, LLC # SPDX-FileCopyrightText: 2022 The meson-python developers # # SPDX-License-Identifier: MIT from __future__ import annotations import contextlib import gzip import os import sys import tarfile import typing from typing import IO if typing.TYPE_CHECKING: # pragma: no cover from typing import Optional, Tuple from mesonpy._compat import Iterable, Iterator, Path @contextlib.contextmanager def chdir(path: Path) -> Iterator[Path]: """Context manager helper to change the current working directory -- cd.""" old_cwd = os.getcwd() os.chdir(os.fspath(path)) try: yield path finally: os.chdir(old_cwd) @contextlib.contextmanager def add_ld_path(paths: Iterable[str]) -> Iterator[None]: """Context manager helper to add a path to LD_LIBRARY_PATH.""" old_value = os.environ.get('LD_LIBRARY_PATH') old_paths = old_value.split(os.pathsep) if old_value else [] os.environ['LD_LIBRARY_PATH'] = os.pathsep.join([*paths, *old_paths]) try: yield finally: if old_value is not None: # pragma: no cover os.environ['LD_LIBRARY_PATH'] = old_value @contextlib.contextmanager def create_targz(path: Path) -> Iterator[Tuple[tarfile.TarFile, Optional[int]]]: """Opens a .tar.gz file in the file system for edition..""" # reproducibility source_date_epoch = os.environ.get('SOURCE_DATE_EPOCH') mtime = int(source_date_epoch) if source_date_epoch else None os.makedirs(os.path.dirname(path), exist_ok=True) file = typing.cast(IO[bytes], gzip.GzipFile( path, mode='wb', mtime=mtime, )) tar = tarfile.TarFile( mode='w', fileobj=file, format=tarfile.PAX_FORMAT, # changed in 3.8 to GNU ) with contextlib.closing(file), tar: yield tar, mtime class CLICounter: def __init__(self, total: int) -> None: self._total = total - 1 self._count = -1 self._current_line = '' def update(self, description: str) -> None: self._count += 1 new_line = f'[{self._count}/{self._total}] {description}' if sys.stdout.isatty(): pad_size = abs(len(self._current_line) - len(new_line)) print(' ' + new_line + ' ' * pad_size, end='\r', flush=True) else: print(new_line) self._current_line = new_line def finish(self) -> None: if sys.stdout.isatty(): print(f'\r{self._current_line}') @contextlib.contextmanager def cli_counter(total: int) -> Iterator[CLICounter]: counter = CLICounter(total) yield counter counter.finish() PKV+2mesonpy/_wheelfile.py# SPDX-FileCopyrightText: 2022 The meson-python developers # # SPDX-License-Identifier: MIT from __future__ import annotations import base64 import csv import hashlib import io import os import re import stat import time import typing import zipfile if typing.TYPE_CHECKING: # pragma: no cover from types import TracebackType from typing import List, Optional, Tuple, Type, Union from mesonpy._compat import Path MIN_TIMESTAMP = 315532800 # 1980-01-01 00:00:00 UTC WHEEL_FILENAME_REGEX = re.compile(r'^(?P[^-]+)-(?P[^-]+)(:?-(?P[^-]+))?-(?P[^-]+-[^-]+-[^-]+).whl$') def _b64encode(data: bytes) -> bytes: return base64.urlsafe_b64encode(data).rstrip(b'=') class WheelFile: """Implement the wheel package binary distribution format. https://packaging.python.org/en/latest/specifications/binary-distribution-format/ """ def __new__(cls, filename: Path, mode: str = 'r', compression: int = zipfile.ZIP_DEFLATED) -> 'WheelFile': if mode == 'w': return super().__new__(WheelFileWriter) raise NotImplementedError @staticmethod def timestamp(mtime: Optional[float] = None) -> Tuple[int, int, int, int, int, int]: timestamp = int(os.environ.get('SOURCE_DATE_EPOCH', mtime or time.time())) # The ZIP file format does not support timestamps before 1980. timestamp = max(timestamp, MIN_TIMESTAMP) return time.gmtime(timestamp)[0:6] @staticmethod def hash(data: bytes) -> str: return 'sha256=' + _b64encode(hashlib.sha256(data).digest()).decode('ascii') def writestr(self, zinfo_or_arcname: Union[str, zipfile.ZipInfo], data: bytes) -> None: raise NotImplementedError def write(self, filename: Path, arcname: Optional[str] = None) -> None: raise NotImplementedError def close(self) -> None: raise NotImplementedError def __enter__(self) -> WheelFile: return self def __exit__(self, exc_type: Type[BaseException], exc_val: BaseException, exc_tb: TracebackType) -> None: self.close() class WheelFileWriter(WheelFile): def __init__(self, filepath: Path, mode: str, compression: int = zipfile.ZIP_DEFLATED): filename = os.path.basename(filepath) match = WHEEL_FILENAME_REGEX.match(filename) if not match: raise ValueError(f'invalid wheel filename: {filename!r}') self.name = match.group('name') self.version = match.group('version') self.entries: List[Tuple[str, str, int]] = [] self.archive = zipfile.ZipFile(filepath, mode='w', compression=compression, allowZip64=True) def writestr(self, zinfo_or_arcname: Union[str, zipfile.ZipInfo], data: bytes) -> None: if isinstance(data, str): data = data.encode('utf-8') if isinstance(zinfo_or_arcname, zipfile.ZipInfo): zinfo = zinfo_or_arcname else: zinfo = zipfile.ZipInfo(zinfo_or_arcname, date_time=self.timestamp()) zinfo.external_attr = 0o664 << 16 self.archive.writestr(zinfo, data) self.entries.append((zinfo.filename, self.hash(data), len(data))) def write(self, filename: Path, arcname: Optional[str] = None) -> None: with open(filename, 'rb') as f: st = os.fstat(f.fileno()) data = f.read() zinfo = zipfile.ZipInfo(arcname or str(filename), date_time=self.timestamp(st.st_mtime)) zinfo.external_attr = (stat.S_IMODE(st.st_mode) | stat.S_IFMT(st.st_mode)) << 16 self.writestr(zinfo, data) def close(self) -> None: record = f'{self.name}-{self.version}.dist-info/RECORD' data = io.StringIO() writer = csv.writer(data, delimiter=',', quotechar='"', lineterminator='\n') writer.writerows(self.entries) writer.writerow((record, '', '')) zi = zipfile.ZipInfo(record, date_time=self.timestamp()) zi.external_attr = 0o664 << 16 self.archive.writestr(zi, data.getvalue()) self.archive.close() PKVp$meson_python-0.13.0.dist-info/RECORDmeson_python-0.13.0.dist-info/METADATA,sha256=Ls0So3Q-pItwj6TngktN8ebMvOuZsOqCPKs6HmhF0T0,4140 meson_python-0.13.0.dist-info/WHEEL,sha256=5J4neoE7k6LMgx4Fz1FHgBiO3YevhJGtNQ3muDrdLQM,75 meson_python-0.13.0.dist-info/MIT.txt,sha256=j4LqnvmlW0f0FwU20C5Itbwz_FE15bShtfjWQEgz7Js,1155 mesonpy/__init__.py,sha256=rs6DkSHdvbvLdGOsNMTHaqeF2VIFjHzCRNqF89sKYTI,43144 mesonpy/_compat.py,sha256=YhhbHNkVKV4cgA5O2xXIQ_PtoYe5a7trMX-uR5QGgfg,1571 mesonpy/_dylib.py,sha256=nCVgeOJEOHbw8f8Trk3jm1B4jJ_nqC5iQBoUhW7Y5LY,1937 mesonpy/_editable.py,sha256=QSKDprkfLN-xhxhXyD9mObXZGJLwKv-FBO-idI26Niw,10392 mesonpy/_elf.py,sha256=HKktFR9XmE5KqKdQedP35ycTzI7D0xgCnLNAEPw24os,1751 mesonpy/_tags.py,sha256=ebtUBLsRFfq81Eaczu0FnFSr87c40vLcLwM9ABcbJlo,6114 mesonpy/_util.py,sha256=5aC0OQfbr79XhbnGuwAjtj0rg7_9DcEVyPfknwX9eNU,2739 mesonpy/_wheelfile.py,sha256=L-oRb5zIab2ENRdSsOooYT7q35mFoJOr-oYAjgCo2Z8,4051 meson_python-0.13.0.dist-info/RECORD,, PKV09f,,&meson_python-0.13.0.dist-info/METADATAPKVx>KK#pmeson_python-0.13.0.dist-info/WHEELPKV%meson_python-0.13.0.dist-info/MIT.txtPKV7'mesonpy/__init__.pyPKVw9C##{mesonpy/_compat.pyPKV,Amesonpy/_dylib.pyPKV1((mesonpy/_editable.pyPKV%dXmesonpy/_elf.pyPKV