cargo: Fix feature resolution

Introduce a global Cargo interpreter state that keeps track of enabled
features on each crate.

Before generating AST of a Cargo subproject, it downloads every
sub-subproject and resolves the set of features enabled on each of them
recursively. When it later generates AST for one its dependencies, its
set of features and dependencies is already determined.
pull/13815/head
Xavier Claessens 9 months ago committed by Xavier Claessens
parent c02e0b7b1e
commit afd89440aa
  1. 21
      docs/markdown/Wrap-dependency-system-manual.md
  2. 14
      docs/markdown/snippets/cargo_features.md
  3. 4
      mesonbuild/cargo/__init__.py
  4. 602
      mesonbuild/cargo/interpreter.py
  5. 3
      mesonbuild/environment.py
  6. 5
      mesonbuild/interpreter/interpreter.py
  7. 4
      test cases/rust/22 cargo subproject/subprojects/bar-0.1-rs/Cargo.toml
  8. 2
      test cases/rust/22 cargo subproject/subprojects/common-0-rs.wrap
  9. 12
      test cases/rust/22 cargo subproject/subprojects/common-0-rs/Cargo.toml
  10. 4
      test cases/rust/22 cargo subproject/subprojects/common-0-rs/lib.rs
  11. 3
      test cases/rust/22 cargo subproject/subprojects/extra-dep-1-rs/Cargo.toml
  12. 2
      test cases/rust/22 cargo subproject/subprojects/extra-dep-1-rs/meson.build
  13. 4
      test cases/rust/22 cargo subproject/subprojects/foo-0-rs/Cargo.toml
  14. 3
      test cases/rust/22 cargo subproject/subprojects/foo-0-rs/lib.rs

@ -354,27 +354,6 @@ method = cargo
dependency_names = foo-bar-0.1-rs dependency_names = foo-bar-0.1-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-1-rs:feature-default=false`. When a cargo subproject
depends on another cargo subproject, it will automatically enable features it
needs using the `dependency('foo-1-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-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,
},
)
```
In addition, if the file `meson/meson.build` exists, Meson will call `subdir('meson')` In addition, if the file `meson/meson.build` exists, Meson will call `subdir('meson')`
where the project can add manual logic that would usually be part of `build.rs`. where the project can add manual logic that would usually be part of `build.rs`.
Some naming conventions need to be respected: Some naming conventions need to be respected:

@ -0,0 +1,14 @@
## Cargo features are resolved globally
When configuring a Cargo dependency, Meson will now resolve its complete
dependency tree and feature set before generating the subproject AST.
This solves many cases of Cargo subprojects being configured with missing
features that the main project had to enable by hand using e.g.
`default_options: ['foo-rs:feature-default=true']`.
Note that there could still be issues in the case there are multiple Cargo
entry points. That happens if the main Meson project makes multiple `dependency()`
calls for different Cargo crates that have common dependencies.
Breaks: This change removes per feature Meson options that were previously
possible to set as shown above or from command line `-Dfoo-rs:feature-foo=true`.

@ -1,6 +1,6 @@
__all__ = [ __all__ = [
'interpret', 'Interpreter',
'load_wraps', 'load_wraps',
] ]
from .interpreter import interpret, load_wraps from .interpreter import Interpreter, load_wraps

@ -24,13 +24,11 @@ import typing as T
from . import builder from . import builder
from . import version from . import version
from ..mesonlib import MesonException, Popen_safe from ..mesonlib import MesonException, Popen_safe
from ..options import OptionKey from .. import coredata, mlog
from .. import coredata, options, mlog
from ..wrap.wrap import PackageDefinition from ..wrap.wrap import PackageDefinition
if T.TYPE_CHECKING: if T.TYPE_CHECKING:
from types import ModuleType from types import ModuleType
from typing import Any
from . import manifest from . import manifest
from .. import mparser from .. import mparser
@ -151,7 +149,10 @@ class Package:
autoexamples: bool = True autoexamples: bool = True
autotests: bool = True autotests: bool = True
autobenches: bool = True autobenches: bool = True
api: str = dataclasses.field(init=False)
def __post_init__(self) -> None:
self.api = _version_to_api(self.version)
@dataclasses.dataclass @dataclasses.dataclass
class Dependency: class Dependency:
@ -280,7 +281,6 @@ class Manifest:
Cargo subprojects can contain what Meson wants to treat as multiple, Cargo subprojects can contain what Meson wants to treat as multiple,
interdependent, subprojects. interdependent, subprojects.
:param subdir: the subdirectory that this cargo project is in
:param path: the path within the cargo subproject. :param path: the path within the cargo subproject.
""" """
@ -295,7 +295,6 @@ class Manifest:
example: T.List[Example] example: T.List[Example]
features: T.Dict[str, T.List[str]] features: T.Dict[str, T.List[str]]
target: T.Dict[str, T.Dict[str, Dependency]] target: T.Dict[str, T.Dict[str, Dependency]]
subdir: str
path: str = '' path: str = ''
def __post_init__(self) -> None: def __post_init__(self) -> None:
@ -326,7 +325,6 @@ def _convert_manifest(raw_manifest: manifest.Manifest, subdir: str, path: str =
raw_manifest.get('features', {}), raw_manifest.get('features', {}),
{k: {k2: Dependency.from_raw(k2, v2) for k2, v2 in v.get('dependencies', {}).items()} {k: {k2: Dependency.from_raw(k2, v2) for k2, v2 in v.get('dependencies', {}).items()}
for k, v in raw_manifest.get('target', {}).items()}, for k, v in raw_manifest.get('target', {}).items()},
subdir,
path, path,
) )
@ -393,18 +391,6 @@ def _dependency_varname(package_name: str) -> str:
return f'{fixup_meson_varname(package_name)}_dep' 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 _options_varname(depname: str) -> str:
return f'{fixup_meson_varname(depname)}_options'
def _extra_args_varname() -> str: def _extra_args_varname() -> str:
return 'extra_args' return 'extra_args'
@ -413,128 +399,174 @@ def _extra_deps_varname() -> str:
return 'extra_deps' return 'extra_deps'
def _create_project(cargo: Manifest, build: builder.Builder) -> T.List[mparser.BaseNode]: class PackageState:
"""Create a function call def __init__(self, manifest: Manifest) -> None:
self.manifest = manifest
:param cargo: The Manifest to generate from self.features: T.Set[str] = set()
:param build: The AST builder self.required_deps: T.Set[str] = set()
:return: a list nodes self.optional_deps_features: T.Dict[str, T.Set[str]] = collections.defaultdict(set)
"""
args: T.List[mparser.BaseNode] = []
args.extend([ @dataclasses.dataclass(frozen=True)
build.string(cargo.package.name), class PackageKey:
build.string('rust'), package_name: str
]) api: str
kwargs: T.Dict[str, mparser.BaseNode] = {
'version': build.string(cargo.package.version),
# Always assume that the generated meson is using the latest features class Interpreter:
# This will warn when when we generate deprecated code, which is helpful def __init__(self, env: Environment) -> None:
# for the upkeep of the module self.environment = env
'meson_version': build.string(f'>= {coredata.stable_version}'), # Map Cargo.toml's subdir to loaded manifest.
'default_options': build.array([build.string(f'rust_std={cargo.package.edition}')]), self.manifests: T.Dict[str, Manifest] = {}
} # Map of cargo package (name + api) to its state
if cargo.package.license: self.packages: T.Dict[PackageKey, PackageState] = {}
kwargs['license'] = build.string(cargo.package.license)
elif cargo.package.license_file: def interpret(self, subdir: str) -> mparser.CodeBlockNode:
kwargs['license_files'] = build.string(cargo.package.license_file) manifest = self._load_manifest(subdir)
pkg, cached = self._fetch_package(manifest.package.name, manifest.package.api)
return [build.function('project', args, kwargs)] if not cached:
# This is an entry point, always enable the 'default' feature.
# FIXME: We should have a Meson option similar to `cargo build --no-default-features`
def _process_feature(cargo: Manifest, feature: str) -> T.Tuple[T.Set[str], T.Dict[str, T.Set[str]], T.Set[str]]: self._enable_feature(pkg, 'default')
# Set of features that must also be enabled if this feature is enabled.
features: T.Set[str] = set() # Build an AST for this package
# Map dependency name to a set of features that must also be enabled on that filename = os.path.join(self.environment.source_dir, subdir, 'Cargo.toml')
# dependency if this feature is enabled. build = builder.Builder(filename)
dep_features: T.Dict[str, T.Set[str]] = collections.defaultdict(set) ast = self._create_project(pkg, build)
# Set of dependencies that are required if this feature is enabled. ast += [
required_deps: T.Set[str] = set() build.assign(build.function('import', [build.string('rust')]), 'rust'),
# Set of features that must be processed recursively. build.function('message', [
to_process: T.Set[str] = {feature} build.string('Enabled features:'),
while to_process: build.array([build.string(f) for f in pkg.features]),
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(): ast += self._create_dependencies(pkg, build)
lines.append(build.plusassign( ast += self._create_meson_subdir(build)
build.dict({build.string(_option_name(f)): build.bool(True) for f in enabled_features}),
_options_varname(depname))) # Libs are always auto-discovered and there's no other way to handle them,
# which is unfortunate for reproducability
ast.append(build.if_(build.function('get_option', [build.string(_option_name(feature))]), build.block(lines))) if os.path.exists(os.path.join(self.environment.source_dir, subdir, pkg.manifest.path, pkg.manifest.lib.path)):
for crate_type in pkg.manifest.lib.crate_type:
ast.append(build.function('message', [ ast.extend(self._create_lib(pkg, build, crate_type))
build.string('Enabled features:'),
build.method('keys', build.identifier('features'))], return build.block(ast)
))
def _fetch_package(self, package_name: str, api: str) -> T.Tuple[PackageState, bool]:
return ast key = PackageKey(package_name, api)
pkg = self.packages.get(key)
if pkg:
def _create_dependencies(cargo: Manifest, build: builder.Builder) -> T.List[mparser.BaseNode]: return pkg, True
ast: T.List[mparser.BaseNode] = [] meson_depname = _dependency_name(package_name, api)
for name, dep in cargo.dependencies.items(): subdir, _ = self.environment.wrap_resolver.resolve(meson_depname)
# xxx_options += {'feature-default': true, ...} manifest = self._load_manifest(subdir)
extra_options: T.Dict[mparser.BaseNode, mparser.BaseNode] = { pkg = PackageState(manifest)
build.string(_option_name('default')): build.bool(dep.default_features), self.packages[key] = pkg
} # Fetch required dependencies recursively.
for depname, dep in manifest.dependencies.items():
if not dep.optional:
self._add_dependency(pkg, depname)
return pkg, False
def _dep_package(self, dep: Dependency) -> PackageState:
return self.packages[PackageKey(dep.package, dep.api)]
def _load_manifest(self, subdir: str) -> Manifest:
manifest_ = self.manifests.get(subdir)
if not manifest_:
filename = os.path.join(self.environment.source_dir, subdir, 'Cargo.toml')
raw = load_toml(filename)
if 'package' in raw:
raw_manifest = T.cast('manifest.Manifest', raw)
manifest_ = _convert_manifest(raw_manifest, subdir)
self.manifests[subdir] = manifest_
else:
raise MesonException(f'{subdir}/Cargo.toml does not have [package] section')
return manifest_
def _add_dependency(self, pkg: PackageState, depname: str) -> None:
if depname in pkg.required_deps:
return
pkg.required_deps.add(depname)
dep = pkg.manifest.dependencies[depname]
dep_pkg, _ = self._fetch_package(dep.package, dep.api)
if dep.default_features:
self._enable_feature(dep_pkg, 'default')
for f in dep.features: for f in dep.features:
extra_options[build.string(_option_name(f))] = build.bool(True) self._enable_feature(dep_pkg, f)
ast.append(build.plusassign(build.dict(extra_options), _options_varname(name))) for f in pkg.optional_deps_features[depname]:
self._enable_feature(dep_pkg, f)
def _enable_feature(self, pkg: PackageState, feature: str) -> None:
if feature in pkg.features:
return
pkg.features.add(feature)
# A feature can also be a dependency.
if feature in pkg.manifest.dependencies:
self._add_dependency(pkg, feature)
# Recurse on extra features and dependencies this feature pulls.
# https://doc.rust-lang.org/cargo/reference/features.html#the-features-section
for f in pkg.manifest.features.get(feature, []):
if '/' in f:
depname, dep_f = f.split('/', 1)
if depname[-1] == '?':
depname = depname[:-1]
if depname in pkg.required_deps:
dep = pkg.manifest.dependencies[depname]
dep_pkg = self._dep_package(dep)
self._enable_feature(dep_pkg, dep_f)
else:
# This feature will be enabled only if that dependency
# is later added.
pkg.optional_deps_features[depname].add(dep_f)
else:
self._add_dependency(pkg, depname)
dep = pkg.manifest.dependencies[depname]
dep_pkg = self._dep_package(dep)
self._enable_feature(dep_pkg, dep_f)
elif f.startswith('dep:'):
self._add_dependency(pkg, f[4:])
else:
self._enable_feature(pkg, f)
def _create_project(self, pkg: PackageState, build: builder.Builder) -> T.List[mparser.BaseNode]:
"""Create the project() function call
:param pkg: The package to generate from
:param build: The AST builder
:return: a list nodes
"""
args: T.List[mparser.BaseNode] = []
args.extend([
build.string(pkg.manifest.package.name),
build.string('rust'),
])
kwargs: T.Dict[str, mparser.BaseNode] = {
'version': build.string(pkg.manifest.package.version),
# Always assume that the generated meson is using the latest features
# This will warn when when we generate deprecated code, which is helpful
# for the upkeep of the module
'meson_version': build.string(f'>= {coredata.stable_version}'),
'default_options': build.array([build.string(f'rust_std={pkg.manifest.package.edition}')]),
}
if pkg.manifest.package.license:
kwargs['license'] = build.string(pkg.manifest.package.license)
elif pkg.manifest.package.license_file:
kwargs['license_files'] = build.string(pkg.manifest.package.license_file)
return [build.function('project', args, kwargs)]
def _create_dependencies(self, pkg: PackageState, build: builder.Builder) -> T.List[mparser.BaseNode]:
ast: T.List[mparser.BaseNode] = []
for depname in pkg.required_deps:
dep = pkg.manifest.dependencies[depname]
ast += self._create_dependency(dep, build)
return ast
def _create_dependency(self, dep: Dependency, build: builder.Builder) -> T.List[mparser.BaseNode]:
pkg = self._dep_package(dep)
kw = { kw = {
'version': build.array([build.string(s) for s in dep.version]), '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)
])
# Lookup for this dependency with the features we want in default_options kwarg. # Lookup for this dependency with the features we want in default_options kwarg.
# #
# However, this subproject could have been previously configured with a # However, this subproject could have been previously configured with a
@ -546,8 +578,8 @@ def _create_dependencies(cargo: Manifest, build: builder.Builder) -> T.List[mpar
# otherwise abort with an error message. The user has to set the corresponding # otherwise abort with an error message. The user has to set the corresponding
# option manually with -Dxxx-rs:feature-yyy=true, or the main project can do # option manually with -Dxxx-rs:feature-yyy=true, or the main project can do
# that in its project(..., default_options: ['xxx-rs:feature-yyy=true']). # that in its project(..., default_options: ['xxx-rs:feature-yyy=true']).
ast.extend([ return [
# xxx_dep = dependency('xxx', version : ..., default_options : xxx_options) # xxx_dep = dependency('xxx', version : ...)
build.assign( build.assign(
build.function( build.function(
'dependency', 'dependency',
@ -556,188 +588,132 @@ def _create_dependencies(cargo: Manifest, build: builder.Builder) -> T.List[mpar
), ),
_dependency_varname(dep.package), _dependency_varname(dep.package),
), ),
# if xxx_dep.found() # actual_features = xxx_dep.get_variable('features', default_value : '').split(',')
build.if_(build.method('found', build.identifier(_dependency_varname(dep.package))), build.block([ build.assign(
# actual_features = xxx_dep.get_variable('features', default_value : '').split(',') build.method(
build.assign( 'split',
build.method( build.method(
'split', 'get_variable',
build.method( build.identifier(_dependency_varname(dep.package)),
'get_variable', [build.string('features')],
build.identifier(_dependency_varname(dep.package)), {'default_value': build.string('')}
[build.string('features')],
{'default_value': build.string('')}
),
[build.string(',')],
), ),
'actual_features' [build.string(',')],
), ),
# needed_features = [] 'actual_features'
# foreach f, _ : xxx_options ),
# needed_features += f.substring(8) # needed_features = [f1, f2, ...]
# endforeach # foreach f : needed_features
build.assign(build.array([]), 'needed_features'), # if f not in actual_features
build.foreach(['f', 'enabled'], build.identifier(_options_varname(name)), build.block([ # error()
build.if_(build.identifier('enabled'), build.block([ # endif
build.plusassign( # endforeach
build.method('substring', build.identifier('f'), [build.number(len(_OPTION_NAME_PREFIX))]), build.assign(build.array([build.string(f) for f in pkg.features]), 'needed_features'),
'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', [
# foreach f : needed_features build.string('Dependency'),
# if f not in actual_features build.string(_dependency_name(dep.package, dep.api)),
# error() build.string('previously configured with features'),
# endif build.identifier('actual_features'),
# endforeach build.string('but need'),
build.foreach(['f'], build.identifier('needed_features'), build.block([ build.identifier('needed_features'),
build.if_(build.not_in(build.identifier('f'), build.identifier('actual_features')), build.block([ ])
build.function('error', [ ]))
build.string('Dependency'),
build.string(_dependency_name(dep.package, dep.api)),
build.string('previously configured with features'),
build.identifier('actual_features'),
build.string('but need'),
build.identifier('needed_features'),
])
]))
])),
])), ])),
]) ]
return ast
def _create_meson_subdir(self, build: builder.Builder) -> T.List[mparser.BaseNode]:
# Allow Cargo subprojects to add extra Rust args in meson/meson.build file.
def _create_meson_subdir(cargo: Manifest, build: builder.Builder) -> T.List[mparser.BaseNode]: # This is used to replace build.rs logic.
# Allow Cargo subprojects to add extra Rust args in meson/meson.build file.
# This is used to replace build.rs logic. # extra_args = []
# extra_deps = []
# extra_args = [] # fs = import('fs')
# extra_deps = [] # if fs.is_dir('meson')
# fs = import('fs') # subdir('meson')
# if fs.is_dir('meson') # endif
# subdir('meson') return [
# endif build.assign(build.array([]), _extra_args_varname()),
return [ build.assign(build.array([]), _extra_deps_varname()),
build.assign(build.array([]), _extra_args_varname()), build.assign(build.function('import', [build.string('fs')]), 'fs'),
build.assign(build.array([]), _extra_deps_varname()), build.if_(build.method('is_dir', build.identifier('fs'), [build.string('meson')]),
build.assign(build.function('import', [build.string('fs')]), 'fs'), build.block([build.function('subdir', [build.string('meson')])]))
build.if_(build.method('is_dir', build.identifier('fs'), [build.string('meson')]), ]
build.block([build.function('subdir', [build.string('meson')])]))
] def _create_lib(self, pkg: PackageState, build: builder.Builder, crate_type: manifest.CRATE_TYPE) -> T.List[mparser.BaseNode]:
dependencies: T.List[mparser.BaseNode] = []
dependency_map: T.Dict[mparser.BaseNode, mparser.BaseNode] = {}
def _create_lib(cargo: Manifest, build: builder.Builder, crate_type: manifest.CRATE_TYPE) -> T.List[mparser.BaseNode]: for name in pkg.required_deps:
dependencies: T.List[mparser.BaseNode] = [] dep = pkg.manifest.dependencies[name]
dependency_map: T.Dict[mparser.BaseNode, mparser.BaseNode] = {} dependencies.append(build.identifier(_dependency_varname(dep.package)))
for name, dep in cargo.dependencies.items(): if name != dep.package:
dependencies.append(build.identifier(_dependency_varname(dep.package))) dependency_map[build.string(fixup_meson_varname(dep.package))] = build.string(name)
if name != dep.package:
dependency_map[build.string(fixup_meson_varname(dep.package))] = build.string(name) rust_args: T.List[mparser.BaseNode] = [
build.identifier('features_args'),
rust_args: T.List[mparser.BaseNode] = [ build.identifier(_extra_args_varname())
build.identifier('features_args'), ]
build.identifier(_extra_args_varname())
] dependencies.append(build.identifier(_extra_deps_varname()))
dependencies.append(build.identifier(_extra_deps_varname())) posargs: T.List[mparser.BaseNode] = [
build.string(fixup_meson_varname(pkg.manifest.package.name)),
posargs: T.List[mparser.BaseNode] = [ build.string(pkg.manifest.lib.path),
build.string(fixup_meson_varname(cargo.package.name)), ]
build.string(cargo.lib.path),
] kwargs: T.Dict[str, mparser.BaseNode] = {
'dependencies': build.array(dependencies),
kwargs: T.Dict[str, mparser.BaseNode] = { 'rust_dependency_map': build.dict(dependency_map),
'dependencies': build.array(dependencies), 'rust_args': build.array(rust_args),
'rust_dependency_map': build.dict(dependency_map), }
'rust_args': build.array(rust_args),
} lib: mparser.BaseNode
if pkg.manifest.lib.proc_macro or crate_type == 'proc-macro':
lib: mparser.BaseNode lib = build.method('proc_macro', build.identifier('rust'), posargs, kwargs)
if cargo.lib.proc_macro or crate_type == 'proc-macro':
lib = build.method('proc_macro', build.identifier('rust'), posargs, kwargs)
else:
if crate_type in {'lib', 'rlib', 'staticlib'}:
target_type = 'static_library'
elif crate_type in {'dylib', 'cdylib'}:
target_type = 'shared_library'
else: else:
raise MesonException(f'Unsupported crate type {crate_type}') if crate_type in {'lib', 'rlib', 'staticlib'}:
if crate_type in {'staticlib', 'cdylib'}: target_type = 'static_library'
kwargs['rust_abi'] = build.string('c') elif crate_type in {'dylib', 'cdylib'}:
lib = build.function(target_type, posargs, kwargs) target_type = 'shared_library'
else:
# features_args = [] raise MesonException(f'Unsupported crate type {crate_type}')
# foreach f, _ : features if crate_type in {'staticlib', 'cdylib'}:
# features_args += ['--cfg', 'feature="' + f + '"'] kwargs['rust_abi'] = build.string('c')
# endforeach lib = build.function(target_type, posargs, kwargs)
# lib = xxx_library()
# dep = declare_dependency() features_args: T.List[mparser.BaseNode] = []
# meson.override_dependency() for f in pkg.features:
return [ features_args += [build.string('--cfg'), build.string(f'feature="{f}"')]
build.assign(build.array([]), 'features_args'),
build.foreach(['f', '_'], build.identifier('features'), build.block([ # features_args = ['--cfg', 'feature="f1"', ...]
build.plusassign( # lib = xxx_library()
build.array([ # dep = declare_dependency()
build.string('--cfg'), # meson.override_dependency()
build.plus(build.string('feature="'), build.plus(build.identifier('f'), build.string('"'))), return [
]), build.assign(build.array(features_args), 'features_args'),
'features_args') build.assign(lib, 'lib'),
]) build.assign(
), build.function(
build.assign(lib, 'lib'), 'declare_dependency',
build.assign( kw={
build.function( 'link_with': build.identifier('lib'),
'declare_dependency', 'variables': build.dict({
kw={ build.string('features'): build.string(','.join(pkg.features)),
'link_with': build.identifier('lib'), })
'variables': build.dict({ },
build.string('features'): build.method('join', build.string(','), [build.method('keys', build.identifier('features'))]), ),
}) 'dep'
}, ),
build.method(
'override_dependency',
build.identifier('meson'),
[
build.string(_dependency_name(pkg.manifest.package.name, pkg.manifest.package.api)),
build.identifier('dep'),
],
), ),
'dep' ]
),
build.method(
'override_dependency',
build.identifier('meson'),
[
build.string(_dependency_name(cargo.package.name, _version_to_api(cargo.package.version))),
build.identifier('dep'),
],
),
]
def interpret(subp_name: str, subdir: str, env: Environment) -> T.Tuple[mparser.CodeBlockNode, dict[OptionKey, options.UserOption[Any]]]:
# subp_name should be in the form "foo-0.1-rs"
package_name = subp_name.rsplit('-', 2)[0]
manifests = _load_manifests(os.path.join(env.source_dir, subdir))
cargo = manifests.get(package_name)
if not cargo:
raise MesonException(f'Cargo package {package_name!r} not found in {subdir}')
filename = os.path.join(cargo.subdir, cargo.path, 'Cargo.toml')
build = builder.Builder(filename)
# Generate project options
project_options: T.Dict[OptionKey, options.UserOption] = {}
for feature in cargo.features:
key = OptionKey(_option_name(feature), subproject=subp_name)
enabled = feature == 'default'
project_options[key] = options.UserBooleanOption(key.name, 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)
ast += _create_meson_subdir(cargo, build)
# Libs are always auto-discovered and there's no other way to handle them,
# which is unfortunate for reproducibility
if os.path.exists(os.path.join(env.source_dir, cargo.subdir, cargo.path, cargo.lib.path)):
for crate_type in cargo.lib.crate_type:
ast.extend(_create_lib(cargo, build, crate_type))
return build.block(ast), project_options
def load_wraps(source_dir: str, subproject_dir: str) -> T.List[PackageDefinition]: def load_wraps(source_dir: str, subproject_dir: str) -> T.List[PackageDefinition]:

@ -45,6 +45,7 @@ if T.TYPE_CHECKING:
from .compilers import Compiler from .compilers import Compiler
from .wrap.wrap import Resolver from .wrap.wrap import Resolver
from . import cargo
CompilersDict = T.Dict[str, Compiler] CompilersDict = T.Dict[str, Compiler]
@ -687,6 +688,8 @@ class Environment:
self.default_cmake = ['cmake'] self.default_cmake = ['cmake']
self.default_pkgconfig = ['pkg-config'] self.default_pkgconfig = ['pkg-config']
self.wrap_resolver: T.Optional['Resolver'] = None self.wrap_resolver: T.Optional['Resolver'] = None
# Store a global state of Cargo dependencies
self.cargo: T.Optional[cargo.Interpreter] = None
def _load_machine_file_options(self, config: 'ConfigParser', properties: Properties, machine: MachineChoice) -> None: def _load_machine_file_options(self, config: 'ConfigParser', properties: Properties, machine: MachineChoice) -> None:
"""Read the contents of a Machine file and put it in the options store.""" """Read the contents of a Machine file and put it in the options store."""

@ -1045,9 +1045,10 @@ class Interpreter(InterpreterBase, HoldableObject):
FeatureNew.single_use('Cargo subproject', '1.3.0', self.subproject, location=self.current_node) FeatureNew.single_use('Cargo subproject', '1.3.0', self.subproject, location=self.current_node)
mlog.warning('Cargo subproject is an experimental feature and has no backwards compatibility guarantees.', mlog.warning('Cargo subproject is an experimental feature and has no backwards compatibility guarantees.',
once=True, location=self.current_node) once=True, location=self.current_node)
if self.environment.cargo is None:
self.environment.cargo = cargo.Interpreter(self.environment)
with mlog.nested(subp_name): with mlog.nested(subp_name):
ast, options = cargo.interpret(subp_name, subdir, self.environment) ast = self.environment.cargo.interpret(subdir)
self.coredata.update_project_options(options, subp_name)
return self._do_subproject_meson( return self._do_subproject_meson(
subp_name, subdir, default_options, kwargs, ast, subp_name, subdir, default_options, kwargs, ast,
# FIXME: Are there other files used by cargo interpreter? # FIXME: Are there other files used by cargo interpreter?

@ -8,6 +8,10 @@ version = "0.1"
optional = true optional = true
version = "1.0" version = "1.0"
[dependencies.common]
version = "0.0.1"
features = ["f2"]
[features] [features]
default = ["f2"] default = ["f2"]
f1 = [] f1 = []

@ -0,0 +1,12 @@
[package]
name = "common"
version = "0.0.1"
edition = "2021"
[lib]
crate-type = ["rlib"]
path = "lib.rs"
[features]
f1 = []
f2 = []

@ -0,0 +1,4 @@
#[cfg(all(feature = "f1", feature = "f2"))]
pub fn common_func() -> i32 {
0
}

@ -0,0 +1,3 @@
[package]
name = "extra-deps"
version = "1.0"

@ -1,7 +1,5 @@
project('extra dep', 'c', version: '1.0') project('extra dep', 'c', version: '1.0')
assert(get_option('feature-default') == true)
l = static_library('extra-dep', 'lib.c') l = static_library('extra-dep', 'lib.c')
d = declare_dependency(link_with: l, d = declare_dependency(link_with: l,
variables: { variables: {

@ -20,6 +20,10 @@ version = "1.0"
[dependencies] [dependencies]
mybar = { version = "0.1", package = "bar", default-features = false } mybar = { version = "0.1", package = "bar", default-features = false }
[dependencies.common]
version = "0.0.1"
features = ["f1"]
[features] [features]
default = ["f1"] default = ["f1"]
f1 = ["f2", "f3"] f1 = ["f2", "f3"]

@ -1,3 +1,5 @@
extern crate common;
extern "C" { extern "C" {
fn extra_func() -> i32; fn extra_func() -> i32;
} }
@ -5,6 +7,7 @@ extern "C" {
#[cfg(feature = "foo")] #[cfg(feature = "foo")]
#[no_mangle] #[no_mangle]
pub extern "C" fn rust_func() -> i32 { pub extern "C" fn rust_func() -> i32 {
assert!(common::common_func() == 0);
let v: i32; let v: i32;
unsafe { unsafe {
v = extra_func(); v = extra_func();

Loading…
Cancel
Save