cargo: parse [patch] table

While at it, run patch validation after building the set of dependencies.
Otherwise, it is not able to match patches against dependencies and will
incorrectly warn.

Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
This commit is contained in:
Paolo Bonzini authored and Paolo Bonzini committed 2026-09-28 14:30:48 +02:00
1 parent 521502c336
commit de8aa318ec
3 files changed
+90 -55

No files matched your search

+10 -8
View File
@@ -25,7 +25,6 @@ from .cfg import eval_cfg
from .toml import load_toml
from .manifest import (
Manifest, CargoLock, CargoLockPackage, Workspace, fixup_meson_varname,
validate_patch,
)
from ..mesonlib import (
as_posix, is_parent_path, late_property, lazy_property, MesonException,
@@ -324,12 +323,9 @@ class Interpreter:
ws = self._get_workspace(manifest, subdir, extra_members, False)
if is_root:
self.root_workspace = ws
# [patch] only takes effect in the top-level Cargo.toml
for warning in validate_patch(ws.workspace.patch, ws.packages_to_member):
mlog.warning(warning)
self.profiles = ws.workspace.profile
self._prepare_entry_point(ws)
ws.workspace.validate_patches(ws.packages_to_member)
return ws
def _prepare_entry_point(self, ws: WorkspaceState) -> None:
@@ -522,7 +518,9 @@ class Interpreter:
if ws:
return ws
workspace = manifest if isinstance(manifest, Workspace) else \
Workspace(root_package=manifest, members=['.'], default_members=['.'])
Workspace(root_package=manifest, members=['.'], default_members=['.'],
patches=manifest.patches,
manifest_path=os.path.join(self.environment.source_dir, subdir))
ws = WorkspaceState(workspace, self.environment.source_dir, subdir,
downloaded=downloaded)
if workspace.root_package:
@@ -745,10 +743,14 @@ class Interpreter:
raise MesonException(f'could not load {subdir}/Cargo.toml: {e}')
self.build_def_files.append(filename)
# [patch] always comes from the top-level Cargo.toml
patches = self.root_workspace.workspace.patches if self.workspaces else None
if 'workspace' in raw_manifest:
manifest_ = Workspace.from_raw(raw_manifest, path)
manifest_ = Workspace.from_raw(raw_manifest, path, patches)
elif 'package' in raw_manifest:
manifest_ = Manifest.from_raw(raw_manifest, path, workspace, member_path)
if workspace is not None:
patches = workspace.patches
manifest_ = Manifest.from_raw(raw_manifest, path, workspace, member_path, patches)
else:
raise MesonException(f'{subdir}/Cargo.toml does not have [package] or [workspace] section')
self.manifests[subdir] = manifest_
+53 -31
View File
@@ -615,9 +615,8 @@ class Manifest:
lints: T.List[Lint] = dataclasses.field(default_factory=list)
profile: T.Dict[str, Profile] = dataclasses.field(default_factory=dict)
# Kept in raw form: Meson does not implement [patch], it only validates it
# for the entry-point crate (see validate_patch).
patch: object = None
# Only [patch.crates-io] is supported.
patches: T.Dict[str, Dependency] = dataclasses.field(default_factory=dict)
def __post_init__(self) -> None:
self.features.setdefault('default', [])
@@ -663,7 +662,10 @@ 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:
def from_raw(cls, raw: raw.Manifest, path: str, workspace: T.Optional[Workspace] = None,
member_path: str = '', patches: T.Optional[T.Dict[str, Dependency]] = None) -> Self:
if patches is None:
patches = _parse_patches(raw.get('patch'), path)
pkg = Package.from_raw(raw['package'], workspace)
autolib = None
@@ -696,7 +698,7 @@ class Manifest:
manifest = _raw_to_dataclass(raw, cls, f'Cargo.toml package {pkg.name}',
raw_from_workspace=workspace.inheritable if workspace else None,
ignored_fields=['badges', 'workspace', 'target'],
ignored_fields=['badges', 'patch', 'workspace', 'target'],
package=ConvertValue(lambda _: pkg),
dependencies=ConvertValue(dependencies_from_raw),
dev_dependencies=ConvertValue(dependencies_from_raw),
@@ -723,6 +725,7 @@ class Manifest:
dep = Dependency.from_raw(name, v, member_path, workspace, target=condition)
deps.setdefault(name, []).append(dep)
manifest.patches = patches
return manifest
@@ -747,8 +750,21 @@ class Workspace:
# A workspace can also have a root package.
root_package: T.Optional[Manifest] = None
# Top-level [patch] table, kept in raw form (see Manifest.validate_patch).
patch: object = None
# Only [patch.crates-io] is supported.
patches: T.Dict[str, Dependency] = dataclasses.field(default_factory=dict)
manifest_path: str = ''
def iter_patch_paths(self) -> T.Iterator[T.Tuple[str, str]]:
for name, dep in self.patches.items():
if not dep.path:
mlog.warning(f'[patch.crates-io] entry {name!r} is not a path dependency')
continue
yield name, as_posix(dep.path, relative_to=self.manifest_path)
def validate_patches(self, resolved: T.Mapping[str, str]) -> None:
for name, path in self.iter_patch_paths():
if resolved.get(name) != path:
mlog.warning(f'[patch.crates-io] entry {name!r} is not for a dependency')
@lazy_property
def inheritable(self) -> T.Dict[str, object]:
@@ -771,14 +787,18 @@ class Workspace:
any(is_parent_path(ex, path) for ex in self.exclude)
@classmethod
def from_raw(cls, raw: raw.Manifest, path: str) -> Self:
def from_raw(cls, raw: raw.Manifest, path: str,
patches: T.Optional[T.Dict[str, Dependency]] = None) -> Self:
if patches is None:
patches = _parse_patches(raw.get('patch'), path)
ws = _raw_to_dataclass(raw['workspace'], cls, 'Workspace')
ws.manifest_path = path
ws.patches = patches
if 'package' in raw:
ws.root_package = Manifest.from_raw(raw, path, ws, '.')
ws.patch = ws.root_package.patch
ws.root_package = Manifest.from_raw(raw, path, ws, '.', ws.patches)
ws.profile = ws.root_package.profile
else:
ws.patch = raw.get('patch')
ws.profile = {k: Profile.from_raw(v) for k, v in raw.get('profile', {}).items()}
ws.members = list(as_posix(m) for m in ws.members)
@@ -868,25 +888,27 @@ class CargoLock:
package=ConvertValue(lambda x: [CargoLockPackage.from_raw(p) for p in x]))
def validate_patch(patch: object, resolved: T.Mapping[str, str]) -> T.Iterator[str]:
# Meson does not implement [patch]; recognize entries that simply point
# at a package Meson already builds from the workspace ('resolved' maps
# each such package name to its path), and warn for everything else.
# This is only done for the top-level Cargo.toml, since Cargo ignores
# [patch] in a dependency's manifest too.
def _parse_patches(patch: object, path: str) -> T.Dict[str, Dependency]:
"""Parse supported top-level [patch] entries and warn about the rest."""
if not patch:
return
return {}
if not isinstance(patch, dict):
yield '[patch] format not recognized'
elif list(patch.keys()) != ['crates-io']:
yield 'Found [patch] for a registry other than crates.io'
elif not isinstance(patch['crates-io'], dict):
yield '[patch.crates-io] format not recognized'
else:
for name, value in patch['crates-io'].items():
if not isinstance(value, dict) or not isinstance(value.get('path'), str):
yield f'Unrecognized [patch.crates-io] entry {name!r}'
elif resolved.get(name) == as_posix(value['path']):
continue
else:
yield f'[patch.crates-io] entry {name!r} is not for a dependency'
mlog.warning('[patch] format not recognized')
return {}
if any(registry != 'crates-io' for registry in patch):
mlog.warning('Found [patch] for a registry other than crates.io')
crates_io = patch.get('crates-io')
if crates_io is None:
return {}
if not isinstance(crates_io, dict):
mlog.warning('[patch.crates-io] format not recognized')
return {}
result: T.Dict[str, Dependency] = {}
for name, value in crates_io.items():
dep = Dependency.from_raw(name, value)
# Anchor to the directory of the Cargo.toml that has the [patch] table.
if dep.path is not None:
dep.path = as_posix(path, dep.path)
result[name] = dep
return result
+27 -16
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import unittest
from unittest import mock
import os
import tempfile
import textwrap
@@ -11,7 +12,7 @@ import typing as T
from mesonbuild.cargo import cfg
from mesonbuild.cargo.cfg import TokenType
from mesonbuild.cargo.interpreter import load_cargo_lock
from mesonbuild.cargo.manifest import Dependency, Lint, Manifest, Package, Workspace, validate_patch
from mesonbuild.cargo.manifest import Dependency, Lint, Manifest, Package, Workspace
from mesonbuild.cargo.toml import load_toml
from mesonbuild.cargo.version import api, cargo_parse, SemVer
from mesonbuild.mesonlib import MachineChoice, MesonException
@@ -830,22 +831,32 @@ class CargoTomlTest(unittest.TestCase):
'member': {'version': '1', 'path': 'vendor/member'},
}}
# Empty or absent [patch]
self.assertFalse(list(validate_patch(None, resolved)))
self.assertFalse(list(validate_patch({}, resolved)))
with mock.patch('mesonbuild.cargo.manifest.mlog.warning') as warning:
def validate_patch(raw_patch: object, packages: T.Dict[str, str]) -> T.List[object]:
warning.reset_mock()
workspace = Workspace.from_raw({'workspace': {}, 'patch': raw_patch}, '')
workspace.validate_patches(packages)
return warning.call_args_list
# Invalid [patch] tables
self.assertTrue(list(validate_patch({'crates-io': {}, 'https://example.com': {}}, resolved)))
self.assertTrue(list(validate_patch({'crates-io': 'nonsense'}, resolved)))
self.assertTrue(list(validate_patch('nonsense', resolved)))
# Empty or absent [patch]
self.assertFalse(validate_patch(None, resolved))
self.assertFalse(validate_patch({}, resolved))
# Invalid entries
self.assertTrue(list(validate_patch({'crates-io': {'cxx': {'git': 'https://example.com'}}}, resolved)))
self.assertTrue(list(validate_patch({'crates-io': {'cxx': '1.0'}}, resolved)))
# Invalid [patch] tables
self.assertTrue(validate_patch({'crates-io': {}, 'https://example.com': {}}, resolved))
self.assertTrue(validate_patch({'crates-io': 'nonsense'}, resolved))
self.assertTrue(validate_patch('nonsense', resolved))
# Unknown crate
self.assertTrue(list(validate_patch({'crates-io': {'unknown': {'path': 'foo'}}}, resolved)))
# Invalid entries
self.assertTrue(validate_patch(
{'crates-io': {'cxx': {'git': 'https://example.com'}}}, resolved))
self.assertTrue(validate_patch({'crates-io': {'cxx': '1.0'}}, resolved))
# The only entries that don't warn point at a package that Meson already builds
self.assertFalse(list(validate_patch(patch, resolved)))
self.assertTrue(list(validate_patch({'crates-io': {'cxx-build': {'path': 'elsewhere'}}}, resolved)))
# Unknown crate
self.assertTrue(validate_patch(
{'crates-io': {'unknown': {'path': 'foo'}}}, resolved))
# The only entries that don't warn point at a package that Meson already builds
self.assertFalse(validate_patch(patch, resolved))
self.assertTrue(validate_patch(
{'crates-io': {'cxx-build': {'path': 'elsewhere'}}}, resolved))