From a90592ae690b50fcf8fe72b28b99c8df850b868a Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 28 Nov 2025 17:49:04 +0100 Subject: [PATCH 01/28] cargo: add --check-cfg cfg(test) unconditionally It should be added even if unexpected_cfgs is not part of Cargo.toml. Signed-off-by: Paolo Bonzini --- mesonbuild/cargo/interpreter.py | 2 ++ mesonbuild/cargo/manifest.py | 3 +-- unittests/cargotests.py | 5 ++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mesonbuild/cargo/interpreter.py b/mesonbuild/cargo/interpreter.py index 25a1b70dbf01..a777a86599bc 100644 --- a/mesonbuild/cargo/interpreter.py +++ b/mesonbuild/cargo/interpreter.py @@ -144,6 +144,8 @@ def get_lint_args(self, rustc: RustCompiler) -> T.List[str]: args.extend(lint.to_arguments(has_check_cfg)) if has_check_cfg: + args.append('--check-cfg') + args.append('cfg(test)') for feature in self.manifest.features: if feature != 'default': args.append('--check-cfg') diff --git a/mesonbuild/cargo/manifest.py b/mesonbuild/cargo/manifest.py index ec84e4b16cf2..2c3eeb3e0cf4 100644 --- a/mesonbuild/cargo/manifest.py +++ b/mesonbuild/cargo/manifest.py @@ -442,8 +442,7 @@ def from_raw(cls, r: T.Union[raw.FromWorkspace, T.Dict[str, T.Dict[str, raw.Lint settings = T.cast('raw.Lint', {'level': settings}) check_cfg = None if name == 'unexpected_cfgs': - # 'cfg(test)' is added automatically by cargo - check_cfg = ['cfg(test)'] + settings.get('check-cfg', []) + check_cfg = settings.get('check-cfg', []) lints[name] = Lint(name=name, level=settings['level'], priority=settings.get('priority', 0), diff --git a/unittests/cargotests.py b/unittests/cargotests.py index 643ceceb49a6..e8eace74fa2e 100644 --- a/unittests/cargotests.py +++ b/unittests/cargotests.py @@ -428,7 +428,7 @@ def test_cargo_toml_lints(self) -> None: self.assertEqual(manifest.lints[2].name, 'unexpected_cfgs') self.assertEqual(manifest.lints[2].level, 'deny') self.assertEqual(manifest.lints[2].priority, 0) - self.assertEqual(manifest.lints[2].check_cfg, ['cfg(test)', 'cfg(MESON)']) + self.assertEqual(manifest.lints[2].check_cfg, ['cfg(MESON)']) def test_cargo_toml_lints_to_args(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: @@ -444,8 +444,7 @@ def test_cargo_toml_lints_to_args(self) -> None: self.assertEqual(manifest.lints[1].to_arguments(True), ['-A', 'unknown_lints']) self.assertEqual(manifest.lints[2].to_arguments(False), ['-D', 'unexpected_cfgs']) self.assertEqual(manifest.lints[2].to_arguments(True), - ['-D', 'unexpected_cfgs', '--check-cfg', 'cfg(test)', - '--check-cfg', 'cfg(MESON)']) + ['-D', 'unexpected_cfgs', '--check-cfg', 'cfg(MESON)']) def test_cargo_toml_dependencies(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: From 6096481f2c2e0a165a34df629b28db2c68881f1e Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 28 Nov 2025 18:25:24 +0100 Subject: [PATCH 02/28] cargo: include the implicit feature for dependencies in the manifest Make the implicit `xyz = ["dep:xyz"]` declaration explicit in the Manifest. This also makes it possible to implement correctly the part of the spec where "If you specify the optional dependency with the dep: prefix anywhere in the [features] table, that disables the implicit feature." Unfortunately, this can only be done after target-specific configurations are resolved, which makes it hard to write a unit test for this. Rustix requires it, though; without this patch, something like [package] name = "rustix" edition = "2021" rust-version = "1.63" version = "0.38.34" [dependencies] alloc = { version = "1.0.0", optional = true, package = "rustc-std-workspace-alloc" } libc = { version = "0.2.0", optional = true, package = "libc" } libc_errno = { version = "0.3.8", optional = true, package = "errno" } [features] alloc = [] default = ["std"] rustc-dep-of-std = ["dep:alloc"] std = ["alloc"] use-libc = ["libc_errno", "libc"] would incorrectly request the rustc-std-workspace-alloc crate via the chain default->std->alloc. The patch blocks it because it finds the "dep:alloc" dependency of feature rustc-dep-of-std. Signed-off-by: Paolo Bonzini --- mesonbuild/cargo/interpreter.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/mesonbuild/cargo/interpreter.py b/mesonbuild/cargo/interpreter.py index a777a86599bc..6acd998cdecd 100644 --- a/mesonbuild/cargo/interpreter.py +++ b/mesonbuild/cargo/interpreter.py @@ -12,6 +12,7 @@ from __future__ import annotations import dataclasses import functools +import itertools import os import pathlib import collections @@ -428,6 +429,20 @@ def _prepare_package(self, pkg: PackageState) -> None: for condition, dependencies in pkg.manifest.target.items(): if eval_cfg(condition, cfgs): pkg.manifest.dependencies.update(dependencies) + + # If you specify the optional dependency with the dep: prefix anywhere in the [features] + # table, that disables the implicit feature. + deps = set(feature[4:] + for feature in itertools.chain.from_iterable(pkg.manifest.features.values()) + if feature.startswith('dep:')) + for name, dep in itertools.chain(pkg.manifest.dependencies.items(), + pkg.manifest.dev_dependencies.items(), + pkg.manifest.build_dependencies.items()): + if dep.optional and name not in deps: + pkg.manifest.features.setdefault(name, []) + pkg.manifest.features[name].append(f'dep:{name}') + deps.add(name) + # Fetch required dependencies recursively. for depname, dep in pkg.manifest.dependencies.items(): if not dep.optional: @@ -499,9 +514,6 @@ def _enable_feature(self, pkg: PackageState, feature: str) -> None: if feature in cfg.features: return cfg.features.add(feature) - # A feature can also be a dependency. - if feature in pkg.manifest.dependencies: - self._add_dependency(pkg, feature) # Recurse on extra features and dependencies this feature pulls. # https://doc.rust-lang.org/cargo/reference/features.html#the-features-section for f in pkg.manifest.features.get(feature, []): From 7438e4ef7cbc0900437de7640d9f16370ef25a17 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Sun, 26 Oct 2025 12:22:03 +0100 Subject: [PATCH 03/28] cargo: allow overriding Meson's Cargo interpreter Some projects may want to override Meson's AST generation for Cargo projects. This was not really doable before without hard coding the results of feature resolution; however, now it will be possible by accessing the results of the global feature resolution from the Rust module's workspace object. At the same time, the subproject must keep using the Cargo method, which is forced by the workspace object's subproject() method, because otherwise the interpreter is not propagated. So just skip the interpretation phase if a Meson.build is present. Signed-off-by: Paolo Bonzini --- mesonbuild/interpreter/interpreter.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 56659460a844..ba2e7e5bc508 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -1060,7 +1060,11 @@ def _do_subproject_cargo(self, subp_name: str, subdir: str, except cargo.TomlImplementationMissing as e: raise MesonException(f'Failed to load Cargo.lock: {e!s}') - ast = cargo_int.interpret(subdir) + if os.path.exists(os.path.join(self.environment.get_source_dir(), subdir, environment.build_filename)): + ast = None + else: + ast = cargo_int.interpret(subdir) + return self._do_subproject_meson( subp_name, subdir, default_options, kwargs, ast, relaxations={InterpreterRuleRelaxation.CARGO_SUBDIR}, From cb61d061481c919d3464f8f7f49079233cc3e78d Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Tue, 18 Nov 2025 09:55:51 +0100 Subject: [PATCH 04/28] cargo: show nice exception if Cargo.toml missing Signed-off-by: Paolo Bonzini --- mesonbuild/cargo/interpreter.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mesonbuild/cargo/interpreter.py b/mesonbuild/cargo/interpreter.py index 6acd998cdecd..658dfd4f6dd3 100644 --- a/mesonbuild/cargo/interpreter.py +++ b/mesonbuild/cargo/interpreter.py @@ -480,8 +480,12 @@ def _load_manifest(self, subdir: str, workspace: T.Optional[Workspace] = None, m return manifest_, True path = os.path.join(self.environment.source_dir, subdir) filename = os.path.join(path, 'Cargo.toml') + try: + raw_manifest = T.cast('raw.Manifest', load_toml(filename)) + except OSError as e: + raise MesonException(f'could not load {subdir}/Cargo.toml: {e}') + self.build_def_files.append(filename) - raw_manifest = T.cast('raw.Manifest', load_toml(filename)) if 'workspace' in raw_manifest: manifest_ = Workspace.from_raw(raw_manifest, path) elif 'package' in raw_manifest: From adc2b0cca1ddb431bc95a8b30d9da329d7955ad3 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 17 Oct 2025 14:45:07 +0200 Subject: [PATCH 05/28] 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 | 22 +++++++++++++ mesonbuild/cargo/__init__.py | 3 +- mesonbuild/cargo/interpreter.py | 19 +++++++---- mesonbuild/modules/rust.py | 32 +++++++++++++++++-- .../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, 252 insertions(+), 9 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..2763edc0461b 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,22 @@ Only a subset of [[shared_library]] keyword arguments are allowed: - link_depends - link_with - override_options + +### workspace() + +``` +cargo = rustmod.workspace() +``` + +*Since 1.11.0* + +Create and return a `workspace` object for managing the project's Cargo +workspace. + +A project that wishes to use Cargo subprojects should have `Cargo.lock` and `Cargo.toml` +files in the root source directory, and should call this function before using +Cargo subprojects. + +The first invocation of `workspace()` establishes the *Cargo interpreter* +that resolves dependencies and features for both the toplevel project (the one +containing `Cargo.lock`) and all subprojects that are invoked with the `cargo` method, 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 658dfd4f6dd3..b9a053f846f1 100644 --- a/mesonbuild/cargo/interpreter.py +++ b/mesonbuild/cargo/interpreter.py @@ -227,6 +227,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: @@ -234,18 +243,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 a8fcc86a06b6..bf14aa9f20fc 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,7 +19,8 @@ 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, MachineChoice, MesonException, PerMachine from ..programs import ExternalProgram, NonExistingExternalProgram @@ -27,6 +28,7 @@ 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,21 @@ 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) + + ws = self.interpreter.cargo.load_workspace(state.root_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 9b5e71ac943cf72c825960099ae104233f661ccc Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 17 Oct 2025 18:50:15 +0200 Subject: [PATCH 06/28] 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 | 40 +++++++++++++++++++++++++++++++++ mesonbuild/cargo/interpreter.py | 22 ++++++++++++++++-- mesonbuild/modules/rust.py | 31 +++++++++++++++++++++---- 3 files changed, 87 insertions(+), 6 deletions(-) diff --git a/docs/markdown/Rust-module.md b/docs/markdown/Rust-module.md index 2763edc0461b..336d5db7d50a 100644 --- a/docs/markdown/Rust-module.md +++ b/docs/markdown/Rust-module.md @@ -174,15 +174,29 @@ Only a subset of [[shared_library]] keyword arguments are allowed: ### workspace() +Basic usage: + ``` cargo = rustmod.workspace() ``` +With custom features: + +``` +feature_list = get_feature('f1') ? ['feature1'] : [] +feature_list += get_feature('f2') ? ['feature2'] : [] +cargo = rust.workspace(features: feature_list) +``` + *Since 1.11.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. +- `features`: (`list[str]`, optional) List of additional features to enable globally. + A project that wishes to use Cargo subprojects should have `Cargo.lock` and `Cargo.toml` files in the root source directory, and should call this function before using Cargo subprojects. @@ -190,3 +204,29 @@ Cargo subprojects. The first invocation of `workspace()` establishes the *Cargo interpreter* that resolves dependencies and features for both the toplevel project (the one containing `Cargo.lock`) and all subprojects that are invoked with the `cargo` method, + +You can optionally customize the feature set, by providing `default_features` +and `features` when the Cargo interpreter is established. If any of these +arguments is not specified, `default_features` is taken as `true` and +`features` as the empty list. + +Once established, the Cargo interpreter's configuration is locked. Later calls to +`workspace()` must either omit all arguments (accepting the existing configuration) +or provide the same set of features as the first call. Mismatched arguments will cause +a build error. + +The recommendation is to not specify any keyword arguments in a subproject, so +that they simply inherit the parent's configuration. Be careful about the +difference between specifying arguments and not doing so: + +``` +# always works regardless of parent configuration +cargo = rustmod.workspace() + +# fails if parent configured different features +cargo = rustmod.workspace(default_features: true) +cargo = rustmod.workspace(features: []) +``` + +The first form says "use whatever features are configured," while the latter forms +say "require this specific configuration," which may conflict with the parent project. diff --git a/mesonbuild/cargo/interpreter.py b/mesonbuild/cargo/interpreter.py index b9a053f846f1..2acf2eb3c3b8 100644 --- a/mesonbuild/cargo/interpreter.py +++ b/mesonbuild/cargo/interpreter.py @@ -24,7 +24,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 @@ -205,6 +205,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 @@ -224,6 +226,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 @@ -240,7 +257,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 bf14aa9f20fc..0dd9e137f302 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 + ws = self.interpreter.cargo.load_workspace(state.root_subdir) return RustWorkspace(self.interpreter, ws) From 7c868336593840a282f4b819f9f4a4f49423cb9f Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Sun, 26 Oct 2025 08:36:16 +0100 Subject: [PATCH 07/28] 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 336d5db7d50a..690c4bd8a50f 100644 --- a/docs/markdown/Rust-module.md +++ b/docs/markdown/Rust-module.md @@ -230,3 +230,38 @@ cargo = rustmod.workspace(features: []) The first form says "use whatever features are configured," while the latter forms say "require this specific configuration," which may conflict with the parent project. + +## 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 2acf2eb3c3b8..cfbcdeb7e66b 100644 --- a/mesonbuild/cargo/interpreter.py +++ b/mesonbuild/cargo/interpreter.py @@ -31,12 +31,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 @@ -184,6 +186,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: @@ -296,13 +334,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)) @@ -414,6 +453,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: @@ -751,7 +797,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 0dd9e137f302..4cb985e6bcc5 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 01f8789cccaaa781a5d5c54bea817d6151d5c3b6 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Wed, 22 Oct 2025 19:14:02 +0200 Subject: [PATCH 08/28] 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 e8c4eb767fb0ca3c5f74aaaee0c27511d1063e36 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 24 Oct 2025 16:49:37 +0200 Subject: [PATCH 09/28] 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 359636787fe04b6f64912b8379ae8e4b24aa088b Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Tue, 28 Oct 2025 18:09:44 +0100 Subject: [PATCH 10/28] 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 cfbcdeb7e66b..0e9948b84efc 100644 --- a/mesonbuild/cargo/interpreter.py +++ b/mesonbuild/cargo/interpreter.py @@ -378,7 +378,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 @@ -673,7 +672,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 @@ -718,12 +726,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 @@ -734,13 +754,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 a17100f201d7d01cb8e06f0ac6c070b7ad62e960 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Wed, 1 Oct 2025 09:22:32 +0200 Subject: [PATCH 11/28] 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 690c4bd8a50f..9fbfd37d85dc 100644 --- a/docs/markdown/Rust-module.md +++ b/docs/markdown/Rust-module.md @@ -233,6 +233,14 @@ say "require this specific configuration," which may conflict with the parent pr ## 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 4cb985e6bcc5..b9084783e782 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 653574ac7baca2edc8e4819cb5d695c36c98ce61 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Wed, 22 Oct 2025 16:52:08 +0200 Subject: [PATCH 12/28] 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 9fbfd37d85dc..ccf237073c6f 100644 --- a/docs/markdown/Rust-module.md +++ b/docs/markdown/Rust-module.md @@ -258,6 +258,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 b9084783e782..7a7e12e0770c 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" +} From 8f7e196db39bb5f5a9bfdae6155490e6ea293f54 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 24 Oct 2025 10:24:45 +0200 Subject: [PATCH 13/28] modules: rust: implement workspace.package() Note that, as shown in the testcase, package() works in the subproject as well. This means that in the future the Cargo code generator can be changed to reduce the amount of generated code and instead rely on the package object. Signed-off-by: Paolo Bonzini --- docs/markdown/Rust-module.md | 29 ++++++++++++++----- mesonbuild/cargo/interpreter.py | 19 ++++++++++-- mesonbuild/modules/rust.py | 29 +++++++++++++++++-- .../31 rust.workspace package/meson.build | 15 ++++++++++ .../subprojects/answer-2.1/meson.build | 4 +++ .../32 rust.workspace workspace/meson.build | 15 ++++++++++ .../subprojects/answer-2.1/meson.build | 4 +++ 7 files changed, 102 insertions(+), 13 deletions(-) diff --git a/docs/markdown/Rust-module.md b/docs/markdown/Rust-module.md index ccf237073c6f..d3527fa80f0a 100644 --- a/docs/markdown/Rust-module.md +++ b/docs/markdown/Rust-module.md @@ -241,6 +241,19 @@ packages = ws.packages() Returns a list of package names in the workspace. +### workspace.package() + +```meson +pkg = ws.package([package_name]) +``` + +Returns a package object for the given package member. If empty, returns +the object for the root package. + +Arguments: +- `package_name`: (str, optional) Name of the package; not needed for the + root package of a workspace + ### workspace.subproject() ```meson @@ -258,15 +271,15 @@ Positional arguments: The package object returned by `workspace.subproject()` provides methods for working with individual packages in a Cargo workspace. -### subproject.name() +### package.name(), subproject.name() ```meson name = pkg.name() ``` -Returns the name of the subproject. +Returns the name of a package or subproject. -### subproject.version() +### package.version(), subproject.version() ```meson version = pkg.version() @@ -274,7 +287,7 @@ version = pkg.version() Returns the normalized version number of the subproject. -### subproject.api() +### package.api(), subproject.api() ```meson api = pkg.api() @@ -283,21 +296,21 @@ api = pkg.api() Returns the API version of the subproject, that is the version up to the first nonzero element. -### subproject.features() +### package.features(), subproject.features() ```meson features = pkg.features() ``` -Returns selected features for a specific subproject. +Returns selected features for a specific package or subproject. -### subproject.all_features() +### package.all_features(), subproject.all_features() ```meson all_features = pkg.all_features() ``` -Returns all defined features for a specific subproject. +Returns all defined features for a specific package or subproject. ### subproject.dependency() diff --git a/mesonbuild/cargo/interpreter.py b/mesonbuild/cargo/interpreter.py index 0e9948b84efc..5a0d1f2d1c95 100644 --- a/mesonbuild/cargo/interpreter.py +++ b/mesonbuild/cargo/interpreter.py @@ -258,8 +258,8 @@ def __init__(self, env: Environment, subdir: str, subprojects_dir: str) -> None: self.build_def_files: T.List[str] = [] # Cargo packages filename = os.path.join(self.environment.get_source_dir(), subdir, 'Cargo.lock') - subprojects_dir = os.path.join(self.environment.get_source_dir(), subprojects_dir) - self.cargolock = load_cargo_lock(filename, subprojects_dir) + full_subprojects_dir = os.path.join(self.environment.get_source_dir(), subprojects_dir) + self.cargolock = load_cargo_lock(filename, full_subprojects_dir) if self.cargolock: self.environment.wrap_resolver.merge_wraps(self.cargolock.wraps) self.build_def_files.append(filename) @@ -298,6 +298,21 @@ def _prepare_entry_point(self, ws: WorkspaceState) -> None: for feature in self.features: self._enable_feature(pkg, feature) + def load_package(self, ws: WorkspaceState, package_name: T.Optional[str]) -> PackageState: + if package_name is None: + if not ws.workspace.root_package: + raise MesonException('no root package in workspace') + path = '.' + else: + try: + path = ws.packages_to_member[package_name] + except KeyError: + raise MesonException(f'workspace member "{package_name}" not found') + + if is_parent_path(self.subprojects_dir, path): + raise MesonException('argument to package() cannot be a subproject') + return ws.packages[path] + def interpret(self, subdir: str, project_root: T.Optional[str] = None) -> mparser.CodeBlockNode: filename = os.path.join(self.environment.source_dir, subdir, 'Cargo.toml') build = builder.Builder(filename) diff --git a/mesonbuild/modules/rust.py b/mesonbuild/modules/rust.py index 7a7e12e0770c..dd3808ad9494 100644 --- a/mesonbuild/modules/rust.py +++ b/mesonbuild/modules/rust.py @@ -101,6 +101,7 @@ def __init__(self, interpreter: Interpreter, ws: cargo.WorkspaceState) -> None: self.ws = ws self.methods.update({ 'packages': self.packages_method, + 'package': self.package_method, 'subproject': self.subproject_method, }) @@ -111,6 +112,12 @@ def packages_method(self, state: ModuleState, args: T.List, kwargs: TYPE_kwargs) package_names = [pkg.manifest.package.name for pkg in self.ws.packages.values()] return sorted(package_names) + @typed_pos_args('workspace.package', optargs=[str]) + def package_method(self, state: 'ModuleState', args: T.List, kwargs: TYPE_kwargs) -> RustPackage: + """Returns a package object.""" + package_name = args[0] if args else None + return RustPackage(self, self.interpreter.cargo.load_package(self.ws, package_name)) + def _do_subproject(self, pkg: cargo.PackageState) -> None: kw: _kwargs.DoSubproject = { 'required': True, @@ -138,8 +145,8 @@ def subproject_method(self, state: ModuleState, args: T.Tuple[str, T.Optional[st return RustSubproject(self, pkg) -class RustSubproject(ModuleObject): - """Represents a Rust package within a workspace.""" +class RustCrate(ModuleObject): + """Abstract base class for Rust crate representations.""" def __init__(self, rust_ws: RustWorkspace, package: cargo.PackageState) -> None: super().__init__() @@ -148,7 +155,6 @@ def __init__(self, rust_ws: RustWorkspace, package: cargo.PackageState) -> None: 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, @@ -184,6 +190,23 @@ def features_method(self, state: ModuleState, args: T.List, kwargs: TYPE_kwargs) """Returns chosen features for specific package.""" return sorted(list(self.package.cfg.features)) + +class RustPackage(RustCrate): + """Represents a Rust package within a workspace.""" + + def __init__(self, rust_ws: RustWorkspace, package: cargo.PackageState) -> None: + super().__init__(rust_ws, package) + + +class RustSubproject(RustCrate): + """Represents a Cargo subproject.""" + + def __init__(self, rust_ws: RustWorkspace, package: cargo.PackageState) -> None: + super().__init__(rust_ws, 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'}))) diff --git a/test cases/rust/31 rust.workspace package/meson.build b/test cases/rust/31 rust.workspace package/meson.build index 8fc85e735c84..ddb4c2e9af51 100644 --- a/test cases/rust/31 rust.workspace package/meson.build +++ b/test cases/rust/31 rust.workspace package/meson.build @@ -6,6 +6,13 @@ cargo = rust.workspace() # Test workspace.packages() method assert(cargo.packages() == ['answer', 'hello', 'package_test']) +main_pkg = cargo.package() +assert(main_pkg.name() == 'package_test') +assert(main_pkg.version() == '0.1.0') +assert(main_pkg.api() == '0.1') +assert(main_pkg.all_features() == ['default', 'feature1', 'feature2']) +assert(main_pkg.features() == ['default', 'feature1']) + hello_rs = cargo.subproject('hello') assert(hello_rs.name() == 'hello') assert(hello_rs.version() == '1.0.0') @@ -25,6 +32,14 @@ e = executable('package-test', 'src/main.rs', ) test('package-test', e) +# failure test cases for package() +testcase expect_error('argument to package() cannot be a subproject') + cargo.package('hello') +endtestcase +testcase expect_error('workspace member "nonexistent" not found') + cargo.package('nonexistent') +endtestcase + # 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') 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 cc8d463b6d11..755888cd1a51 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,10 @@ rust = import('rust') cargo = rust.workspace() assert(cargo.packages() == ['answer']) +answer_pkg = cargo.package() +assert(answer_pkg.all_features() == ['default', 'large']) +assert(answer_pkg.features() == ['default', 'large']) + 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/meson.build b/test cases/rust/32 rust.workspace workspace/meson.build index 476374153e59..15bc673a9aeb 100644 --- a/test cases/rust/32 rust.workspace workspace/meson.build +++ b/test cases/rust/32 rust.workspace workspace/meson.build @@ -6,6 +6,13 @@ cargo = rust.workspace() # Test workspace.packages() method assert(cargo.packages() == ['answer', 'hello', 'workspace_test']) +main_pkg = cargo.package() +assert(main_pkg.name() == 'workspace_test') +assert(main_pkg.version() == '0.1.0') +assert(main_pkg.api() == '0.1') +assert(main_pkg.all_features() == ['default', 'feature1', 'feature2']) +assert(main_pkg.features() == ['default', 'feature1']) + hello_rs = cargo.subproject('hello') assert(hello_rs.name() == 'hello') assert(hello_rs.version() == '1.0.0') @@ -25,6 +32,14 @@ e = executable('workspace-test', 'src/main.rs', ) test('workspace-test', e) +# failure test cases for package() +testcase expect_error('argument to package() cannot be a subproject') + cargo.package('hello') +endtestcase +testcase expect_error('workspace member "nonexistent" not found') + cargo.package('nonexistent') +endtestcase + # 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') 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 cc8d463b6d11..755888cd1a51 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,10 @@ rust = import('rust') cargo = rust.workspace() assert(cargo.packages() == ['answer']) +answer_pkg = cargo.package() +assert(answer_pkg.all_features() == ['default', 'large']) +assert(answer_pkg.features() == ['default', 'large']) + 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) From e04e56c45a9897bc5d6bbccc506c8ac2e49e78e1 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 24 Oct 2025 10:37:39 +0200 Subject: [PATCH 14/28] test: rust.workspace: add another member to the workspace Signed-off-by: Paolo Bonzini --- test cases/rust/32 rust.workspace workspace/Cargo.lock | 5 +++++ test cases/rust/32 rust.workspace workspace/Cargo.toml | 3 ++- test cases/rust/32 rust.workspace workspace/meson.build | 6 ++++-- .../rust/32 rust.workspace workspace/more/Cargo.toml | 7 +++++++ .../rust/32 rust.workspace workspace/more/meson.build | 8 ++++++++ .../rust/32 rust.workspace workspace/more/src/lib.rs | 3 +++ test cases/rust/32 rust.workspace workspace/src/main.rs | 1 + 7 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 test cases/rust/32 rust.workspace workspace/more/Cargo.toml create mode 100644 test cases/rust/32 rust.workspace workspace/more/meson.build create mode 100644 test cases/rust/32 rust.workspace workspace/more/src/lib.rs diff --git a/test cases/rust/32 rust.workspace workspace/Cargo.lock b/test cases/rust/32 rust.workspace workspace/Cargo.lock index 0434b60286f7..7a697ea54da6 100644 --- a/test cases/rust/32 rust.workspace workspace/Cargo.lock +++ b/test cases/rust/32 rust.workspace workspace/Cargo.lock @@ -10,10 +10,15 @@ version = "2.1.0" name = "hello" version = "1.0.0" +[[package]] +name = "more" +version = "0.1.0" + [[package]] name = "workspace_test" version = "0.1.0" dependencies = [ "answer", "hello", + "more", ] diff --git a/test cases/rust/32 rust.workspace workspace/Cargo.toml b/test cases/rust/32 rust.workspace workspace/Cargo.toml index 44e4a18ef226..ae2ae4aa796f 100644 --- a/test cases/rust/32 rust.workspace workspace/Cargo.toml +++ b/test cases/rust/32 rust.workspace workspace/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["."] +members = [".", "more"] [package] name = "workspace_test" @@ -14,3 +14,4 @@ feature2 = [] [dependencies] hello = { version = "1.0", path = "subprojects/hello-1.0", optional = true } answer = { version = "2.1", path = "subprojects/answer-2.1", optional = true } +more = { path = "more" } diff --git a/test cases/rust/32 rust.workspace workspace/meson.build b/test cases/rust/32 rust.workspace workspace/meson.build index 15bc673a9aeb..5a2f77a1b34c 100644 --- a/test cases/rust/32 rust.workspace workspace/meson.build +++ b/test cases/rust/32 rust.workspace workspace/meson.build @@ -4,7 +4,7 @@ rust = import('rust') cargo = rust.workspace() # Test workspace.packages() method -assert(cargo.packages() == ['answer', 'hello', 'workspace_test']) +assert(cargo.packages() == ['answer', 'hello', 'more', 'workspace_test']) main_pkg = cargo.package() assert(main_pkg.name() == 'workspace_test') @@ -27,8 +27,10 @@ assert(answer_rs.api() == '2') assert(answer_rs.all_features() == ['default', 'large']) assert(answer_rs.features() == ['default', 'large']) +subdir('more') + e = executable('workspace-test', 'src/main.rs', - dependencies: [hello_rs.dependency(), answer_rs.dependency()], + dependencies: [hello_rs.dependency(), answer_rs.dependency(), more_dep], ) test('workspace-test', e) diff --git a/test cases/rust/32 rust.workspace workspace/more/Cargo.toml b/test cases/rust/32 rust.workspace workspace/more/Cargo.toml new file mode 100644 index 000000000000..1e7e46a5ff3d --- /dev/null +++ b/test cases/rust/32 rust.workspace workspace/more/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "more" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["rlib"] \ No newline at end of file diff --git a/test cases/rust/32 rust.workspace workspace/more/meson.build b/test cases/rust/32 rust.workspace workspace/more/meson.build new file mode 100644 index 000000000000..2780d3bd0b0c --- /dev/null +++ b/test cases/rust/32 rust.workspace workspace/more/meson.build @@ -0,0 +1,8 @@ +more_pkg = cargo.package('more') + +assert(more_pkg.name() == 'more') +assert(more_pkg.features() == ['default']) +assert(more_pkg.all_features() == ['default']) + +l = static_library('more', 'src/lib.rs') +more_dep = declare_dependency(link_with: l) diff --git a/test cases/rust/32 rust.workspace workspace/more/src/lib.rs b/test cases/rust/32 rust.workspace workspace/more/src/lib.rs new file mode 100644 index 000000000000..178adaca9352 --- /dev/null +++ b/test cases/rust/32 rust.workspace workspace/more/src/lib.rs @@ -0,0 +1,3 @@ +pub fn do_something() { + println!("Doing something in more crate"); +} \ No newline at end of file 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 13c02dd64ad8..3c8c968e2748 100644 --- a/test cases/rust/32 rust.workspace workspace/src/main.rs +++ b/test cases/rust/32 rust.workspace workspace/src/main.rs @@ -5,4 +5,5 @@ fn main() { println!("{}", farewell()); println!("{}", answer::answer()); println!("{}", answer::large_answer()); + more::do_something(); } From cdf1042e4244c2e198da1ff7c4f3a48f7cd17f95 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 24 Oct 2025 09:37:54 +0200 Subject: [PATCH 15/28] modules: rust: add workspace methods returning arguments for build targets Add rustc_args(), env(), and rust_dependency_map() methods to the RustPackage class. They simply delegate to PackageState and PackageConfiguration. --- docs/markdown/Rust-module.md | 30 ++++++++++++++++++- mesonbuild/modules/rust.py | 21 +++++++++++++ .../31 rust.workspace package/meson.build | 2 ++ .../subprojects/answer-2.1/meson.build | 4 ++- .../32 rust.workspace workspace/meson.build | 2 ++ .../more/meson.build | 5 +++- .../subprojects/answer-2.1/meson.build | 4 ++- 7 files changed, 64 insertions(+), 4 deletions(-) diff --git a/docs/markdown/Rust-module.md b/docs/markdown/Rust-module.md index d3527fa80f0a..f00c47923a99 100644 --- a/docs/markdown/Rust-module.md +++ b/docs/markdown/Rust-module.md @@ -312,7 +312,35 @@ all_features = pkg.all_features() Returns all defined features for a specific package or subproject. -### subproject.dependency() +### Packages only + +#### package.rust_args() + +```meson +args = pkg.rustc_args() +``` + +Returns rustc arguments for this package. + +#### package.env() + +```meson +env_vars = pkg.env() +``` + +Returns environment variables for this package. + +#### package.rust_dependency_map() + +```meson +dep_map = pkg.rust_dependency_map() +``` + +Returns rust dependency mapping for this package. + +### Subprojects only + +#### subproject.dependency() ```meson dep = subproject.dependency(...) diff --git a/mesonbuild/modules/rust.py b/mesonbuild/modules/rust.py index dd3808ad9494..98878f8e6b76 100644 --- a/mesonbuild/modules/rust.py +++ b/mesonbuild/modules/rust.py @@ -158,6 +158,9 @@ def __init__(self, rust_ws: RustWorkspace, package: cargo.PackageState) -> None: 'features': self.features_method, 'name': self.name_method, 'version': self.version_method, + 'rust_args': self.rust_args_method, + 'env': self.env_method, # type: ignore[dict-item] + 'rust_dependency_map': self.rust_dependency_map_method, # type: ignore[dict-item] }) @noPosargs @@ -190,6 +193,24 @@ def features_method(self, state: ModuleState, args: T.List, kwargs: TYPE_kwargs) """Returns chosen features for specific package.""" return sorted(list(self.package.cfg.features)) + @noPosargs + @noKwargs + def rust_args_method(self, state: ModuleState, args: T.List, kwargs: TYPE_kwargs) -> T.List[str]: + """Returns rustc arguments for this package.""" + return self.package.get_rustc_args(state.environment, state.subdir, mesonlib.MachineChoice.HOST) + + @noPosargs + @noKwargs + def env_method(self, state: ModuleState, args: T.List, kwargs: TYPE_kwargs) -> T.Dict[str, str]: + """Returns environment variables for this package.""" + return self.package.get_env_dict(state.environment, state.subdir) + + @noPosargs + @noKwargs + def rust_dependency_map_method(self, state: ModuleState, args: T.List, kwargs: TYPE_kwargs) -> T.Dict[str, str]: + """Returns rust dependency mapping for this package.""" + return self.package.cfg.get_dependency_map(self.package.manifest) + class RustPackage(RustCrate): """Represents a Rust package within a workspace.""" diff --git a/test cases/rust/31 rust.workspace package/meson.build b/test cases/rust/31 rust.workspace package/meson.build index ddb4c2e9af51..f6854843e112 100644 --- a/test cases/rust/31 rust.workspace package/meson.build +++ b/test cases/rust/31 rust.workspace package/meson.build @@ -29,6 +29,8 @@ assert(answer_rs.features() == ['default', 'large']) e = executable('package-test', 'src/main.rs', dependencies: [hello_rs.dependency(), answer_rs.dependency()], + rust_args: main_pkg.rust_args(), + rust_dependency_map: main_pkg.rust_dependency_map(), ) test('package-test', e) 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 755888cd1a51..9564c93fba2a 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 @@ -8,6 +8,8 @@ answer_pkg = cargo.package() assert(answer_pkg.all_features() == ['default', 'large']) assert(answer_pkg.features() == ['default', 'large']) -l = static_library('answer', 'src/lib.rs', rust_args: ['--cfg', 'feature="large"']) +l = static_library('answer', 'src/lib.rs', + rust_args: answer_pkg.rust_args(), + rust_dependency_map: answer_pkg.rust_dependency_map()) dep = declare_dependency(link_with: l) meson.override_dependency('answer-2-rs', dep) diff --git a/test cases/rust/32 rust.workspace workspace/meson.build b/test cases/rust/32 rust.workspace workspace/meson.build index 5a2f77a1b34c..0c9cd4d5268b 100644 --- a/test cases/rust/32 rust.workspace workspace/meson.build +++ b/test cases/rust/32 rust.workspace workspace/meson.build @@ -31,6 +31,8 @@ subdir('more') e = executable('workspace-test', 'src/main.rs', dependencies: [hello_rs.dependency(), answer_rs.dependency(), more_dep], + rust_args: main_pkg.rust_args(), + rust_dependency_map: main_pkg.rust_dependency_map(), ) test('workspace-test', e) diff --git a/test cases/rust/32 rust.workspace workspace/more/meson.build b/test cases/rust/32 rust.workspace workspace/more/meson.build index 2780d3bd0b0c..40dcc8cdf73e 100644 --- a/test cases/rust/32 rust.workspace workspace/more/meson.build +++ b/test cases/rust/32 rust.workspace workspace/more/meson.build @@ -4,5 +4,8 @@ assert(more_pkg.name() == 'more') assert(more_pkg.features() == ['default']) assert(more_pkg.all_features() == ['default']) -l = static_library('more', 'src/lib.rs') +l = static_library('more', 'src/lib.rs', + rust_args: more_pkg.rust_args(), + rust_dependency_map: more_pkg.rust_dependency_map(), +) more_dep = declare_dependency(link_with: l) 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 755888cd1a51..9564c93fba2a 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 @@ -8,6 +8,8 @@ answer_pkg = cargo.package() assert(answer_pkg.all_features() == ['default', 'large']) assert(answer_pkg.features() == ['default', 'large']) -l = static_library('answer', 'src/lib.rs', rust_args: ['--cfg', 'feature="large"']) +l = static_library('answer', 'src/lib.rs', + rust_args: answer_pkg.rust_args(), + rust_dependency_map: answer_pkg.rust_dependency_map()) dep = declare_dependency(link_with: l) meson.override_dependency('answer-2-rs', dep) From 92b63b9b5e81fd83ace9d430a82ce39763e27f13 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Sun, 26 Oct 2025 10:21:04 +0100 Subject: [PATCH 16/28] dependencies: make arguments to InternalDependency.__init__ optional Signed-off-by: Paolo Bonzini --- mesonbuild/build.py | 10 ++++----- mesonbuild/dependencies/base.py | 40 +++++++++++++++++---------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 138f1ae03233..c8ce3c052d4e 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -1443,12 +1443,10 @@ def add_deps(self, deps): self.link_whole_targets.extend(dep.whole_libraries) if dep.get_compile_args() or dep.get_link_args(): # Those parts that are external. - extpart = dependencies.InternalDependency('undefined', - [], - dep.get_compile_args(), - dep.get_link_args(), - [], [], [], [], [], {}, [], [], [], - dep.name) + extpart = dependencies.InternalDependency(dep.version, + compile_args=dep.get_compile_args(), + link_args=dep.get_link_args(), + name=dep.name) self.external_deps.append(extpart) # Deps of deps. self.add_deps(dep.ext_deps) diff --git a/mesonbuild/dependencies/base.py b/mesonbuild/dependencies/base.py index 4536746d940f..a782792eee79 100644 --- a/mesonbuild/dependencies/base.py +++ b/mesonbuild/dependencies/base.py @@ -298,29 +298,31 @@ def get_as_shared(self, recursive: bool) -> Dependency: return self class InternalDependency(Dependency): - def __init__(self, version: str, incdirs: T.List['IncludeDirs'], compile_args: T.List[str], - link_args: T.List[str], - libraries: T.List[LibTypes], - whole_libraries: T.List[T.Union[StaticLibrary, CustomTarget, CustomTargetIndex]], - sources: T.Sequence[T.Union[mesonlib.File, GeneratedTypes, StructuredSources]], - extra_files: T.Sequence[mesonlib.File], - ext_deps: T.List[Dependency], variables: T.Dict[str, str], - d_module_versions: T.List[T.Union[str, int]], d_import_dirs: T.List['IncludeDirs'], - objects: T.List['ExtractedObjects'], + def __init__(self, version: str, incdirs: T.Optional[T.List['IncludeDirs']] = None, + compile_args: T.Optional[T.List[str]] = None, + link_args: T.Optional[T.List[str]] = None, + libraries: T.Optional[T.List[LibTypes]] = None, + whole_libraries: T.Optional[T.List[T.Union[StaticLibrary, CustomTarget, CustomTargetIndex]]] = None, + sources: T.Optional[T.Sequence[T.Union[mesonlib.File, GeneratedTypes, StructuredSources]]] = None, + extra_files: T.Optional[T.Sequence[mesonlib.File]] = None, + ext_deps: T.Optional[T.List[Dependency]] = None, variables: T.Optional[T.Dict[str, str]] = None, + d_module_versions: T.Optional[T.List[T.Union[str, int]]] = None, + d_import_dirs: T.Optional[T.List['IncludeDirs']] = None, + objects: T.Optional[T.List['ExtractedObjects']] = None, name: T.Optional[str] = None): super().__init__(DependencyTypeName('internal'), {}) self.version = version self.is_found = True - self.include_directories = incdirs - self.compile_args = compile_args - self.link_args = link_args - self.libraries = libraries - self.whole_libraries = whole_libraries - self.sources = list(sources) - self.extra_files = list(extra_files) - self.ext_deps = ext_deps - self.variables = variables - self.objects = objects + self.include_directories = incdirs or [] + self.compile_args = compile_args or [] + self.link_args = link_args or [] + self.libraries = libraries or [] + self.whole_libraries = whole_libraries or [] + self.sources = list(sources or []) + self.extra_files = list(extra_files or []) + self.ext_deps = ext_deps or [] + self.variables = variables or {} + self.objects = objects or [] if d_module_versions: self.d_features['versions'] = d_module_versions if d_import_dirs: From 97e8a2b22ccb9998fa1df109d2bfe75698ed548d Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Sun, 26 Oct 2025 09:15:54 +0100 Subject: [PATCH 17/28] rust: add to_system_dependency Move the logic for system dependencies outside Cargo.interpreter and into the rust module, so that it can be reused by the workspace object. Signed-off-by: Paolo Bonzini --- docs/markdown/Rust-module.md | 13 +++++++++++++ mesonbuild/build.py | 8 ++++---- mesonbuild/compilers/rust.py | 8 ++++++++ mesonbuild/modules/rust.py | 17 +++++++++++++++++ 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/docs/markdown/Rust-module.md b/docs/markdown/Rust-module.md index f00c47923a99..78dd7e7b2410 100644 --- a/docs/markdown/Rust-module.md +++ b/docs/markdown/Rust-module.md @@ -172,6 +172,19 @@ Only a subset of [[shared_library]] keyword arguments are allowed: - link_with - override_options +### to_system_dependency() + +```meson +rustmod.to_system_dependency(dep[, name]) +``` + +Create and return an internal dependency that wraps `dep` and +defines `cfg(system_deps_have_NAME)`. This is compatible with +how the `system-deps` crate reports the availability of a system +dependency to Rust code. + +If omitted, the name defaults to the name of the dependency. + ### workspace() Basic usage: diff --git a/mesonbuild/build.py b/mesonbuild/build.py index c8ce3c052d4e..5f09738d2bcd 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -1443,10 +1443,10 @@ def add_deps(self, deps): self.link_whole_targets.extend(dep.whole_libraries) if dep.get_compile_args() or dep.get_link_args(): # Those parts that are external. - extpart = dependencies.InternalDependency(dep.version, - compile_args=dep.get_compile_args(), - link_args=dep.get_link_args(), - name=dep.name) + extpart = type(dep)(dep.version, + compile_args=dep.get_compile_args(), + link_args=dep.get_link_args(), + name=dep.name) self.external_deps.append(extpart) # Deps of deps. self.add_deps(dep.ext_deps) diff --git a/mesonbuild/compilers/rust.py b/mesonbuild/compilers/rust.py index 4088b5ca9c65..ab039606998c 100644 --- a/mesonbuild/compilers/rust.py +++ b/mesonbuild/compilers/rust.py @@ -11,6 +11,7 @@ import typing as T from .. import options +from ..dependencies import InternalDependency from ..mesonlib import EnvironmentException, MesonException, Popen_safe_logged, version_compare from ..linkers.linkers import VisualStudioLikeLinkerMixin from ..options import OptionKey @@ -73,6 +74,11 @@ def rustc_link_args(args: T.List[str]) -> T.List[str]: rustc_args.append(f'link-arg={arg}') return rustc_args + +class RustSystemDependency(InternalDependency): + pass + + class RustCompiler(Compiler): # rustc doesn't invoke the compiler itself, it doesn't need a LINKER_PREFIX @@ -323,6 +329,8 @@ def get_options(self) -> MutableKeyedOptionDictType: return opts def get_dependency_compile_args(self, dep: 'Dependency') -> T.List[str]: + if isinstance(dep, RustSystemDependency): + return dep.get_compile_args() # Rust doesn't have dependency compile arguments so simply return # nothing here. Dependencies are linked and all required metadata is # provided by the linker flags. diff --git a/mesonbuild/modules/rust.py b/mesonbuild/modules/rust.py index 98878f8e6b76..31925890bd24 100644 --- a/mesonbuild/modules/rust.py +++ b/mesonbuild/modules/rust.py @@ -15,6 +15,7 @@ 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 ..compilers.rust import RustSystemDependency 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, @@ -259,6 +260,7 @@ def __init__(self, interpreter: Interpreter) -> None: 'doctest': self.doctest, 'bindgen': self.bindgen, 'proc_macro': self.proc_macro, + 'to_system_dependency': self.to_system_dependency, 'workspace': self.workspace, }) @@ -657,6 +659,21 @@ 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.to_system_dependency', '1.10.0') + @typed_pos_args('rust.to_system_dependency', Dependency, optargs=[str]) + @noKwargs + def to_system_dependency(self, state: ModuleState, args: T.Tuple[Dependency, T.Optional[str]], kwargs: TYPE_kwargs) -> Dependency: + dep, depname = args + if not dep.found(): + return dep + if not depname: + if not dep.name: + raise MesonException("rust.to_system_dependency() called with an unnamed dependency and no explicit name") + depname = dep.name + depname = re.sub(r'[^a-zA-Z0-9]', '_', depname) + rust_args = ['--cfg', f'system_deps_have_{depname}'] + return RustSystemDependency(dep.version, compile_args=rust_args, ext_deps=[dep], name=dep.name) + @FeatureNew('rust.workspace', '1.10.0') @noPosargs @typed_kwargs( From 904e9e30bf4fada1f5bc216c75caf041ac474264 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Sun, 26 Oct 2025 10:32:36 +0100 Subject: [PATCH 18/28] cargo: use rust.to_system_dependency Signed-off-by: Paolo Bonzini --- mesonbuild/cargo/interpreter.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/mesonbuild/cargo/interpreter.py b/mesonbuild/cargo/interpreter.py index 5a0d1f2d1c95..907a7ee4b747 100644 --- a/mesonbuild/cargo/interpreter.py +++ b/mesonbuild/cargo/interpreter.py @@ -706,7 +706,6 @@ def _create_dependencies(self, pkg: PackageState, build: builder.Builder) -> T.L dep_pkg = self._dep_package(pkg, dep) if dep_pkg.manifest.lib: ast += self._create_dependency(dep_pkg, dep, build) - ast.append(build.assign(build.array([]), 'system_deps_args')) for name, sys_dep in pkg.manifest.system_dependencies.items(): if sys_dep.enabled(cfg.features): ast += self._create_system_dependency(name, sys_dep, build) @@ -719,24 +718,20 @@ def _create_system_dependency(self, name: str, dep: SystemDependency, build: bui 'required': build.bool(not dep.optional), } varname = f'{fixup_meson_varname(name)}_system_dep' - cfg = f'system_deps_have_{fixup_meson_varname(name)}' return [ build.assign( - build.function( - 'dependency', - [build.string(dep.name)], - kw, - ), + build.method( + 'to_system_dependency', + build.identifier('rust'), [ + build.function( + 'dependency', + [build.string(dep.name)], + kw, + ), + build.string(name) + ]), varname, ), - build.if_( - build.method('found', build.identifier(varname)), build.block([ - build.plusassign( - build.array([build.string('--cfg'), build.string(cfg)]), - 'system_deps_args' - ), - ]) - ), ] def _create_dependency(self, pkg: PackageState, dep: Dependency, build: builder.Builder) -> T.List[mparser.BaseNode]: @@ -845,10 +840,8 @@ def _create_lib(self, pkg: PackageState, build: builder.Builder, subdir: str, rustc_args_list = pkg.get_rustc_args(self.environment, subdir, MachineChoice.HOST) extra_args_ref = build.identifier(_extra_args_varname()) - system_deps_args_ref = build.identifier('system_deps_args') rust_args: T.List[mparser.BaseNode] = [build.string(a) for a in rustc_args_list] rust_args.append(extra_args_ref) - rust_args.append(system_deps_args_ref) dependencies.append(build.identifier(_extra_deps_varname())) From f89b799d1e343850e3448bf0bdf18910f43a9225 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 24 Oct 2025 15:18:29 +0200 Subject: [PATCH 19/28] cargo: use rust.workspace() to build the arguments Signed-off-by: Paolo Bonzini --- mesonbuild/cargo/interpreter.py | 52 +++++++++++++++------------------ 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/mesonbuild/cargo/interpreter.py b/mesonbuild/cargo/interpreter.py index 907a7ee4b747..12e95c315409 100644 --- a/mesonbuild/cargo/interpreter.py +++ b/mesonbuild/cargo/interpreter.py @@ -334,9 +334,10 @@ def interpret_package(self, manifest: Manifest, build: builder.Builder, subdir: return build.block(ast) def _create_package(self, pkg: PackageState, build: builder.Builder, subdir: str) -> T.List[mparser.BaseNode]: - cfg = pkg.cfg ast: T.List[mparser.BaseNode] = [ - build.assign(build.array([build.string(f) for f in cfg.features]), 'features'), + build.assign(build.method('package', build.identifier('cargo'), + [build.string(pkg.manifest.package.name)]), 'pkg_obj'), + build.assign(build.method('features', build.identifier('pkg_obj')), 'features'), build.function('message', [ build.string('Enabled features:'), build.identifier('features'), @@ -669,23 +670,19 @@ def _create_project(self, name: str, pkg: T.Optional[PackageState], build: build # for the upkeep of the module 'meson_version': build.string(f'>= {coredata.stable_version}'), } - if not pkg: - return [ - build.function('project', args, kwargs), - ] - - default_options: T.Dict[str, mparser.BaseNode] = {} - if pkg.downloaded: - default_options['warning_level'] = build.string('0') - - kwargs.update({ - 'version': build.string(pkg.manifest.package.version), - 'default_options': build.dict({build.string(k): v for k, v in default_options.items()}), - }) - if pkg.manifest.package.license: - kwargs['license'] = build.string(pkg.manifest.package.license) - elif pkg.manifest.package.license_file: - kwargs['license_files'] = build.string(pkg.manifest.package.license_file) + if pkg: + default_options: T.Dict[str, mparser.BaseNode] = {} + if pkg.downloaded: + default_options['warning_level'] = build.string('0') + + kwargs.update({ + 'version': build.string(pkg.manifest.package.version), + 'default_options': build.dict({build.string(k): v for k, v in default_options.items()}), + }) + if pkg.manifest.package.license: + kwargs['license'] = build.string(pkg.manifest.package.license) + elif pkg.manifest.package.license_file: + kwargs['license_files'] = build.string(pkg.manifest.package.license_file) # project(...) # rust = import('rust') @@ -831,17 +828,13 @@ def _create_lib(self, pkg: PackageState, build: builder.Builder, subdir: str, dep = pkg.manifest.dependencies[name] dependencies.append(build.identifier(_dependency_varname(dep))) - dependency_map: T.Dict[mparser.BaseNode, mparser.BaseNode] = { - build.string(k): build.string(v) for k, v in cfg.get_dependency_map(pkg.manifest).items()} - for name, sys_dep in pkg.manifest.system_dependencies.items(): if sys_dep.enabled(cfg.features): dependencies.append(build.identifier(f'{fixup_meson_varname(name)}_system_dep')) - rustc_args_list = pkg.get_rustc_args(self.environment, subdir, MachineChoice.HOST) + package_rust_args = build.method('rust_args', build.identifier('pkg_obj')) extra_args_ref = build.identifier(_extra_args_varname()) - rust_args: T.List[mparser.BaseNode] = [build.string(a) for a in rustc_args_list] - rust_args.append(extra_args_ref) + rust_args = build.plus(package_rust_args, extra_args_ref) dependencies.append(build.identifier(_extra_deps_varname())) @@ -856,8 +849,8 @@ def _create_lib(self, pkg: PackageState, build: builder.Builder, subdir: str, kwargs: T.Dict[str, mparser.BaseNode] = { 'dependencies': build.array(dependencies), - 'rust_dependency_map': build.dict(dependency_map), - 'rust_args': build.array(rust_args), + 'rust_dependency_map': build.method('rust_dependency_map', build.identifier('pkg_obj')), + 'rust_args': rust_args, 'override_options': build.dict(override_options), } @@ -887,9 +880,10 @@ def _create_lib(self, pkg: PackageState, build: builder.Builder, subdir: str, kw={ 'link_with': build.identifier('lib'), 'variables': build.dict({ - build.string('features'): build.string(','.join(cfg.features)), + build.string('features'): build.method('join', build.string(','), + [build.identifier('features')]), }), - 'version': build.string(pkg.manifest.package.version), + 'version': build.method('version', build.identifier('pkg_obj')), }, ), 'dep' From 83348fac8cd404d69c8822391541f5c94bc35bb3 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Mon, 27 Oct 2025 13:15:54 +0100 Subject: [PATCH 20/28] modules: allow passing an array for dependency versions Signed-off-by: Paolo Bonzini --- mesonbuild/modules/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesonbuild/modules/__init__.py b/mesonbuild/modules/__init__.py index ed8deff0635b..8639fa8273b6 100644 --- a/mesonbuild/modules/__init__.py +++ b/mesonbuild/modules/__init__.py @@ -117,7 +117,7 @@ def overridden_dependency(self, depname: str, for_machine: MachineChoice = Machi 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': + wanted: T.Optional[T.Union[str, T.List[str]]] = None) -> 'Dependency': kwargs: T.Dict[str, object] = {'native': native, 'required': required} if wanted: kwargs['version'] = wanted From 65c668580b97544000b9d8f556d353a16ecc5f5b Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Tue, 28 Oct 2025 13:43:53 +0100 Subject: [PATCH 21/28] modules: rust: add dependencies() method to package object Signed-off-by: Paolo Bonzini --- docs/markdown/Rust-module.md | 28 ++++++++++ mesonbuild/modules/rust.py | 54 +++++++++++++++---- .../31 rust.workspace package/meson.build | 2 +- .../32 rust.workspace workspace/meson.build | 2 +- .../more/meson.build | 1 + 5 files changed, 76 insertions(+), 11 deletions(-) diff --git a/docs/markdown/Rust-module.md b/docs/markdown/Rust-module.md index 78dd7e7b2410..aa5c13fd264a 100644 --- a/docs/markdown/Rust-module.md +++ b/docs/markdown/Rust-module.md @@ -267,6 +267,19 @@ Arguments: - `package_name`: (str, optional) Name of the package; not needed for the root package of a workspace +Example usage: +```meson +rust = import('rust') +cargo = rust.workspace() +pkg = cargo.package() + +executable('my_app', 'src/main.rs', + dependencies: pkg.dependencies(), + rust_args: pkg.rust_args(), + rust_dependency_map: pkg.rust_dependency_map(), +) +``` + ### workspace.subproject() ```meson @@ -351,6 +364,21 @@ dep_map = pkg.rust_dependency_map() Returns rust dependency mapping for this package. +#### package.dependencies() + +```meson +deps = pkg.dependencies(...) +``` + +Returns a list of dependency objects for all the dependencies required by this +Rust package, including both Rust crate dependencies and system dependencies. +The returned dependencies can be used directly in build target declarations. + +Keyword arguments: +- `dependencies`: (`bool`, default: true) Whether to include regular Rust crate dependencies +- `dev_dependencies`: (`bool`, default: false) Whether to include development dependencies (not yet implemented) +- `system_dependencies`: (`bool`, default: true) Whether to include system dependencies + ### Subprojects only #### subproject.dependency() diff --git a/mesonbuild/modules/rust.py b/mesonbuild/modules/rust.py index 31925890bd24..77123bfd2f34 100644 --- a/mesonbuild/modules/rust.py +++ b/mesonbuild/modules/rust.py @@ -91,6 +91,16 @@ def no_spaces_validator(arg: T.Optional[T.Union[str, T.List]]) -> T.Optional[str return 'must not contain spaces due to limitations of rustdoc' return None +def dep_to_system_dependency(dep: Dependency, depname: str) -> Dependency: + if not dep.found(): + return dep + if not depname: + if not dep.name: + raise MesonException("rust.to_system_dependency() called with an unnamed dependency and no explicit name") + depname = dep.name + depname = re.sub(r'[^a-zA-Z0-9]', '_', depname) + rust_args = ['--cfg', f'system_deps_have_{depname}'] + return RustSystemDependency(dep.version, compile_args=rust_args, ext_deps=[dep], name=dep.name) class RustWorkspace(ModuleObject): """Represents a Rust workspace, controlling the build of packages @@ -218,6 +228,40 @@ class RustPackage(RustCrate): def __init__(self, rust_ws: RustWorkspace, package: cargo.PackageState) -> None: super().__init__(rust_ws, package) + self.methods.update({ + 'dependencies': self.dependencies_method, + }) + + @noPosargs + @typed_kwargs('package.dependencies', + KwargInfo('dependencies', bool, default=True), + KwargInfo('dev_dependencies', bool, default=False), + KwargInfo('system_dependencies', bool, default=True)) + def dependencies_method(self, state: ModuleState, args: T.List, kwargs: T.Dict[str, T.Any]) -> T.List[Dependency]: + """Returns the dependencies for this package.""" + dependencies: T.List[Dependency] = [] + cfg = self.package.cfg + + if kwargs['dependencies']: + for dep_key, dep_pkg in cfg.dep_packages.items(): + if dep_pkg.manifest.lib: + # Get the dependency name for this package + depname = dep_pkg.get_dependency_name(None) + dependency = state.overridden_dependency(depname) + dependencies.append(dependency) + + if kwargs['dev_dependencies']: + raise MesonException('dev_dependencies is not implemented yet') + + if kwargs['system_dependencies']: + for name, sys_dep in self.package.manifest.system_dependencies.items(): + if sys_dep.enabled(cfg.features): + # System dependencies use the original dependency name from Cargo.toml + dependency = state.dependency(sys_dep.name, required=not sys_dep.optional, + wanted=sys_dep.meson_version) + dependencies.append(dep_to_system_dependency(dependency, name)) + + return dependencies class RustSubproject(RustCrate): @@ -664,15 +708,7 @@ def proc_macro(self, state: ModuleState, args: T.Tuple[str, SourcesVarargsType], @noKwargs def to_system_dependency(self, state: ModuleState, args: T.Tuple[Dependency, T.Optional[str]], kwargs: TYPE_kwargs) -> Dependency: dep, depname = args - if not dep.found(): - return dep - if not depname: - if not dep.name: - raise MesonException("rust.to_system_dependency() called with an unnamed dependency and no explicit name") - depname = dep.name - depname = re.sub(r'[^a-zA-Z0-9]', '_', depname) - rust_args = ['--cfg', f'system_deps_have_{depname}'] - return RustSystemDependency(dep.version, compile_args=rust_args, ext_deps=[dep], name=dep.name) + return dep_to_system_dependency(dep, depname) @FeatureNew('rust.workspace', '1.10.0') @noPosargs diff --git a/test cases/rust/31 rust.workspace package/meson.build b/test cases/rust/31 rust.workspace package/meson.build index f6854843e112..05a8b9f50152 100644 --- a/test cases/rust/31 rust.workspace package/meson.build +++ b/test cases/rust/31 rust.workspace package/meson.build @@ -28,7 +28,7 @@ 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()], + dependencies: main_pkg.dependencies(), rust_args: main_pkg.rust_args(), rust_dependency_map: main_pkg.rust_dependency_map(), ) diff --git a/test cases/rust/32 rust.workspace workspace/meson.build b/test cases/rust/32 rust.workspace workspace/meson.build index 0c9cd4d5268b..63e9ef79075b 100644 --- a/test cases/rust/32 rust.workspace workspace/meson.build +++ b/test cases/rust/32 rust.workspace workspace/meson.build @@ -30,7 +30,7 @@ assert(answer_rs.features() == ['default', 'large']) subdir('more') e = executable('workspace-test', 'src/main.rs', - dependencies: [hello_rs.dependency(), answer_rs.dependency(), more_dep], + dependencies: main_pkg.dependencies(), rust_args: main_pkg.rust_args(), rust_dependency_map: main_pkg.rust_dependency_map(), ) diff --git a/test cases/rust/32 rust.workspace workspace/more/meson.build b/test cases/rust/32 rust.workspace workspace/more/meson.build index 40dcc8cdf73e..c037f8dca3fa 100644 --- a/test cases/rust/32 rust.workspace workspace/more/meson.build +++ b/test cases/rust/32 rust.workspace workspace/more/meson.build @@ -9,3 +9,4 @@ l = static_library('more', 'src/lib.rs', rust_dependency_map: more_pkg.rust_dependency_map(), ) more_dep = declare_dependency(link_with: l) +meson.override_dependency('more-0.1-rs', more_dep) From 9239a7e2b6bc31ad4c8590e8d0ea694ee981f365 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Tue, 28 Oct 2025 13:33:09 +0100 Subject: [PATCH 22/28] modules: rust: invoke subprojects automatically from dependencies() Signed-off-by: Paolo Bonzini --- mesonbuild/cargo/interpreter.py | 10 +++++++++- mesonbuild/modules/__init__.py | 1 + mesonbuild/modules/rust.py | 10 +++++++++- .../rust/31 rust.workspace package/meson.build | 14 +++++++------- .../rust/32 rust.workspace workspace/meson.build | 14 +++++++------- 5 files changed, 33 insertions(+), 16 deletions(-) diff --git a/mesonbuild/cargo/interpreter.py b/mesonbuild/cargo/interpreter.py index 12e95c315409..97cbf4cb7799 100644 --- a/mesonbuild/cargo/interpreter.py +++ b/mesonbuild/cargo/interpreter.py @@ -24,7 +24,9 @@ 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, unique_list, version_compare +from ..mesonlib import ( + is_parent_path, lazy_property, MesonException, MachineChoice, + unique_list, version_compare) from .. import coredata, mlog from ..wrap.wrap import PackageDefinition @@ -105,6 +107,12 @@ class PackageState: # Package configuration state cfg: T.Optional[PackageConfiguration] = None + @lazy_property + def path(self) -> T.Optional[str]: + if not self.ws_subdir: + return None + return os.path.normpath(os.path.join(self.ws_subdir, self.ws_member)) + def get_env_dict(self, environment: Environment, subdir: str) -> T.Dict[str, str]: """Get environment variables for this package.""" # Common variables for build.rs and crates diff --git a/mesonbuild/modules/__init__.py b/mesonbuild/modules/__init__.py index 8639fa8273b6..8c6ed2fecb38 100644 --- a/mesonbuild/modules/__init__.py +++ b/mesonbuild/modules/__init__.py @@ -36,6 +36,7 @@ def __init__(self, interpreter: 'Interpreter') -> None: self.source_root = interpreter.environment.get_source_dir() self.build_to_src = relpath(interpreter.environment.get_source_dir(), interpreter.environment.get_build_dir()) + self.subproject_dir = interpreter.subproject_dir self.subproject = interpreter.subproject self.subdir = interpreter.subdir self.root_subdir = interpreter.root_subdir diff --git a/mesonbuild/modules/rust.py b/mesonbuild/modules/rust.py index 77123bfd2f34..e98b1b1e6dc4 100644 --- a/mesonbuild/modules/rust.py +++ b/mesonbuild/modules/rust.py @@ -23,7 +23,7 @@ ) 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 ..mesonlib import (is_parent_path, File, MachineChoice, MesonException, PerMachine) from ..programs import ExternalProgram, NonExistingExternalProgram if T.TYPE_CHECKING: @@ -116,6 +116,10 @@ def __init__(self, interpreter: Interpreter, ws: cargo.WorkspaceState) -> None: 'subproject': self.subproject_method, }) + @property + def subdir(self) -> str: + return self.ws.subdir + @noPosargs @noKwargs def packages_method(self, state: ModuleState, args: T.List, kwargs: TYPE_kwargs) -> T.List[str]: @@ -245,6 +249,10 @@ def dependencies_method(self, state: ModuleState, args: T.List, kwargs: T.Dict[s if kwargs['dependencies']: for dep_key, dep_pkg in cfg.dep_packages.items(): if dep_pkg.manifest.lib: + if dep_pkg.ws_subdir != self.rust_ws.subdir or \ + is_parent_path(os.path.join(self.rust_ws.subdir, state.subproject_dir), + dep_pkg.path): + self.rust_ws._do_subproject(dep_pkg) # Get the dependency name for this package depname = dep_pkg.get_dependency_name(None) dependency = state.overridden_dependency(depname) diff --git a/test cases/rust/31 rust.workspace package/meson.build b/test cases/rust/31 rust.workspace package/meson.build index 05a8b9f50152..3bd716e792fd 100644 --- a/test cases/rust/31 rust.workspace package/meson.build +++ b/test cases/rust/31 rust.workspace package/meson.build @@ -20,13 +20,6 @@ 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: main_pkg.dependencies(), rust_args: main_pkg.rust_args(), @@ -34,6 +27,13 @@ e = executable('package-test', 'src/main.rs', ) test('package-test', e) +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']) + # failure test cases for package() testcase expect_error('argument to package() cannot be a subproject') cargo.package('hello') diff --git a/test cases/rust/32 rust.workspace workspace/meson.build b/test cases/rust/32 rust.workspace workspace/meson.build index 63e9ef79075b..2fafb7063d84 100644 --- a/test cases/rust/32 rust.workspace workspace/meson.build +++ b/test cases/rust/32 rust.workspace workspace/meson.build @@ -20,13 +20,6 @@ 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']) - subdir('more') e = executable('workspace-test', 'src/main.rs', @@ -36,6 +29,13 @@ e = executable('workspace-test', 'src/main.rs', ) test('workspace-test', e) +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']) + # failure test cases for package() testcase expect_error('argument to package() cannot be a subproject') cargo.package('hello') From 686c50c6c3d1fa627a350ff44ce98725daa6bbc2 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Mon, 27 Oct 2025 12:26:06 +0100 Subject: [PATCH 23/28] cargo: use dependencies() method to get dependencies Signed-off-by: Paolo Bonzini --- mesonbuild/cargo/interpreter.py | 84 +++++++++------------------------ 1 file changed, 22 insertions(+), 62 deletions(-) diff --git a/mesonbuild/cargo/interpreter.py b/mesonbuild/cargo/interpreter.py index 97cbf4cb7799..e07a9f5cffcc 100644 --- a/mesonbuild/cargo/interpreter.py +++ b/mesonbuild/cargo/interpreter.py @@ -35,7 +35,7 @@ from .. import mparser from typing_extensions import Literal - from .manifest import Dependency, SystemDependency + from .manifest import Dependency from ..environment import Environment from ..interpreterbase import SubProject from ..compilers.rust import RustCompiler @@ -47,10 +47,6 @@ def _dependency_name(package_name: str, api: str, suffix: str = '-rs') -> str: return f'{basename}-{api}{suffix}' -def _dependency_varname(dep: Dependency) -> str: - return f'{fixup_meson_varname(dep.package)}_{(dep.api.replace(".", "_"))}_dep' - - def _library_name(name: str, api: str, lib_type: Literal['rust', 'c', 'proc-macro'] = 'rust') -> str: # Add the API version to the library name to avoid conflicts when multiple # versions of the same crate are used. The Ninja backend removed everything @@ -711,40 +707,15 @@ def _create_dependencies(self, pkg: PackageState, build: builder.Builder) -> T.L dep_pkg = self._dep_package(pkg, dep) if dep_pkg.manifest.lib: ast += self._create_dependency(dep_pkg, dep, build) - for name, sys_dep in pkg.manifest.system_dependencies.items(): - if sys_dep.enabled(cfg.features): - ast += self._create_system_dependency(name, sys_dep, build) return ast - def _create_system_dependency(self, name: str, dep: SystemDependency, build: builder.Builder) -> T.List[mparser.BaseNode]: - # TODO: handle feature_overrides - kw = { - 'version': build.array([build.string(s) for s in dep.meson_version]), - 'required': build.bool(not dep.optional), - } - varname = f'{fixup_meson_varname(name)}_system_dep' - return [ - build.assign( - build.method( - 'to_system_dependency', - build.identifier('rust'), [ - build.function( - 'dependency', - [build.string(dep.name)], - kw, - ), - build.string(name) - ]), - varname, - ), - ] - def _create_dependency(self, pkg: PackageState, dep: Dependency, build: builder.Builder) -> T.List[mparser.BaseNode]: cfg = pkg.cfg - dep_obj: mparser.BaseNode + feat_obj: mparser.BaseNode if self.cargolock and self.resolve_package(dep.package, dep.api): - dep_obj = build.method( - 'dependency', + # actual_features = cargo.subproject(...).features() + feat_obj = build.method( + 'features', build.method( 'subproject', build.identifier('cargo'), @@ -754,10 +725,21 @@ def _create_dependency(self, pkg: PackageState, dep: Dependency, build: builder. kw = { 'version': build.array([build.string(s) for s in version_]), } + # actual_features = dependency(...).get_variable('features', default_value : '').split(',') dep_obj = build.function( 'dependency', [build.string(_dependency_name(dep.package, dep.api))], kw) + feat_obj = build.method( + 'split', + build.method( + 'get_variable', + dep_obj, + [build.string('features')], + {'default_value': build.string('')} + ), + [build.string(',')], + ) # However, this subproject could have been previously configured with a # different set of features. Cargo collects the set of features globally @@ -769,23 +751,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 = cargo.subproject('xxx', 'api').dependency() + # actual_features = dependency(...).get_variable('features', default_value : '').split(',') build.assign( - dep_obj, - _dependency_varname(dep), - ), - # actual_features = xxx_dep.get_variable('features', default_value : '').split(',') - build.assign( - build.method( - 'split', - build.method( - 'get_variable', - build.identifier(_dependency_varname(dep)), - [build.string('features')], - {'default_value': build.string('')} - ), - [build.string(',')], - ), + feat_obj, 'actual_features' ), # needed_features = [f1, f2, ...] @@ -830,22 +798,14 @@ 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: RUST_ABI, static: bool = False, shared: bool = False) -> T.List[mparser.BaseNode]: - cfg = pkg.cfg - dependencies: T.List[mparser.BaseNode] = [] - for name in cfg.required_deps: - dep = pkg.manifest.dependencies[name] - dependencies.append(build.identifier(_dependency_varname(dep))) - - for name, sys_dep in pkg.manifest.system_dependencies.items(): - if sys_dep.enabled(cfg.features): - dependencies.append(build.identifier(f'{fixup_meson_varname(name)}_system_dep')) + pkg_dependencies = build.method('dependencies', build.identifier('pkg_obj')) + extra_deps_ref = build.identifier(_extra_deps_varname()) + dependencies = build.plus(pkg_dependencies, extra_deps_ref) package_rust_args = build.method('rust_args', build.identifier('pkg_obj')) extra_args_ref = build.identifier(_extra_args_varname()) rust_args = build.plus(package_rust_args, extra_args_ref) - dependencies.append(build.identifier(_extra_deps_varname())) - override_options: T.Dict[mparser.BaseNode, mparser.BaseNode] = { build.string('rust_std'): build.string(pkg.manifest.package.edition), } @@ -856,7 +816,7 @@ def _create_lib(self, pkg: PackageState, build: builder.Builder, subdir: str, ] kwargs: T.Dict[str, mparser.BaseNode] = { - 'dependencies': build.array(dependencies), + 'dependencies': dependencies, 'rust_dependency_map': build.method('rust_dependency_map', build.identifier('pkg_obj')), 'rust_args': rust_args, 'override_options': build.dict(override_options), From f410831745bfaad9574329e1636728eb76db0e8c Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Tue, 28 Oct 2025 18:16:05 +0100 Subject: [PATCH 24/28] modules: rust: add package.library/package.proc_macro methods --- docs/markdown/Rust-module.md | 30 ++++++++ mesonbuild/cargo/interpreter.py | 89 ++++++++++------------- mesonbuild/modules/rust.py | 125 +++++++++++++++++++++++++++++++- 3 files changed, 191 insertions(+), 53 deletions(-) diff --git a/docs/markdown/Rust-module.md b/docs/markdown/Rust-module.md index aa5c13fd264a..f2c3611b330a 100644 --- a/docs/markdown/Rust-module.md +++ b/docs/markdown/Rust-module.md @@ -379,6 +379,36 @@ Keyword arguments: - `dev_dependencies`: (`bool`, default: false) Whether to include development dependencies (not yet implemented) - `system_dependencies`: (`bool`, default: true) Whether to include system dependencies +#### package.library() + +```meson +lib = pkg.library(...) +``` + +Builds library targets for a workspace package. The method requires that +the package's `Cargo.toml` file contains the `[lib]` section or that it +is discovered from the contents of the file system. Static vs. shared library is +decided based on the crate types in `Cargo.toml` + +Positional arguments: +- `target_name`: (`str`, optional) Name of the binary target to build. +- `sources`: (`StructuredSources`, optional) Source files for the executable. If omitted, + uses the path specified in the `[lib]` section of `Cargo.toml`. + +Accepts all keyword arguments from [[shared_library]] and [[static_library]]. +`rust_abi` must match the crate types and is mandatory if more than one +ABI is exposed by the crate. + +#### package.proc_macro() + +```meson +lib = pkg.proc_macro(...) +``` + +Builds a proc-macro crate for a workspace package. + +Accepts all keyword arguments from [[shared_library]]. + ### Subprojects only #### subproject.dependency() diff --git a/mesonbuild/cargo/interpreter.py b/mesonbuild/cargo/interpreter.py index e07a9f5cffcc..f0f025bc43b3 100644 --- a/mesonbuild/cargo/interpreter.py +++ b/mesonbuild/cargo/interpreter.py @@ -47,15 +47,6 @@ def _dependency_name(package_name: str, api: str, suffix: str = '-rs') -> str: return f'{basename}-{api}{suffix}' -def _library_name(name: str, api: str, lib_type: Literal['rust', 'c', 'proc-macro'] = 'rust') -> str: - # Add the API version to the library name to avoid conflicts when multiple - # versions of the same crate are used. The Ninja backend removed everything - # after the + to form the crate name. - if lib_type == 'c': - return name - return f'{name}+{api.replace(".", "_")}' - - def _extra_args_varname() -> str: return 'extra_args' @@ -87,7 +78,7 @@ def get_dependency_map(self, manifest: Manifest) -> T.Dict[str, str]: dep = manifest.dependencies[name] dep_key = PackageKey(dep.package, dep.api) dep_pkg = self.dep_packages[dep_key] - dep_lib_name = _library_name(dep_pkg.manifest.lib.name, dep_pkg.manifest.package.api) + dep_lib_name = dep_pkg.library_name() dep_crate_name = name if name != dep.package else dep_pkg.manifest.lib.name dependency_map[dep_lib_name] = dep_crate_name return dependency_map @@ -109,6 +100,15 @@ def path(self) -> T.Optional[str]: return None return os.path.normpath(os.path.join(self.ws_subdir, self.ws_member)) + def library_name(self, lib_type: RUST_ABI = 'rust') -> str: + # Add the API version to the library name to avoid conflicts when multiple + # versions of the same crate are used. The Ninja backend removed everything + # after the + to form the crate name. + name = fixup_meson_varname(self.manifest.package.name) + if lib_type == 'c': + return name + return f'{name}+{self.manifest.package.api.replace(".", "_")}' + def get_env_dict(self, environment: Environment, subdir: str) -> T.Dict[str, str]: """Get environment variables for this package.""" # Common variables for build.rs and crates @@ -205,17 +205,33 @@ def supported_abis(self) -> T.Set[RUST_ABI]: 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.""" + def abi_resolve_default(self, rust_abi: T.Optional[RUST_ABI]) -> RUST_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)) + return 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}') + return rust_abi + + def abi_has_shared(self, rust_abi: RUST_ABI) -> bool: + if rust_abi == 'proc-macro': + return True + return ('cdylib' if rust_abi == 'c' else 'dylib') in self.manifest.lib.crate_type + + def abi_has_static(self, rust_abi: RUST_ABI) -> bool: + if rust_abi == 'proc-macro': + return False + crate_type = self.manifest.lib.crate_type + if rust_abi == 'c': + return 'staticlib' in crate_type + return 'lib' in crate_type or 'rlib' in crate_type + def get_dependency_name(self, rust_abi: T.Optional[RUST_ABI]) -> str: + """Get the dependency name for a package with the given ABI.""" + rust_abi = self.abi_resolve_default(rust_abi) package_name = self.manifest.package.name api = self.manifest.package.api @@ -354,17 +370,8 @@ 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') - abis = pkg.supported_abis() - if 'proc-macro' in abis: - ast.extend(self._create_lib(pkg, build, subdir, 'proc-macro', shared=True)) - 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 'c' in abis: - ast.extend(self._create_lib(pkg, build, subdir, 'c', - static='staticlib' in crate_type, - shared='cdylib' in crate_type)) + for abi in pkg.supported_abis(): + ast.extend(self._create_lib(pkg, build, subdir, abi)) return ast @@ -796,46 +803,24 @@ 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: RUST_ABI, - static: bool = False, shared: bool = False) -> T.List[mparser.BaseNode]: - pkg_dependencies = build.method('dependencies', build.identifier('pkg_obj')) - extra_deps_ref = build.identifier(_extra_deps_varname()) - dependencies = build.plus(pkg_dependencies, extra_deps_ref) - - package_rust_args = build.method('rust_args', build.identifier('pkg_obj')) - extra_args_ref = build.identifier(_extra_args_varname()) - rust_args = build.plus(package_rust_args, extra_args_ref) - - override_options: T.Dict[mparser.BaseNode, mparser.BaseNode] = { - build.string('rust_std'): build.string(pkg.manifest.package.edition), - } - + lib_type: RUST_ABI) -> T.List[mparser.BaseNode]: posargs: T.List[mparser.BaseNode] = [ - build.string(_library_name(pkg.manifest.lib.name, pkg.manifest.package.api, lib_type)), - build.string(pkg.manifest.lib.path), + build.string(pkg.library_name(lib_type)), ] kwargs: T.Dict[str, mparser.BaseNode] = { - 'dependencies': dependencies, - 'rust_dependency_map': build.method('rust_dependency_map', build.identifier('pkg_obj')), - 'rust_args': rust_args, - 'override_options': build.dict(override_options), + 'dependencies': build.identifier(_extra_deps_varname()), + 'rust_args': build.identifier(_extra_args_varname()), } depname_suffix = '' if lib_type == 'c' else '-rs' depname = _dependency_name(pkg.manifest.package.name, pkg.manifest.package.api, depname_suffix) - lib: mparser.BaseNode if lib_type == 'proc-macro': - lib = build.method('proc_macro', build.identifier('rust'), posargs, kwargs) + lib = build.method('proc_macro', build.identifier('pkg_obj'), posargs, kwargs) else: - if static and shared: - target_type = 'both_libraries' - else: - target_type = 'shared_library' if shared else 'static_library' - kwargs['rust_abi'] = build.string(lib_type) - lib = build.function(target_type, posargs, kwargs) + lib = build.method('library', build.identifier('pkg_obj'), posargs, kwargs) # lib = xxx_library() # dep = declare_dependency() diff --git a/mesonbuild/modules/rust.py b/mesonbuild/modules/rust.py index e98b1b1e6dc4..6652535df91b 100644 --- a/mesonbuild/modules/rust.py +++ b/mesonbuild/modules/rust.py @@ -19,7 +19,8 @@ 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 + OUTPUT_KW, INCLUDE_DIRECTORIES, SOURCES_VARARGS, NoneType, in_set_validator, + LIBRARY_KWS, _BASE_LANG_KW ) from ..interpreterbase import ContainerTypeInfo, InterpreterException, KwargInfo, typed_kwargs, typed_pos_args, noKwargs, noPosargs, permittedKwargs from ..interpreter.interpreterobjects import Doctest @@ -75,6 +76,11 @@ class FuncWorkspace(TypedDict): class FuncDependency(TypedDict): rust_abi: T.Optional[RUST_ABI] + class RustPackageLibrary(_kwargs.Library): + dependencies: T.List[T.Union[Dependency, ExternalLibrary]] + link_with: T.List[LibTypes] + link_whole: T.List[LibTypes] + RUST_TEST_KWS: T.List[KwargInfo] = [ KwargInfo( 'rust_args', @@ -234,6 +240,8 @@ def __init__(self, rust_ws: RustWorkspace, package: cargo.PackageState) -> None: super().__init__(rust_ws, package) self.methods.update({ 'dependencies': self.dependencies_method, + 'library': self.library_method, + 'proc_macro': self.proc_macro_method, }) @noPosargs @@ -271,6 +279,121 @@ def dependencies_method(self, state: ModuleState, args: T.List, kwargs: T.Dict[s return dependencies + @staticmethod + def validate_pos_args(name: str, args: T.Tuple[ + T.Optional[T.Union[str, StructuredSources]], + T.Optional[StructuredSources]]) -> T.Tuple[T.Optional[str], T.Optional[StructuredSources]]: + if isinstance(args[0], str): + return args[0], args[1] + if args[1] is not None: + raise MesonException(f"{name} only accepts one StructuredSources parameter") + return None, args[0] + + def merge_kw_args(self, state: ModuleState, kwargs: RustPackageLibrary) -> None: + deps = kwargs['dependencies'] + kwargs['dependencies'] = self.dependencies_method(state, [], {}) + kwargs['dependencies'].extend(deps) + + depmap = kwargs['rust_dependency_map'] + kwargs['rust_dependency_map'] = self.rust_dependency_map_method(state, [], {}) + kwargs['rust_dependency_map'].update(depmap) + + rust_args = kwargs['rust_args'] + kwargs['rust_args'] = self.rust_args_method(state, [], {}) + kwargs['rust_args'].extend(rust_args) + + kwargs['override_options'].setdefault('rust_std', self.package.manifest.package.edition) + + def _library_method(self, state: ModuleState, args: T.Tuple[ + T.Optional[T.Union[str, StructuredSources]], + T.Optional[StructuredSources]], kwargs: RustPackageLibrary, + static: bool, shared: bool) -> T.Union[BothLibraries, SharedLibrary, StaticLibrary]: + tgt_args = self.validate_pos_args('package.library', args) + if not self.package.manifest.lib: + raise MesonException("no [lib] section in Cargo package") + + sources: T.Union[StructuredSources, str] + tgt_name, sources = tgt_args + if not tgt_name: + rust_abi: RUST_ABI + if kwargs['rust_crate_type'] is not None: + rust_abi = 'rust' if kwargs['rust_crate_type'] in {'lib', 'rlib', 'dylib', 'proc-macro'} else 'c' + else: + rust_abi = kwargs['rust_abi'] + tgt_name = self.package.library_name(rust_abi) + if not sources: + sources = self.package.manifest.lib.path + + lib_args: T.Tuple[str, SourcesVarargsType] = (tgt_name, [sources]) + self.merge_kw_args(state, kwargs) + + if static and shared: + return state._interpreter.build_both_libraries(state.current_node, lib_args, kwargs) + elif shared: + return state._interpreter.build_target(state.current_node, lib_args, + T.cast('_kwargs.SharedLibrary', kwargs), + SharedLibrary) + else: + return state._interpreter.build_target(state.current_node, lib_args, + T.cast('_kwargs.StaticLibrary', kwargs), + StaticLibrary) + + def _proc_macro_method(self, state: 'ModuleState', args: T.Tuple[ + T.Optional[T.Union[str, StructuredSources]], + T.Optional[StructuredSources]], kwargs: RustPackageLibrary) -> SharedLibrary: + kwargs['native'] = MachineChoice.BUILD + kwargs['rust_abi'] = None + kwargs['rust_crate_type'] = 'proc-macro' + kwargs['rust_args'] = kwargs['rust_args'] + ['--extern', 'proc_macro'] + result = self._library_method(state, args, kwargs, shared=True, static=False) + return T.cast('SharedLibrary', result) + + @typed_pos_args('package.library', optargs=[(str, StructuredSources), StructuredSources]) + @typed_kwargs( + 'package.library', + *LIBRARY_KWS, + DEPENDENCIES_KW, + LINK_WITH_KW, + LINK_WHOLE_KW, + _BASE_LANG_KW.evolve(name='rust_args'), + ) + def library_method(self, state: ModuleState, args: T.Tuple[ + T.Optional[T.Union[str, StructuredSources]], + T.Optional[StructuredSources]], kwargs: RustPackageLibrary) -> T.Union[BothLibraries, SharedLibrary, StaticLibrary]: + if not self.package.manifest.lib: + raise MesonException("no [lib] section in Cargo package") + if kwargs['rust_crate_type'] is not None: + static = kwargs['rust_crate_type'] in {'lib', 'rlib', 'staticlib'} + shared = kwargs['rust_crate_type'] in {'dylib', 'cdylib', 'proc-macro'} + else: + rust_abi = self.package.abi_resolve_default(kwargs['rust_abi']) + static = self.package.abi_has_static(rust_abi) + shared = self.package.abi_has_shared(rust_abi) + if rust_abi == 'proc-macro': + kwargs['rust_crate_type'] = 'proc-macro' + kwargs['rust_abi'] = None + else: + kwargs['rust_abi'] = rust_abi + return self._library_method(state, args, kwargs, static=static, shared=shared) + + @typed_pos_args('package.proc_macro', optargs=[(str, StructuredSources), StructuredSources]) + @typed_kwargs( + 'package.proc_macro', + *SHARED_LIB_KWS, + DEPENDENCIES_KW, + LINK_WITH_KW, + LINK_WHOLE_KW, + _BASE_LANG_KW.evolve(name='rust_args'), + ) + def proc_macro_method(self, state: 'ModuleState', args: T.Tuple[ + T.Optional[T.Union[str, StructuredSources]], + T.Optional[StructuredSources]], kwargs: RustPackageLibrary) -> SharedLibrary: + if not self.package.manifest.lib: + raise MesonException("no [lib] section in Cargo package") + if 'proc-macro' not in self.package.manifest.lib.crate_type: + raise MesonException("not a procedural macro crate") + return self._proc_macro_method(state, args, kwargs) + class RustSubproject(RustCrate): """Represents a Cargo subproject.""" From 6a4a24b341fac70d38a3327bb844ac32730e0a46 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Tue, 28 Oct 2025 16:59:23 +0100 Subject: [PATCH 25/28] cargo: populate bin table from src/bin/ Signed-off-by: Paolo Bonzini --- mesonbuild/cargo/manifest.py | 54 ++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/mesonbuild/cargo/manifest.py b/mesonbuild/cargo/manifest.py index 2c3eeb3e0cf4..1253199724a5 100644 --- a/mesonbuild/cargo/manifest.py +++ b/mesonbuild/cargo/manifest.py @@ -79,6 +79,28 @@ def convert(self, v: T.Any, ws_v: T.Any) -> object: return self.func(v if v is not None else ws_v) +class DictMergeValue(ConvertValue): + """Merge the incoming array of tables with a dictionary; + a user-provided function maps each table to one of the + entries of the dictionary.""" + + def __init__(self, func: T.Callable[[T.Any], T.List[object]], key: T.Callable[[T.Any], str], base: T.Mapping[str, object] = None) -> None: + super().__init__(func, base) + self.key = key + + def convert(self, v: T.Any, ws_v: T.Any) -> object: + out = self.func(v if v is not None else ws_v) + assert isinstance(out, list) # for mypy + assert isinstance(self.default, dict) # for mypy + + d = {self.key(x): x for x in out} + # FIXME: check how auto-discovered items are merged with Cargo.toml + for k, v in self.default.items(): + if k not in d: + d[k] = v + return d + + def _raw_to_dataclass(raw: T.Mapping[str, object], cls: T.Type[_DI], msg: str, raw_from_workspace: T.Optional[T.Mapping[str, object]] = None, ignored_fields: T.Optional[T.List[str]] = None, @@ -490,7 +512,7 @@ class Manifest: dev_dependencies: T.Dict[str, Dependency] = dataclasses.field(default_factory=dict) build_dependencies: T.Dict[str, Dependency] = dataclasses.field(default_factory=dict) lib: T.Optional[Library] = None - bin: T.List[Binary] = dataclasses.field(default_factory=list) + bin: T.Dict[str, Binary] = dataclasses.field(default_factory=dict) test: T.List[Test] = dataclasses.field(default_factory=list) bench: T.List[Benchmark] = dataclasses.field(default_factory=list) example: T.List[Example] = dataclasses.field(default_factory=list) @@ -515,6 +537,32 @@ def from_raw(cls, raw: raw.Manifest, path: str, workspace: T.Optional[Workspace] if pkg.autolib and os.path.exists(os.path.join(path, 'src/lib.rs')): autolib = Library.from_raw({}, pkg) + def _discover_targets(subdir: str) -> T.Generator[T.Tuple[str, str], None, None]: + """Discover .rs files in a subdirectory and yield (name, path) tuples.""" + target_dir = os.path.join(path, subdir) + if os.path.isdir(target_dir): + for entry in os.listdir(target_dir): + if entry.endswith('.rs'): + target_name = entry[:-3] # Remove .rs extension + yield target_name, f'{subdir}/{entry}' + + autobins: T.Dict[str, Binary] = {} + if pkg.autobins: + # Check for default binary (src/main.rs) + if os.path.exists(os.path.join(path, 'src/main.rs')): + autobins[pkg.name] = Binary.from_raw({'name': pkg.name, 'path': 'src/main.rs'}, pkg) + # Add additional binaries from src/bin/ + for bin_name, bin_path in _discover_targets('src/bin'): + autobins[bin_name] = Binary.from_raw({'name': bin_name, 'path': bin_path}, pkg) + + # Check for additional binaries in src/bin/ + bin_dir = os.path.join(path, 'src/bin') + if os.path.isdir(bin_dir): + for entry in os.listdir(bin_dir): + if entry.endswith('.rs'): + bin_name = entry[:-3] # Remove .rs extension + autobins[bin_name] = Binary.from_raw({'name': bin_name, 'path': f'src/bin/{entry}'}, pkg) + def dependencies_from_raw(x: T.Dict[str, T.Any]) -> T.Dict[str, Dependency]: return {k: Dependency.from_raw(k, v, member_path, workspace) for k, v in x.items()} @@ -527,7 +575,9 @@ def dependencies_from_raw(x: T.Dict[str, T.Any]) -> T.Dict[str, Dependency]: build_dependencies=ConvertValue(dependencies_from_raw), lints=ConvertValue(Lint.from_raw), lib=ConvertValue(lambda x: Library.from_raw(x, pkg), default=autolib), - bin=ConvertValue(lambda x: [Binary.from_raw(b, pkg) for b in x]), + bin=DictMergeValue(lambda x: [Binary.from_raw(b, pkg) for b in x], + lambda x: x.name, + base=autobins), test=ConvertValue(lambda x: [Test.from_raw(b, pkg) for b in x]), bench=ConvertValue(lambda x: [Benchmark.from_raw(b, pkg) for b in x]), example=ConvertValue(lambda x: [Example.from_raw(b, pkg) for b in x]), From b91d68e29911f8ea9591026bfb63db68f799ec48 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Wed, 29 Oct 2025 09:36:33 +0100 Subject: [PATCH 26/28] modules: rust: add package.executable Signed-off-by: Paolo Bonzini --- docs/markdown/Rust-module.md | 27 ++++++++++++++++----- mesonbuild/modules/rust.py | 47 ++++++++++++++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/docs/markdown/Rust-module.md b/docs/markdown/Rust-module.md index f2c3611b330a..48aa059904ca 100644 --- a/docs/markdown/Rust-module.md +++ b/docs/markdown/Rust-module.md @@ -272,12 +272,7 @@ Example usage: rust = import('rust') cargo = rust.workspace() pkg = cargo.package() - -executable('my_app', 'src/main.rs', - dependencies: pkg.dependencies(), - rust_args: pkg.rust_args(), - rust_dependency_map: pkg.rust_dependency_map(), -) +pkg.executable(install: true) ``` ### workspace.subproject() @@ -409,6 +404,26 @@ Builds a proc-macro crate for a workspace package. Accepts all keyword arguments from [[shared_library]]. +#### package.executable() + +```meson +exe = pkg.executable([target_name], [sources], ...) +``` + +Builds an executable target for a workspace package. The method requires that the +package has at least one `[[bin]]` section defined in its `Cargo.toml` file, +or one binary discovered from the contents of the file system. + +Positional arguments: +- `target_name`: (`str`, optional) Name of the binary target to build. If the package + has multiple `[[bin]]` sections in `Cargo.toml`, this argument is required and must + match one of the binary names. If omitted and there's only one binary, that binary + will be built automatically. +- `sources`: (`StructuredSources`, optional) Source files for the executable. If omitted, + uses the path specified in the corresponding `[[bin]]` section of `Cargo.toml`. + +Accepts all keyword arguments from [[executable]]. + ### Subprojects only #### subproject.dependency() diff --git a/mesonbuild/modules/rust.py b/mesonbuild/modules/rust.py index 6652535df91b..12a2d11b9ed4 100644 --- a/mesonbuild/modules/rust.py +++ b/mesonbuild/modules/rust.py @@ -20,7 +20,7 @@ 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, - LIBRARY_KWS, _BASE_LANG_KW + EXECUTABLE_KWS, LIBRARY_KWS, _BASE_LANG_KW ) from ..interpreterbase import ContainerTypeInfo, InterpreterException, KwargInfo, typed_kwargs, typed_pos_args, noKwargs, noPosargs, permittedKwargs from ..interpreter.interpreterobjects import Doctest @@ -76,6 +76,11 @@ class FuncWorkspace(TypedDict): class FuncDependency(TypedDict): rust_abi: T.Optional[RUST_ABI] + class RustPackageExecutable(_kwargs.Executable): + dependencies: T.List[T.Union[Dependency, ExternalLibrary]] + link_with: T.List[LibTypes] + link_whole: T.List[LibTypes] + class RustPackageLibrary(_kwargs.Library): dependencies: T.List[T.Union[Dependency, ExternalLibrary]] link_with: T.List[LibTypes] @@ -242,6 +247,7 @@ def __init__(self, rust_ws: RustWorkspace, package: cargo.PackageState) -> None: 'dependencies': self.dependencies_method, 'library': self.library_method, 'proc_macro': self.proc_macro_method, + 'executable': self.executable_method, }) @noPosargs @@ -289,7 +295,7 @@ def validate_pos_args(name: str, args: T.Tuple[ raise MesonException(f"{name} only accepts one StructuredSources parameter") return None, args[0] - def merge_kw_args(self, state: ModuleState, kwargs: RustPackageLibrary) -> None: + def merge_kw_args(self, state: ModuleState, kwargs: T.Union[RustPackageExecutable, RustPackageLibrary]) -> None: deps = kwargs['dependencies'] kwargs['dependencies'] = self.dependencies_method(state, [], {}) kwargs['dependencies'].extend(deps) @@ -394,6 +400,43 @@ def proc_macro_method(self, state: 'ModuleState', args: T.Tuple[ raise MesonException("not a procedural macro crate") return self._proc_macro_method(state, args, kwargs) + @typed_pos_args('package.executable', optargs=[(str, StructuredSources), StructuredSources]) + @typed_kwargs( + 'package.executable', + *EXECUTABLE_KWS, + DEPENDENCIES_KW, + LINK_WITH_KW, + LINK_WHOLE_KW, + _BASE_LANG_KW.evolve(name='rust_args'), + ) + def executable_method(self, state: 'ModuleState', args: T.Tuple[ + T.Optional[T.Union[str, StructuredSources]], + T.Optional[StructuredSources]], kwargs: RustPackageExecutable) -> Executable: + """Builds executable targets from workspace bins.""" + tgt_args = self.validate_pos_args('package.executable', args) + if not self.package.manifest.bin: + raise MesonException("no [[bin]] section in Cargo package") + + sources: T.Union[StructuredSources, str] + tgt_name, sources = tgt_args + # If there's more than one binary, the first argument must be specified + # and must be one of the keys in pkg.bin + if not tgt_name: + if len(self.package.manifest.bin) > 1: + raise MesonException("Package has multiple binaries, you must specify which one to build as the first argument") + # Single binary, use it + tgt_name = next(iter(self.package.manifest.bin.keys())) + else: + if tgt_name not in self.package.manifest.bin: + raise MesonException(f"Binary '{tgt_name}' not found.") + + if not sources: + sources = self.package.manifest.bin[tgt_name].path + + exe_args: T.Tuple[str, SourcesVarargsType] = (tgt_name, [sources]) + self.merge_kw_args(state, kwargs) + return state._interpreter.build_target(state.current_node, exe_args, kwargs, Executable) + class RustSubproject(RustCrate): """Represents a Cargo subproject.""" From 2df2a4814251178d81c20a536272a03c2debaf78 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Wed, 12 Nov 2025 14:22:00 +0100 Subject: [PATCH 27/28] modules: rust: add package.shared_module method --- docs/markdown/Rust-module.md | 10 ++++++++++ mesonbuild/modules/rust.py | 36 +++++++++++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/docs/markdown/Rust-module.md b/docs/markdown/Rust-module.md index 48aa059904ca..040da3aed105 100644 --- a/docs/markdown/Rust-module.md +++ b/docs/markdown/Rust-module.md @@ -394,6 +394,16 @@ Accepts all keyword arguments from [[shared_library]] and [[static_library]]. `rust_abi` must match the crate types and is mandatory if more than one ABI is exposed by the crate. +#### package.shared_module() + +```meson +lib = pkg.shared_module(...) +``` + +Builds the `cdylib` for a workspace package as a shared module. + +Accepts all keyword arguments from [[shared_module]]. + #### package.proc_macro() ```meson diff --git a/mesonbuild/modules/rust.py b/mesonbuild/modules/rust.py index 12a2d11b9ed4..12bea3252ca8 100644 --- a/mesonbuild/modules/rust.py +++ b/mesonbuild/modules/rust.py @@ -13,14 +13,15 @@ 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) + CustomTarget, InvalidArguments, Jar, StructuredSources, SharedLibrary, StaticLibrary, + SharedModule) from ..compilers.compilers import are_asserts_disabled_for_subproject, lang_suffixes from ..compilers.rust import RustSystemDependency 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, - EXECUTABLE_KWS, LIBRARY_KWS, _BASE_LANG_KW + EXECUTABLE_KWS, LIBRARY_KWS, SHARED_MOD_KWS, _BASE_LANG_KW ) from ..interpreterbase import ContainerTypeInfo, InterpreterException, KwargInfo, typed_kwargs, typed_pos_args, noKwargs, noPosargs, permittedKwargs from ..interpreter.interpreterobjects import Doctest @@ -247,6 +248,7 @@ def __init__(self, rust_ws: RustWorkspace, package: cargo.PackageState) -> None: 'dependencies': self.dependencies_method, 'library': self.library_method, 'proc_macro': self.proc_macro_method, + 'shared_module': self.shared_module_method, 'executable': self.executable_method, }) @@ -313,7 +315,8 @@ def merge_kw_args(self, state: ModuleState, kwargs: T.Union[RustPackageExecutabl def _library_method(self, state: ModuleState, args: T.Tuple[ T.Optional[T.Union[str, StructuredSources]], T.Optional[StructuredSources]], kwargs: RustPackageLibrary, - static: bool, shared: bool) -> T.Union[BothLibraries, SharedLibrary, StaticLibrary]: + static: bool, shared: bool, + shared_mod: bool = False) -> T.Union[BothLibraries, SharedLibrary, StaticLibrary]: tgt_args = self.validate_pos_args('package.library', args) if not self.package.manifest.lib: raise MesonException("no [lib] section in Cargo package") @@ -333,6 +336,11 @@ def _library_method(self, state: ModuleState, args: T.Tuple[ lib_args: T.Tuple[str, SourcesVarargsType] = (tgt_name, [sources]) self.merge_kw_args(state, kwargs) + if shared_mod: + return state._interpreter.build_target(state.current_node, lib_args, + T.cast('_kwargs.SharedModule', kwargs), + SharedModule) + if static and shared: return state._interpreter.build_both_libraries(state.current_node, lib_args, kwargs) elif shared: @@ -400,6 +408,28 @@ def proc_macro_method(self, state: 'ModuleState', args: T.Tuple[ raise MesonException("not a procedural macro crate") return self._proc_macro_method(state, args, kwargs) + @typed_pos_args('package.shared_module', optargs=[(str, StructuredSources), StructuredSources]) + @typed_kwargs( + 'package.shared_module', + *SHARED_MOD_KWS, + DEPENDENCIES_KW, + LINK_WITH_KW, + LINK_WHOLE_KW, + _BASE_LANG_KW.evolve(name='rust_args'), + ) + def shared_module_method(self, state: 'ModuleState', args: T.Tuple[ + T.Optional[T.Union[str, StructuredSources]], + T.Optional[StructuredSources]], kwargs: RustPackageLibrary) -> SharedModule: + if not self.package.manifest.lib: + raise MesonException("no [lib] section in Cargo package") + if 'cdylib' not in self.package.manifest.lib.crate_type: + raise MesonException("not a cdylib crate") + + kwargs['rust_abi'] = None + kwargs['rust_crate_type'] = 'cdylib' + result = self._library_method(state, args, kwargs, shared=True, static=False, shared_mod=True) + return T.cast('SharedModule', result) + @typed_pos_args('package.executable', optargs=[(str, StructuredSources), StructuredSources]) @typed_kwargs( 'package.executable', From 6444de97d3ba960962411ba71b7eb3bc41a31062 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Tue, 18 Nov 2025 12:02:31 +0100 Subject: [PATCH 28/28] docs: update info on Cargo workspace object --- docs/markdown/Rust-module.md | 7 +++++ docs/markdown/Rust.md | 28 +++++++++++++++---- .../snippets/cargo-workspace-object.md | 5 ++-- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/docs/markdown/Rust-module.md b/docs/markdown/Rust-module.md index 040da3aed105..61f9284df5d9 100644 --- a/docs/markdown/Rust-module.md +++ b/docs/markdown/Rust-module.md @@ -335,6 +335,13 @@ Returns all defined features for a specific package or subproject. ### Packages only +Package objects are able to extract information from `Cargo.toml` files, +and provide methods to query how Cargo would build this package. They +also contain convenience wrappers for non-Rust-specific functions +(`executable`, `library`, `meson.override_dependency`, etc.), that +automatically add dependencies and compiler arguments from `Cargo.toml` +information. + #### package.rust_args() ```meson diff --git a/docs/markdown/Rust.md b/docs/markdown/Rust.md index 8f4b736333a2..22103687965a 100644 --- a/docs/markdown/Rust.md +++ b/docs/markdown/Rust.md @@ -133,13 +133,29 @@ The workspace object also enables configuration of Cargo features, for example from Meson options: ```meson -ws = rust.workspace( +cargo = rust.workspace( features: ['feature1', 'feature2']) ``` -### Limitations +Finally, the workspace object is able to build targets specified in `[lib]` +or `[[bins]]` sections, extracting compiler arguments for dependencies and +diagnostics from the Cargo.toml file. The simplest case is that of building +a simple binary crate: -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. +```meson +cargo.package().executable(install: true) +``` + +Or for a workspace: + +```meson +cargo.package('myproject-lib').library(install: false) +cargo.package().executable(install: true) +``` + +Sources are automatically discovered, but can be specified as a +[[@structured_src]] if they are partly generated. + +It is still possible to use keyword arguments to link non-Rust build targets, +or even to use the usual Meson functions such as [[static_library]] or +[[executable]]. diff --git a/docs/markdown/snippets/cargo-workspace-object.md b/docs/markdown/snippets/cargo-workspace-object.md index c6bc4d4bac25..1ef767529bf8 100644 --- a/docs/markdown/snippets/cargo-workspace-object.md +++ b/docs/markdown/snippets/cargo-workspace-object.md @@ -6,8 +6,9 @@ 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. +The returned object allows retrieving features and dependencies +for Cargo subprojects, and contains method to build targets +declared in `Cargo.toml` files. While Cargo subprojects remain experimental, the Meson project will try to keep the workspace object reasonably backwards-compatible.