PK0%W&|&meson_python-0.14.0.dist-info/METADATAMetadata-Version: 2.1 Name: meson-python Version: 0.14.0 Summary: Meson Python build backend (PEP 517) Keywords: meson build backend pep517 package Home-page: https://github.com/mesonbuild/meson-python Maintainer: Thomas Li Maintainer-Email: Ralf Gommers , Daniele Nicolodi , Henry Schreiner 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 :: 5 - Production/Stable Classifier: Programming Language :: Python Classifier: Topic :: Software Development :: Build Tools 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: 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-copybutton>=0.5.0; extra == "docs" Requires-Dist: sphinx-design>=0.1.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/ PK0%Wx>KK#meson_python-0.14.0.dist-info/WHEELWheel-Version: 1.0 Generator: meson Root-Is-Purelib: true Tag: py3-none-anyPKF/%W%meson_python-0.14.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. PKF/%W5##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 json import os import pathlib import platform import re import shutil import subprocess import sys import sysconfig import tarfile import tempfile import textwrap import time 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._rpath 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, 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.14.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_SUFFIX_REGEX = re.compile(r'^[^.]+\.(?:(?P[^.]+)\.)?(?:so|pyd|dll)$') assert all(re.match(_EXTENSION_SUFFIX_REGEX, f'foo{x}') for x in importlib.machinery.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 key, group in sources.items(): for src, target in group.items(): destination = pathlib.Path(target['destination']) anchor = destination.parts[0] dst = pathlib.Path(*destination.parts[1:]) 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. ' f'It is recommended to set it in "import(\'python\').find_installation()"') if key == 'install_subdirs': assert os.path.isdir(src) exclude_files = {os.path.normpath(x) for x in target.get('exclude_files', [])} exclude_dirs = {os.path.normpath(x) for x in target.get('exclude_dirs', [])} for root, dirnames, filenames in os.walk(src): for name in dirnames.copy(): dirsrc = os.path.join(root, name) relpath = os.path.relpath(dirsrc, src) if relpath in exclude_dirs: dirnames.remove(name) # sort to process directories determninistically dirnames.sort() for name in sorted(filenames): filesrc = os.path.join(root, name) relpath = os.path.relpath(filesrc, src) if relpath in exclude_files: continue filedst = dst / relpath wheel_files[path].append((filedst, filesrc)) else: wheel_files[path].append((dst, 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 Metadata(pyproject_metadata.StandardMetadata): # The class method from the pyproject_metadata base class is not # typed in a subclassing friendly way, thus annotations to ignore # typing are needed. @classmethod def from_pyproject(cls, data: Mapping[str, Any], project_dir: Path) -> Metadata: # type: ignore[override] metadata = super().from_pyproject(data, project_dir) # Check for missing version field. if not metadata.version and 'version' not in metadata.dynamic: raise pyproject_metadata.ConfigurationError( 'Required "project.version" field is missing and not declared as dynamic') # Check for unsupported dynamic fields. unsupported_dynamic = set(metadata.dynamic) - {'version', } if unsupported_dynamic: fields = ', '.join(f'"{x}"' for x in unsupported_dynamic) raise pyproject_metadata.ConfigurationError(f'Unsupported dynamic fields: {fields}') return metadata # type: ignore[return-value] # Local fix for a bug in pyproject-metadata. See # https://github.com/mesonbuild/meson-python/issues/454 def _update_dynamic(self, value: Any) -> None: if value and 'version' in self.dynamic: self.dynamic.remove('version') class _WheelBuilder(): """Helper class to build wheels from projects.""" def __init__( self, project: Project, metadata: Metadata, 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 @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.get('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.get('platlib')) @property def normalized_name(self) -> str: return self._project.name.replace('-', '_') @property def basename(self) -> str: """Normalized wheel name and version.""" return f'{self.normalized_name}-{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 f'{self.basename}-{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.""" 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]: if self._project._limited_api: # Verify stabe ABI compatibility: examine files installed # in {platlib} that look like extension modules, and raise # an exception if any of them has a Python version # specific extension filename suffix ABI tag. for path, _ in self._wheel_files['platlib']: match = _EXTENSION_SUFFIX_REGEX.match(path.name) if match: abi = match.group('abi') if abi is not None and abi != 'abi3': raise BuildError( f'The package declares compatibility with Python limited API but extension ' f'module {os.fspath(path)!r} is tagged for a specific Python version.') return 'abi3' 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 sys.platform == 'linux': return f.read(4) == b'\x7fELF' # ELF elif sys.platform == '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 sys.platform == 'win32': 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(self, wheel_file: mesonpy._wheelfile.WheelFile, origin: Path, destination: pathlib.Path) -> None: """Add a file to the wheel.""" if self._has_internal_libs: if self._is_native(os.fspath(origin)): # When an executable, libray, or Python extension module is # dynamically linked to a library built as part of the project, # Meson adds a library load path to it pointing to the build # directory, in the form of a relative RPATH entry. meson-python # relocates the shared libraries to the $project.mesonpy.libs # folder. Rewrite the RPATH to point to that folder instead. libspath = os.path.relpath(f'.{self._project.name}.mesonpy.libs', destination.parent) mesonpy._rpath.fix_rpath(origin, libspath) try: wheel_file.write(origin, destination.as_posix()) 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() wheel_file = pathlib.Path(directory, f'{self.name}.whl') with mesonpy._wheelfile.WheelFile(wheel_file, 'w') as whl: self._wheel_write_metadata(whl) with mesonpy._util.cli_counter(sum(len(x) for x in self._wheel_files.values())) as counter: root = 'purelib' if self.is_pure else 'platlib' for path, entries in self._wheel_files.items(): for dst, src in entries: counter.update(src) if path == root: pass elif path == 'mesonpy-libs': # custom installation path for bundled libraries dst = pathlib.Path(f'.{self._project.name}.mesonpy.libs', dst) else: dst = pathlib.Path(self.data_dir, path, dst) self._install_path(whl, src, dst) 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 def _bool(value: Any, name: str) -> bool: if not isinstance(value, bool): raise ConfigError(f'Configuration entry "{name}" must be a boolean') return value scheme = _table({ 'limited-api': _bool, '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.""" def __init__( # noqa: C901 self, source_dir: Path, build_dir: Path, meson_args: Optional[MesonArgs] = None, editable_verbose: bool = False, ) -> None: self._source_dir = pathlib.Path(source_dir).absolute() self._build_dir = pathlib.Path(build_dir).absolute() self._editable_verbose = editable_verbose 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._limited_api = False _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.') os.environ.setdefault('NINJA', self._ninja) # make sure the build dir exists self._build_dir.mkdir(exist_ok=True, parents=True) # setuptools-like ARCHFLAGS environment variable support if sysconfig.get_platform().startswith('macosx-'): archflags = os.environ.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 = os.environ.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}] objc = ['cc', '-arch', {arch!r}] objcpp = ['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) # 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 = Metadata.from_pyproject(pyproject, self._source_dir) # set version from meson.build if version is declared as dynamic if 'version' in self._metadata.dynamic: version = self._meson_version if version == 'undefined': raise pyproject_metadata.ConfigurationError( 'Field "version" declared as dynamic but version is not defined in meson.build') self._metadata.version = packaging.version.Version(version) else: # if project section is missing, use minimal metdata from meson.build name, version = self._meson_name, self._meson_version if version == 'undefined': raise pyproject_metadata.ConfigurationError( 'Section "project" missing in pyproject.toml and version is not defined in meson.build') self._metadata = Metadata(name=name, version=packaging.version.Version(version)) # verify that we are running on a supported 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'Package requires Python version {self._metadata.requires_python}, ' f'running on {platform.python_version()}') # limited API self._limited_api = pyproject_config.get('limited-api', False) if self._limited_api: # check whether limited API is disabled for the Meson project options = self._info('intro-buildoptions') value = next((option['value'] for option in options if option['name'] == 'python.allow_limited_api'), None) if not value: self._limited_api = False def _run(self, cmd: Sequence[str]) -> None: """Invoke a subprocess.""" # Flush the line to ensure that the log line with the executed # command line appears before the command output. Without it, # the lines appear in the wrong order in pip output. print('{cyan}{bold}+ {}{reset}'.format(' '.join(cmd), **_STYLES), flush=True) r = subprocess.run(cmd, 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]) @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 sys.platform == 'win32': # 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) @functools.lru_cache() def _info(self, name: str) -> Any: """Read info from meson-info directory.""" info = self._build_dir.joinpath('meson-info', f'{name}.json') return json.loads(info.read_text()) @property def _install_plan(self) -> Dict[str, Dict[str, Dict[str, str]]]: """Meson install_plan metadata.""" install_plan = self._info('intro-install_plan') # parse install args to extract --tags and --skip-subprojects parser = argparse.ArgumentParser() parser.add_argument('--tags') parser.add_argument('--skip-subprojects', nargs='?', const='*', default='') args, _ = parser.parse_known_args(self._meson_args['install']) install_tags = {t.strip() for t in args.tags.split(',')} if args.tags else None skip_subprojects = {p for p in (p.strip() for p in args.skip_subprojects.split(',')) if p} manifest: DefaultDict[str, Dict[str, Dict[str, str]]] = collections.defaultdict(dict) # filter install_plan accordingly for key, targets in install_plan.items(): for target, details in targets.items(): if install_tags is not None and details['tag'] not in install_tags: continue subproject = details.get('subproject') if subproject is not None and (subproject in skip_subprojects or '*' in skip_subprojects): continue manifest[key][target] = details return manifest @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]: 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: 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.mtime = member.mtime 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') pkginfo_info.mtime = time.time() # type: ignore[assignment] 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]] = None) -> Iterator[Project]: """Create the project given the given config settings.""" settings = _validate_config_settings(config_settings or {}) meson_args = typing.cast(MesonArgs, {name: settings.get(f'{name}-args', []) for name in _MESON_ARGS_KEYS}) source_dir = os.path.curdir build_dir = settings.get('builddir') editable_verbose = bool(settings.get('editable-verbose')) with contextlib.ExitStack() as ctx: if build_dir is None: build_dir = ctx.enter_context(tempfile.TemporaryDirectory(prefix='.mesonpy-', dir=source_dir)) yield Project(source_dir, build_dir, meson_args, editable_verbose) 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() PKF/%Ww9C##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', ] PKF/%WG((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 from typing_extensions import Buffer 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: Buffer, *, _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)) PKF/%W}mesonpy/_rpath.py# SPDX-FileCopyrightText: 2023 The meson-python developers # # SPDX-License-Identifier: MIT from __future__ import annotations import os import subprocess import sys import typing if typing.TYPE_CHECKING: from typing import List from mesonpy._compat import Iterable, Path if sys.platform == 'linux': def _get_rpath(filepath: Path) -> List[str]: r = subprocess.run(['patchelf', '--print-rpath', os.fspath(filepath)], capture_output=True, text=True) return r.stdout.strip().split(':') def _set_rpath(filepath: Path, rpath: Iterable[str]) -> None: subprocess.run(['patchelf','--set-rpath', ':'.join(rpath), os.fspath(filepath)], check=True) def fix_rpath(filepath: Path, libs_relative_path: str) -> None: rpath = _get_rpath(filepath) if '$ORIGIN/' in rpath: rpath = [('$ORIGIN/' + libs_relative_path if path == '$ORIGIN/' else path) for path in rpath] _set_rpath(filepath, rpath) elif sys.platform == 'darwin': def _get_rpath(filepath: Path) -> List[str]: rpath = [] r = subprocess.run(['otool', '-l', os.fspath(filepath)], capture_output=True, text=True) rpath_tag = False for line in [x.split() for x in r.stdout.split('\n')]: if line == ['cmd', 'LC_RPATH']: rpath_tag = True elif len(line) >= 2 and line[0] == 'path' and rpath_tag: rpath.append(line[1]) rpath_tag = False return rpath def _replace_rpath(filepath: Path, old: str, new: str) -> None: subprocess.run(['install_name_tool', '-rpath', old, new, os.fspath(filepath)], check=True) def fix_rpath(filepath: Path, libs_relative_path: str) -> None: rpath = _get_rpath(filepath) if '@loader_path/' in rpath: _replace_rpath(filepath, '@loader_path/', '@loader_path/' + libs_relative_path) else: def fix_rpath(filepath: Path, libs_relative_path: str) -> None: raise NotImplementedError(f'Bundling libraries in wheel is not supported on {sys.platform}') PKF/%W 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}' PKF/%WZf--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 itertools import os import sys import tarfile import typing from typing import IO if typing.TYPE_CHECKING: # pragma: no cover from mesonpy._compat import 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 create_targz(path: Path) -> Iterator[tarfile.TarFile]: """Opens a .tar.gz file in the file system for edition..""" os.makedirs(os.path.dirname(path), exist_ok=True) file = typing.cast(IO[bytes], gzip.GzipFile( path, mode='wb', )) tar = tarfile.TarFile( mode='w', fileobj=file, format=tarfile.PAX_FORMAT, # changed in 3.8 to GNU ) with contextlib.closing(file), tar: yield tar class CLICounter: def __init__(self, total: int) -> None: self._total = total - 1 self._count = itertools.count() def update(self, description: str) -> None: line = f'[{next(self._count)}/{self._total}] {description}' if sys.stdout.isatty(): print('\r', line, sep='', end='\33[0K', flush=True) else: print(line) def finish(self) -> None: if sys.stdout.isatty(): print() @contextlib.contextmanager def cli_counter(total: int) -> Iterator[CLICounter]: counter = CLICounter(total) yield counter counter.finish() PKF/%W+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() PK0%WvNN$meson_python-0.14.0.dist-info/RECORDmeson_python-0.14.0.dist-info/METADATA,sha256=Blm9RJIJajF7EeJaKBBIjK-mDfv3-5dYo2SryvYO5XY,4052 meson_python-0.14.0.dist-info/WHEEL,sha256=5J4neoE7k6LMgx4Fz1FHgBiO3YevhJGtNQ3muDrdLQM,75 meson_python-0.14.0.dist-info/MIT.txt,sha256=j4LqnvmlW0f0FwU20C5Itbwz_FE15bShtfjWQEgz7Js,1155 mesonpy/__init__.py,sha256=5eh77RmKpw6V5rDtP8T0g-a2U2yGKRlNutLQ6aFOZp0,42531 mesonpy/_compat.py,sha256=YhhbHNkVKV4cgA5O2xXIQ_PtoYe5a7trMX-uR5QGgfg,1571 mesonpy/_editable.py,sha256=WabtyIVmIavDfdO_mppwW3fPyyB1hp_zxNcHZyp-CHM,10438 mesonpy/_rpath.py,sha256=zITnqckbBh4V7FZQ4SYFJVrHw0O3iqqlxOe_Pl5T4fo,2078 mesonpy/_tags.py,sha256=ebtUBLsRFfq81Eaczu0FnFSr87c40vLcLwM9ABcbJlo,6114 mesonpy/_util.py,sha256=jzcEpB0ddPUFR8lHjp9eNqXYRhGtZpBcS4psLKPhgpg,1837 mesonpy/_wheelfile.py,sha256=L-oRb5zIab2ENRdSsOooYT7q35mFoJOr-oYAjgCo2Z8,4051 meson_python-0.14.0.dist-info/RECORD,, PK0%W&|&meson_python-0.14.0.dist-info/METADATAPK0%Wx>KK#meson_python-0.14.0.dist-info/WHEELPKF/%W%meson_python-0.14.0.dist-info/MIT.txtPKF/%W5##jmesonpy/__init__.pyPKF/%Ww9C##mesonpy/_compat.pyPKF/%WG((mesonpy/_editable.pyPKF/%W} mesonpy/_rpath.pyPKF/%W