diff options
| -rw-r--r-- | mesonbuild/cargo/manifest.py | 344 | ||||
| -rw-r--r-- | unittests/cargotests.py | 13 |
2 files changed, 183 insertions, 174 deletions
diff --git a/mesonbuild/cargo/manifest.py b/mesonbuild/cargo/manifest.py index 7cbbed076..ca3c3b11c 100644 --- a/mesonbuild/cargo/manifest.py +++ b/mesonbuild/cargo/manifest.py @@ -10,6 +10,7 @@ import dataclasses import os import typing as T + from . import version from ..mesonlib import MesonException, lazy_property, Version from .. import mlog @@ -26,7 +27,6 @@ if T.TYPE_CHECKING: __dataclass_fields__: T.ClassVar[dict[str, dataclasses.Field[T.Any]]] _DI = T.TypeVar('_DI', bound='DataclassInstance') -_R = T.TypeVar('_R', bound='raw._BaseBuildTarget') _EXTRA_KEYS_WARNING = ( "This may (unlikely) be an error in the cargo manifest, or may be a missing " @@ -46,10 +46,45 @@ def fixup_meson_varname(name: str) -> str: return name.replace('-', '_') -def _raw_to_dataclass(raw: T.Mapping[str, object], cls: T.Type[_DI], - msg: str, **kwargs: T.Callable[[T.Any], object]) -> _DI: - """Fixup raw cargo mappings to ones more suitable for python to consume as dataclass. +class DefaultValue: + """Base class to converts a raw value from cargo manifest to a meson value + + It returns the value from current manifest, or fallback to the + workspace value. If both are None, its default value is used. Subclasses can + override the convert() method to implement custom conversion logic. + """ + + def __init__(self, default: object = None) -> None: + self.default = default + + def convert(self, v: T.Any, ws_v: T.Any) -> object: + return v if v is not None else ws_v + + +class MergeValue(DefaultValue): + def __init__(self, func: T.Callable[[T.Any, T.Any], object], default: object = None) -> None: + super().__init__(default) + self.func = func + + def convert(self, v: T.Any, ws_v: T.Any) -> object: + return self.func(v, ws_v) + +class ConvertValue(DefaultValue): + def __init__(self, func: T.Callable[[T.Any], object], default: object = None) -> None: + super().__init__(default) + self.func = func + + def convert(self, v: T.Any, ws_v: T.Any) -> object: + return self.func(v if v is not None else ws_v) + + +def _raw_to_dataclass(raw: T.Mapping[str, object], cls: T.Type[_DI], msg: str, + raw_from_workspace: T.Optional[T.Mapping[str, object]] = None, + **kwargs: DefaultValue) -> _DI: + """Fixup raw cargo mappings to a dataclass. + + * Inherit values from the workspace. * Replaces any `-` with `_` in the keys. * Optionally pass values through the functions in kwargs, in order to do recursive conversions. @@ -60,72 +95,64 @@ def _raw_to_dataclass(raw: T.Mapping[str, object], cls: T.Type[_DI], 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 raw: 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. + :param raw_from_workspace: If inheriting from a workspace, the raw data from the workspace. + :param kwargs: DefaultValue instances to convert values. + :return: A @cls instance. """ new_dict = {} unexpected = set() fields = {x.name for x in dataclasses.fields(cls)} + raw_from_workspace = raw_from_workspace or {} + inherit = raw.get('workspace', False) + for orig_k, v in raw.items(): + if orig_k == 'workspace': + continue + ws_v = None + if isinstance(v, dict) and v.get('workspace', False): + # foo.workspace = true, take value from workspace. + ws_v = raw_from_workspace[orig_k] + v = None + elif inherit: + # foo = {}, give the workspace value, if any, to the converter + # function in the case it wants to merge values. + ws_v = raw_from_workspace.get(orig_k) k = fixup_meson_varname(orig_k) if k not in fields: unexpected.add(orig_k) continue if k in kwargs: - v = kwargs[k](v) - new_dict[k] = v + new_dict[k] = kwargs[k].convert(v, ws_v) + else: + new_dict[k] = v if v is not None else ws_v + + if inherit: + # Inherit any keys from the workspace that we don't have yet. + for orig_k, ws_v in raw_from_workspace.items(): + k = fixup_meson_varname(orig_k) + if k not in fields: + unexpected.add(orig_k) + continue + if k in new_dict: + continue + if k in kwargs: + new_dict[k] = kwargs[k].convert(None, ws_v) + else: + new_dict[k] = ws_v + + # Finally, set default values. + for k, convertor in kwargs.items(): + if k not in new_dict and convertor.default is not None: + new_dict[k] = convertor.default if unexpected: mlog.warning(msg, 'has unexpected keys', '"{}".'.format(', '.join(sorted(unexpected))), _EXTRA_KEYS_WARNING) - return cls(**new_dict) - - -@T.overload -def _inherit_from_workspace(raw: raw.Package, - raw_from_workspace: T.Optional[T.Mapping[str, object]], - msg: str, - **kwargs: T.Callable[[T.Any, T.Any], object]) -> raw.Package: ... - -@T.overload -def _inherit_from_workspace(raw: T.Union[raw.FromWorkspace, raw.Dependency], - raw_from_workspace: T.Optional[T.Mapping[str, object]], - msg: str, - **kwargs: T.Callable[[T.Any, T.Any], object]) -> raw.Dependency: ... - -def _inherit_from_workspace(raw_: T.Union[raw.FromWorkspace, raw.Package, raw.Dependency], # type: ignore[misc] - raw_from_workspace: T.Optional[T.Mapping[str, object]], - msg: str, - **kwargs: T.Callable[[T.Any, T.Any], object]) -> T.Mapping[str, object]: - # allow accesses by non-literal key below - raw = T.cast('T.Mapping[str, object]', raw_) - - if not raw_from_workspace: - if raw.get('workspace', False) or \ - any(isinstance(v, dict) and v.get('workspace', False) for v in raw): - raise MesonException(f'Cargo.toml file requests {msg} from workspace') - - return raw - result = {k: v for k, v in raw.items() if k != 'workspace'} - for k, v in raw.items(): - if isinstance(v, dict) and v.get('workspace', False): - if k in raw_from_workspace: - result[k] = raw_from_workspace[k] - if k in kwargs: - result[k] = kwargs[k](v, result[k]) - else: - del result[k] - - if raw.get('workspace', False): - for k, v in raw_from_workspace.items(): - if k not in result or k in kwargs: - if k in kwargs: - v = kwargs[k](raw.get(k), v) - result[k] = v - return result + return cls(**new_dict) @dataclasses.dataclass @@ -168,12 +195,8 @@ class Package: @classmethod def from_raw(cls, raw_pkg: raw.Package, workspace: T.Optional[Workspace] = None) -> Self: - raw_ws_pkg = None - if workspace is not None: - raw_ws_pkg = workspace.package - - raw_pkg = _inherit_from_workspace(raw_pkg, raw_ws_pkg, f'Package entry {raw_pkg["name"]}') - return _raw_to_dataclass(raw_pkg, cls, f'Package entry {raw_pkg["name"]}') + raw_ws_pkg = workspace.package if workspace else None + return _raw_to_dataclass(raw_pkg, cls, f'Package entry {raw_pkg["name"]}', raw_ws_pkg) @dataclasses.dataclass class SystemDependency: @@ -190,7 +213,7 @@ class SystemDependency: feature_overrides: T.Dict[str, T.Dict[str, str]] = dataclasses.field(default_factory=dict) @classmethod - def from_raw(cls, name: str, raw: T.Union[T.Dict[str, T.Any], str]) -> SystemDependency: + def from_raw(cls, name: str, raw: T.Union[T.Dict[str, T.Any], str]) -> Self: if isinstance(raw, str): raw = {'version': raw} name = raw.get('name', name) @@ -264,24 +287,23 @@ class Dependency: return {'version': depv} if isinstance(depv, str) else depv @classmethod - def from_raw_dict(cls, name: str, raw_dep: T.Union[raw.FromWorkspace, raw.Dependency], member_path: str = '', raw_ws_dep: T.Optional[raw.Dependency] = None) -> Dependency: - raw_dep = _inherit_from_workspace(raw_dep, raw_ws_dep, - f'Dependency entry {name}', - path=lambda pkg_path, ws_path: os.path.relpath(ws_path, member_path), - features=lambda pkg_path, ws_path: (pkg_path or []) + (ws_path or [])) - raw_dep.setdefault('package', name) - return _raw_to_dataclass(raw_dep, cls, f'Dependency entry {name}') - - @classmethod - def from_raw(cls, name: str, raw_depv: T.Union[raw.FromWorkspace, raw.DependencyV], member_path: str = '', workspace: T.Optional[Workspace] = None) -> Dependency: + def from_raw(cls, name: str, raw_depv: T.Union[raw.FromWorkspace, raw.DependencyV], member_path: str = '', workspace: T.Optional[Workspace] = None) -> Self: """Create a dependency from a raw cargo dictionary or string""" - raw_ws_dep: T.Optional[raw.Dependency] = None - if workspace is not None: - raw_ws_depv = workspace.dependencies.get(name, {}) - raw_ws_dep = cls._depv_to_dep(raw_ws_depv) - + raw_ws_dep = workspace.dependencies.get(name) if workspace else None + raw_ws_dep = cls._depv_to_dep(raw_ws_dep or {}) raw_dep = cls._depv_to_dep(raw_depv) - return cls.from_raw_dict(name, raw_dep, member_path, raw_ws_dep) + + def path_convertor(path: T.Optional[str], ws_path: T.Optional[str]) -> T.Optional[str]: + if path: + return path + if ws_path: + return os.path.relpath(ws_path, member_path) + return None + + return _raw_to_dataclass(raw_dep, cls, f'Dependency entry {name}', raw_ws_dep, + package=DefaultValue(name), + path=MergeValue(path_convertor), + features=MergeValue(lambda features, ws_features: (features or []) + (ws_features or []))) def update_version(self, v: str) -> None: self.version = v @@ -296,114 +318,99 @@ class Dependency: @dataclasses.dataclass -class BuildTarget(T.Generic[_R]): +class BuildTarget: + # https://doc.rust-lang.org/cargo/reference/cargo-targets.html + # Some default values are overridden in subclasses name: str path: str - crate_type: T.List[CRATE_TYPE] - - # https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-test-field - # True for lib, bin, test + edition: EDITION 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 + doctest: bool = True bench: bool = True - - # https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-doc-field - # True for libraries and binaries - doc: bool = False - + doc: bool = True harness: bool = True - edition: EDITION = '2015' + crate_type: T.List[CRATE_TYPE] = dataclasses.field(default_factory=lambda: ['bin']) required_features: T.List[str] = dataclasses.field(default_factory=list) plugin: bool = False - @classmethod - def from_raw(cls, raw: _R) -> Self: - name = raw.get('name', '<anonymous>') - return _raw_to_dataclass(raw, cls, f'Binary entry {name}') @dataclasses.dataclass -class Library(BuildTarget['raw.LibTarget']): +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[CRATE_TYPE] = dataclasses.field(default_factory=lambda: ['lib']) - doc_scrape_examples: bool = True @classmethod - def from_raw(cls, raw: raw.LibTarget, fallback_name: str) -> Self: # type: ignore[override] - # We need to set the name field if it's not set manually, including if - # other fields are set in the lib section - raw.setdefault('name', fallback_name) - return _raw_to_dataclass(raw, cls, f'Library entry {raw["name"]}') + def from_raw(cls, raw: raw.LibTarget, pkg: Package) -> Self: + name = raw.get('name', fixup_meson_varname(pkg.name)) + proc_macro = raw.get('proc-macro', False) + crate_type = 'proc-macro' if proc_macro else 'lib' + return _raw_to_dataclass(raw, cls, f'Library entry {name}', + name=DefaultValue(name), + path=DefaultValue('src/lib.rs'), + edition=DefaultValue(pkg.edition), + crate_type=DefaultValue([crate_type])) @dataclasses.dataclass -class Binary(BuildTarget['raw.BuildTarget']): +class Binary(BuildTarget): """Representation of a Cargo Bin Entry.""" - doc: bool = True - crate_type: T.List[CRATE_TYPE] = dataclasses.field(default_factory=lambda: ['bin']) - @classmethod - def from_raw(cls, raw: raw.BuildTarget) -> Self: - if 'path' not in raw: - raw['path'] = os.path.join('bin', raw['name'] + '.rs') - return super().from_raw(raw) + def from_raw(cls, raw: raw.BuildTarget, pkg: Package) -> Self: + name = raw["name"] + return _raw_to_dataclass(raw, cls, f'Binary entry {name}', + path=DefaultValue(f'bin/{name}.rs'), + edition=DefaultValue(pkg.edition)) @dataclasses.dataclass -class Test(BuildTarget['raw.BuildTarget']): +class Test(BuildTarget): """Representation of a Cargo Test Entry.""" - bench: bool = True - crate_type: T.List[CRATE_TYPE] = dataclasses.field(default_factory=lambda: ['bin']) - @classmethod - def from_raw(cls, raw: raw.BuildTarget) -> Self: - if 'path' not in raw: - raw['path'] = os.path.join('tests', raw['name'] + '.rs') - return super().from_raw(raw) + def from_raw(cls, raw: raw.BuildTarget, pkg: Package) -> Self: + name = raw["name"] + return _raw_to_dataclass(raw, cls, f'Test entry {name}', + path=DefaultValue(f'tests/{name}.rs'), + edition=DefaultValue(pkg.edition), + bench=DefaultValue(False), + doc=DefaultValue(False)) + @dataclasses.dataclass -class Benchmark(BuildTarget['raw.BuildTarget']): +class Benchmark(BuildTarget): """Representation of a Cargo Benchmark Entry.""" - test: bool = True - crate_type: T.List[CRATE_TYPE] = dataclasses.field(default_factory=lambda: ['bin']) - @classmethod - def from_raw(cls, raw: raw.BuildTarget) -> Self: - if 'path' not in raw: - raw['path'] = os.path.join('benches', raw['name'] + '.rs') - return super().from_raw(raw) + def from_raw(cls, raw: raw.BuildTarget, pkg: Package) -> Self: + name = raw["name"] + return _raw_to_dataclass(raw, cls, f'Benchmark entry {name}', + path=DefaultValue(f'benches/{name}.rs'), + edition=DefaultValue(pkg.edition), + test=DefaultValue(False), + doc=DefaultValue(False)) @dataclasses.dataclass -class Example(BuildTarget['raw.BuildTarget']): +class Example(BuildTarget): """Representation of a Cargo Example Entry.""" - crate_type: T.List[CRATE_TYPE] = dataclasses.field(default_factory=lambda: ['bin']) - @classmethod - def from_raw(cls, raw: raw.BuildTarget) -> Self: - if 'path' not in raw: - raw['path'] = os.path.join('examples', raw['name'] + '.rs') - return super().from_raw(raw) + def from_raw(cls, raw: raw.BuildTarget, pkg: Package) -> Self: + name = raw["name"] + return _raw_to_dataclass(raw, cls, f'Example entry {name}', + path=DefaultValue(f'examples/{name}.rs'), + edition=DefaultValue(pkg.edition), + test=DefaultValue(False), + bench=DefaultValue(False), + doc=DefaultValue(False)) @dataclasses.dataclass @@ -432,8 +439,6 @@ class Manifest: features: T.Dict[str, T.List[str]] = dataclasses.field(default_factory=dict) target: T.Dict[str, T.Dict[str, Dependency]] = dataclasses.field(default_factory=dict) - path: str = '' - def __post_init__(self) -> None: self.features.setdefault('default', []) @@ -442,27 +447,27 @@ class Manifest: return {k: SystemDependency.from_raw(k, v) for k, v in self.package.metadata.get('system-deps', {}).items()} @classmethod - def from_raw(cls, raw: raw.Manifest, path: str = '', workspace: T.Optional[Workspace] = None, member_path: str = '') -> Self: - # Libs are always auto-discovered and there's no other way to handle them, - # which is unfortunate for reproducability + def from_raw(cls, raw: raw.Manifest, path: str, workspace: T.Optional[Workspace] = None, member_path: str = '') -> Self: pkg = Package.from_raw(raw['package'], workspace) - if pkg.autolib and 'lib' not in raw and \ - os.path.exists(os.path.join(path, 'src/lib.rs')): - raw['lib'] = {} - fixed = _raw_to_dataclass(raw, cls, f'Cargo.toml package {raw["package"]["name"]}', - package=lambda x: pkg, - dependencies=lambda x: {k: Dependency.from_raw(k, v, member_path, workspace) for k, v in x.items()}, - dev_dependencies=lambda x: {k: Dependency.from_raw(k, v, member_path, workspace) for k, v in x.items()}, - build_dependencies=lambda x: {k: Dependency.from_raw(k, v, member_path, workspace) for k, v in x.items()}, - lib=lambda x: Library.from_raw(x, raw['package']['name']), - bin=lambda x: [Binary.from_raw(b) for b in x], - test=lambda x: [Test.from_raw(b) for b in x], - bench=lambda x: [Benchmark.from_raw(b) for b in x], - example=lambda x: [Example.from_raw(b) for b in x], - target=lambda x: {k: {k2: Dependency.from_raw(k2, v2, member_path, workspace) for k2, v2 in v.get('dependencies', {}).items()} - for k, v in x.items()}) - fixed.path = path - return fixed + + autolib = None + if pkg.autolib and os.path.exists(os.path.join(path, 'src/lib.rs')): + autolib = Library.from_raw({}, pkg) + + def dependencies_from_raw(x: T.Dict[str, T.Any]) -> T.Dict[str, Dependency]: + return {k: Dependency.from_raw(k, v, member_path, workspace) for k, v in x.items()} + + return _raw_to_dataclass(raw, cls, f'Cargo.toml package {pkg.name}', + package=ConvertValue(lambda _: pkg), + dependencies=ConvertValue(dependencies_from_raw), + dev_dependencies=ConvertValue(dependencies_from_raw), + build_dependencies=ConvertValue(dependencies_from_raw), + lib=ConvertValue(lambda x: Library.from_raw(x, pkg), default=autolib), + bin=ConvertValue(lambda x: [Binary.from_raw(b, pkg) for b in x]), + test=ConvertValue(lambda x: [Test.from_raw(b, pkg) for b in x]), + bench=ConvertValue(lambda x: [Benchmark.from_raw(b, pkg) for b in x]), + example=ConvertValue(lambda x: [Example.from_raw(b, pkg) for b in x]), + target=ConvertValue(lambda x: {k: dependencies_from_raw(v.get('dependencies', {})) for k, v in x.items()})) @dataclasses.dataclass @@ -476,7 +481,7 @@ class Workspace: exclude: T.List[str] = dataclasses.field(default_factory=list) default_members: T.List[str] = dataclasses.field(default_factory=list) - # inheritable settings are kept in raw format, for use with _inherit_from_workspace + # inheritable settings are kept in raw format, for use with _raw_to_dataclass package: T.Optional[raw.Package] = None dependencies: T.Dict[str, raw.Dependency] = dataclasses.field(default_factory=dict) lints: T.Dict[str, T.Any] = dataclasses.field(default_factory=dict) @@ -486,7 +491,7 @@ class Workspace: root_package: T.Optional[Manifest] = None @classmethod - def from_raw(cls, raw: raw.Manifest, path: str) -> Workspace: + def from_raw(cls, raw: raw.Manifest, path: str) -> Self: ws = _raw_to_dataclass(raw['workspace'], cls, 'Workspace') if 'package' in raw: ws.root_package = Manifest.from_raw(raw, path, ws, '.') @@ -513,7 +518,7 @@ class CargoLockPackage: return f'{self.name}-{self.api}-rs' @classmethod - def from_raw(cls, raw: raw.CargoLockPackage) -> CargoLockPackage: + def from_raw(cls, raw: raw.CargoLockPackage) -> Self: return _raw_to_dataclass(raw, cls, 'Cargo.lock package') @@ -528,9 +533,9 @@ class CargoLock: wraps: T.Dict[str, PackageDefinition] = dataclasses.field(default_factory=dict) @classmethod - def from_raw(cls, raw: raw.CargoLock) -> CargoLock: + def from_raw(cls, raw: raw.CargoLock) -> Self: return _raw_to_dataclass(raw, cls, 'Cargo.lock', - package=lambda x: [CargoLockPackage.from_raw(p) for p in x]) + package=RawValueConvertor(lambda x: [CargoLockPackage.from_raw(p) for p in x])) def named(self, name: str) -> T.Sequence[CargoLockPackage]: return self._versions[name] @@ -543,3 +548,8 @@ class CargoLock: for pkg_versions in versions.values(): pkg_versions.sort(reverse=True, key=lambda pkg: Version(pkg.version)) return versions + + @classmethod + def from_raw(cls, raw: raw.CargoLock) -> Self: + return _raw_to_dataclass(raw, cls, 'Cargo.lock', + package=ConvertValue(lambda x: [CargoLockPackage.from_raw(p) for p in x])) diff --git a/unittests/cargotests.py b/unittests/cargotests.py index 89b360512..22d77b35f 100644 --- a/unittests/cargotests.py +++ b/unittests/cargotests.py @@ -416,29 +416,28 @@ class CargoTomlTest(unittest.TestCase): self.assertEqual(manifest.lib.name, 'pango') self.assertEqual(manifest.lib.crate_type, ['lib']) - self.assertEqual(manifest.lib.path, os.path.join('src', 'lib.rs')) + self.assertEqual(manifest.lib.path, 'src/lib.rs') self.assertEqual(manifest.lib.test, True) self.assertEqual(manifest.lib.doctest, True) self.assertEqual(manifest.lib.bench, True) self.assertEqual(manifest.lib.doc, True) self.assertEqual(manifest.lib.harness, True) - self.assertEqual(manifest.lib.edition, '2015') + self.assertEqual(manifest.lib.edition, '2021') self.assertEqual(manifest.lib.required_features, []) self.assertEqual(manifest.lib.plugin, False) self.assertEqual(manifest.lib.proc_macro, False) - self.assertEqual(manifest.lib.doc_scrape_examples, True) self.assertEqual(len(manifest.test), 1) self.assertEqual(manifest.test[0].name, 'check_gir') self.assertEqual(manifest.test[0].crate_type, ['bin']) self.assertEqual(manifest.test[0].path, 'tests/check_gir.rs') - self.assertEqual(manifest.lib.path, os.path.join('src', 'lib.rs')) + self.assertEqual(manifest.lib.path, 'src/lib.rs') self.assertEqual(manifest.test[0].test, True) - self.assertEqual(manifest.test[0].doctest, False) - self.assertEqual(manifest.test[0].bench, True) + self.assertEqual(manifest.test[0].doctest, True) + self.assertEqual(manifest.test[0].bench, False) self.assertEqual(manifest.test[0].doc, False) self.assertEqual(manifest.test[0].harness, True) - self.assertEqual(manifest.test[0].edition, '2015') + self.assertEqual(manifest.test[0].edition, '2021') self.assertEqual(manifest.test[0].required_features, []) self.assertEqual(manifest.test[0].plugin, False) |
