From 92c3754aa5c10504b94711810fec358d84a029a3 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Sun, 24 Aug 2025 23:05:39 +0200 Subject: [PATCH] cargo: reintroduce feature options Without feature options there is no way for the superproject to reconcile differences between different invocations of dependency(). Reintroduce options as they were before afd89440a ("cargo: Fix feature resolution", Meson 1.7.0), but also "complete" the default_options provided by _do_subproject_cargo() with all the features that were enabled via Cargo.toml. Signed-off-by: Paolo Bonzini --- .../markdown/Wrap-dependency-system-manual.md | 20 ++++++ mesonbuild/cargo/interpreter.py | 72 +++++++++++++++---- mesonbuild/interpreter/interpreter.py | 3 +- 3 files changed, 82 insertions(+), 13 deletions(-) diff --git a/docs/markdown/Wrap-dependency-system-manual.md b/docs/markdown/Wrap-dependency-system-manual.md index 302546ebbe4f..9e905a1767b4 100644 --- a/docs/markdown/Wrap-dependency-system-manual.md +++ b/docs/markdown/Wrap-dependency-system-manual.md @@ -363,6 +363,26 @@ Some naming conventions need to be respected: - The `extra_deps` variable is pre-defined and can be used to add extra dependencies. This is typically used as `extra_deps += dependency('foo')`. +Cargo features are exposed as Meson boolean options, with the `feature-` prefix. +This can be used to request features with `dependency('foo', default_options: '...')`. + +Currently, Meson is able to manage the set of enabled features for all crates found by a +single invocation of `dependency()`, but not globally. Let's assume +the main project depends on `foo-1-rs` and `bar-1-rs`, and they both depend on +`common-1-rs`. The main project will first look up `foo-1-rs` which itself will +configure `common-rs` with a set of features. Later, when `bar-1-rs` does a lookup +for `common-1-rs` it has already been configured and the set of features cannot be +changed. If `bar-1-rs` wants extra features from `common-1-rs`, Meson will error out. +It is currently the responsibility of the main project to resolve those +issues by enabling extra features on each subproject: +```meson +project(..., + default_options: { + 'common-1-rs:feature-something': true, + }, +) +``` + Since *1.5.0* Cargo wraps can also be provided with `Cargo.lock` file at the root of (sub)project source tree. Meson will automatically load that file and convert it into a series of wraps definitions. diff --git a/mesonbuild/cargo/interpreter.py b/mesonbuild/cargo/interpreter.py index a0d4371be527..bd3ab1dfa97f 100644 --- a/mesonbuild/cargo/interpreter.py +++ b/mesonbuild/cargo/interpreter.py @@ -21,7 +21,8 @@ from .toml import load_toml, TomlImplementationMissing from .manifest import Manifest, CargoLock, fixup_meson_varname from ..mesonlib import MesonException, MachineChoice -from .. import coredata, mlog +from ..options import OptionKey +from .. import coredata, mlog, options from ..wrap.wrap import PackageDefinition if T.TYPE_CHECKING: @@ -29,6 +30,7 @@ from .. import mparser from .manifest import Dependency, SystemDependency from ..environment import Environment + from ..options import OptionDict from ..interpreterbase import SubProject from ..compilers.rust import RustCompiler @@ -41,6 +43,14 @@ def _dependency_varname(package_name: str) -> str: return f'{fixup_meson_varname(package_name)}_dep' +_OPTION_NAME_PREFIX = 'feature-' + + +def _option_name(feature: str) -> str: + # Add a prefix to avoid collision with Meson reserved options (e.g. "debug") + return _OPTION_NAME_PREFIX + feature + + def _extra_args_varname() -> str: return 'extra_args' @@ -78,7 +88,7 @@ def __init__(self, env: Environment) -> None: def get_build_def_files(self) -> T.List[str]: return [os.path.join(subdir, 'Cargo.toml') for subdir in self.manifests] - def interpret(self, subdir: str) -> mparser.CodeBlockNode: + def interpret(self, subp_name: str, subdir: str, default_options: OptionDict) -> T.Tuple[mparser.CodeBlockNode, T.Dict[OptionKey, options.UserOption]]: manifest = self._load_manifest(subdir) pkg, cached = self._fetch_package(manifest.package.name, manifest.package.api) if not cached: @@ -86,15 +96,53 @@ def interpret(self, subdir: str) -> mparser.CodeBlockNode: # FIXME: We should have a Meson option similar to `cargo build --no-default-features` self._enable_feature(pkg, 'default') + # default_options -> features + for key in default_options.keys(): + if key.name.startswith(_OPTION_NAME_PREFIX) and default_options[key] is True: + self._enable_feature(pkg, key.name[len(_OPTION_NAME_PREFIX):]) + + # features -> default_options + for f in pkg.features: + key = OptionKey(_option_name(f)) + default_options[key] = True + # Build an AST for this package filename = os.path.join(self.environment.source_dir, subdir, 'Cargo.toml') build = builder.Builder(filename) ast = self._create_project(pkg, build) + + # features = [] + # features_args = [] + ast += [ + build.assign(build.array([]), 'features'), + build.assign(build.array([]), 'features_args')] + + project_options: T.Dict[OptionKey, options.UserOption] = {} + for f in itertools.chain(pkg.manifest.dependencies.keys(), pkg.manifest.features.keys()): + if f == 'default': + continue + + # option('f1', type: 'boolean', value: true/false, description: 'Cargo feature f1') + key = OptionKey(_option_name(f), subproject=subp_name) + project_options[key] = options.UserBooleanOption(key.name, f'Cargo feature {f}', False) + + # if get_option('feature-f1') + # features_args += ['--cfg', 'feature="f1"'] + # features += ['--f1'] + # endif + lines: T.List[mparser.BaseNode] = [ + build.plusassign(build.array([build.string('--cfg'), build.string(f'feature="{f}"')]), + 'features_args'), + build.plusassign(build.array([build.string(f)]), + 'features')] + + ast.append(build.if_(build.function('get_option', [build.string(_option_name(f))]), build.block(lines))) + ast += [ build.assign(build.function('import', [build.string('rust')]), 'rust'), build.function('message', [ build.string('Enabled features:'), - build.array([build.string(f) for f in pkg.features]), + build.method('join', build.string(','), [build.identifier('features')]) ]), ] ast += self._create_dependencies(pkg, build) @@ -104,7 +152,7 @@ def interpret(self, subdir: str) -> mparser.CodeBlockNode: for crate_type in pkg.manifest.lib.crate_type: ast.extend(self._create_lib(pkg, build, crate_type)) - return build.block(ast) + return build.block(ast), project_options def _fetch_package(self, package_name: str, api: str) -> T.Tuple[PackageState, bool]: key = PackageKey(package_name, api) @@ -289,8 +337,14 @@ def _create_system_dependency(self, name: str, dep: SystemDependency, build: bui def _create_dependency(self, dep: Dependency, build: builder.Builder) -> T.List[mparser.BaseNode]: pkg = self._dep_package(dep) + + # { 'feature-f1': true } + options = build.dict({build.string(f'feature-{f}'): build.bool(True) + for f in pkg.features}) + kw = { 'version': build.array([build.string(s) for s in dep.meson_version]), + 'default_options': options, } # Lookup for this dependency with the features we want in default_options kwarg. # @@ -333,7 +387,7 @@ def _create_dependency(self, dep: Dependency, build: builder.Builder) -> T.List[ # error() # endif # endforeach - build.assign(build.array([build.string(f) for f in pkg.features]), 'needed_features'), + build.assign(build.array([build.string(f) for f in pkg.features if f != 'default']), 'needed_features'), build.foreach(['f'], build.identifier('needed_features'), build.block([ build.if_(build.not_in(build.identifier('f'), build.identifier('actual_features')), build.block([ build.function('error', [ @@ -416,16 +470,10 @@ def _create_lib(self, pkg: PackageState, build: builder.Builder, crate_type: raw kwargs['rust_abi'] = build.string('c') lib = build.function(target_type, posargs, kwargs) - features_args: T.List[mparser.BaseNode] = [] - for f in pkg.features: - features_args += [build.string('--cfg'), build.string(f'feature="{f}"')] - - # features_args = ['--cfg', 'feature="f1"', ...] # lib = xxx_library() # dep = declare_dependency() # meson.override_dependency() return [ - build.assign(build.array(features_args), 'features_args'), build.assign(lib, 'lib'), build.assign( build.function( @@ -433,7 +481,7 @@ def _create_lib(self, pkg: PackageState, build: builder.Builder, crate_type: raw kw={ 'link_with': build.identifier('lib'), 'variables': build.dict({ - build.string('features'): build.string(','.join(pkg.features)), + build.string('features'): build.method('join', build.string(','), [build.identifier('features')]), }), 'version': build.string(pkg.manifest.package.version), }, diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 2cf5b7a5bf10..0eb7956f15a1 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -1065,7 +1065,8 @@ def _do_subproject_cargo(self, subp_name: str, subdir: str, self.add_languages(['rust'], True, MachineChoice.HOST) self.environment.cargo = cargo.Interpreter(self.environment) with mlog.nested(subp_name): - ast = self.environment.cargo.interpret(subdir) + ast, options = self.environment.cargo.interpret(subp_name, subdir, default_options) + self.coredata.optstore.update_project_options(options, subp_name) return self._do_subproject_meson( subp_name, subdir, default_options, kwargs, ast, # FIXME: Are there other files used by cargo interpreter?