diff options
Diffstat (limited to 'mesonbuild/cargo/interpreter.py')
-rw-r--r-- | mesonbuild/cargo/interpreter.py | 530 |
1 files changed, 76 insertions, 454 deletions
diff --git a/mesonbuild/cargo/interpreter.py b/mesonbuild/cargo/interpreter.py index af272a8..a0d4371 100644 --- a/mesonbuild/cargo/interpreter.py +++ b/mesonbuild/cargo/interpreter.py @@ -11,439 +11,30 @@ port will be required. from __future__ import annotations import dataclasses -import importlib -import json import os -import shutil import collections import urllib.parse import itertools import typing as T -from . import builder -from . import version -from ..mesonlib import MesonException, Popen_safe +from . import builder, version, cfg +from .toml import load_toml, TomlImplementationMissing +from .manifest import Manifest, CargoLock, fixup_meson_varname +from ..mesonlib import MesonException, MachineChoice from .. import coredata, mlog from ..wrap.wrap import PackageDefinition if T.TYPE_CHECKING: - from types import ModuleType - - from typing_extensions import Protocol, Self - - from . import manifest + from . import raw from .. import mparser + from .manifest import Dependency, SystemDependency from ..environment import Environment from ..interpreterbase import SubProject + from ..compilers.rust import RustCompiler - # Copied from typeshed. Blarg that they don't expose this - class DataclassInstance(Protocol): - __dataclass_fields__: T.ClassVar[dict[str, dataclasses.Field[T.Any]]] - - _UnknownKeysT = T.TypeVar('_UnknownKeysT', manifest.FixedPackage, - manifest.FixedDependency, manifest.FixedLibTarget, - manifest.FixedBuildTarget) - - -# tomllib is present in python 3.11, before that it is a pypi module called tomli, -# we try to import tomllib, then tomli, -# TODO: add a fallback to toml2json? -tomllib: T.Optional[ModuleType] = None -toml2json: T.Optional[str] = None -for t in ['tomllib', 'tomli']: - try: - tomllib = importlib.import_module(t) - break - except ImportError: - pass -else: - # TODO: it would be better to use an Executable here, which could be looked - # up in the cross file or provided by a wrap. However, that will have to be - # passed in externally, since we don't have (and I don't think we should), - # have access to the `Environment` for that in this module. - toml2json = shutil.which('toml2json') - - -_EXTRA_KEYS_WARNING = ( - "This may (unlikely) be an error in the cargo manifest, or may be a missing " - "implementation in Meson. If this issue can be reproduced with the latest " - "version of Meson, please help us by opening an issue at " - "https://github.com/mesonbuild/meson/issues. Please include the crate and " - "version that is generating this warning if possible." -) - -class TomlImplementationMissing(MesonException): - pass - - -def load_toml(filename: str) -> T.Dict[object, object]: - if tomllib: - with open(filename, 'rb') as f: - raw = tomllib.load(f) - else: - if toml2json is None: - raise TomlImplementationMissing('Could not find an implementation of tomllib, nor toml2json') - - p, out, err = Popen_safe([toml2json, filename]) - if p.returncode != 0: - raise MesonException('toml2json failed to decode output\n', err) - - raw = json.loads(out) - - if not isinstance(raw, dict): - raise MesonException("Cargo.toml isn't a dictionary? How did that happen?") - - return raw - - -def fixup_meson_varname(name: str) -> str: - """Fixup a meson variable name - - :param name: The name to fix - :return: the fixed name - """ - return name.replace('-', '_') - - -# Pylance can figure out that these do not, in fact, overlap, but mypy can't -@T.overload -def _fixup_raw_mappings(d: manifest.BuildTarget) -> manifest.FixedBuildTarget: ... # type: ignore - -@T.overload -def _fixup_raw_mappings(d: manifest.LibTarget) -> manifest.FixedLibTarget: ... # type: ignore - -@T.overload -def _fixup_raw_mappings(d: manifest.Dependency) -> manifest.FixedDependency: ... - -def _fixup_raw_mappings(d: T.Union[manifest.BuildTarget, manifest.LibTarget, manifest.Dependency] - ) -> T.Union[manifest.FixedBuildTarget, manifest.FixedLibTarget, - manifest.FixedDependency]: - """Fixup raw cargo mappings to ones more suitable for python to consume. - - This does the following: - * replaces any `-` with `_`, cargo likes the former, but python dicts make - keys with `-` in them awkward to work with - * Convert Dependency versions from the cargo format to something meson - understands - - :param d: The mapping to fix - :return: the fixed string - """ - raw = {fixup_meson_varname(k): v for k, v in d.items()} - if 'version' in raw: - assert isinstance(raw['version'], str), 'for mypy' - raw['version'] = version.convert(raw['version']) - return T.cast('T.Union[manifest.FixedBuildTarget, manifest.FixedLibTarget, manifest.FixedDependency]', raw) - - -def _handle_unknown_keys(data: _UnknownKeysT, cls: T.Union[DataclassInstance, T.Type[DataclassInstance]], - msg: str) -> _UnknownKeysT: - """Remove and warn on keys that are coming from cargo, but are unknown to - our representations. - - This is intended to give users the possibility of things proceeding when a - new key is added to Cargo.toml that we don't yet handle, but to still warn - them that things might not work. - - :param data: The raw data to look at - :param cls: The Dataclass derived type that will be created - :param msg: the header for the error message. Usually something like "In N structure". - :return: The original data structure, but with all unknown keys removed. - """ - unexpected = set(data) - {x.name for x in dataclasses.fields(cls)} - if unexpected: - mlog.warning(msg, 'has unexpected keys', '"{}".'.format(', '.join(sorted(unexpected))), - _EXTRA_KEYS_WARNING) - for k in unexpected: - # Mypy and Pyright can't prove that this is okay - del data[k] # type: ignore[misc] - return data - - -@dataclasses.dataclass -class Package: - - """Representation of a Cargo Package entry, with defaults filled in.""" - - name: str - version: str - description: T.Optional[str] = None - resolver: T.Optional[str] = None - authors: T.List[str] = dataclasses.field(default_factory=list) - edition: manifest.EDITION = '2015' - rust_version: T.Optional[str] = None - documentation: T.Optional[str] = None - readme: T.Optional[str] = None - homepage: T.Optional[str] = None - repository: T.Optional[str] = None - license: T.Optional[str] = None - license_file: T.Optional[str] = None - keywords: T.List[str] = dataclasses.field(default_factory=list) - categories: T.List[str] = dataclasses.field(default_factory=list) - workspace: T.Optional[str] = None - build: T.Optional[str] = None - links: T.Optional[str] = None - exclude: T.List[str] = dataclasses.field(default_factory=list) - include: T.List[str] = dataclasses.field(default_factory=list) - publish: bool = True - metadata: T.Dict[str, T.Any] = dataclasses.field(default_factory=dict) - default_run: T.Optional[str] = None - autolib: bool = True - autobins: bool = True - autoexamples: bool = True - autotests: bool = True - autobenches: bool = True - api: str = dataclasses.field(init=False) - - def __post_init__(self) -> None: - self.api = _version_to_api(self.version) - - @classmethod - def from_raw(cls, raw: manifest.Package) -> Self: - pkg = T.cast('manifest.FixedPackage', - {fixup_meson_varname(k): v for k, v in raw.items()}) - pkg = _handle_unknown_keys(pkg, cls, f'Package entry {pkg["name"]}') - return cls(**pkg) - -@dataclasses.dataclass -class SystemDependency: - - """ Representation of a Cargo system-deps entry - https://docs.rs/system-deps/latest/system_deps - """ - - name: str - version: T.List[str] - optional: bool = False - feature: T.Optional[str] = None - feature_overrides: T.Dict[str, T.Dict[str, str]] = dataclasses.field(default_factory=dict) - - @classmethod - def from_raw(cls, name: str, raw: T.Any) -> SystemDependency: - if isinstance(raw, str): - return cls(name, SystemDependency.convert_version(raw)) - name = raw.get('name', name) - version = SystemDependency.convert_version(raw.get('version')) - optional = raw.get('optional', False) - feature = raw.get('feature') - # Everything else are overrides when certain features are enabled. - feature_overrides = {k: v for k, v in raw.items() if k not in {'name', 'version', 'optional', 'feature'}} - return cls(name, version, optional, feature, feature_overrides) - - @staticmethod - def convert_version(version: T.Optional[str]) -> T.List[str]: - vers = version.split(',') if version is not None else [] - result: T.List[str] = [] - for v in vers: - v = v.strip() - if v[0] not in '><=': - v = f'>={v}' - result.append(v) - return result - - def enabled(self, features: T.Set[str]) -> bool: - return self.feature is None or self.feature in features - -@dataclasses.dataclass -class Dependency: - - """Representation of a Cargo Dependency Entry.""" - - name: dataclasses.InitVar[str] - version: T.List[str] - registry: T.Optional[str] = None - git: T.Optional[str] = None - branch: T.Optional[str] = None - rev: T.Optional[str] = None - path: T.Optional[str] = None - optional: bool = False - package: str = '' - default_features: bool = True - features: T.List[str] = dataclasses.field(default_factory=list) - api: str = dataclasses.field(init=False) - - def __post_init__(self, name: str) -> None: - self.package = self.package or name - # Extract wanted API version from version constraints. - api = set() - for v in self.version: - if v.startswith(('>=', '==')): - api.add(_version_to_api(v[2:].strip())) - elif v.startswith('='): - api.add(_version_to_api(v[1:].strip())) - if not api: - self.api = '0' - elif len(api) == 1: - self.api = api.pop() - else: - raise MesonException(f'Cannot determine minimum API version from {self.version}.') - - @classmethod - def from_raw(cls, name: str, raw: manifest.DependencyV) -> Dependency: - """Create a dependency from a raw cargo dictionary""" - if isinstance(raw, str): - return cls(name, version.convert(raw)) - fixed = _handle_unknown_keys(_fixup_raw_mappings(raw), cls, f'Dependency entry {name}') - return cls(name, **fixed) - - -@dataclasses.dataclass -class BuildTarget: - - name: str - crate_type: T.List[manifest.CRATE_TYPE] = dataclasses.field(default_factory=lambda: ['lib']) - path: dataclasses.InitVar[T.Optional[str]] = None - - # https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-test-field - # True for lib, bin, test - test: bool = True - - # https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-doctest-field - # True for lib - doctest: bool = False - - # https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-bench-field - # True for lib, bin, benchmark - bench: bool = True - - # https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-doc-field - # True for libraries and binaries - doc: bool = False - - harness: bool = True - edition: manifest.EDITION = '2015' - required_features: T.List[str] = dataclasses.field(default_factory=list) - plugin: bool = False - - @classmethod - def from_raw(cls, raw: manifest.BuildTarget) -> Self: - name = raw.get('name', '<anonymous>') - build = _handle_unknown_keys(_fixup_raw_mappings(raw), cls, f'Binary entry {name}') - return cls(**build) - -@dataclasses.dataclass -class Library(BuildTarget): - - """Representation of a Cargo Library Entry.""" - - doctest: bool = True - doc: bool = True - path: str = os.path.join('src', 'lib.rs') - proc_macro: bool = False - crate_type: T.List[manifest.CRATE_TYPE] = dataclasses.field(default_factory=lambda: ['lib']) - doc_scrape_examples: bool = True - - @classmethod - def from_raw(cls, raw: manifest.LibTarget, fallback_name: str) -> Self: # type: ignore[override] - fixed = _fixup_raw_mappings(raw) - - # We need to set the name field if it's not set manually, including if - # other fields are set in the lib section - if 'name' not in fixed: - fixed['name'] = fallback_name - fixed = _handle_unknown_keys(fixed, cls, f'Library entry {fixed["name"]}') - - return cls(**fixed) - - -@dataclasses.dataclass -class Binary(BuildTarget): - - """Representation of a Cargo Bin Entry.""" - - doc: bool = True - - -@dataclasses.dataclass -class Test(BuildTarget): - - """Representation of a Cargo Test Entry.""" - - bench: bool = True - - -@dataclasses.dataclass -class Benchmark(BuildTarget): - - """Representation of a Cargo Benchmark Entry.""" - - test: bool = True - - -@dataclasses.dataclass -class Example(BuildTarget): - - """Representation of a Cargo Example Entry.""" - - crate_type: T.List[manifest.CRATE_TYPE] = dataclasses.field(default_factory=lambda: ['bin']) - - -@dataclasses.dataclass -class Manifest: - - """Cargo Manifest definition. - - Most of these values map up to the Cargo Manifest, but with default values - if not provided. - - Cargo subprojects can contain what Meson wants to treat as multiple, - interdependent, subprojects. - - :param path: the path within the cargo subproject. - """ - - package: Package - dependencies: T.Dict[str, Dependency] - dev_dependencies: T.Dict[str, Dependency] - build_dependencies: T.Dict[str, Dependency] - system_dependencies: T.Dict[str, SystemDependency] = dataclasses.field(init=False) - lib: Library - bin: T.List[Binary] - test: T.List[Test] - bench: T.List[Benchmark] - example: T.List[Example] - features: T.Dict[str, T.List[str]] - target: T.Dict[str, T.Dict[str, Dependency]] - path: str = '' - - def __post_init__(self) -> None: - self.features.setdefault('default', []) - self.system_dependencies = {k: SystemDependency.from_raw(k, v) for k, v in self.package.metadata.get('system-deps', {}).items()} - - -def _convert_manifest(raw_manifest: manifest.Manifest, subdir: str, path: str = '') -> Manifest: - return Manifest( - Package.from_raw(raw_manifest['package']), - {k: Dependency.from_raw(k, v) for k, v in raw_manifest.get('dependencies', {}).items()}, - {k: Dependency.from_raw(k, v) for k, v in raw_manifest.get('dev-dependencies', {}).items()}, - {k: Dependency.from_raw(k, v) for k, v in raw_manifest.get('build-dependencies', {}).items()}, - Library.from_raw(raw_manifest.get('lib', {}), raw_manifest['package']['name']), - [Binary.from_raw(b) for b in raw_manifest.get('bin', {})], - [Test.from_raw(b) for b in raw_manifest.get('test', {})], - [Benchmark.from_raw(b) for b in raw_manifest.get('bench', {})], - [Example.from_raw(b) for b in raw_manifest.get('example', {})], - raw_manifest.get('features', {}), - {k: {k2: Dependency.from_raw(k2, v2) for k2, v2 in v.get('dependencies', {}).items()} - for k, v in raw_manifest.get('target', {}).items()}, - path, - ) - - -def _version_to_api(version: str) -> str: - # x.y.z -> x - # 0.x.y -> 0.x - # 0.0.x -> 0 - vers = version.split('.') - if int(vers[0]) != 0: - return vers[0] - elif len(vers) >= 2 and int(vers[1]) != 0: - return f'0.{vers[1]}' - return '0' - - -def _dependency_name(package_name: str, api: str) -> str: - basename = package_name[:-3] if package_name.endswith('-rs') else package_name - return f'{basename}-{api}-rs' +def _dependency_name(package_name: str, api: str, suffix: str = '-rs') -> str: + basename = package_name[:-len(suffix)] if package_name.endswith(suffix) else package_name + return f'{basename}-{api}{suffix}' def _dependency_varname(package_name: str) -> str: @@ -458,13 +49,13 @@ def _extra_deps_varname() -> str: return 'extra_deps' +@dataclasses.dataclass class PackageState: - def __init__(self, manifest: Manifest, downloaded: bool) -> None: - self.manifest = manifest - self.downloaded = downloaded - self.features: T.Set[str] = set() - self.required_deps: T.Set[str] = set() - self.optional_deps_features: T.Dict[str, T.Set[str]] = collections.defaultdict(set) + manifest: Manifest + downloaded: bool = False + features: T.Set[str] = dataclasses.field(default_factory=set) + required_deps: T.Set[str] = dataclasses.field(default_factory=set) + optional_deps_features: T.Dict[str, T.Set[str]] = dataclasses.field(default_factory=lambda: collections.defaultdict(set)) @dataclasses.dataclass(frozen=True) @@ -476,10 +67,16 @@ class PackageKey: class Interpreter: def __init__(self, env: Environment) -> None: self.environment = env + self.host_rustc = T.cast('RustCompiler', self.environment.coredata.compilers[MachineChoice.HOST]['rust']) # Map Cargo.toml's subdir to loaded manifest. self.manifests: T.Dict[str, Manifest] = {} # Map of cargo package (name + api) to its state self.packages: T.Dict[PackageKey, PackageState] = {} + # Rustc's config + self.cfgs = self._get_cfgs() + + def get_build_def_files(self) -> T.List[str]: + return [os.path.join(subdir, 'Cargo.toml') for subdir in self.manifests] def interpret(self, subdir: str) -> mparser.CodeBlockNode: manifest = self._load_manifest(subdir) @@ -503,9 +100,7 @@ class Interpreter: ast += self._create_dependencies(pkg, build) ast += self._create_meson_subdir(build) - # Libs are always auto-discovered and there's no other way to handle them, - # which is unfortunate for reproducability - if os.path.exists(os.path.join(self.environment.source_dir, subdir, pkg.manifest.path, pkg.manifest.lib.path)): + if pkg.manifest.lib: for crate_type in pkg.manifest.lib.crate_type: ast.extend(self._create_lib(pkg, build, crate_type)) @@ -526,6 +121,10 @@ class Interpreter: self.environment.wrap_resolver.wraps[meson_depname].type is not None pkg = PackageState(manifest, downloaded) self.packages[key] = pkg + # Merge target specific dependencies that are enabled + for condition, dependencies in manifest.target.items(): + if cfg.eval_cfg(condition, self.cfgs): + manifest.dependencies.update(dependencies) # Fetch required dependencies recursively. for depname, dep in manifest.dependencies.items(): if not dep.optional: @@ -538,11 +137,12 @@ class Interpreter: def _load_manifest(self, subdir: str) -> Manifest: manifest_ = self.manifests.get(subdir) if not manifest_: - filename = os.path.join(self.environment.source_dir, subdir, 'Cargo.toml') - raw = load_toml(filename) - if 'package' in raw: - raw_manifest = T.cast('manifest.Manifest', raw) - manifest_ = _convert_manifest(raw_manifest, subdir) + path = os.path.join(self.environment.source_dir, subdir) + filename = os.path.join(path, 'Cargo.toml') + toml = load_toml(filename) + if 'package' in toml: + raw_manifest = T.cast('raw.Manifest', toml) + manifest_ = Manifest.from_raw(raw_manifest, path) self.manifests[subdir] = manifest_ else: raise MesonException(f'{subdir}/Cargo.toml does not have [package] section') @@ -599,6 +199,23 @@ class Interpreter: else: self._enable_feature(pkg, f) + def _get_cfgs(self) -> T.Dict[str, str]: + cfgs = self.host_rustc.get_cfgs().copy() + rustflags = self.environment.coredata.get_external_args(MachineChoice.HOST, 'rust') + rustflags_i = iter(rustflags) + for i in rustflags_i: + if i == '--cfg': + cfgs.append(next(rustflags_i)) + return dict(self._split_cfg(i) for i in cfgs) + + @staticmethod + def _split_cfg(cfg: str) -> T.Tuple[str, str]: + pair = cfg.split('=', maxsplit=1) + value = pair[1] if len(pair) > 1 else '' + if value and value[0] == '"': + value = value[1:-1] + return pair[0], value + def _create_project(self, pkg: PackageState, build: builder.Builder) -> T.List[mparser.BaseNode]: """Create the project() function call @@ -608,6 +225,7 @@ class Interpreter: """ default_options: T.List[mparser.BaseNode] = [] default_options.append(build.string(f'rust_std={pkg.manifest.package.edition}')) + default_options.append(build.string(f'build.rust_std={pkg.manifest.package.edition}')) if pkg.downloaded: default_options.append(build.string('warning_level=0')) @@ -643,8 +261,9 @@ class Interpreter: return ast def _create_system_dependency(self, name: str, dep: SystemDependency, build: builder.Builder) -> T.List[mparser.BaseNode]: + # TODO: handle feature_overrides kw = { - 'version': build.array([build.string(s) for s in dep.version]), + 'version': build.array([build.string(s) for s in dep.meson_version]), 'required': build.bool(not dep.optional), } varname = f'{fixup_meson_varname(name)}_system_dep' @@ -671,7 +290,7 @@ class Interpreter: def _create_dependency(self, dep: Dependency, build: builder.Builder) -> T.List[mparser.BaseNode]: pkg = self._dep_package(dep) kw = { - 'version': build.array([build.string(s) for s in dep.version]), + 'version': build.array([build.string(s) for s in dep.meson_version]), } # Lookup for this dependency with the features we want in default_options kwarg. # @@ -747,7 +366,7 @@ class Interpreter: build.block([build.function('subdir', [build.string('meson')])])) ] - def _create_lib(self, pkg: PackageState, build: builder.Builder, crate_type: manifest.CRATE_TYPE) -> T.List[mparser.BaseNode]: + def _create_lib(self, pkg: PackageState, build: builder.Builder, crate_type: raw.CRATE_TYPE) -> T.List[mparser.BaseNode]: dependencies: T.List[mparser.BaseNode] = [] dependency_map: T.Dict[mparser.BaseNode, mparser.BaseNode] = {} for name in pkg.required_deps: @@ -780,6 +399,9 @@ class Interpreter: 'rust_args': build.array(rust_args), } + depname_suffix = '-rs' if crate_type in {'lib', 'rlib', 'proc-macro'} else f'-{crate_type}' + depname = _dependency_name(pkg.manifest.package.name, pkg.manifest.package.api, depname_suffix) + lib: mparser.BaseNode if pkg.manifest.lib.proc_macro or crate_type == 'proc-macro': lib = build.method('proc_macro', build.identifier('rust'), posargs, kwargs) @@ -812,7 +434,8 @@ class Interpreter: 'link_with': build.identifier('lib'), 'variables': build.dict({ build.string('features'): build.string(','.join(pkg.features)), - }) + }), + 'version': build.string(pkg.manifest.package.version), }, ), 'dep' @@ -821,7 +444,7 @@ class Interpreter: 'override_dependency', build.identifier('meson'), [ - build.string(_dependency_name(pkg.manifest.package.name, pkg.manifest.package.api)), + build.string(depname), build.identifier('dep'), ], ), @@ -835,24 +458,23 @@ def load_wraps(source_dir: str, subproject_dir: str) -> T.List[PackageDefinition filename = os.path.join(source_dir, 'Cargo.lock') if os.path.exists(filename): try: - cargolock = T.cast('manifest.CargoLock', load_toml(filename)) + toml = load_toml(filename) except TomlImplementationMissing as e: mlog.warning('Failed to load Cargo.lock:', str(e), fatal=False) return wraps - for package in cargolock['package']: - name = package['name'] - version = package['version'] - subp_name = _dependency_name(name, _version_to_api(version)) - source = package.get('source') - if source is None: + raw_cargolock = T.cast('raw.CargoLock', toml) + cargolock = CargoLock.from_raw(raw_cargolock) + for package in cargolock.package: + subp_name = _dependency_name(package.name, version.api(package.version)) + if package.source is None: # This is project's package, or one of its workspace members. pass - elif source == 'registry+https://github.com/rust-lang/crates.io-index': - checksum = package.get('checksum') + elif package.source == 'registry+https://github.com/rust-lang/crates.io-index': + checksum = package.checksum if checksum is None: - checksum = cargolock['metadata'][f'checksum {name} {version} ({source})'] - url = f'https://crates.io/api/v1/crates/{name}/{version}/download' - directory = f'{name}-{version}' + checksum = cargolock.metadata[f'checksum {package.name} {package.version} ({package.source})'] + url = f'https://crates.io/api/v1/crates/{package.name}/{package.version}/download' + directory = f'{package.name}-{package.version}' wraps.append(PackageDefinition.from_values(subp_name, subproject_dir, 'file', { 'directory': directory, 'source_url': url, @@ -860,18 +482,18 @@ def load_wraps(source_dir: str, subproject_dir: str) -> T.List[PackageDefinition 'source_hash': checksum, 'method': 'cargo', })) - elif source.startswith('git+'): - parts = urllib.parse.urlparse(source[4:]) + elif package.source.startswith('git+'): + parts = urllib.parse.urlparse(package.source[4:]) query = urllib.parse.parse_qs(parts.query) branch = query['branch'][0] if 'branch' in query else '' revision = parts.fragment or branch url = urllib.parse.urlunparse(parts._replace(params='', query='', fragment='')) wraps.append(PackageDefinition.from_values(subp_name, subproject_dir, 'git', { - 'directory': name, + 'directory': package.name, 'url': url, 'revision': revision, 'method': 'cargo', })) else: - mlog.warning(f'Unsupported source URL in {filename}: {source}') + mlog.warning(f'Unsupported source URL in {filename}: {package.source}') return wraps |