cargo: flatten [target] into the unconditional tables

This simplifies the parsing and makes it possible to handle correctly
dependency-named features, which apply equally to all dependencies,
even those that are disabled.  In other words, even if a "win"
dependency is only declared under [target.'cfg(windows)'.dependencies],
"cargo build --features win" succeeds on non-Windows platforms as well.

Due to the new representation, move the merged table into
PackageConfiguration.  This is cleaner, because it doesn't modify the
manifest, and it makes it possible to fully separate dependencies for
the host and build machines.

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 5f6eb8a7c8
commit 4b3ceff48c
4 files changed
+125 -99

No files matched your search

+32 -21
View File
@@ -64,6 +64,9 @@ def _extra_deps_varname() -> str:
class PackageConfiguration:
"""Configuration for a package during dependency resolution."""
for_machine: MachineChoice
# Dependency table of the package, with the [target.*] sections that apply
# to this machine merged in.
dependencies: T.Dict[str, Dependency] = dataclasses.field(default_factory=dict)
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))
@@ -77,11 +80,11 @@ class PackageConfiguration:
args.extend(['--cfg', f'feature="{feature}"'])
return args
def get_dependency_map(self, manifest: Manifest) -> T.Dict[str, str]:
def get_dependency_map(self) -> T.Dict[str, str]:
"""Get the rust dependency mapping for this package configuration."""
dependency_map: T.Dict[str, str] = {}
for name in sorted(self.required_deps):
dep = manifest.dependencies[name]
dep = self.dependencies[name]
dep_key = PackageKey(dep.package, dep.api)
dep_pkg = self.dep_packages[dep_key]
dep_lib_name = dep_pkg.library_name(self.for_machine)
@@ -458,7 +461,7 @@ class Interpreter:
if not cfg:
continue
for depname in cfg.required_deps:
dep = pkg.manifest.dependencies[depname]
dep = cfg.dependencies[depname]
if dep.path:
dep_member = as_posix(pkg.ws_member, dep.path)
if not ws.workspace.is_excluded(dep_member):
@@ -652,30 +655,38 @@ class Interpreter:
if pkg.cfg[machine] is not None:
return # Already prepared for this machine
pkg.cfg[machine] = PackageConfiguration(for_machine=machine)
cfg = PackageConfiguration(for_machine=machine)
pkg.cfg[machine] = cfg
# Merge target-specific dependencies that are enabled for this machine
# Keep the dependencies whose [target.*] condition holds for this machine.
# A later entry wins, so an unconditional declaration is the fallback for
# the name and a matching [target.*] one overrides it.
rustc = T.cast('RustCompiler', self.environment.coredata.compilers[machine]['rust'])
target_cfgs = self._get_cfgs(machine, pkg.get_subproject_name())
for condition, dependencies in pkg.manifest.target.items():
if condition == rustc.get_target_triple() or eval_cfg(condition, target_cfgs):
pkg.manifest.dependencies.update(dependencies)
def enabled(dep: Dependency) -> bool:
return dep.target is None or dep.target == rustc.get_target_triple() or \
eval_cfg(dep.target, target_cfgs)
cfg.dependencies = {name: dep
for name, deps in pkg.manifest.dependencies.items()
for dep in deps if enabled(dep)}
# If you specify the optional dependency with the dep: prefix anywhere in the [features]
# table, that disables the implicit feature.
deps = set(feature[4:]
for feature in itertools.chain.from_iterable(pkg.manifest.features.values())
if feature.startswith('dep:'))
for name, dep in itertools.chain(pkg.manifest.dependencies.items(),
pkg.manifest.dev_dependencies.items(),
pkg.manifest.build_dependencies.items()):
if dep.optional and name not in deps:
pkg.manifest.features.setdefault(name, [])
pkg.manifest.features[name].append(f'dep:{name}')
deps.add(name)
explicit = set(feature[4:]
for feature in itertools.chain.from_iterable(pkg.manifest.features.values())
if feature.startswith('dep:'))
# Cargo forbids [dev-dependencies] to be optional, so skip them
for table in (pkg.manifest.dependencies, pkg.manifest.build_dependencies):
for name, deps in table.items():
if any(dep.optional for dep in deps) and name not in explicit:
pkg.manifest.features.setdefault(name, [])
pkg.manifest.features[name].append(f'dep:{name}')
explicit.add(name)
# Fetch required dependencies recursively for this machine
for depname, dep in pkg.manifest.dependencies.items():
for depname, dep in cfg.dependencies.items():
if not dep.optional:
self._add_dependency(pkg, depname, machine)
@@ -741,7 +752,7 @@ class Interpreter:
cfg = pkg.cfg[machine]
if depname in cfg.required_deps:
return
dep = pkg.manifest.dependencies.get(depname)
dep = cfg.dependencies.get(depname)
if not dep:
# It could be build/dev/target dependency. Just ignore it.
return
@@ -772,7 +783,7 @@ class Interpreter:
else:
self._add_dependency(pkg, depname, machine)
if depname in cfg.required_deps:
dep = pkg.manifest.dependencies[depname]
dep = cfg.dependencies[depname]
dep_pkg = self._dep_package(pkg, dep, cfg)
# Use machines_from() to determine which machines the dependency needs
for dep_machine in dep_pkg.manifest.machines_from(machine, self.is_cross):
+49 -34
View File
@@ -25,6 +25,8 @@ if T.TYPE_CHECKING:
from ..options import ElementaryOptionValues
from ..wrap.wrap import PackageDefinition
RawTargetTables = T.Mapping[str, T.Mapping[str, T.Mapping[str, raw.FromWorkspace | raw.DependencyV | str]]]
# Copied from typeshed. Blarg that they don't expose this
class DataclassInstance(Protocol):
__dataclass_fields__: T.ClassVar[dict[str, dataclasses.Field[T.Any]]]
@@ -305,6 +307,8 @@ class Dependency:
optional: bool = False
default_features: bool = True
features: T.List[str] = dataclasses.field(default_factory=list)
# The [target.<cfg>] condition this entry was declared under, if any.
target: T.Optional[str] = None
@lazy_property
def accepts_version(self) -> T.Callable[[str], bool]:
@@ -340,7 +344,7 @@ class Dependency:
return {'version': depv} if isinstance(depv, str) else depv
@classmethod
def from_raw(cls, name: str, raw_depv: T.Union[raw.FromWorkspace, raw.DependencyV], member_path: str = '', workspace: T.Optional[Workspace] = None) -> Self:
def from_raw(cls, name: str, raw_depv: T.Union[raw.FromWorkspace, raw.DependencyV], member_path: str = '', workspace: T.Optional[Workspace] = None, target: T.Optional[str] = None) -> Self:
"""Create a dependency from a raw cargo dictionary or string"""
raw_ws_dep = workspace.dependencies.get(name) if workspace else None
raw_ws_dep = cls._depv_to_dep(raw_ws_dep or {})
@@ -353,10 +357,12 @@ class Dependency:
return as_posix(ws_path, relative_to=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 [])))
dep = _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 [])))
dep.target = target
return dep
@dataclasses.dataclass
@@ -596,16 +602,16 @@ class Manifest:
"""
package: Package
dependencies: T.Dict[str, Dependency] = dataclasses.field(default_factory=dict)
dev_dependencies: T.Dict[str, Dependency] = dataclasses.field(default_factory=dict)
build_dependencies: T.Dict[str, Dependency] = dataclasses.field(default_factory=dict)
# Note that these three fields also include the [target] sections.
dependencies: T.Dict[str, T.List[Dependency]] = dataclasses.field(default_factory=dict)
dev_dependencies: T.Dict[str, T.List[Dependency]] = dataclasses.field(default_factory=dict)
build_dependencies: T.Dict[str, T.List[Dependency]] = dataclasses.field(default_factory=dict)
lib: T.Optional[Library] = None
bin: T.Dict[str, Binary] = dataclasses.field(default_factory=dict)
test: T.List[Test] = dataclasses.field(default_factory=list)
bench: T.List[Benchmark] = dataclasses.field(default_factory=list)
example: T.List[Example] = dataclasses.field(default_factory=list)
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)
lints: T.List[Lint] = dataclasses.field(default_factory=list)
profile: T.Dict[str, Profile] = dataclasses.field(default_factory=dict)
@@ -620,11 +626,8 @@ class Manifest:
"""Every path dependency of the package, including the ones that a
[target] condition or an unused optional keeps out of the resolved
dependency graph."""
for dep in self.dependencies.values():
if dep.path:
yield dep
for deps in self.target.values():
for dep in deps.values():
for deps in self.dependencies.values():
for dep in deps:
if dep.path:
yield dep
@@ -688,27 +691,39 @@ class Manifest:
f'({autobins[bin_name].path} and {bin_path})')
autobins[bin_name] = Binary.from_raw({'name': bin_name, 'path': bin_path}, 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()}
def dependencies_from_raw(x: T.Dict[str, T.Any]) -> T.Dict[str, T.List[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}',
raw_from_workspace=workspace.inheritable if workspace else None,
ignored_fields=['badges', 'workspace'],
package=ConvertValue(lambda _: pkg),
dependencies=ConvertValue(dependencies_from_raw),
dev_dependencies=ConvertValue(dependencies_from_raw),
build_dependencies=ConvertValue(dependencies_from_raw),
lints=ConvertValue(Lint.from_raw),
lib=ConvertValue(lambda x: Library.from_raw(x, pkg), default=autolib),
bin=DictMergeValue(lambda x: [Binary.from_raw(b, pkg) for b in x],
merge_key=lambda x: x.path,
out_key=lambda x: x.name,
base=autobins),
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()}),
profile=ConvertValue(lambda x: {k: Profile.from_raw(v) for k, v in x.items()}))
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'],
package=ConvertValue(lambda _: pkg),
dependencies=ConvertValue(dependencies_from_raw),
dev_dependencies=ConvertValue(dependencies_from_raw),
build_dependencies=ConvertValue(dependencies_from_raw),
lints=ConvertValue(Lint.from_raw),
lib=ConvertValue(lambda x: Library.from_raw(x, pkg), default=autolib),
bin=DictMergeValue(lambda x: [Binary.from_raw(b, pkg) for b in x],
merge_key=lambda x: x.path,
out_key=lambda x: x.name,
base=autobins),
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]),
profile=ConvertValue(lambda x: {k: Profile.from_raw(v) for k, v in x.items()}))
# Merge [target.<cfg>] into the three tables for unconditional dependencies.
# They go in after the unconditional entries, so that a matching condition
# overrides the plain declaration of the same name.
target_tables = T.cast('RawTargetTables', raw.get('target', {}))
for condition, tables in target_tables.items():
for table_name in ('dependencies', 'dev-dependencies', 'build-dependencies'):
deps = getattr(manifest, fixup_meson_varname(table_name))
for name, v in tables.get(table_name, {}).items():
dep = Dependency.from_raw(name, v, member_path, workspace, target=condition)
deps.setdefault(name, []).append(dep)
return manifest
@dataclasses.dataclass
+2 -2
View File
@@ -301,7 +301,7 @@ class RustCrate(ModuleObject):
@TypedArgs('rust_crate.rust_dependency_map')
def rust_dependency_map_method(self, state: ModuleState, args: T.List, kwargs: TYPE_kwargs) -> T.Dict[str, str]:
"""Returns rust dependency mapping for this package."""
return self.cfg.get_dependency_map(self.package.manifest)
return self.cfg.get_dependency_map()
class RustPackage(RustCrate):
@@ -394,7 +394,7 @@ class RustPackage(RustCrate):
kwargs['dependencies'].extend(deps)
depmap = kwargs['rust_dependency_map']
kwargs['rust_dependency_map'] = cfg.get_dependency_map(self.package.manifest)
kwargs['rust_dependency_map'] = cfg.get_dependency_map()
kwargs['rust_dependency_map'].update(depmap)
rust_args = kwargs['rust_args']
+42 -42
View File
@@ -633,50 +633,50 @@ class CargoTomlTest(unittest.TestCase):
manifest = Manifest.from_raw(manifest_toml, 'Cargo.toml')
self.assertEqual(len(manifest.dependencies), 6)
self.assertEqual(manifest.dependencies['gtk'].package, 'gtk4')
self.assertEqual(manifest.dependencies['gtk'].version, '0.9')
self.assertTrue(manifest.dependencies['gtk'].accepts_version('0.9'))
self.assertFalse(manifest.dependencies['gtk'].accepts_version('0.10'))
self.assertEqual(manifest.dependencies['gtk'].api, '0.9')
self.assertEqual(manifest.dependencies['num-complex'].package, 'num-complex')
self.assertEqual(manifest.dependencies['num-complex'].version, '0.4')
self.assertTrue(manifest.dependencies['num-complex'].accepts_version('0.4'))
self.assertFalse(manifest.dependencies['num-complex'].accepts_version('0.5'))
self.assertEqual(manifest.dependencies['rayon'].package, 'rayon')
self.assertEqual(manifest.dependencies['rayon'].version, '1.0')
self.assertTrue(manifest.dependencies['rayon'].accepts_version('1.0'))
self.assertTrue(manifest.dependencies['rayon'].accepts_version('1.23'))
self.assertFalse(manifest.dependencies['rayon'].accepts_version('2.0'))
self.assertFalse(manifest.dependencies['rayon'].accepts_version('2.0-pre1'))
self.assertEqual(manifest.dependencies['rayon'].api, '1')
self.assertEqual(manifest.dependencies['once_cell'].package, 'once_cell')
self.assertEqual(manifest.dependencies['once_cell'].version, '1')
self.assertTrue(manifest.dependencies['once_cell'].accepts_version('1.0'))
self.assertTrue(manifest.dependencies['once_cell'].accepts_version('1.23'))
self.assertFalse(manifest.dependencies['once_cell'].accepts_version('2.0'))
self.assertFalse(manifest.dependencies['once_cell'].accepts_version('2.0-pre1'))
self.assertEqual(manifest.dependencies['once_cell'].api, '1')
self.assertEqual(manifest.dependencies['async-channel'].package, 'async-channel')
self.assertEqual(manifest.dependencies['async-channel'].version, '2.1')
self.assertFalse(manifest.dependencies['async-channel'].accepts_version('2.0'))
self.assertTrue(manifest.dependencies['async-channel'].accepts_version('2.1'))
self.assertTrue(manifest.dependencies['async-channel'].accepts_version('2.23'))
self.assertFalse(manifest.dependencies['async-channel'].accepts_version('2.0'))
self.assertFalse(manifest.dependencies['async-channel'].accepts_version('2.0-pre1'))
self.assertEqual(manifest.dependencies['async-channel'].api, '2')
self.assertEqual(manifest.dependencies['zerocopy'].package, 'zerocopy')
self.assertEqual(manifest.dependencies['zerocopy'].version, '0.7')
self.assertTrue(manifest.dependencies['zerocopy'].accepts_version('0.7'))
self.assertFalse(manifest.dependencies['zerocopy'].accepts_version('0.8'))
self.assertEqual(manifest.dependencies['zerocopy'].features, ['derive'])
self.assertEqual(manifest.dependencies['zerocopy'].api, '0.7')
self.assertEqual(manifest.dependencies['gtk'][0].package, 'gtk4')
self.assertEqual(manifest.dependencies['gtk'][0].version, '0.9')
self.assertTrue(manifest.dependencies['gtk'][0].accepts_version('0.9'))
self.assertFalse(manifest.dependencies['gtk'][0].accepts_version('0.10'))
self.assertEqual(manifest.dependencies['gtk'][0].api, '0.9')
self.assertEqual(manifest.dependencies['num-complex'][0].package, 'num-complex')
self.assertEqual(manifest.dependencies['num-complex'][0].version, '0.4')
self.assertTrue(manifest.dependencies['num-complex'][0].accepts_version('0.4'))
self.assertFalse(manifest.dependencies['num-complex'][0].accepts_version('0.5'))
self.assertEqual(manifest.dependencies['rayon'][0].package, 'rayon')
self.assertEqual(manifest.dependencies['rayon'][0].version, '1.0')
self.assertTrue(manifest.dependencies['rayon'][0].accepts_version('1.0'))
self.assertTrue(manifest.dependencies['rayon'][0].accepts_version('1.23'))
self.assertFalse(manifest.dependencies['rayon'][0].accepts_version('2.0'))
self.assertFalse(manifest.dependencies['rayon'][0].accepts_version('2.0-pre1'))
self.assertEqual(manifest.dependencies['rayon'][0].api, '1')
self.assertEqual(manifest.dependencies['once_cell'][0].package, 'once_cell')
self.assertEqual(manifest.dependencies['once_cell'][0].version, '1')
self.assertTrue(manifest.dependencies['once_cell'][0].accepts_version('1.0'))
self.assertTrue(manifest.dependencies['once_cell'][0].accepts_version('1.23'))
self.assertFalse(manifest.dependencies['once_cell'][0].accepts_version('2.0'))
self.assertFalse(manifest.dependencies['once_cell'][0].accepts_version('2.0-pre1'))
self.assertEqual(manifest.dependencies['once_cell'][0].api, '1')
self.assertEqual(manifest.dependencies['async-channel'][0].package, 'async-channel')
self.assertEqual(manifest.dependencies['async-channel'][0].version, '2.1')
self.assertFalse(manifest.dependencies['async-channel'][0].accepts_version('2.0'))
self.assertTrue(manifest.dependencies['async-channel'][0].accepts_version('2.1'))
self.assertTrue(manifest.dependencies['async-channel'][0].accepts_version('2.23'))
self.assertFalse(manifest.dependencies['async-channel'][0].accepts_version('2.0'))
self.assertFalse(manifest.dependencies['async-channel'][0].accepts_version('2.0-pre1'))
self.assertEqual(manifest.dependencies['async-channel'][0].api, '2')
self.assertEqual(manifest.dependencies['zerocopy'][0].package, 'zerocopy')
self.assertEqual(manifest.dependencies['zerocopy'][0].version, '0.7')
self.assertTrue(manifest.dependencies['zerocopy'][0].accepts_version('0.7'))
self.assertFalse(manifest.dependencies['zerocopy'][0].accepts_version('0.8'))
self.assertEqual(manifest.dependencies['zerocopy'][0].features, ['derive'])
self.assertEqual(manifest.dependencies['zerocopy'][0].api, '0.7')
self.assertEqual(len(manifest.dev_dependencies), 1)
self.assertEqual(manifest.dev_dependencies['gir-format-check'].package, 'gir-format-check')
self.assertEqual(manifest.dev_dependencies['gir-format-check'].version, '^0.1')
self.assertTrue(manifest.dev_dependencies['gir-format-check'].accepts_version('0.1'))
self.assertFalse(manifest.dev_dependencies['gir-format-check'].accepts_version('0.2'))
self.assertEqual(manifest.dev_dependencies['gir-format-check'].api, '0.1')
self.assertEqual(manifest.dev_dependencies['gir-format-check'][0].package, 'gir-format-check')
self.assertEqual(manifest.dev_dependencies['gir-format-check'][0].version, '^0.1')
self.assertTrue(manifest.dev_dependencies['gir-format-check'][0].accepts_version('0.1'))
self.assertFalse(manifest.dev_dependencies['gir-format-check'][0].accepts_version('0.2'))
self.assertEqual(manifest.dev_dependencies['gir-format-check'][0].api, '0.1')
def test_cargo_toml_proc_macro(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir: