cargo: Expose features as Meson boolean options

pull/11867/head
Xavier Claessens 1 year ago
parent d075bdb3ca
commit 114e032e6a
  1. 20
      docs/markdown/Wrap-dependency-system-manual.md
  2. 135
      mesonbuild/cargo/interpreter.py
  3. 3
      mesonbuild/interpreter/interpreter.py
  4. 11
      test cases/rust/22 cargo subproject/subprojects/bar-rs/Cargo.toml
  5. 4
      test cases/rust/22 cargo subproject/subprojects/extra-dep-rs/lib.c
  6. 10
      test cases/rust/22 cargo subproject/subprojects/extra-dep-rs/meson.build
  7. 1
      test cases/rust/22 cargo subproject/subprojects/extra-dep-rs/meson_options.txt
  8. 19
      test cases/rust/22 cargo subproject/subprojects/foo-rs/Cargo.toml
  9. 10
      test cases/rust/22 cargo subproject/subprojects/foo-rs/src/lib.rs

@ -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

@ -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

@ -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?

@ -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"]

@ -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)

@ -0,0 +1 @@
option('feature-default', type: 'boolean', value: true)

@ -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"]

@ -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
}

Loading…
Cancel
Save