From 114e032e6a27d0eb9ef5de1a811ce7b0461c3efc Mon Sep 17 00:00:00 2001 From: Xavier Claessens Date: Sat, 10 Jun 2023 16:07:21 -0400 Subject: [PATCH] cargo: Expose features as Meson boolean options --- .../markdown/Wrap-dependency-system-manual.md | 20 +++ mesonbuild/cargo/interpreter.py | 135 +++++++++++++++++- mesonbuild/interpreter/interpreter.py | 3 +- .../subprojects/bar-rs/Cargo.toml | 11 ++ .../subprojects/extra-dep-rs/lib.c | 4 + .../subprojects/extra-dep-rs/meson.build | 10 ++ .../extra-dep-rs/meson_options.txt | 1 + .../subprojects/foo-rs/Cargo.toml | 19 ++- .../subprojects/foo-rs/src/lib.rs | 10 +- 9 files changed, 206 insertions(+), 7 deletions(-) create mode 100644 test cases/rust/22 cargo subproject/subprojects/extra-dep-rs/lib.c create mode 100644 test cases/rust/22 cargo subproject/subprojects/extra-dep-rs/meson.build create mode 100644 test cases/rust/22 cargo subproject/subprojects/extra-dep-rs/meson_options.txt diff --git a/docs/markdown/Wrap-dependency-system-manual.md b/docs/markdown/Wrap-dependency-system-manual.md index e1e947479..7a0cea6fe 100644 --- a/docs/markdown/Wrap-dependency-system-manual.md +++ b/docs/markdown/Wrap-dependency-system-manual.md @@ -335,6 +335,26 @@ method = cargo dependency_names = foo-bar-rs ``` +Cargo features are exposed as Meson boolean options, with the `feature-` prefix. +For example the `default` feature is named `feature-default` and can be set from +the command line with `-Dfoo-rs:feature-default=false`. When a cargo subproject +depends on another cargo subproject, it will automatically enable features it +needs using the `dependency('foo-rs', default_options: ...)` mechanism. However, +unlike Cargo, the set of enabled features is not managed globally. Let's assume +the main project depends on `foo-rs` and `bar-rs`, and they both depend on +`common-rs`. The main project will first look up `foo-rs` which itself will +configure `common-rs` with a set of features. Later, when `bar-rs` does a lookup +for `common-rs` it has already been configured and the set of features cannot be +changed. It is currently the responsability of the main project to resolve those +issues by enabling extra features on each subproject: +```meson +project(..., + default_options: { + 'common-rs:feature-something': true, + }, +) +``` + ## Using wrapped projects Wraps provide a convenient way of obtaining a project into your diff --git a/mesonbuild/cargo/interpreter.py b/mesonbuild/cargo/interpreter.py index c5791ae4c..570087d6b 100644 --- a/mesonbuild/cargo/interpreter.py +++ b/mesonbuild/cargo/interpreter.py @@ -17,11 +17,13 @@ import itertools import json import os import shutil +import collections import typing as T from . import builder from . import version -from ..mesonlib import MesonException, Popen_safe +from ..mesonlib import MesonException, Popen_safe, OptionKey +from .. import coredata if T.TYPE_CHECKING: from types import ModuleType @@ -29,6 +31,7 @@ if T.TYPE_CHECKING: from . import manifest from .. import mparser from ..environment import Environment + from ..coredata import KeyedOptionDictType # tomllib is present in python 3.11, before that it is a pypi module called tomli, # we try to import tomllib, then tomli, @@ -156,7 +159,7 @@ class Dependency: path: T.Optional[str] = None optional: bool = False package: T.Optional[str] = None - default_features: bool = False + default_features: bool = True features: T.List[str] = dataclasses.field(default_factory=list) @classmethod @@ -269,6 +272,9 @@ class Manifest: subdir: str path: str = '' + def __post_init__(self) -> None: + self.features.setdefault('default', []) + def _convert_manifest(raw_manifest: manifest.Manifest, subdir: str, path: str = '') -> Manifest: # This cast is a bit of a hack to deal with proc-macro @@ -348,6 +354,15 @@ def _dependency_varname(package_name: str) -> str: return f'{fixup_meson_varname(package_name)}_dep' +def _option_name(feature: str) -> str: + # Add a prefix to avoid collision with Meson reserved options (e.g. "debug") + return f'feature-{feature}' + + +def _options_varname(depname: str) -> str: + return f'{fixup_meson_varname(depname)}_options' + + def _create_project(cargo: Manifest, build: builder.Builder) -> T.List[mparser.BaseNode]: """Create a function call @@ -376,13 +391,97 @@ def _create_project(cargo: Manifest, build: builder.Builder) -> T.List[mparser.B return [build.function('project', args, kwargs)] +def _process_feature(cargo: Manifest, feature: str) -> T.Tuple[T.Set[str], T.Dict[str, T.Set[str]], T.Set[str]]: + # Set of features that must also be enabled if this feature is enabled. + features: T.Set[str] = set() + # Map dependency name to a set of features that must also be enabled on that + # dependency if this feature is enabled. + dep_features: T.Dict[str, T.Set[str]] = collections.defaultdict(set) + # Set of dependencies that are required if this feature is enabled. + required_deps: T.Set[str] = set() + # Set of features that must be processed recursively. + to_process: T.Set[str] = {feature} + while to_process: + f = to_process.pop() + if '/' in f: + dep, dep_f = f.split('/', 1) + if dep[-1] == '?': + dep = dep[:-1] + else: + required_deps.add(dep) + dep_features[dep].add(dep_f) + elif f.startswith('dep:'): + required_deps.add(f[4:]) + elif f not in features: + features.add(f) + to_process.update(cargo.features.get(f, [])) + # A feature can also be a dependency + if f in cargo.dependencies: + required_deps.add(f) + return features, dep_features, required_deps + + +def _create_features(cargo: Manifest, build: builder.Builder) -> T.List[mparser.BaseNode]: + # https://doc.rust-lang.org/cargo/reference/features.html#the-features-section + + # Declare a dict that map enabled features to true. One for current project + # and one per dependency. + ast: T.List[mparser.BaseNode] = [] + ast.append(build.assign(build.dict({}), 'features')) + for depname in cargo.dependencies: + ast.append(build.assign(build.dict({}), _options_varname(depname))) + + # Declare a dict that map required dependencies to true + ast.append(build.assign(build.dict({}), 'required_deps')) + + for feature in cargo.features: + # if get_option(feature) + # required_deps += {'dep': true, ...} + # features += {'foo': true, ...} + # xxx_options += {'feature-foo': true, ...} + # ... + # endif + features, dep_features, required_deps = _process_feature(cargo, feature) + lines: T.List[mparser.BaseNode] = [ + build.plusassign( + build.dict({build.string(d): build.bool(True) for d in required_deps}), + 'required_deps'), + build.plusassign( + build.dict({build.string(f): build.bool(True) for f in features}), + 'features'), + ] + for depname, enabled_features in dep_features.items(): + lines.append(build.plusassign( + build.dict({build.string(_option_name(f)): build.bool(True) for f in enabled_features}), + _options_varname(depname))) + + ast.append(build.if_(build.function('get_option', [build.string(_option_name(feature))]), build.block(lines))) + + return ast + + def _create_dependencies(cargo: Manifest, build: builder.Builder) -> T.List[mparser.BaseNode]: ast: T.List[mparser.BaseNode] = [] for name, dep in cargo.dependencies.items(): package_name = dep.package or name + + # xxx_options += {'feature-default': true, ...} + extra_options: T.Dict[mparser.BaseNode, mparser.BaseNode] = { + build.string(_option_name('default')): build.bool(dep.default_features), + } + for f in dep.features: + extra_options[build.string(_option_name(f))] = build.bool(True) + ast.append(build.plusassign(build.dict(extra_options), _options_varname(name))) + kw = { 'version': build.array([build.string(s) for s in dep.version]), + 'default_options': build.identifier(_options_varname(name)), } + if dep.optional: + kw['required'] = build.method('get', build.identifier('required_deps'), [ + build.string(name), build.bool(False) + ]) + ast.extend([ build.assign( build.function( @@ -405,6 +504,8 @@ def _create_lib(cargo: Manifest, build: builder.Builder, crate_type: manifest.CR if name != package_name: dependency_map[build.string(fixup_meson_varname(package_name))] = build.string(name) + rust_args: T.List[mparser.BaseNode] = [build.identifier('features_args')] + posargs: T.List[mparser.BaseNode] = [ build.string(fixup_meson_varname(cargo.package.name)), build.string(os.path.join('src', 'lib.rs')), @@ -413,6 +514,7 @@ def _create_lib(cargo: Manifest, build: builder.Builder, crate_type: manifest.CR kwargs: T.Dict[str, mparser.BaseNode] = { 'dependencies': build.array(dependencies), 'rust_dependency_map': build.dict(dependency_map), + 'rust_args': build.array(rust_args), } lib: mparser.BaseNode @@ -429,7 +531,24 @@ def _create_lib(cargo: Manifest, build: builder.Builder, crate_type: manifest.CR kwargs['rust_abi'] = build.string('c') lib = build.function(target_type, posargs, kwargs) + # features_args = [] + # foreach f, _ : features + # features_args += ['--cfg', 'feature="' + f + '"'] + # endforeach + # lib = xxx_library() + # dep = declare_dependency() + # meson.override_dependency() return [ + build.assign(build.array([]), 'features_args'), + build.foreach(['f', '_'], build.identifier('features'), build.block([ + build.plusassign( + build.array([ + build.string('--cfg'), + build.plus(build.string('feature="'), build.plus(build.identifier('f'), build.string('"'))), + ]), + 'features_args') + ]) + ), build.assign(lib, 'lib'), build.assign( build.function( @@ -451,7 +570,7 @@ def _create_lib(cargo: Manifest, build: builder.Builder, crate_type: manifest.CR ] -def interpret(subp_name: str, subdir: str, env: Environment) -> mparser.CodeBlockNode: +def interpret(subp_name: str, subdir: str, env: Environment) -> T.Tuple[mparser.CodeBlockNode, KeyedOptionDictType]: package_name = subp_name[:-3] if subp_name.endswith('-rs') else subp_name manifests = _load_manifests(os.path.join(env.source_dir, subdir)) cargo = manifests.get(package_name) @@ -461,8 +580,16 @@ def interpret(subp_name: str, subdir: str, env: Environment) -> mparser.CodeBloc filename = os.path.join(cargo.subdir, cargo.path, 'Cargo.toml') build = builder.Builder(filename) + # Generate project options + options: T.Dict[OptionKey, coredata.UserOption] = {} + for feature in cargo.features: + key = OptionKey(_option_name(feature), subproject=subp_name) + enabled = feature == 'default' + options[key] = coredata.UserBooleanOption(f'Cargo {feature} feature', enabled) + ast = _create_project(cargo, build) ast += [build.assign(build.function('import', [build.string('rust')]), 'rust')] + ast += _create_features(cargo, build) ast += _create_dependencies(cargo, build) # Libs are always auto-discovered and there's no other way to handle them, @@ -471,4 +598,4 @@ def interpret(subp_name: str, subdir: str, env: Environment) -> mparser.CodeBloc for crate_type in cargo.lib.crate_type: ast.extend(_create_lib(cargo, build, crate_type)) - return build.block(ast) + return build.block(ast), options diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 99c4f9625..79a61ed0e 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -1034,7 +1034,8 @@ class Interpreter(InterpreterBase, HoldableObject): from .. import cargo FeatureNew.single_use('Cargo subproject', '1.3.0', self.subproject, location=self.current_node) with mlog.nested(subp_name): - ast = cargo.interpret(subp_name, subdir, self.environment) + ast, options = cargo.interpret(subp_name, subdir, self.environment) + self.coredata.update_project_options(options) return self._do_subproject_meson( subp_name, subdir, default_options, kwargs, ast, # FIXME: Are there other files used by cargo interpreter? diff --git a/test cases/rust/22 cargo subproject/subprojects/bar-rs/Cargo.toml b/test cases/rust/22 cargo subproject/subprojects/bar-rs/Cargo.toml index 232b4d7d4..d60a5d8f1 100644 --- a/test cases/rust/22 cargo subproject/subprojects/bar-rs/Cargo.toml +++ b/test cases/rust/22 cargo subproject/subprojects/bar-rs/Cargo.toml @@ -1,3 +1,14 @@ [package] name = "bar" version = "0.1" + +# This dependency does not exist, it is required by default but this subproject +# is called with default-features=false. +[dependencies.notfound] +optional = true +version = "1.0" + +[features] +default = ["f2"] +f1 = [] +f2 = ["notfound"] diff --git a/test cases/rust/22 cargo subproject/subprojects/extra-dep-rs/lib.c b/test cases/rust/22 cargo subproject/subprojects/extra-dep-rs/lib.c new file mode 100644 index 000000000..c2a0777ce --- /dev/null +++ b/test cases/rust/22 cargo subproject/subprojects/extra-dep-rs/lib.c @@ -0,0 +1,4 @@ +int extra_func(void) +{ + return 0; +} diff --git a/test cases/rust/22 cargo subproject/subprojects/extra-dep-rs/meson.build b/test cases/rust/22 cargo subproject/subprojects/extra-dep-rs/meson.build new file mode 100644 index 000000000..3ba7852cf --- /dev/null +++ b/test cases/rust/22 cargo subproject/subprojects/extra-dep-rs/meson.build @@ -0,0 +1,10 @@ +project('extra dep', 'c', version: '1.0') + +assert(get_option('feature-default') == true) + +l = static_library('extra-dep', 'lib.c') +d = declare_dependency(link_with: l, + variables: { + 'features': 'default', + }) +meson.override_dependency('extra-dep-rs', d) diff --git a/test cases/rust/22 cargo subproject/subprojects/extra-dep-rs/meson_options.txt b/test cases/rust/22 cargo subproject/subprojects/extra-dep-rs/meson_options.txt new file mode 100644 index 000000000..9311d9e8f --- /dev/null +++ b/test cases/rust/22 cargo subproject/subprojects/extra-dep-rs/meson_options.txt @@ -0,0 +1 @@ +option('feature-default', type: 'boolean', value: true) diff --git a/test cases/rust/22 cargo subproject/subprojects/foo-rs/Cargo.toml b/test cases/rust/22 cargo subproject/subprojects/foo-rs/Cargo.toml index 214c3279c..796548d63 100644 --- a/test cases/rust/22 cargo subproject/subprojects/foo-rs/Cargo.toml +++ b/test cases/rust/22 cargo subproject/subprojects/foo-rs/Cargo.toml @@ -6,5 +6,22 @@ edition = "2021" [lib] crate-type = ["cdylib"] +# This dependency does not exist, verify optional works. +[dependencies.notfound] +optional = true +version = "1.0" + +# This dependency is optional but required for f3 which is on by default. +[dependencies.extra-dep] +optional = true +version = "1.0" + [dependencies] -mybar = { version = "0.1", package = "bar" } +mybar = { version = "0.1", package = "bar", default-features = false } + +[features] +default = ["f1"] +f1 = ["f2", "f3"] +f2 = ["f1"] +f3 = ["mybar/f1", "dep:extra-dep", "notfound?/invalid"] +f4 = ["dep:notfound"] diff --git a/test cases/rust/22 cargo subproject/subprojects/foo-rs/src/lib.rs b/test cases/rust/22 cargo subproject/subprojects/foo-rs/src/lib.rs index 732d7d20b..4f0a31079 100644 --- a/test cases/rust/22 cargo subproject/subprojects/foo-rs/src/lib.rs +++ b/test cases/rust/22 cargo subproject/subprojects/foo-rs/src/lib.rs @@ -1,4 +1,12 @@ +extern "C" { + fn extra_func() -> i32; +} + #[no_mangle] pub extern "C" fn rust_func() -> i32 { - mybar::VALUE + let v: i32; + unsafe { + v = extra_func(); + }; + mybar::VALUE + v }