diff --git a/mesonbuild/cargo/interpreter.py b/mesonbuild/cargo/interpreter.py index 4110311ac..6eee5a364 100644 --- a/mesonbuild/cargo/interpreter.py +++ b/mesonbuild/cargo/interpreter.py @@ -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): diff --git a/mesonbuild/cargo/manifest.py b/mesonbuild/cargo/manifest.py index 4ea3b81a5..a3469161d 100644 --- a/mesonbuild/cargo/manifest.py +++ b/mesonbuild/cargo/manifest.py @@ -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.] 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.] 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 diff --git a/mesonbuild/modules/rust.py b/mesonbuild/modules/rust.py index de9fb953a..73500b98f 100644 --- a/mesonbuild/modules/rust.py +++ b/mesonbuild/modules/rust.py @@ -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'] diff --git a/unittests/cargotests.py b/unittests/cargotests.py index f3abe1150..1e77597ce 100644 --- a/unittests/cargotests.py +++ b/unittests/cargotests.py @@ -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: