Skip to content

Commit 164c128

Browse files
bonzinidcbaker
authored andcommitted
cargo: create dataclasses for Cargo.lock
Start introducing a new simpler API for conversion of TypedDicts to dataclasses, and use it already for Cargo.lock. Signed-off-by: Paolo Bonzini <[email protected]>
1 parent 09e547f commit 164c128

File tree

3 files changed

+133
-40
lines changed

3 files changed

+133
-40
lines changed

mesonbuild/cargo/interpreter.py

Lines changed: 19 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from . import builder, version, cfg
2121
from .toml import load_toml, TomlImplementationMissing
22+
from .manifest import fixup_meson_varname, CargoLock
2223
from ..mesonlib import MesonException, MachineChoice
2324
from .. import coredata, mlog
2425
from ..wrap.wrap import PackageDefinition
@@ -48,15 +49,6 @@ class DataclassInstance(Protocol):
4849
)
4950

5051

51-
def fixup_meson_varname(name: str) -> str:
52-
"""Fixup a meson variable name
53-
54-
:param name: The name to fix
55-
:return: the fixed name
56-
"""
57-
return name.replace('-', '_')
58-
59-
6052
def _fixup_raw_mappings(d: T.Mapping[str, T.Any], convert_version: bool = True) -> T.MutableMapping[str, T.Any]:
6153
"""Fixup raw cargo mappings to ones more suitable for python to consume.
6254
@@ -135,7 +127,7 @@ class Package:
135127
api: str = dataclasses.field(init=False)
136128

137129
def __post_init__(self) -> None:
138-
self.api = _version_to_api(self.version)
130+
self.api = version.api(self.version)
139131

140132
@classmethod
141133
def from_raw(cls, raw: raw.Package) -> Self:
@@ -206,9 +198,9 @@ def __post_init__(self, name: str) -> None:
206198
api = set()
207199
for v in self.version:
208200
if v.startswith(('>=', '==')):
209-
api.add(_version_to_api(v[2:].strip()))
201+
api.add(version.api(v[2:].strip()))
210202
elif v.startswith('='):
211-
api.add(_version_to_api(v[1:].strip()))
203+
api.add(version.api(v[1:].strip()))
212204
if not api:
213205
self.api = '0'
214206
elif len(api) == 1:
@@ -367,18 +359,6 @@ def from_raw(cls, raw: raw.Manifest, path: str = '') -> Self:
367359
)
368360

369361

370-
def _version_to_api(version: str) -> str:
371-
# x.y.z -> x
372-
# 0.x.y -> 0.x
373-
# 0.0.x -> 0
374-
vers = version.split('.')
375-
if int(vers[0]) != 0:
376-
return vers[0]
377-
elif len(vers) >= 2 and int(vers[1]) != 0:
378-
return f'0.{vers[1]}'
379-
return '0'
380-
381-
382362
def _dependency_name(package_name: str, api: str, suffix: str = '-rs') -> str:
383363
basename = package_name[:-len(suffix)] if package_name.endswith(suffix) else package_name
384364
return f'{basename}-{api}{suffix}'
@@ -805,43 +785,42 @@ def load_wraps(source_dir: str, subproject_dir: str) -> T.List[PackageDefinition
805785
filename = os.path.join(source_dir, 'Cargo.lock')
806786
if os.path.exists(filename):
807787
try:
808-
cargolock = T.cast('raw.CargoLock', load_toml(filename))
788+
toml = load_toml(filename)
809789
except TomlImplementationMissing as e:
810790
mlog.warning('Failed to load Cargo.lock:', str(e), fatal=False)
811791
return wraps
812-
for package in cargolock['package']:
813-
name = package['name']
814-
version = package['version']
815-
subp_name = _dependency_name(name, _version_to_api(version))
816-
source = package.get('source')
817-
if source is None:
792+
raw_cargolock = T.cast('raw.CargoLock', toml)
793+
cargolock = CargoLock.from_raw(raw_cargolock)
794+
for package in cargolock.package:
795+
subp_name = _dependency_name(package.name, version.api(package.version))
796+
if package.source is None:
818797
# This is project's package, or one of its workspace members.
819798
pass
820-
elif source == 'registry+https://github.com/rust-lang/crates.io-index':
821-
checksum = package.get('checksum')
799+
elif package.source == 'registry+https://github.com/rust-lang/crates.io-index':
800+
checksum = package.checksum
822801
if checksum is None:
823-
checksum = cargolock['metadata'][f'checksum {name} {version} ({source})']
824-
url = f'https://crates.io/api/v1/crates/{name}/{version}/download'
825-
directory = f'{name}-{version}'
802+
checksum = cargolock.metadata[f'checksum {package.name} {package.version} ({package.source})']
803+
url = f'https://crates.io/api/v1/crates/{package.name}/{package.version}/download'
804+
directory = f'{package.name}-{package.version}'
826805
wraps.append(PackageDefinition.from_values(subp_name, subproject_dir, 'file', {
827806
'directory': directory,
828807
'source_url': url,
829808
'source_filename': f'{directory}.tar.gz',
830809
'source_hash': checksum,
831810
'method': 'cargo',
832811
}))
833-
elif source.startswith('git+'):
834-
parts = urllib.parse.urlparse(source[4:])
812+
elif package.source.startswith('git+'):
813+
parts = urllib.parse.urlparse(package.source[4:])
835814
query = urllib.parse.parse_qs(parts.query)
836815
branch = query['branch'][0] if 'branch' in query else ''
837816
revision = parts.fragment or branch
838817
url = urllib.parse.urlunparse(parts._replace(params='', query='', fragment=''))
839818
wraps.append(PackageDefinition.from_values(subp_name, subproject_dir, 'git', {
840-
'directory': name,
819+
'directory': package.name,
841820
'url': url,
842821
'revision': revision,
843822
'method': 'cargo',
844823
}))
845824
else:
846-
mlog.warning(f'Unsupported source URL in {filename}: {source}')
825+
mlog.warning(f'Unsupported source URL in {filename}: {package.source}')
847826
return wraps

mesonbuild/cargo/manifest.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,105 @@
44
"""Type definitions for cargo manifest files."""
55

66
from __future__ import annotations
7+
8+
import dataclasses
9+
import typing as T
10+
11+
from .. import mlog
12+
13+
if T.TYPE_CHECKING:
14+
from typing_extensions import Protocol
15+
16+
from . import raw
17+
18+
# Copied from typeshed. Blarg that they don't expose this
19+
class DataclassInstance(Protocol):
20+
__dataclass_fields__: T.ClassVar[dict[str, dataclasses.Field[T.Any]]]
21+
22+
_DI = T.TypeVar('_DI', bound='DataclassInstance')
23+
24+
_EXTRA_KEYS_WARNING = (
25+
"This may (unlikely) be an error in the cargo manifest, or may be a missing "
26+
"implementation in Meson. If this issue can be reproduced with the latest "
27+
"version of Meson, please help us by opening an issue at "
28+
"https://github.com/mesonbuild/meson/issues. Please include the crate and "
29+
"version that is generating this warning if possible."
30+
)
31+
32+
33+
def fixup_meson_varname(name: str) -> str:
34+
"""Fixup a meson variable name
35+
36+
:param name: The name to fix
37+
:return: the fixed name
38+
"""
39+
return name.replace('-', '_')
40+
41+
42+
def _raw_to_dataclass(raw: T.Mapping[str, object], cls: T.Type[_DI],
43+
msg: str, **kwargs: T.Callable[[T.Any], object]) -> _DI:
44+
"""Fixup raw cargo mappings to ones more suitable for python to consume as dataclass.
45+
46+
* Replaces any `-` with `_` in the keys.
47+
* Optionally pass values through the functions in kwargs, in order to do
48+
recursive conversions.
49+
* Remove and warn on keys that are coming from cargo, but are unknown to
50+
our representations.
51+
52+
This is intended to give users the possibility of things proceeding when a
53+
new key is added to Cargo.toml that we don't yet handle, but to still warn
54+
them that things might not work.
55+
56+
:param data: The raw data to look at
57+
:param cls: The Dataclass derived type that will be created
58+
:param msg: the header for the error message. Usually something like "In N structure".
59+
:param convert_version: whether to convert the version field to a Meson compatible one.
60+
:return: The original data structure, but with all unknown keys removed.
61+
"""
62+
new_dict = {}
63+
unexpected = set()
64+
fields = {x.name for x in dataclasses.fields(cls)}
65+
for orig_k, v in raw.items():
66+
k = fixup_meson_varname(orig_k)
67+
if k not in fields:
68+
unexpected.add(orig_k)
69+
continue
70+
if k in kwargs:
71+
v = kwargs[k](v)
72+
new_dict[k] = v
73+
74+
if unexpected:
75+
mlog.warning(msg, 'has unexpected keys', '"{}".'.format(', '.join(sorted(unexpected))),
76+
_EXTRA_KEYS_WARNING)
77+
return cls(**new_dict)
78+
79+
80+
@dataclasses.dataclass
81+
class CargoLockPackage:
82+
83+
"""A description of a package in the Cargo.lock file format."""
84+
85+
name: str
86+
version: str
87+
source: T.Optional[str] = None
88+
checksum: T.Optional[str] = None
89+
dependencies: T.List[str] = dataclasses.field(default_factory=list)
90+
91+
@classmethod
92+
def from_raw(cls, raw: raw.CargoLockPackage) -> CargoLockPackage:
93+
return _raw_to_dataclass(raw, cls, 'Cargo.lock package')
94+
95+
96+
@dataclasses.dataclass
97+
class CargoLock:
98+
99+
"""A description of the Cargo.lock file format."""
100+
101+
version: int = 1
102+
package: T.List[CargoLockPackage] = dataclasses.field(default_factory=list)
103+
metadata: T.Dict[str, str] = dataclasses.field(default_factory=dict)
104+
105+
@classmethod
106+
def from_raw(cls, raw: raw.CargoLock) -> CargoLock:
107+
return _raw_to_dataclass(raw, cls, 'Cargo.lock',
108+
package=lambda x: [CargoLockPackage.from_raw(p) for p in x])

mesonbuild/cargo/version.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@
77
import typing as T
88

99

10+
def api(version: str) -> str:
11+
# x.y.z -> x
12+
# 0.x.y -> 0.x
13+
# 0.0.x -> 0
14+
vers = version.split('.')
15+
if int(vers[0]) != 0:
16+
return vers[0]
17+
elif len(vers) >= 2 and int(vers[1]) != 0:
18+
return f'0.{vers[1]}'
19+
return '0'
20+
21+
1022
def convert(cargo_ver: str) -> T.List[str]:
1123
"""Convert a Cargo compatible version into a Meson compatible one.
1224

0 commit comments

Comments
 (0)