From 452c73893cd8fcab1f75b1cc5e7526392ca797f9 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 17 Oct 2025 14:45:07 +0200 Subject: [PATCH 1/8] rust: add rust.workspace() skeleton implementation rust.workspace() is the entry point for global feature resolution. It loads a Cargo.toml file and ensures that all dependencies will be built with the correct set of features. Fixes: #13404 --- docs/markdown/Rust-module.md | 21 +++++++++++ mesonbuild/cargo/__init__.py | 3 +- mesonbuild/cargo/interpreter.py | 19 ++++++---- mesonbuild/modules/rust.py | 35 +++++++++++++++++-- .../rust/31 rust.workspace package/Cargo.lock | 19 ++++++++++ .../rust/31 rust.workspace package/Cargo.toml | 11 ++++++ .../31 rust.workspace package/meson.build | 12 +++++++ .../31 rust.workspace package/src/main.rs | 6 ++++ .../subprojects/answer-2-rs.wrap | 2 ++ .../subprojects/answer-2.1/Cargo.toml | 10 ++++++ .../subprojects/answer-2.1/meson.build | 7 ++++ .../subprojects/answer-2.1/src/lib.rs | 10 ++++++ .../subprojects/hello-1-rs.wrap | 3 ++ .../subprojects/hello-1.0/Cargo.toml | 7 ++++ .../subprojects/hello-1.0/src/lib.rs | 4 +++ .../32 rust.workspace workspace/Cargo.lock | 19 ++++++++++ .../32 rust.workspace workspace/Cargo.toml | 14 ++++++++ .../32 rust.workspace workspace/meson.build | 12 +++++++ .../32 rust.workspace workspace/src/main.rs | 6 ++++ .../subprojects/answer-2-rs.wrap | 2 ++ .../subprojects/answer-2.1/Cargo.toml | 10 ++++++ .../subprojects/answer-2.1/meson.build | 7 ++++ .../subprojects/answer-2.1/src/lib.rs | 10 ++++++ .../subprojects/hello-1-rs.wrap | 3 ++ .../subprojects/hello-1.0/Cargo.toml | 7 ++++ .../subprojects/hello-1.0/src/lib.rs | 4 +++ 26 files changed, 253 insertions(+), 10 deletions(-) create mode 100644 test cases/rust/31 rust.workspace package/Cargo.lock create mode 100644 test cases/rust/31 rust.workspace package/Cargo.toml create mode 100644 test cases/rust/31 rust.workspace package/meson.build create mode 100644 test cases/rust/31 rust.workspace package/src/main.rs create mode 100644 test cases/rust/31 rust.workspace package/subprojects/answer-2-rs.wrap create mode 100644 test cases/rust/31 rust.workspace package/subprojects/answer-2.1/Cargo.toml create mode 100644 test cases/rust/31 rust.workspace package/subprojects/answer-2.1/meson.build create mode 100644 test cases/rust/31 rust.workspace package/subprojects/answer-2.1/src/lib.rs create mode 100644 test cases/rust/31 rust.workspace package/subprojects/hello-1-rs.wrap create mode 100644 test cases/rust/31 rust.workspace package/subprojects/hello-1.0/Cargo.toml create mode 100644 test cases/rust/31 rust.workspace package/subprojects/hello-1.0/src/lib.rs create mode 100644 test cases/rust/32 rust.workspace workspace/Cargo.lock create mode 100644 test cases/rust/32 rust.workspace workspace/Cargo.toml create mode 100644 test cases/rust/32 rust.workspace workspace/meson.build create mode 100644 test cases/rust/32 rust.workspace workspace/src/main.rs create mode 100644 test cases/rust/32 rust.workspace workspace/subprojects/answer-2-rs.wrap create mode 100644 test cases/rust/32 rust.workspace workspace/subprojects/answer-2.1/Cargo.toml create mode 100644 test cases/rust/32 rust.workspace workspace/subprojects/answer-2.1/meson.build create mode 100644 test cases/rust/32 rust.workspace workspace/subprojects/answer-2.1/src/lib.rs create mode 100644 test cases/rust/32 rust.workspace workspace/subprojects/hello-1-rs.wrap create mode 100644 test cases/rust/32 rust.workspace workspace/subprojects/hello-1.0/Cargo.toml create mode 100644 test cases/rust/32 rust.workspace workspace/subprojects/hello-1.0/src/lib.rs diff --git a/docs/markdown/Rust-module.md b/docs/markdown/Rust-module.md index 5ce0fdcdb0d6..49b02aa410ec 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,21 @@ 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. + +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. diff --git a/mesonbuild/cargo/__init__.py b/mesonbuild/cargo/__init__.py index c5b157f3c791..d461d10bbf03 100644 --- a/mesonbuild/cargo/__init__.py +++ b/mesonbuild/cargo/__init__.py @@ -1,7 +1,8 @@ __all__ = [ 'Interpreter', 'TomlImplementationMissing', + 'WorkspaceState', ] -from .interpreter import Interpreter +from .interpreter import Interpreter, WorkspaceState from .toml import TomlImplementationMissing diff --git a/mesonbuild/cargo/interpreter.py b/mesonbuild/cargo/interpreter.py index 297ff5e8745f..41df67389713 100644 --- a/mesonbuild/cargo/interpreter.py +++ b/mesonbuild/cargo/interpreter.py @@ -223,6 +223,15 @@ def __init__(self, env: Environment, subdir: str, subprojects_dir: str) -> None: 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: @@ -230,18 +239,16 @@ def _prepare_entry_point(self, ws: WorkspaceState) -> None: self._enable_feature(pkg, 'default') 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 diff --git a/mesonbuild/modules/rust.py b/mesonbuild/modules/rust.py index 1836ba9c290b..43a8d82b6bf0 100644 --- a/mesonbuild/modules/rust.py +++ b/mesonbuild/modules/rust.py @@ -10,7 +10,7 @@ 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) @@ -19,14 +19,16 @@ 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 ..interpreterbase.baseobjects import TYPE_kwargs 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 ..compilers.rust import RustCompiler from ..dependencies import Dependency, ExternalLibrary from ..interpreter import Interpreter @@ -81,6 +83,16 @@ 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 + + class RustModule(ExtensionModule): """A module that holds helper functions for rust.""" @@ -103,6 +115,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 +513,22 @@ 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 + @noKwargs + def workspace(self, state: ModuleState, args: T.List, kwargs: TYPE_kwargs) -> 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) + + # 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..53bc49528bdd --- /dev/null +++ b/test cases/rust/31 rust.workspace package/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "package_test" +version = "0.1.0" +edition = "2021" + +[features] +default = ["dep:answer"] + +[dependencies] +hello = { version = "1.0", path = "subprojects/hello-1.0" } +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..7deb13672789 --- /dev/null +++ b/test cases/rust/31 rust.workspace package/meson.build @@ -0,0 +1,12 @@ +project('package test', 'rust', default_options: ['rust_std=2021']) + +rust = import('rust') +cargo = rust.workspace() + +hello_dep = dependency('hello-1-rs') +answer_dep = dependency('answer-2-rs') + +e = executable('package-test', 'src/main.rs', + dependencies: [hello_dep, answer_dep], +) +test('package-test', e) 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..39b32480146c --- /dev/null +++ b/test cases/rust/31 rust.workspace package/src/main.rs @@ -0,0 +1,6 @@ +use hello::greet; + +fn main() { + println!("{}", greet()); + println!("{}", answer::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..dc7df4bba6e6 --- /dev/null +++ b/test cases/rust/31 rust.workspace package/subprojects/answer-2.1/meson.build @@ -0,0 +1,7 @@ +project('answer', 'rust', default_options: ['rust_std=2021']) + +rust = import('rust') + +l = static_library('answer', 'src/lib.rs') +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..ad0ae458c44e --- /dev/null +++ b/test cases/rust/31 rust.workspace package/subprojects/hello-1.0/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "hello" +version = "1.0.0" +edition = "2021" + +[lib] +crate-type = ["lib"] \ No newline at end of file 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..0631292f376b --- /dev/null +++ b/test cases/rust/31 rust.workspace package/subprojects/hello-1.0/src/lib.rs @@ -0,0 +1,4 @@ +pub fn greet() -> &'static str +{ + "hello world" +} 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..ac3a340bdbd2 --- /dev/null +++ b/test cases/rust/32 rust.workspace workspace/Cargo.toml @@ -0,0 +1,14 @@ +[workspace] +members = ["."] + +[package] +name = "workspace_test" +version = "0.1.0" +edition = "2021" + +[features] +default = ["dep:answer"] + +[dependencies] +hello = { version = "1.0", path = "subprojects/hello-1.0" } +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..eef8f4018dba --- /dev/null +++ b/test cases/rust/32 rust.workspace workspace/meson.build @@ -0,0 +1,12 @@ +project('workspace test', 'rust', default_options: ['rust_std=2021']) + +rust = import('rust') +cargo = rust.workspace() + +hello_dep = dependency('hello-1-rs') +answer_dep = dependency('answer-2-rs') + +e = executable('workspace-test', 'src/main.rs', + dependencies: [hello_dep, answer_dep], +) +test('workspace-test', e) 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..39b32480146c --- /dev/null +++ b/test cases/rust/32 rust.workspace workspace/src/main.rs @@ -0,0 +1,6 @@ +use hello::greet; + +fn main() { + println!("{}", greet()); + println!("{}", answer::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..dc7df4bba6e6 --- /dev/null +++ b/test cases/rust/32 rust.workspace workspace/subprojects/answer-2.1/meson.build @@ -0,0 +1,7 @@ +project('answer', 'rust', default_options: ['rust_std=2021']) + +rust = import('rust') + +l = static_library('answer', 'src/lib.rs') +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..ad0ae458c44e --- /dev/null +++ b/test cases/rust/32 rust.workspace workspace/subprojects/hello-1.0/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "hello" +version = "1.0.0" +edition = "2021" + +[lib] +crate-type = ["lib"] \ No newline at end of file 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..0631292f376b --- /dev/null +++ b/test cases/rust/32 rust.workspace workspace/subprojects/hello-1.0/src/lib.rs @@ -0,0 +1,4 @@ +pub fn greet() -> &'static str +{ + "hello world" +} From 809f6c440034d164eae3639530d7c0793ae008f4 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 17 Oct 2025 18:50:15 +0200 Subject: [PATCH 2/8] cargo: add configurable features to Interpreter Add features property to cargo.Interpreter to make default features configurable; customization of which features are enabled by default is triggered by rust.workspace(). Fixes: #14290 --- docs/markdown/Rust-module.md | 12 ++++++++++++ mesonbuild/cargo/interpreter.py | 22 ++++++++++++++++++++-- mesonbuild/modules/rust.py | 31 +++++++++++++++++++++++++++---- 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/docs/markdown/Rust-module.md b/docs/markdown/Rust-module.md index 49b02aa410ec..34c3aa4cfd84 100644 --- a/docs/markdown/Rust-module.md +++ b/docs/markdown/Rust-module.md @@ -183,9 +183,21 @@ rustmod.workspace(...) 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. diff --git a/mesonbuild/cargo/interpreter.py b/mesonbuild/cargo/interpreter.py index 41df67389713..388c3940f235 100644 --- a/mesonbuild/cargo/interpreter.py +++ b/mesonbuild/cargo/interpreter.py @@ -22,7 +22,7 @@ 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 @@ -201,6 +201,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,6 +222,21 @@ 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 @@ -236,7 +253,8 @@ 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: filename = os.path.join(self.environment.source_dir, subdir, 'Cargo.toml') diff --git a/mesonbuild/modules/rust.py b/mesonbuild/modules/rust.py index 43a8d82b6bf0..c6a760c7db03 100644 --- a/mesonbuild/modules/rust.py +++ b/mesonbuild/modules/rust.py @@ -19,8 +19,7 @@ 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, noKwargs, noPosargs, permittedKwargs -from ..interpreterbase.baseobjects import TYPE_kwargs +from ..interpreterbase import ContainerTypeInfo, InterpreterException, KwargInfo, typed_kwargs, typed_pos_args, noPosargs, permittedKwargs from ..interpreter.interpreterobjects import Doctest from ..mesonlib import File, MachineChoice, MesonException, PerMachine from ..programs import ExternalProgram, NonExistingExternalProgram @@ -65,6 +64,9 @@ 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] RUST_TEST_KWS: T.List[KwargInfo] = [ KwargInfo( @@ -515,8 +517,17 @@ def proc_macro(self, state: ModuleState, args: T.Tuple[str, SourcesVarargsType], @FeatureNew('rust.workspace', '1.10.0') @noPosargs - @noKwargs - def workspace(self, state: ModuleState, args: T.List, kwargs: TYPE_kwargs) -> RustWorkspace: + @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: @@ -525,6 +536,18 @@ def workspace(self, state: ModuleState, args: T.List, kwargs: TYPE_kwargs) -> Ru 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) From 83f9e6f1e0cc3402a7f98053fad64f302539773f Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Sun, 26 Oct 2025 08:36:16 +0100 Subject: [PATCH 3/8] modules: rust: implement workspace.subproject() and package.dependency() Signed-off-by: Paolo Bonzini --- docs/markdown/Rust-module.md | 35 +++++++++++ mesonbuild/cargo/__init__.py | 3 +- mesonbuild/cargo/interpreter.py | 56 ++++++++++++++++-- mesonbuild/modules/__init__.py | 10 +++- mesonbuild/modules/rust.py | 59 ++++++++++++++++++- .../31 rust.workspace package/meson.build | 14 ++++- .../32 rust.workspace workspace/meson.build | 14 ++++- 7 files changed, 176 insertions(+), 15 deletions(-) diff --git a/docs/markdown/Rust-module.md b/docs/markdown/Rust-module.md index 34c3aa4cfd84..b65343436b24 100644 --- a/docs/markdown/Rust-module.md +++ b/docs/markdown/Rust-module.md @@ -201,3 +201,38 @@ are specified. When `features` is provided without `default_features: false`, the 'default' feature is automatically included. + +## Workspace object + +### 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.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/mesonbuild/cargo/__init__.py b/mesonbuild/cargo/__init__.py index d461d10bbf03..65e018a9d3b1 100644 --- a/mesonbuild/cargo/__init__.py +++ b/mesonbuild/cargo/__init__.py @@ -1,8 +1,9 @@ __all__ = [ 'Interpreter', + 'PackageState', 'TomlImplementationMissing', 'WorkspaceState', ] -from .interpreter import Interpreter, WorkspaceState +from .interpreter import Interpreter, PackageState, WorkspaceState from .toml import TomlImplementationMissing diff --git a/mesonbuild/cargo/interpreter.py b/mesonbuild/cargo/interpreter.py index 388c3940f235..6eb9db2f2a20 100644 --- a/mesonbuild/cargo/interpreter.py +++ b/mesonbuild/cargo/interpreter.py @@ -29,12 +29,14 @@ 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: @@ -292,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)) @@ -410,6 +449,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: @@ -732,7 +778,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 c6a760c7db03..3c72e4a59738 100644 --- a/mesonbuild/modules/rust.py +++ b/mesonbuild/modules/rust.py @@ -15,11 +15,12 @@ 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, MachineChoice, MesonException, PerMachine from ..programs import ExternalProgram, NonExistingExternalProgram @@ -28,12 +29,14 @@ 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 @@ -68,6 +71,9 @@ 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( 'rust_args', @@ -93,6 +99,55 @@ def __init__(self, interpreter: Interpreter, ws: cargo.WorkspaceState) -> None: super().__init__() self.interpreter = interpreter self.ws = ws + self.methods.update({ + 'subproject': self.subproject_method, + }) + + 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({ + 'dependency': self.dependency_method, + }) + + @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): diff --git a/test cases/rust/31 rust.workspace package/meson.build b/test cases/rust/31 rust.workspace package/meson.build index 7deb13672789..4a3695860210 100644 --- a/test cases/rust/31 rust.workspace package/meson.build +++ b/test cases/rust/31 rust.workspace package/meson.build @@ -3,10 +3,18 @@ project('package test', 'rust', default_options: ['rust_std=2021']) rust = import('rust') cargo = rust.workspace() -hello_dep = dependency('hello-1-rs') -answer_dep = dependency('answer-2-rs') +hello_rs = cargo.subproject('hello') +answer_rs = cargo.subproject('answer', '2') e = executable('package-test', 'src/main.rs', - dependencies: [hello_dep, answer_dep], + 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/32 rust.workspace workspace/meson.build b/test cases/rust/32 rust.workspace workspace/meson.build index eef8f4018dba..bec5c57c5edc 100644 --- a/test cases/rust/32 rust.workspace workspace/meson.build +++ b/test cases/rust/32 rust.workspace workspace/meson.build @@ -3,10 +3,18 @@ project('workspace test', 'rust', default_options: ['rust_std=2021']) rust = import('rust') cargo = rust.workspace() -hello_dep = dependency('hello-1-rs') -answer_dep = dependency('answer-2-rs') +hello_rs = cargo.subproject('hello') +answer_rs = cargo.subproject('answer', '2') e = executable('workspace-test', 'src/main.rs', - dependencies: [hello_dep, answer_dep], + 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 From ceaa430128f1e1fb35b187b066f6768c2dec10ff Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Wed, 22 Oct 2025 19:14:02 +0200 Subject: [PATCH 4/8] docs: add release notes for rust.workspace() Signed-off-by: Paolo Bonzini --- docs/markdown/Rust.md | 37 +++++++++++++++++++ .../markdown/Wrap-dependency-system-manual.md | 5 +-- .../snippets/cargo-workspace-object.md | 13 +++++++ 3 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 docs/markdown/snippets/cargo-workspace-object.md 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. From 39412eb001f09ae9496798885d30f2bf95ebfcfd Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 24 Oct 2025 16:49:37 +0200 Subject: [PATCH 5/8] ast: printer: give a precedence to all kinds of functions Signed-off-by: Paolo Bonzini --- mesonbuild/ast/printer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 934d81e98a6b37a2d6e0ab164a79bfd4e07f8b0a Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Tue, 28 Oct 2025 18:09:44 +0100 Subject: [PATCH 6/8] cargo: use cargo.subproject().dependency() This is up to 2x faster because it avoids checks for pkg-config and cmake. Signed-off-by: Paolo Bonzini --- mesonbuild/cargo/interpreter.py | 44 ++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/mesonbuild/cargo/interpreter.py b/mesonbuild/cargo/interpreter.py index 6eb9db2f2a20..c40e544ec7d3 100644 --- a/mesonbuild/cargo/interpreter.py +++ b/mesonbuild/cargo/interpreter.py @@ -374,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 @@ -654,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 @@ -699,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 @@ -715,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(',') From 9e0f90ebddcfba1930593a79f59bc6de1b728f6b Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Wed, 1 Oct 2025 09:22:32 +0200 Subject: [PATCH 7/8] modules: rust: implement workspace.packages() Signed-off-by: Paolo Bonzini --- docs/markdown/Rust-module.md | 8 ++++++++ mesonbuild/modules/rust.py | 8 ++++++++ test cases/rust/31 rust.workspace package/meson.build | 3 +++ .../subprojects/answer-2.1/meson.build | 2 ++ test cases/rust/32 rust.workspace workspace/meson.build | 3 +++ .../subprojects/answer-2.1/meson.build | 2 ++ 6 files changed, 26 insertions(+) diff --git a/docs/markdown/Rust-module.md b/docs/markdown/Rust-module.md index b65343436b24..8dc85fc22c82 100644 --- a/docs/markdown/Rust-module.md +++ b/docs/markdown/Rust-module.md @@ -204,6 +204,14 @@ automatically included. ## Workspace object +### workspace.packages() + +```meson +packages = ws.packages() +``` + +Returns a list of package names in the workspace. + ### workspace.subproject() ```meson diff --git a/mesonbuild/modules/rust.py b/mesonbuild/modules/rust.py index 3c72e4a59738..6167a733e386 100644 --- a/mesonbuild/modules/rust.py +++ b/mesonbuild/modules/rust.py @@ -100,9 +100,17 @@ def __init__(self, interpreter: Interpreter, ws: cargo.WorkspaceState) -> None: 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, diff --git a/test cases/rust/31 rust.workspace package/meson.build b/test cases/rust/31 rust.workspace package/meson.build index 4a3695860210..d909e5e9a654 100644 --- a/test cases/rust/31 rust.workspace package/meson.build +++ b/test cases/rust/31 rust.workspace package/meson.build @@ -3,6 +3,9 @@ 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') answer_rs = cargo.subproject('answer', '2') 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 index dc7df4bba6e6..9a8308230aee 100644 --- 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 @@ -1,6 +1,8 @@ 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') dep = declare_dependency(link_with: l) diff --git a/test cases/rust/32 rust.workspace workspace/meson.build b/test cases/rust/32 rust.workspace workspace/meson.build index bec5c57c5edc..dde43b097210 100644 --- a/test cases/rust/32 rust.workspace workspace/meson.build +++ b/test cases/rust/32 rust.workspace workspace/meson.build @@ -3,6 +3,9 @@ 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') answer_rs = cargo.subproject('answer', '2') 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 index dc7df4bba6e6..9a8308230aee 100644 --- 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 @@ -1,6 +1,8 @@ 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') dep = declare_dependency(link_with: l) From b2099d607a04cf937a751c49086aea2727778743 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Wed, 22 Oct 2025 16:52:08 +0200 Subject: [PATCH 8/8] modules: rust: implement more package accessors Signed-off-by: Paolo Bonzini --- docs/markdown/Rust-module.md | 41 +++++++++++++++++++ mesonbuild/modules/rust.py | 35 ++++++++++++++++ .../rust/31 rust.workspace package/Cargo.toml | 6 ++- .../31 rust.workspace package/meson.build | 11 +++++ .../31 rust.workspace package/src/main.rs | 4 +- .../subprojects/answer-2.1/meson.build | 2 +- .../subprojects/hello-1.0/Cargo.toml | 5 ++- .../subprojects/hello-1.0/src/lib.rs | 6 +++ .../32 rust.workspace workspace/Cargo.toml | 6 ++- .../32 rust.workspace workspace/meson.build | 11 +++++ .../32 rust.workspace workspace/src/main.rs | 4 +- .../subprojects/answer-2.1/meson.build | 2 +- .../subprojects/hello-1.0/Cargo.toml | 5 ++- .../subprojects/hello-1.0/src/lib.rs | 6 +++ 14 files changed, 134 insertions(+), 10 deletions(-) diff --git a/docs/markdown/Rust-module.md b/docs/markdown/Rust-module.md index 8dc85fc22c82..fc924ee71be2 100644 --- a/docs/markdown/Rust-module.md +++ b/docs/markdown/Rust-module.md @@ -229,6 +229,47 @@ Positional arguments: 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 diff --git a/mesonbuild/modules/rust.py b/mesonbuild/modules/rust.py index 6167a733e386..ab325e4895fa 100644 --- a/mesonbuild/modules/rust.py +++ b/mesonbuild/modules/rust.py @@ -146,9 +146,44 @@ def __init__(self, rust_ws: RustWorkspace, package: cargo.PackageState) -> None: 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'}))) diff --git a/test cases/rust/31 rust.workspace package/Cargo.toml b/test cases/rust/31 rust.workspace package/Cargo.toml index 53bc49528bdd..00bb0878e1f2 100644 --- a/test cases/rust/31 rust.workspace package/Cargo.toml +++ b/test cases/rust/31 rust.workspace package/Cargo.toml @@ -4,8 +4,10 @@ version = "0.1.0" edition = "2021" [features] -default = ["dep:answer"] +default = ["feature1", "hello?/goodbye"] +feature1 = ["answer/large", "dep:hello"] +feature2 = [] [dependencies] -hello = { version = "1.0", path = "subprojects/hello-1.0" } +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 index d909e5e9a654..8fc85e735c84 100644 --- a/test cases/rust/31 rust.workspace package/meson.build +++ b/test cases/rust/31 rust.workspace package/meson.build @@ -7,7 +7,18 @@ cargo = rust.workspace() 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()], diff --git a/test cases/rust/31 rust.workspace package/src/main.rs b/test cases/rust/31 rust.workspace package/src/main.rs index 39b32480146c..13c02dd64ad8 100644 --- a/test cases/rust/31 rust.workspace package/src/main.rs +++ b/test cases/rust/31 rust.workspace package/src/main.rs @@ -1,6 +1,8 @@ -use hello::greet; +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.1/meson.build b/test cases/rust/31 rust.workspace package/subprojects/answer-2.1/meson.build index 9a8308230aee..cc8d463b6d11 100644 --- 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 @@ -4,6 +4,6 @@ rust = import('rust') cargo = rust.workspace() assert(cargo.packages() == ['answer']) -l = static_library('answer', 'src/lib.rs') +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/hello-1.0/Cargo.toml b/test cases/rust/31 rust.workspace package/subprojects/hello-1.0/Cargo.toml index ad0ae458c44e..f6ab8eb91eeb 100644 --- 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 @@ -4,4 +4,7 @@ version = "1.0.0" edition = "2021" [lib] -crate-type = ["lib"] \ No newline at end of file +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 index 0631292f376b..47346350bd21 100644 --- 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 @@ -2,3 +2,9 @@ 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.toml b/test cases/rust/32 rust.workspace workspace/Cargo.toml index ac3a340bdbd2..44e4a18ef226 100644 --- a/test cases/rust/32 rust.workspace workspace/Cargo.toml +++ b/test cases/rust/32 rust.workspace workspace/Cargo.toml @@ -7,8 +7,10 @@ version = "0.1.0" edition = "2021" [features] -default = ["dep:answer"] +default = ["feature1", "hello?/goodbye"] +feature1 = ["answer/large", "dep:hello"] +feature2 = [] [dependencies] -hello = { version = "1.0", path = "subprojects/hello-1.0" } +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 index dde43b097210..476374153e59 100644 --- a/test cases/rust/32 rust.workspace workspace/meson.build +++ b/test cases/rust/32 rust.workspace workspace/meson.build @@ -7,7 +7,18 @@ cargo = rust.workspace() 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()], diff --git a/test cases/rust/32 rust.workspace workspace/src/main.rs b/test cases/rust/32 rust.workspace workspace/src/main.rs index 39b32480146c..13c02dd64ad8 100644 --- a/test cases/rust/32 rust.workspace workspace/src/main.rs +++ b/test cases/rust/32 rust.workspace workspace/src/main.rs @@ -1,6 +1,8 @@ -use hello::greet; +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.1/meson.build b/test cases/rust/32 rust.workspace workspace/subprojects/answer-2.1/meson.build index 9a8308230aee..cc8d463b6d11 100644 --- 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 @@ -4,6 +4,6 @@ rust = import('rust') cargo = rust.workspace() assert(cargo.packages() == ['answer']) -l = static_library('answer', 'src/lib.rs') +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/hello-1.0/Cargo.toml b/test cases/rust/32 rust.workspace workspace/subprojects/hello-1.0/Cargo.toml index ad0ae458c44e..f6ab8eb91eeb 100644 --- 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 @@ -4,4 +4,7 @@ version = "1.0.0" edition = "2021" [lib] -crate-type = ["lib"] \ No newline at end of file +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 index 0631292f376b..47346350bd21 100644 --- 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 @@ -2,3 +2,9 @@ pub fn greet() -> &'static str { "hello world" } + +#[cfg(feature = "goodbye")] +pub fn farewell() -> &'static str +{ + "goodbye" +}