diff --git a/docs/markdown/Wrap-dependency-system-manual.md b/docs/markdown/Wrap-dependency-system-manual.md index 3983d28771e9..40fff97cedcb 100644 --- a/docs/markdown/Wrap-dependency-system-manual.md +++ b/docs/markdown/Wrap-dependency-system-manual.md @@ -348,27 +348,6 @@ method = cargo 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 responsability 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')` where the project can add manual logic that would usually be part of `build.rs`. Some naming conventions need to be respected: diff --git a/docs/markdown/snippets/cargo_features.md b/docs/markdown/snippets/cargo_features.md new file mode 100644 index 000000000000..7d841997358a --- /dev/null +++ b/docs/markdown/snippets/cargo_features.md @@ -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 option That were previously +possible to set as show above or from command line `-Dfoo-rs:feature-foo=true`. diff --git a/mesonbuild/cargo/__init__.py b/mesonbuild/cargo/__init__.py index 10cb0be103c0..0a4d5f2abdcb 100644 --- a/mesonbuild/cargo/__init__.py +++ b/mesonbuild/cargo/__init__.py @@ -1,6 +1,6 @@ __all__ = [ - 'interpret', + 'Interpreter', 'load_wraps', ] -from .interpreter import interpret, load_wraps +from .interpreter import Interpreter, load_wraps diff --git a/mesonbuild/cargo/interpreter.py b/mesonbuild/cargo/interpreter.py index 33b9d6073e4b..4e10110a5aea 100644 --- a/mesonbuild/cargo/interpreter.py +++ b/mesonbuild/cargo/interpreter.py @@ -23,8 +23,8 @@ from . import builder from . import version -from ..mesonlib import MesonException, Popen_safe, OptionKey -from .. import coredata, options, mlog +from ..mesonlib import MesonException, Popen_safe +from .. import coredata, mlog from ..wrap.wrap import PackageDefinition if T.TYPE_CHECKING: @@ -146,7 +146,10 @@ class Package: autoexamples: bool = True autotests: 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 class Dependency: @@ -275,7 +278,6 @@ class Manifest: Cargo subprojects can contain what Meson wants to treat as multiple, interdependent, subprojects. - :param subdir: the subdirectory that this cargo project is in :param path: the path within the cargo subproject. """ @@ -290,7 +292,6 @@ class Manifest: example: T.List[Example] features: T.Dict[str, T.List[str]] target: T.Dict[str, T.Dict[str, Dependency]] - subdir: str path: str = '' def __post_init__(self) -> None: @@ -321,7 +322,6 @@ def _convert_manifest(raw_manifest: manifest.Manifest, subdir: str, path: str = raw_manifest.get('features', {}), {k: {k2: Dependency.from_raw(k2, v2) for k2, v2 in v.get('dependencies', {}).items()} for k, v in raw_manifest.get('target', {}).items()}, - subdir, path, ) @@ -388,18 +388,6 @@ 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 _options_varname(depname: str) -> str: - return f'{fixup_meson_varname(depname)}_options' - - def _extra_args_varname() -> str: return 'extra_args' @@ -408,128 +396,174 @@ def _extra_deps_varname() -> str: return 'extra_deps' -def _create_project(cargo: Manifest, build: builder.Builder) -> T.List[mparser.BaseNode]: - """Create a function call - - :param cargo: The Manifest to generate from - :param build: The AST builder - :return: a list nodes - """ - args: T.List[mparser.BaseNode] = [] - args.extend([ - build.string(cargo.package.name), - build.string('rust'), - ]) - kwargs: T.Dict[str, mparser.BaseNode] = { - 'version': build.string(cargo.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={cargo.package.edition}')]), - } - if cargo.package.license: - kwargs['license'] = build.string(cargo.package.license) - elif cargo.package.license_file: - kwargs['license_files'] = build.string(cargo.package.license_file) - - 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'), +class PackageState: + def __init__(self, manifest: Manifest) -> None: + self.manifest = manifest + self.features: T.Set[str] = set() + self.required_deps: T.Set[str] = set() + self.optional_deps_features: T.Dict[str, T.Set[str]] = collections.defaultdict(set) + + +@dataclasses.dataclass(frozen=True) +class PackageKey: + package_name: str + api: str + + +class Interpreter: + def __init__(self, env: Environment) -> None: + self.environment = env + # Map Cargo.toml's subdir to loaded manifest. + self.manifests: T.Dict[str, Manifest] = {} + # Map of cargo package (name + api) to its state + self.packages: T.Dict[PackageKey, PackageState] = {} + + def interpret(self, subdir: str) -> mparser.CodeBlockNode: + manifest = self._load_manifest(subdir) + pkg, cached = self._fetch_package(manifest.package.name, manifest.package.api) + 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` + self._enable_feature(pkg, 'default') + + # 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) + 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]), + ]), ] - 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))) - - ast.append(build.function('message', [ - build.string('Enabled features:'), - build.method('keys', build.identifier('features'))], - )) - - 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(): - # xxx_options += {'feature-default': true, ...} - extra_options: T.Dict[mparser.BaseNode, mparser.BaseNode] = { - build.string(_option_name('default')): build.bool(dep.default_features), - } + ast += self._create_dependencies(pkg, build) + ast += self._create_meson_subdir(build) + + # Libs are always auto-discovered and there's no other way to handle them, + # which is unfortunate for reproducability + 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.extend(self._create_lib(pkg, build, crate_type)) + + return build.block(ast) + + def _fetch_package(self, package_name: str, api: str) -> T.Tuple[PackageState, bool]: + key = PackageKey(package_name, api) + pkg = self.packages.get(key) + if pkg: + return pkg, True + meson_depname = _dependency_name(package_name, api) + subdir, _ = self.environment.wrap_resolver.resolve(meson_depname) + manifest = self._load_manifest(subdir) + pkg = PackageState(manifest) + 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: - extra_options[build.string(_option_name(f))] = build.bool(True) - ast.append(build.plusassign(build.dict(extra_options), _options_varname(name))) - + self._enable_feature(dep_pkg, f) + 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 = { '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. # # However, this subproject could have been previously configured with a @@ -541,8 +575,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 # 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']). - ast.extend([ - # xxx_dep = dependency('xxx', version : ..., default_options : xxx_options) + return [ + # xxx_dep = dependency('xxx', version : ...) build.assign( build.function( 'dependency', @@ -551,188 +585,132 @@ def _create_dependencies(cargo: Manifest, build: builder.Builder) -> T.List[mpar ), _dependency_varname(dep.package), ), - # if xxx_dep.found() - build.if_(build.method('found', build.identifier(_dependency_varname(dep.package))), build.block([ - # actual_features = xxx_dep.get_variable('features', default_value : '').split(',') - build.assign( + # actual_features = xxx_dep.get_variable('features', default_value : '').split(',') + build.assign( + build.method( + 'split', build.method( - 'split', - build.method( - 'get_variable', - build.identifier(_dependency_varname(dep.package)), - [build.string('features')], - {'default_value': build.string('')} - ), - [build.string(',')], + 'get_variable', + build.identifier(_dependency_varname(dep.package)), + [build.string('features')], + {'default_value': build.string('')} ), - 'actual_features' + [build.string(',')], ), - # needed_features = [] - # foreach f, _ : xxx_options - # needed_features += f.substring(8) - # endforeach - build.assign(build.array([]), 'needed_features'), - build.foreach(['f', 'enabled'], build.identifier(_options_varname(name)), build.block([ - build.if_(build.identifier('enabled'), build.block([ - build.plusassign( - build.method('substring', build.identifier('f'), [build.number(len(_OPTION_NAME_PREFIX))]), - 'needed_features'), - ])), - ])), - # foreach f : needed_features - # if f not in actual_features - # error() - # endif - # endforeach - 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', [ - 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'), - ]) - ])) - ])), + 'actual_features' + ), + # needed_features = [f1, f2, ...] + # foreach f : needed_features + # if f not in actual_features + # error() + # endif + # endforeach + build.assign(build.array([build.string(f) for f in pkg.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', [ + 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(cargo: Manifest, build: builder.Builder) -> T.List[mparser.BaseNode]: - # 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 = [] - # fs = import('fs') - # if fs.is_dir('meson') - # subdir('meson') - # endif - return [ - build.assign(build.array([]), _extra_args_varname()), - build.assign(build.array([]), _extra_deps_varname()), - build.assign(build.function('import', [build.string('fs')]), 'fs'), - build.if_(build.method('is_dir', build.identifier('fs'), [build.string('meson')]), - build.block([build.function('subdir', [build.string('meson')])])) - ] - - -def _create_lib(cargo: Manifest, 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] = {} - for name, dep in cargo.dependencies.items(): - dependencies.append(build.identifier(_dependency_varname(dep.package))) - 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'), - build.identifier(_extra_args_varname()) - ] - - dependencies.append(build.identifier(_extra_deps_varname())) - - posargs: T.List[mparser.BaseNode] = [ - build.string(fixup_meson_varname(cargo.package.name)), - build.string(cargo.lib.path), - ] - - 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 - 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' + ] + + 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. + # This is used to replace build.rs logic. + + # extra_args = [] + # extra_deps = [] + # fs = import('fs') + # if fs.is_dir('meson') + # subdir('meson') + # endif + return [ + build.assign(build.array([]), _extra_args_varname()), + build.assign(build.array([]), _extra_deps_varname()), + build.assign(build.function('import', [build.string('fs')]), 'fs'), + 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] = {} + for name in pkg.required_deps: + dep = pkg.manifest.dependencies[name] + dependencies.append(build.identifier(_dependency_varname(dep.package))) + 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'), + build.identifier(_extra_args_varname()) + ] + + dependencies.append(build.identifier(_extra_deps_varname())) + + posargs: T.List[mparser.BaseNode] = [ + build.string(fixup_meson_varname(pkg.manifest.package.name)), + build.string(pkg.manifest.lib.path), + ] + + 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 + if pkg.manifest.lib.proc_macro or crate_type == 'proc-macro': + lib = build.method('proc_macro', build.identifier('rust'), posargs, kwargs) else: - raise MesonException(f'Unsupported crate type {crate_type}') - if crate_type in {'staticlib', 'cdylib'}: - 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( - 'declare_dependency', - kw={ - 'link_with': build.identifier('lib'), - 'variables': build.dict({ - build.string('features'): build.method('join', build.string(','), [build.method('keys', build.identifier('features'))]), - }) - }, + if crate_type in {'lib', 'rlib', 'staticlib'}: + target_type = 'static_library' + elif crate_type in {'dylib', 'cdylib'}: + target_type = 'shared_library' + else: + raise MesonException(f'Unsupported crate type {crate_type}') + if crate_type in {'staticlib', 'cdylib'}: + 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( + 'declare_dependency', + kw={ + 'link_with': build.identifier('lib'), + 'variables': build.dict({ + build.string('features'): build.string(','.join(pkg.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 reproducability - 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]: diff --git a/mesonbuild/environment.py b/mesonbuild/environment.py index be40dbcfd4be..bb92c8e9107f 100644 --- a/mesonbuild/environment.py +++ b/mesonbuild/environment.py @@ -44,6 +44,7 @@ from .compilers import Compiler from .wrap.wrap import Resolver + from . import cargo CompilersDict = T.Dict[str, Compiler] @@ -654,6 +655,8 @@ def __init__(self, source_dir: str, build_dir: str, cmd_options: coredata.Shared self.default_cmake = ['cmake'] self.default_pkgconfig = ['pkg-config'] 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: """Read the contents of a Machine file and put it in the options store.""" diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index ef28d864fb47..200396b2bd65 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -1040,9 +1040,10 @@ def _do_subproject_cargo(self, subp_name: str, subdir: str, kwargs: kwtypes.DoSubproject) -> SubprojectHolder: from .. import cargo FeatureNew.single_use('Cargo subproject', '1.3.0', self.subproject, location=self.current_node) + if self.environment.cargo is None: + self.environment.cargo = cargo.Interpreter(self.environment) with mlog.nested(subp_name): - ast, options = cargo.interpret(subp_name, subdir, self.environment) - self.coredata.update_project_options(options, subp_name) + ast = self.environment.cargo.interpret(subdir) 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-0.1-rs/Cargo.toml b/test cases/rust/22 cargo subproject/subprojects/bar-0.1-rs/Cargo.toml index d60a5d8f1c76..c0f7ffd8dd1e 100644 --- a/test cases/rust/22 cargo subproject/subprojects/bar-0.1-rs/Cargo.toml +++ b/test cases/rust/22 cargo subproject/subprojects/bar-0.1-rs/Cargo.toml @@ -8,6 +8,10 @@ version = "0.1" optional = true version = "1.0" +[dependencies.common] +version = "0.0.1" +features = ["f2"] + [features] default = ["f2"] f1 = [] diff --git a/test cases/rust/22 cargo subproject/subprojects/common-0-rs.wrap b/test cases/rust/22 cargo subproject/subprojects/common-0-rs.wrap new file mode 100644 index 000000000000..99686e90e78e --- /dev/null +++ b/test cases/rust/22 cargo subproject/subprojects/common-0-rs.wrap @@ -0,0 +1,2 @@ +[wrap-file] +method = cargo diff --git a/test cases/rust/22 cargo subproject/subprojects/common-0-rs/Cargo.toml b/test cases/rust/22 cargo subproject/subprojects/common-0-rs/Cargo.toml new file mode 100644 index 000000000000..b22e1accf288 --- /dev/null +++ b/test cases/rust/22 cargo subproject/subprojects/common-0-rs/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "common" +version = "0.0.1" +edition = "2021" + +[lib] +crate-type = ["rlib"] +path = "lib.rs" + +[features] +f1 = [] +f2 = [] diff --git a/test cases/rust/22 cargo subproject/subprojects/common-0-rs/lib.rs b/test cases/rust/22 cargo subproject/subprojects/common-0-rs/lib.rs new file mode 100644 index 000000000000..a7adf8f62953 --- /dev/null +++ b/test cases/rust/22 cargo subproject/subprojects/common-0-rs/lib.rs @@ -0,0 +1,4 @@ +#[cfg(all(feature = "f1", feature = "f2"))] +pub fn common_func() -> i32 { + 0 +} diff --git a/test cases/rust/22 cargo subproject/subprojects/extra-dep-1-rs/Cargo.toml b/test cases/rust/22 cargo subproject/subprojects/extra-dep-1-rs/Cargo.toml new file mode 100644 index 000000000000..4b6fa5777f45 --- /dev/null +++ b/test cases/rust/22 cargo subproject/subprojects/extra-dep-1-rs/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "extra-deps" +version = "1.0" diff --git a/test cases/rust/22 cargo subproject/subprojects/extra-dep-1-rs/meson.build b/test cases/rust/22 cargo subproject/subprojects/extra-dep-1-rs/meson.build index 40d109b2d0f8..b5ca439513dc 100644 --- a/test cases/rust/22 cargo subproject/subprojects/extra-dep-1-rs/meson.build +++ b/test cases/rust/22 cargo subproject/subprojects/extra-dep-1-rs/meson.build @@ -1,7 +1,5 @@ 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: { diff --git a/test cases/rust/22 cargo subproject/subprojects/foo-0-rs/Cargo.toml b/test cases/rust/22 cargo subproject/subprojects/foo-0-rs/Cargo.toml index 0f0225d06cc9..858efa4dd9cc 100644 --- a/test cases/rust/22 cargo subproject/subprojects/foo-0-rs/Cargo.toml +++ b/test cases/rust/22 cargo subproject/subprojects/foo-0-rs/Cargo.toml @@ -20,6 +20,10 @@ version = "1.0" [dependencies] mybar = { version = "0.1", package = "bar", default-features = false } +[dependencies.common] +version = "0.0.1" +features = ["f1"] + [features] default = ["f1"] f1 = ["f2", "f3"] diff --git a/test cases/rust/22 cargo subproject/subprojects/foo-0-rs/lib.rs b/test cases/rust/22 cargo subproject/subprojects/foo-0-rs/lib.rs index 1c8cbc9d3fb5..a1a976a80339 100644 --- a/test cases/rust/22 cargo subproject/subprojects/foo-0-rs/lib.rs +++ b/test cases/rust/22 cargo subproject/subprojects/foo-0-rs/lib.rs @@ -1,3 +1,5 @@ +extern crate common; + extern "C" { fn extra_func() -> i32; } @@ -5,6 +7,7 @@ extern "C" { #[cfg(feature = "foo")] #[no_mangle] pub extern "C" fn rust_func() -> i32 { + assert!(common::common_func() == 0); let v: i32; unsafe { v = extra_func();