diff --git a/docs/markdown/Rust-module.md b/docs/markdown/Rust-module.md index 5ce0fdcdb0d6..fc924ee71be2 100644 --- a/docs/markdown/Rust-module.md +++ b/docs/markdown/Rust-module.md @@ -4,6 +4,9 @@ authors: - name: Dylan Baker email: dylan@pnwbakers.com years: [2020, 2021, 2022, 2024] + - name: Paolo Bonzini + email: bonzini@gnu.org + years: [2025] ... # Rust module @@ -168,3 +171,117 @@ Only a subset of [[shared_library]] keyword arguments are allowed: - link_depends - link_with - override_options + +### workspace() + +```meson +rustmod.workspace(...) +``` + +*Since 1.10.0* + +Create and return a `workspace` object for managing the project's Cargo +workspace. + +Keyword arguments: +- `default_features`: (`bool`, optional) Whether to enable default features. + If not specified and `features` is provided, defaults to true. +- `features`: (`list[str]`, optional) List of additional features to enable globally + +The function must be called in a project with `Cargo.lock` and `Cargo.toml` +files in the root source directory. While the object currently has +no methods, upon its creation Meson analyzes the `Cargo.toml` file and +computes the full set of dependencies and features needed to build the +package in `Cargo.toml`. Therefore, this function should be invoked before +using Cargo subprojects. + +If either argument is provided, the build will use a custom set of features. +Features can only be set once - subsequent calls will fail if different features +are specified. + +When `features` is provided without `default_features: false`, the 'default' feature is +automatically included. + +## Workspace object + +### workspace.packages() + +```meson +packages = ws.packages() +``` + +Returns a list of package names in the workspace. + +### workspace.subproject() + +```meson +package = ws.subproject(package_name, api) +``` + +Returns a `package` object for managing a specific package within the workspace. + +Positional arguments: +- `package_name`: (`str`) The name of the package to retrieve +- `api`: (`str`, optional) The version constraints for the package in Cargo format + +## Package object + +The package object returned by `workspace.subproject()` provides methods +for working with individual packages in a Cargo workspace. + +### subproject.name() + +```meson +name = pkg.name() +``` + +Returns the name of the subproject. + +### subproject.version() + +```meson +version = pkg.version() +``` + +Returns the normalized version number of the subproject. + +### subproject.api() + +```meson +api = pkg.api() +``` + +Returns the API version of the subproject, that is the version up to the first +nonzero element. + +### subproject.features() + +```meson +features = pkg.features() +``` + +Returns selected features for a specific subproject. + +### subproject.all_features() + +```meson +all_features = pkg.all_features() +``` + +Returns all defined features for a specific subproject. + +### subproject.dependency() + +```meson +dep = subproject.dependency(...) +``` + +Returns a dependency object for the subproject that can be used with other Meson targets. + +*Note*: right now, this method is implemented on top of the normal Meson function +[[dependency]]; this is subject to change in future releases. It is recommended +to always retrieve a Cargo subproject's dependency object via this method. + +Keyword arguments: +- `rust_abi`: (`str`, optional) The ABI to use for the dependency. Valid values are + `'rust'`, `'c'`, or `'proc-macro'`. The package must support the specified ABI. diff --git a/docs/markdown/Rust.md b/docs/markdown/Rust.md index 67bbdec5bdf3..8f4b736333a2 100644 --- a/docs/markdown/Rust.md +++ b/docs/markdown/Rust.md @@ -106,3 +106,40 @@ target name. First, dashes, spaces and dots are replaced with underscores. Sec *since 1.10.0* anything after the first `+` is dropped. This allows creating multiple targets for the same crate name, for example when the same crate is built multiple times with different features, or for both the build and the host machine. + +## Cargo interaction + +*Since 1.10.0* + +In most cases, a Rust program will use Cargo to download crates. Meson is able +to build Rust library crates based on a `Cargo.toml` file; each external crate +corresponds to a subproject. Rust module's ` that do not need a `build.rs` file +need no intervention, whereas if a `build.rs` file is present it needs to be +converted manually to Meson code. + +To enable automatic configuration of Cargo dependencies, your project must +have `Cargo.toml` and `Cargo.lock` files in the root source directory; +this enables proper feature resolution across crates. You can then +create a workspace object using the Rust module, and retrieve specific +packages from the workspace: + +```meson +rust = import('rust') +cargo = rust.workspace() +anyhow_dep = ws.subproject('anyhow').dependency() +``` + +The workspace object also enables configuration of Cargo features, for example +from Meson options: + +```meson +ws = rust.workspace( + features: ['feature1', 'feature2']) +``` + +### Limitations + +All your own crates must be built using the usual Meson functions such as +[[static_library]] or [[executable]]. In the future, workspace object +functionality will be extended to help building rustc command lines +based on features, dependency names, and so on. diff --git a/docs/markdown/Wrap-dependency-system-manual.md b/docs/markdown/Wrap-dependency-system-manual.md index 077535a8db4d..a4c7081a4e1e 100644 --- a/docs/markdown/Wrap-dependency-system-manual.md +++ b/docs/markdown/Wrap-dependency-system-manual.md @@ -368,10 +368,7 @@ Since *1.5.0* Cargo wraps can also be provided with `Cargo.lock` file at the roo of (sub)project source tree. Meson will automatically load that file and convert it into a series of wraps definitions. -Since *1.10.0* Workspace Cargo.toml are supported. For the time being it is -recommended to regroup all Cargo dependencies inside a single workspace invoked -from the main Meson project. When invoking multiple different Cargo subprojects -from Meson, feature resolution of common dependencies might be wrong. +Since *1.10.0* Workspace Cargo.toml are supported. ## Using wrapped projects diff --git a/docs/markdown/snippets/cargo-workspace-object.md b/docs/markdown/snippets/cargo-workspace-object.md new file mode 100644 index 000000000000..c6bc4d4bac25 --- /dev/null +++ b/docs/markdown/snippets/cargo-workspace-object.md @@ -0,0 +1,13 @@ +## Cargo workspace object + +Meson now is able to parse the toplevel `Cargo.toml` file of the +project when the `workspace()` method of the Rust module is called. +This guarantees that features are resolved according to what is +in the `Cargo.toml` file, and in fact enables configuration of +features for the build. + +The returned object also allows retrieving features and dependencies +for Cargo subprojects. + +While Cargo subprojects remain experimental, the Meson project will +try to keep the workspace object reasonably backwards-compatible. diff --git a/mesonbuild/ast/printer.py b/mesonbuild/ast/printer.py index 0d0c821d4797..7838aea5d24c 100644 --- a/mesonbuild/ast/printer.py +++ b/mesonbuild/ast/printer.py @@ -31,7 +31,7 @@ def precedence_level(node: mparser.BaseNode) -> int: return 6 elif isinstance(node, (mparser.NotNode, mparser.UMinusNode)): return 7 - elif isinstance(node, mparser.FunctionNode): + elif isinstance(node, (mparser.FunctionNode, mparser.IndexNode, mparser.MethodNode)): return 8 elif isinstance(node, (mparser.ArrayNode, mparser.DictNode)): return 9 diff --git a/mesonbuild/cargo/__init__.py b/mesonbuild/cargo/__init__.py index c5b157f3c791..65e018a9d3b1 100644 --- a/mesonbuild/cargo/__init__.py +++ b/mesonbuild/cargo/__init__.py @@ -1,7 +1,9 @@ __all__ = [ 'Interpreter', + 'PackageState', 'TomlImplementationMissing', + 'WorkspaceState', ] -from .interpreter import Interpreter +from .interpreter import Interpreter, PackageState, WorkspaceState from .toml import TomlImplementationMissing diff --git a/mesonbuild/cargo/interpreter.py b/mesonbuild/cargo/interpreter.py index 297ff5e8745f..c40e544ec7d3 100644 --- a/mesonbuild/cargo/interpreter.py +++ b/mesonbuild/cargo/interpreter.py @@ -22,19 +22,21 @@ from .cfg import eval_cfg from .toml import load_toml from .manifest import Manifest, CargoLock, CargoLockPackage, Workspace, fixup_meson_varname -from ..mesonlib import is_parent_path, MesonException, MachineChoice, version_compare +from ..mesonlib import is_parent_path, MesonException, MachineChoice, unique_list, version_compare from .. import coredata, mlog from ..wrap.wrap import PackageDefinition if T.TYPE_CHECKING: from . import raw from .. import mparser + from typing_extensions import Literal + from .manifest import Dependency, SystemDependency from ..environment import Environment from ..interpreterbase import SubProject from ..compilers.rust import RustCompiler - from typing_extensions import Literal + RUST_ABI = Literal['rust', 'c', 'proc-macro'] def _dependency_name(package_name: str, api: str, suffix: str = '-rs') -> str: basename = package_name[:-len(suffix)] if suffix and package_name.endswith(suffix) else package_name @@ -180,6 +182,42 @@ def get_rustc_args(self, environment: Environment, subdir: str, machine: Machine args.extend(self.get_env_args(rustc, environment, subdir)) return args + def supported_abis(self) -> T.Set[RUST_ABI]: + """Return which ABIs are exposed by the package's crate_types.""" + crate_types = self.manifest.lib.crate_type + abis: T.Set[RUST_ABI] = set() + if any(ct in {'lib', 'rlib', 'dylib'} for ct in crate_types): + abis.add('rust') + if any(ct in {'staticlib', 'cdylib'} for ct in crate_types): + abis.add('c') + if 'proc-macro' in crate_types: + abis.add('proc-macro') + return abis + + def get_subproject_name(self) -> str: + return _dependency_name(self.manifest.package.name, self.manifest.package.api) + + def get_dependency_name(self, rust_abi: T.Optional[RUST_ABI]) -> str: + """Get the dependency name for a package with the given ABI.""" + supported_abis = self.supported_abis() + if rust_abi is None: + if len(supported_abis) > 1: + raise MesonException(f'Package {self.manifest.package.name} support more than one ABI') + rust_abi = next(iter(supported_abis)) + else: + if rust_abi not in supported_abis: + raise MesonException(f'Package {self.manifest.package.name} does not support ABI {rust_abi}') + + package_name = self.manifest.package.name + api = self.manifest.package.api + + if rust_abi in {'rust', 'proc-macro'}: + return _dependency_name(package_name, api) + elif rust_abi == 'c': + return _dependency_name(package_name, api, '') + else: + raise MesonException(f'Unknown rust_abi: {rust_abi}') + @dataclasses.dataclass(frozen=True) class PackageKey: @@ -201,6 +239,8 @@ class WorkspaceState: class Interpreter: + _features: T.Optional[T.List[str]] = None + def __init__(self, env: Environment, subdir: str, subprojects_dir: str) -> None: self.environment = env self.subprojects_dir = subprojects_dir @@ -220,28 +260,51 @@ def __init__(self, env: Environment, subdir: str, subprojects_dir: str) -> None: self.environment.wrap_resolver.merge_wraps(self.cargolock.wraps) self.build_def_files.append(filename) + @property + def features(self) -> T.List[str]: + """Get the features list. Once read, it cannot be modified.""" + if self._features is None: + self._features = ['default'] + return self._features + + @features.setter + def features(self, value: T.List[str]) -> None: + """Set the features list. Can only be set before first read.""" + value_unique = sorted(unique_list(value)) + if self._features is not None and value_unique != self._features: + raise MesonException("Cannot modify features after they have been selected or used") + self._features = value_unique + def get_build_def_files(self) -> T.List[str]: return self.build_def_files + def load_workspace(self, subdir: str) -> WorkspaceState: + """Load the root Cargo.toml package and prepare it with features and dependencies.""" + subdir = os.path.normpath(subdir) + manifest, cached = self._load_manifest(subdir) + ws = self._get_workspace(manifest, subdir, False) + if not cached: + self._prepare_entry_point(ws) + return ws + def _prepare_entry_point(self, ws: WorkspaceState) -> None: pkgs = [self._require_workspace_member(ws, m) for m in ws.workspace.default_members] for pkg in pkgs: self._prepare_package(pkg) - self._enable_feature(pkg, 'default') + for feature in self.features: + self._enable_feature(pkg, feature) def interpret(self, subdir: str, project_root: T.Optional[str] = None) -> mparser.CodeBlockNode: - manifest, cached = self._load_manifest(subdir) filename = os.path.join(self.environment.source_dir, subdir, 'Cargo.toml') build = builder.Builder(filename) if project_root: # this is a subdir() + manifest, _ = self._load_manifest(subdir) assert isinstance(manifest, Manifest) return self.interpret_package(manifest, build, subdir, project_root) - - ws = self._get_workspace(manifest, subdir, downloaded=False) - if not cached: - self._prepare_entry_point(ws) - return self.interpret_workspace(ws, build, subdir) + else: + ws = self.load_workspace(subdir) + return self.interpret_workspace(ws, build, subdir) def interpret_package(self, manifest: Manifest, build: builder.Builder, subdir: str, project_root: str) -> mparser.CodeBlockNode: # Build an AST for this package @@ -267,13 +330,14 @@ def _create_package(self, pkg: PackageState, build: builder.Builder, subdir: str crate_type = pkg.manifest.lib.crate_type if 'dylib' in crate_type and 'cdylib' in crate_type: raise MesonException('Cannot build both dylib and cdylib due to file name conflict') - if 'proc-macro' in crate_type: + abis = pkg.supported_abis() + if 'proc-macro' in abis: ast.extend(self._create_lib(pkg, build, subdir, 'proc-macro', shared=True)) - if any(x in crate_type for x in ['lib', 'rlib', 'dylib']): + if 'rust' in abis: ast.extend(self._create_lib(pkg, build, subdir, 'rust', static=('lib' in crate_type or 'rlib' in crate_type), shared='dylib' in crate_type)) - if any(x in crate_type for x in ['staticlib', 'cdylib']): + if 'c' in abis: ast.extend(self._create_lib(pkg, build, subdir, 'c', static='staticlib' in crate_type, shared='cdylib' in crate_type)) @@ -310,7 +374,6 @@ def _process_member(member: str) -> None: ast.append(build.function('subdir', [build.string(member)])) processed_members[member] = pkg - ast.append(build.assign(build.function('import', [build.string('rust')]), 'rust')) for member in ws.required_members: _process_member(member) ast = self._create_project(name, processed_members.get('.'), build) + ast @@ -385,6 +448,13 @@ def _resolve_package(self, package_name: str, version_constraints: T.List[str]) raise MesonException(f'Cannot determine version of cargo package {package_name}') return None + def resolve_package(self, package_name: str, api: str) -> T.Optional[PackageState]: + cargo_pkg = self._resolve_package(package_name, version.convert(api)) + if not cargo_pkg: + return None + api = version.api(cargo_pkg.version) + return self._fetch_package(package_name, api) + def _fetch_package_from_subproject(self, package_name: str, meson_depname: str) -> PackageState: subp_name, _ = self.environment.wrap_resolver.find_dep_provider(meson_depname) if subp_name is None: @@ -583,7 +653,16 @@ def _create_project(self, name: str, pkg: T.Optional[PackageState], build: build elif pkg.manifest.package.license_file: kwargs['license_files'] = build.string(pkg.manifest.package.license_file) - return [build.function('project', args, kwargs)] + # project(...) + # rust = import('rust') + # cargo = rust.workspace(dev_dependencies: False) + return [ + build.function('project', args, kwargs), + build.assign(build.function('import', [build.string('rust')]), + 'rust'), + build.assign(build.method('workspace', build.identifier('rust'), []), + 'cargo') + ] def _create_dependencies(self, pkg: PackageState, build: builder.Builder) -> T.List[mparser.BaseNode]: cfg = pkg.cfg @@ -628,12 +707,24 @@ def _create_system_dependency(self, name: str, dep: SystemDependency, build: bui def _create_dependency(self, pkg: PackageState, dep: Dependency, build: builder.Builder) -> T.List[mparser.BaseNode]: cfg = pkg.cfg - version_ = dep.meson_version or [pkg.manifest.package.version] - kw = { - 'version': build.array([build.string(s) for s in version_]), - } - # Lookup for this dependency with the features we want in default_options kwarg. - # + dep_obj: mparser.BaseNode + if self.cargolock and self.resolve_package(dep.package, dep.api): + dep_obj = build.method( + 'dependency', + build.method( + 'subproject', + build.identifier('cargo'), + [build.string(dep.package), build.string(dep.api)])) + else: + version_ = dep.meson_version or [pkg.manifest.package.version] + kw = { + 'version': build.array([build.string(s) for s in version_]), + } + dep_obj = build.function( + 'dependency', + [build.string(_dependency_name(dep.package, dep.api))], + kw) + # However, this subproject could have been previously configured with a # different set of features. Cargo collects the set of features globally # but Meson can only use features enabled by the first call that triggered @@ -644,13 +735,9 @@ def _create_dependency(self, pkg: PackageState, dep: Dependency, build: builder. # 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']). return [ - # xxx_dep = dependency('xxx', version : ...) + # xxx_dep = cargo.subproject('xxx', 'api').dependency() build.assign( - build.function( - 'dependency', - [build.string(_dependency_name(dep.package, dep.api))], - kw, - ), + dep_obj, _dependency_varname(dep), ), # actual_features = xxx_dep.get_variable('features', default_value : '').split(',') @@ -707,7 +794,7 @@ def _create_meson_subdir(self, build: builder.Builder) -> T.List[mparser.BaseNod ] def _create_lib(self, pkg: PackageState, build: builder.Builder, subdir: str, - lib_type: Literal['rust', 'c', 'proc-macro'], + lib_type: RUST_ABI, static: bool = False, shared: bool = False) -> T.List[mparser.BaseNode]: cfg = pkg.cfg dependencies: T.List[mparser.BaseNode] = [] diff --git a/mesonbuild/modules/__init__.py b/mesonbuild/modules/__init__.py index 3ff9368d907f..ed8deff0635b 100644 --- a/mesonbuild/modules/__init__.py +++ b/mesonbuild/modules/__init__.py @@ -7,7 +7,7 @@ import dataclasses import typing as T -from .. import build, mesonlib +from .. import build, dependencies, mesonlib from ..options import OptionKey from ..build import IncludeDirs from ..interpreterbase.decorators import noKwargs, noPosargs @@ -46,6 +46,7 @@ def __init__(self, interpreter: 'Interpreter') -> None: # The backend object is under-used right now, but we will need it: # https://github.com/mesonbuild/meson/issues/1419 self.backend = interpreter.backend + self.dependency_overrides = interpreter.build.dependency_overrides self.targets = interpreter.build.targets self.data = interpreter.build.data self.headers = interpreter.build.get_headers() @@ -108,6 +109,13 @@ def find_tool(self, name: str, depname: str, varname: str, required: bool = True # Normal program lookup return self.find_program(name, required=required, wanted=wanted) + def overridden_dependency(self, depname: str, for_machine: MachineChoice = MachineChoice.HOST) -> Dependency: + identifier = dependencies.get_dep_identifier(depname, {}) + try: + return self.dependency_overrides[for_machine][identifier].dep + except KeyError: + raise mesonlib.MesonException(f'dependency "{depname}" was not overridden for the {for_machine}') + def dependency(self, depname: str, native: bool = False, required: bool = True, wanted: T.Optional[str] = None) -> 'Dependency': kwargs: T.Dict[str, object] = {'native': native, 'required': required} diff --git a/mesonbuild/modules/rust.py b/mesonbuild/modules/rust.py index 89c20230b425..28b06994dfe0 100644 --- a/mesonbuild/modules/rust.py +++ b/mesonbuild/modules/rust.py @@ -10,29 +10,33 @@ from mesonbuild.interpreterbase.decorators import FeatureNew -from . import ExtensionModule, ModuleReturnValue, ModuleInfo +from . import ExtensionModule, ModuleReturnValue, ModuleInfo, ModuleObject from .. import mesonlib, mlog from ..build import (BothLibraries, BuildTarget, CustomTargetIndex, Executable, ExtractedObjects, GeneratedList, CustomTarget, InvalidArguments, Jar, StructuredSources, SharedLibrary, StaticLibrary) from ..compilers.compilers import are_asserts_disabled_for_subproject, lang_suffixes +from ..dependencies import Dependency from ..interpreter.type_checking import ( DEPENDENCIES_KW, LINK_WITH_KW, LINK_WHOLE_KW, SHARED_LIB_KWS, TEST_KWS, TEST_KWS_NO_ARGS, OUTPUT_KW, INCLUDE_DIRECTORIES, SOURCES_VARARGS, NoneType, in_set_validator ) -from ..interpreterbase import ContainerTypeInfo, InterpreterException, KwargInfo, typed_kwargs, typed_pos_args, noPosargs, permittedKwargs +from ..interpreterbase import ContainerTypeInfo, InterpreterException, KwargInfo, typed_kwargs, typed_pos_args, noKwargs, noPosargs, permittedKwargs from ..interpreter.interpreterobjects import Doctest -from ..mesonlib import File, MesonException, PerMachine +from ..mesonlib import File, MachineChoice, MesonException, PerMachine from ..programs import ExternalProgram, NonExistingExternalProgram if T.TYPE_CHECKING: from . import ModuleState from ..build import BuildTargetTypes, ExecutableKeywordArguments, IncludeDirs, LibTypes + from .. import cargo + from ..cargo.interpreter import RUST_ABI from ..compilers.rust import RustCompiler - from ..dependencies import Dependency, ExternalLibrary + from ..dependencies import ExternalLibrary from ..interpreter import Interpreter from ..interpreter import kwargs as _kwargs from ..interpreter.interpreter import SourceInputs, SourceOutputs from ..interpreter.interpreterobjects import Test + from ..interpreterbase import TYPE_kwargs from ..programs import OverrideProgram from ..interpreter.type_checking import SourcesVarargsType @@ -63,6 +67,12 @@ class FuncBindgen(TypedDict): language: T.Optional[Literal['c', 'cpp']] bindgen_version: T.List[str] + class FuncWorkspace(TypedDict): + default_features: T.Optional[bool] + features: T.List[str] + + class FuncDependency(TypedDict): + rust_abi: T.Optional[RUST_ABI] RUST_TEST_KWS: T.List[KwargInfo] = [ KwargInfo( @@ -81,6 +91,108 @@ def no_spaces_validator(arg: T.Optional[T.Union[str, T.List]]) -> T.Optional[str return None +class RustWorkspace(ModuleObject): + """Represents a Rust workspace, controlling the build of packages + recorded in a Cargo.lock file.""" + + def __init__(self, interpreter: Interpreter, ws: cargo.WorkspaceState) -> None: + super().__init__() + self.interpreter = interpreter + self.ws = ws + self.methods.update({ + 'packages': self.packages_method, + 'subproject': self.subproject_method, + }) + + @noPosargs + @noKwargs + def packages_method(self, state: ModuleState, args: T.List, kwargs: TYPE_kwargs) -> T.List[str]: + """Returns list of package names in workspace.""" + package_names = [pkg.manifest.package.name for pkg in self.ws.packages.values()] + return sorted(package_names) + + def _do_subproject(self, pkg: cargo.PackageState) -> None: + kw: _kwargs.DoSubproject = { + 'required': True, + 'version': None, + 'options': None, + 'cmake_options': [], + 'default_options': {}, + } + subp_name = pkg.get_subproject_name() + self.interpreter.do_subproject(subp_name, kw, force_method='cargo') + + @typed_pos_args('workspace.subproject', str, optargs=[str]) + @noKwargs + def subproject_method(self, state: ModuleState, args: T.Tuple[str, T.Optional[str]], kwargs: TYPE_kwargs) -> RustSubproject: + """Returns a package object for a subproject package.""" + package_name = args[0] + pkg = self.interpreter.cargo.resolve_package(package_name, args[1] or '') + if pkg is None: + if args[1]: + raise MesonException(f'No version of cargo package "{package_name}" provides API {args[1]}') + else: + raise MesonException(f'Cargo package "{package_name}" not available') + + self._do_subproject(pkg) + return RustSubproject(self, pkg) + + +class RustSubproject(ModuleObject): + """Represents a Rust package within a workspace.""" + + def __init__(self, rust_ws: RustWorkspace, package: cargo.PackageState) -> None: + super().__init__() + self.rust_ws = rust_ws + self.package = package + self.methods.update({ + 'all_features': self.all_features_method, + 'api': self.api_method, + 'dependency': self.dependency_method, + 'features': self.features_method, + 'name': self.name_method, + 'version': self.version_method, + }) + + @noPosargs + @noKwargs + def name_method(self, state: ModuleState, args: T.List, kwargs: TYPE_kwargs) -> str: + """Returns the name of the package.""" + return self.package.manifest.package.name + + @noPosargs + @noKwargs + def api_method(self, state: ModuleState, args: T.List, kwargs: TYPE_kwargs) -> str: + """Returns the API version of the package.""" + return self.package.manifest.package.api + + @noPosargs + @noKwargs + def version_method(self, state: ModuleState, args: T.List, kwargs: TYPE_kwargs) -> str: + """Returns the version of the package.""" + return self.package.manifest.package.version + + @noPosargs + @noKwargs + def all_features_method(self, state: ModuleState, args: T.List, kwargs: TYPE_kwargs) -> T.List[str]: + """Returns all features for specific package.""" + return sorted(list(self.package.manifest.features.keys())) + + @noPosargs + @noKwargs + def features_method(self, state: ModuleState, args: T.List, kwargs: TYPE_kwargs) -> T.List[str]: + """Returns chosen features for specific package.""" + return sorted(list(self.package.cfg.features)) + + @noPosargs + @typed_kwargs('package.dependency', + KwargInfo('rust_abi', (str, NoneType), default=None, validator=in_set_validator({'rust', 'c', 'proc-macro'}))) + def dependency_method(self, state: ModuleState, args: T.List, kwargs: FuncDependency) -> Dependency: + """Returns dependency for the package with the given ABI.""" + depname = self.package.get_dependency_name(kwargs['rust_abi']) + return state.overridden_dependency(depname) + + class RustModule(ExtensionModule): """A module that holds helper functions for rust.""" @@ -103,6 +215,7 @@ def __init__(self, interpreter: Interpreter) -> None: 'doctest': self.doctest, 'bindgen': self.bindgen, 'proc_macro': self.proc_macro, + 'workspace': self.workspace, }) def test_common(self, funcname: str, state: ModuleState, args: T.Tuple[str, BuildTarget], kwargs: FuncRustTest) -> T.Tuple[Executable, _kwargs.FuncTest]: @@ -500,6 +613,43 @@ def proc_macro(self, state: ModuleState, args: T.Tuple[str, SourcesVarargsType], target = state._interpreter.build_target(state.current_node, args, kwargs, SharedLibrary) return target + @FeatureNew('rust.workspace', '1.10.0') + @noPosargs + @typed_kwargs( + 'rust.workspace', + KwargInfo('default_features', (bool, NoneType), default=None), + KwargInfo( + 'features', + (ContainerTypeInfo(list, str), NoneType), + default=None, + listify=True, + ), + ) + def workspace(self, state: ModuleState, args: T.List, kwargs: FuncWorkspace) -> RustWorkspace: + """Creates a Rust workspace object, controlling the build of + all the packages in a Cargo.lock file.""" + if self.interpreter.cargo is None: + raise MesonException("rust.workspace() requires a Cargo project (Cargo.toml and Cargo.lock)") + + self.interpreter.add_languages(['rust'], True, MachineChoice.HOST) + self.interpreter.add_languages(['rust'], True, MachineChoice.BUILD) + + default_features = kwargs['default_features'] + features = kwargs['features'] + if default_features is not None or features is not None: + # If custom features are provided, default_features = None should be treated as True + if default_features is None: + default_features = True + + cargo_features = ['default'] if default_features else [] + if features is not None: + cargo_features.extend(features) + self.interpreter.cargo.features = cargo_features + + # Check if we already have a cached workspace for this cargo interpreter + ws = self.interpreter.cargo.load_workspace(state.subdir) + return RustWorkspace(self.interpreter, ws) + def initialize(interp: Interpreter) -> RustModule: return RustModule(interp) diff --git a/test cases/rust/31 rust.workspace package/Cargo.lock b/test cases/rust/31 rust.workspace package/Cargo.lock new file mode 100644 index 000000000000..989f6ff5b3a7 --- /dev/null +++ b/test cases/rust/31 rust.workspace package/Cargo.lock @@ -0,0 +1,19 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "answer" +version = "2.1.0" + +[[package]] +name = "hello" +version = "1.0.0" + +[[package]] +name = "package_test" +version = "0.1.0" +dependencies = [ + "answer", + "hello", +] diff --git a/test cases/rust/31 rust.workspace package/Cargo.toml b/test cases/rust/31 rust.workspace package/Cargo.toml new file mode 100644 index 000000000000..00bb0878e1f2 --- /dev/null +++ b/test cases/rust/31 rust.workspace package/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "package_test" +version = "0.1.0" +edition = "2021" + +[features] +default = ["feature1", "hello?/goodbye"] +feature1 = ["answer/large", "dep:hello"] +feature2 = [] + +[dependencies] +hello = { version = "1.0", path = "subprojects/hello-1.0", optional = true } +answer = { version = "2.1", path = "subprojects/answer-2.1", optional = true } diff --git a/test cases/rust/31 rust.workspace package/meson.build b/test cases/rust/31 rust.workspace package/meson.build new file mode 100644 index 000000000000..8fc85e735c84 --- /dev/null +++ b/test cases/rust/31 rust.workspace package/meson.build @@ -0,0 +1,34 @@ +project('package test', 'rust', default_options: ['rust_std=2021']) + +rust = import('rust') +cargo = rust.workspace() + +# Test workspace.packages() method +assert(cargo.packages() == ['answer', 'hello', 'package_test']) + +hello_rs = cargo.subproject('hello') +assert(hello_rs.name() == 'hello') +assert(hello_rs.version() == '1.0.0') +assert(hello_rs.api() == '1') +assert(hello_rs.all_features() == ['default', 'goodbye']) +assert(hello_rs.features() == ['default', 'goodbye']) + +answer_rs = cargo.subproject('answer', '2') +assert(answer_rs.name() == 'answer') +assert(answer_rs.version() == '2.1.0') +assert(answer_rs.api() == '2') +assert(answer_rs.all_features() == ['default', 'large']) +assert(answer_rs.features() == ['default', 'large']) + +e = executable('package-test', 'src/main.rs', + dependencies: [hello_rs.dependency(), answer_rs.dependency()], +) +test('package-test', e) + +# failure test cases for dependency() +testcase expect_error('package.dependency.*must be one of c, proc-macro, rust.*', how: 're') + hello_rs.dependency(rust_abi: 'something else') +endtestcase +testcase expect_error('Package hello does not support ABI c') + hello_rs.dependency(rust_abi: 'c') +endtestcase diff --git a/test cases/rust/31 rust.workspace package/src/main.rs b/test cases/rust/31 rust.workspace package/src/main.rs new file mode 100644 index 000000000000..13c02dd64ad8 --- /dev/null +++ b/test cases/rust/31 rust.workspace package/src/main.rs @@ -0,0 +1,8 @@ +use hello::{farewell, greet}; + +fn main() { + println!("{}", greet()); + println!("{}", farewell()); + println!("{}", answer::answer()); + println!("{}", answer::large_answer()); +} diff --git a/test cases/rust/31 rust.workspace package/subprojects/answer-2-rs.wrap b/test cases/rust/31 rust.workspace package/subprojects/answer-2-rs.wrap new file mode 100644 index 000000000000..d16d1f7a8483 --- /dev/null +++ b/test cases/rust/31 rust.workspace package/subprojects/answer-2-rs.wrap @@ -0,0 +1,2 @@ +[wrap-file] +directory = answer-2.1 diff --git a/test cases/rust/31 rust.workspace package/subprojects/answer-2.1/Cargo.toml b/test cases/rust/31 rust.workspace package/subprojects/answer-2.1/Cargo.toml new file mode 100644 index 000000000000..b8778226474f --- /dev/null +++ b/test cases/rust/31 rust.workspace package/subprojects/answer-2.1/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "answer" +version = "2.1.0" +edition = "2021" + +[lib] +crate-type = ["lib"] + +[features] +large = [] diff --git a/test cases/rust/31 rust.workspace package/subprojects/answer-2.1/meson.build b/test cases/rust/31 rust.workspace package/subprojects/answer-2.1/meson.build new file mode 100644 index 000000000000..cc8d463b6d11 --- /dev/null +++ b/test cases/rust/31 rust.workspace package/subprojects/answer-2.1/meson.build @@ -0,0 +1,9 @@ +project('answer', 'rust', default_options: ['rust_std=2021']) + +rust = import('rust') +cargo = rust.workspace() +assert(cargo.packages() == ['answer']) + +l = static_library('answer', 'src/lib.rs', rust_args: ['--cfg', 'feature="large"']) +dep = declare_dependency(link_with: l) +meson.override_dependency('answer-2-rs', dep) diff --git a/test cases/rust/31 rust.workspace package/subprojects/answer-2.1/src/lib.rs b/test cases/rust/31 rust.workspace package/subprojects/answer-2.1/src/lib.rs new file mode 100644 index 000000000000..b7a721b05f1d --- /dev/null +++ b/test cases/rust/31 rust.workspace package/subprojects/answer-2.1/src/lib.rs @@ -0,0 +1,10 @@ +pub fn answer() -> u8 +{ + 42 +} + +#[cfg(feature = "large")] +pub fn large_answer() -> u64 +{ + 42 +} diff --git a/test cases/rust/31 rust.workspace package/subprojects/hello-1-rs.wrap b/test cases/rust/31 rust.workspace package/subprojects/hello-1-rs.wrap new file mode 100644 index 000000000000..25e7751d0c9f --- /dev/null +++ b/test cases/rust/31 rust.workspace package/subprojects/hello-1-rs.wrap @@ -0,0 +1,3 @@ +[wrap-file] +directory = hello-1.0 +method = cargo diff --git a/test cases/rust/31 rust.workspace package/subprojects/hello-1.0/Cargo.toml b/test cases/rust/31 rust.workspace package/subprojects/hello-1.0/Cargo.toml new file mode 100644 index 000000000000..f6ab8eb91eeb --- /dev/null +++ b/test cases/rust/31 rust.workspace package/subprojects/hello-1.0/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "hello" +version = "1.0.0" +edition = "2021" + +[lib] +crate-type = ["lib"] + +[features] +goodbye = [] diff --git a/test cases/rust/31 rust.workspace package/subprojects/hello-1.0/src/lib.rs b/test cases/rust/31 rust.workspace package/subprojects/hello-1.0/src/lib.rs new file mode 100644 index 000000000000..47346350bd21 --- /dev/null +++ b/test cases/rust/31 rust.workspace package/subprojects/hello-1.0/src/lib.rs @@ -0,0 +1,10 @@ +pub fn greet() -> &'static str +{ + "hello world" +} + +#[cfg(feature = "goodbye")] +pub fn farewell() -> &'static str +{ + "goodbye" +} diff --git a/test cases/rust/32 rust.workspace workspace/Cargo.lock b/test cases/rust/32 rust.workspace workspace/Cargo.lock new file mode 100644 index 000000000000..0434b60286f7 --- /dev/null +++ b/test cases/rust/32 rust.workspace workspace/Cargo.lock @@ -0,0 +1,19 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "answer" +version = "2.1.0" + +[[package]] +name = "hello" +version = "1.0.0" + +[[package]] +name = "workspace_test" +version = "0.1.0" +dependencies = [ + "answer", + "hello", +] diff --git a/test cases/rust/32 rust.workspace workspace/Cargo.toml b/test cases/rust/32 rust.workspace workspace/Cargo.toml new file mode 100644 index 000000000000..44e4a18ef226 --- /dev/null +++ b/test cases/rust/32 rust.workspace workspace/Cargo.toml @@ -0,0 +1,16 @@ +[workspace] +members = ["."] + +[package] +name = "workspace_test" +version = "0.1.0" +edition = "2021" + +[features] +default = ["feature1", "hello?/goodbye"] +feature1 = ["answer/large", "dep:hello"] +feature2 = [] + +[dependencies] +hello = { version = "1.0", path = "subprojects/hello-1.0", optional = true } +answer = { version = "2.1", path = "subprojects/answer-2.1", optional = true } diff --git a/test cases/rust/32 rust.workspace workspace/meson.build b/test cases/rust/32 rust.workspace workspace/meson.build new file mode 100644 index 000000000000..476374153e59 --- /dev/null +++ b/test cases/rust/32 rust.workspace workspace/meson.build @@ -0,0 +1,34 @@ +project('workspace test', 'rust', default_options: ['rust_std=2021']) + +rust = import('rust') +cargo = rust.workspace() + +# Test workspace.packages() method +assert(cargo.packages() == ['answer', 'hello', 'workspace_test']) + +hello_rs = cargo.subproject('hello') +assert(hello_rs.name() == 'hello') +assert(hello_rs.version() == '1.0.0') +assert(hello_rs.api() == '1') +assert(hello_rs.all_features() == ['default', 'goodbye']) +assert(hello_rs.features() == ['default', 'goodbye']) + +answer_rs = cargo.subproject('answer', '2') +assert(answer_rs.name() == 'answer') +assert(answer_rs.version() == '2.1.0') +assert(answer_rs.api() == '2') +assert(answer_rs.all_features() == ['default', 'large']) +assert(answer_rs.features() == ['default', 'large']) + +e = executable('workspace-test', 'src/main.rs', + dependencies: [hello_rs.dependency(), answer_rs.dependency()], +) +test('workspace-test', e) + +# failure test cases for dependency() +testcase expect_error('package.dependency.*must be one of c, proc-macro, rust.*', how: 're') + hello_rs.dependency(rust_abi: 'something else') +endtestcase +testcase expect_error('Package hello does not support ABI c') + hello_rs.dependency(rust_abi: 'c') +endtestcase diff --git a/test cases/rust/32 rust.workspace workspace/src/main.rs b/test cases/rust/32 rust.workspace workspace/src/main.rs new file mode 100644 index 000000000000..13c02dd64ad8 --- /dev/null +++ b/test cases/rust/32 rust.workspace workspace/src/main.rs @@ -0,0 +1,8 @@ +use hello::{farewell, greet}; + +fn main() { + println!("{}", greet()); + println!("{}", farewell()); + println!("{}", answer::answer()); + println!("{}", answer::large_answer()); +} diff --git a/test cases/rust/32 rust.workspace workspace/subprojects/answer-2-rs.wrap b/test cases/rust/32 rust.workspace workspace/subprojects/answer-2-rs.wrap new file mode 100644 index 000000000000..d16d1f7a8483 --- /dev/null +++ b/test cases/rust/32 rust.workspace workspace/subprojects/answer-2-rs.wrap @@ -0,0 +1,2 @@ +[wrap-file] +directory = answer-2.1 diff --git a/test cases/rust/32 rust.workspace workspace/subprojects/answer-2.1/Cargo.toml b/test cases/rust/32 rust.workspace workspace/subprojects/answer-2.1/Cargo.toml new file mode 100644 index 000000000000..b8778226474f --- /dev/null +++ b/test cases/rust/32 rust.workspace workspace/subprojects/answer-2.1/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "answer" +version = "2.1.0" +edition = "2021" + +[lib] +crate-type = ["lib"] + +[features] +large = [] diff --git a/test cases/rust/32 rust.workspace workspace/subprojects/answer-2.1/meson.build b/test cases/rust/32 rust.workspace workspace/subprojects/answer-2.1/meson.build new file mode 100644 index 000000000000..cc8d463b6d11 --- /dev/null +++ b/test cases/rust/32 rust.workspace workspace/subprojects/answer-2.1/meson.build @@ -0,0 +1,9 @@ +project('answer', 'rust', default_options: ['rust_std=2021']) + +rust = import('rust') +cargo = rust.workspace() +assert(cargo.packages() == ['answer']) + +l = static_library('answer', 'src/lib.rs', rust_args: ['--cfg', 'feature="large"']) +dep = declare_dependency(link_with: l) +meson.override_dependency('answer-2-rs', dep) diff --git a/test cases/rust/32 rust.workspace workspace/subprojects/answer-2.1/src/lib.rs b/test cases/rust/32 rust.workspace workspace/subprojects/answer-2.1/src/lib.rs new file mode 100644 index 000000000000..b7a721b05f1d --- /dev/null +++ b/test cases/rust/32 rust.workspace workspace/subprojects/answer-2.1/src/lib.rs @@ -0,0 +1,10 @@ +pub fn answer() -> u8 +{ + 42 +} + +#[cfg(feature = "large")] +pub fn large_answer() -> u64 +{ + 42 +} diff --git a/test cases/rust/32 rust.workspace workspace/subprojects/hello-1-rs.wrap b/test cases/rust/32 rust.workspace workspace/subprojects/hello-1-rs.wrap new file mode 100644 index 000000000000..25e7751d0c9f --- /dev/null +++ b/test cases/rust/32 rust.workspace workspace/subprojects/hello-1-rs.wrap @@ -0,0 +1,3 @@ +[wrap-file] +directory = hello-1.0 +method = cargo diff --git a/test cases/rust/32 rust.workspace workspace/subprojects/hello-1.0/Cargo.toml b/test cases/rust/32 rust.workspace workspace/subprojects/hello-1.0/Cargo.toml new file mode 100644 index 000000000000..f6ab8eb91eeb --- /dev/null +++ b/test cases/rust/32 rust.workspace workspace/subprojects/hello-1.0/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "hello" +version = "1.0.0" +edition = "2021" + +[lib] +crate-type = ["lib"] + +[features] +goodbye = [] diff --git a/test cases/rust/32 rust.workspace workspace/subprojects/hello-1.0/src/lib.rs b/test cases/rust/32 rust.workspace workspace/subprojects/hello-1.0/src/lib.rs new file mode 100644 index 000000000000..47346350bd21 --- /dev/null +++ b/test cases/rust/32 rust.workspace workspace/subprojects/hello-1.0/src/lib.rs @@ -0,0 +1,10 @@ +pub fn greet() -> &'static str +{ + "hello world" +} + +#[cfg(feature = "goodbye")] +pub fn farewell() -> &'static str +{ + "goodbye" +}