summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXavier Claessens <xavier.claessens@collabora.com>2023-06-10 16:07:21 -0400
committerXavier Claessens <xavier.claessens@collabora.com>2024-02-26 10:03:52 -0500
commit114e032e6a27d0eb9ef5de1a811ce7b0461c3efc (patch)
treef20b3b240b4cb32ba54644bd7388e97038311dfc
parentd075bdb3ca39a077994fa65e7fafb98cdebf5da6 (diff)
downloadmeson-114e032e6a27d0eb9ef5de1a811ce7b0461c3efc.tar.gz
cargo: Expose features as Meson boolean options
-rw-r--r--docs/markdown/Wrap-dependency-system-manual.md20
-rw-r--r--mesonbuild/cargo/interpreter.py135
-rw-r--r--mesonbuild/interpreter/interpreter.py3
-rw-r--r--test cases/rust/22 cargo subproject/subprojects/bar-rs/Cargo.toml11
-rw-r--r--test cases/rust/22 cargo subproject/subprojects/extra-dep-rs/lib.c4
-rw-r--r--test cases/rust/22 cargo subproject/subprojects/extra-dep-rs/meson.build10
-rw-r--r--test cases/rust/22 cargo subproject/subprojects/extra-dep-rs/meson_options.txt1
-rw-r--r--test cases/rust/22 cargo subproject/subprojects/foo-rs/Cargo.toml19
-rw-r--r--test cases/rust/22 cargo subproject/subprojects/foo-rs/src/lib.rs10
9 files changed, 206 insertions, 7 deletions
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
}