Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions docs/markdown/Rust-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ authors:
- name: Dylan Baker
email: [email protected]
years: [2020, 2021, 2022, 2024]
- name: Paolo Bonzini
email: [email protected]
years: [2025]
...

# Rust module
Expand Down Expand Up @@ -168,3 +171,76 @@ Only a subset of [[shared_library]] keyword arguments are allowed:
- link_depends
- link_with
- override_options

### workspace()

```meson
rustmod.workspace(...)
```

*Since 1.10.0*

Create and return a `workspace` object for managing the project's Cargo
workspace.

Keyword arguments:
- `default_features`: (`bool`, optional) Whether to enable default features.
If not specified and `features` is provided, defaults to true.
- `features`: (`list[str]`, optional) List of additional features to enable globally

The function must be called in a project with `Cargo.lock` and `Cargo.toml`
files in the root source directory. While the object currently has
no methods, upon its creation Meson analyzes the `Cargo.toml` file and
computes the full set of dependencies and features needed to build the
package in `Cargo.toml`. Therefore, this function should be invoked before
using Cargo subprojects. Methods will be added in future versions of Meson.

If either argument is provided, the build will use a custom set of features.
Features can only be set once - subsequent calls will fail if different features
are specified.

When `features` is provided without `default_features`, the 'default' feature is
automatically included.

#### workspace.subproject()

```meson
package = ws.subproject(package_name, ...)
```

Returns a `package` object for managing a specific package within the workspace.

Positional arguments:
- `package_name`: (`str`) The name of the package to retrieve

Keyword arguments:
- `version`: (`list[str]`, optional) List of version constraints for the package

## Package object

The package object returned by `workspace.subproject()` provides methods for working with individual packages in a Cargo workspace.

#### package.features()

```meson
features = pkg.features()
```

Returns selected features for a specific package.

#### package.all_features()

```meson
all_features = pkg.all_features()
```

### package.dependency()

```meson
dep = package.dependency(...)
```

Returns a dependency object for the package that can be used with other Meson targets.

Keyword arguments:
- `rust_abi`: (`str`, optional) The ABI to use for the dependency. Valid values are `'rust'` (default), `'c'`, or `'proc-macro'`. The package must support the specified ABI.
4 changes: 3 additions & 1 deletion mesonbuild/cargo/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
__all__ = [
'Interpreter',
'PackageState',
'TomlImplementationMissing',
'WorkspaceState',
]

from .interpreter import Interpreter
from .interpreter import Interpreter, PackageState, WorkspaceState
from .toml import TomlImplementationMissing
156 changes: 131 additions & 25 deletions mesonbuild/cargo/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,22 @@
from . import builder, version, cfg
from .toml import load_toml
from .manifest import Manifest, CargoLock, Workspace, fixup_meson_varname
from ..mesonlib import MesonException, MachineChoice, version_compare
from ..mesonlib import MesonException, MachineChoice, unique_list, version_compare
from .. import coredata, mlog
from ..wrap.wrap import PackageDefinition

if T.TYPE_CHECKING:
from . import raw
from .. import mparser
from typing_extensions import Literal

from .manifest import Dependency, SystemDependency
from ..environment import Environment
from ..interpreterbase import SubProject
from ..compilers.rust import RustCompiler

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 package_name.endswith(suffix) else package_name
return f'{basename}-{api}{suffix}'
Expand All @@ -55,11 +59,41 @@ class PackageState:
downloaded: bool = False
features: T.Set[str] = dataclasses.field(default_factory=set)
required_deps: T.Set[str] = dataclasses.field(default_factory=set)
visited_deps: T.Set[str] = dataclasses.field(default_factory=set)
optional_deps_features: T.Dict[str, T.Set[str]] = dataclasses.field(default_factory=lambda: collections.defaultdict(set))
dev_dependencies: bool = False
# If this package is member of a workspace.
ws_subdir: T.Optional[str] = None
ws_member: T.Optional[str] = None

def uses_abi(self, abi: str) -> bool:
"""Check if this package supports the given ABI."""
crate_types = self.manifest.lib.crate_type
if abi == 'rust':
return any(ct in {'lib', 'rlib'} for ct in crate_types)
elif abi == 'c':
return any(ct in {'staticlib', 'cdylib'} for ct in crate_types)
elif abi == 'proc-macro':
return 'proc-macro' in crate_types
else:
return False

def get_dependency_name(self, rust_abi: RUST_ABI) -> str:
"""Get the dependency name for a package with the given ABI."""
if not self.uses_abi(rust_abi):
raise MesonException(f'Package {self.manifest.package.name} does not support ABI {rust_abi}')

# TODO: Implement dependency name generation
package_name = self.manifest.package.name
api = self.manifest.package.api

if rust_abi in {'rust', 'proc-macro'}:
return f'{package_name}-{api}-rs'
elif rust_abi == 'c':
return f'{package_name}-{api}'
else:
raise MesonException(f'Unknown rust_abi: {rust_abi}')


@dataclasses.dataclass(frozen=True)
class PackageKey:
Expand All @@ -80,6 +114,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
# Map Cargo.toml's subdir to loaded manifest.
Expand All @@ -90,6 +126,7 @@ def __init__(self, env: Environment, subdir: str, subprojects_dir: str) -> None:
self.workspaces: T.Dict[str, WorkspaceState] = {}
# Files that should trigger a reconfigure if modified
self.build_def_files: T.List[str] = []
self.dev_dependencies = False
# 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)
Expand All @@ -98,9 +135,54 @@ def __init__(self, env: Environment, subdir: str, subprojects_dir: str) -> None:
self.environment.wrap_resolver.merge_wraps(self.cargolock.wraps)
self.build_def_files.append(filename)

@property
def features(self) -> T.List[str]:
"""Get the features list. Once read, it cannot be modified."""
if self._features is None:
self._features = ['default']
return self._features

@features.setter
def features(self, value: T.List[str]) -> None:
"""Set the features list. Can only be set before first read."""
value_unique = sorted(unique_list(value))
if self._features is not None and value_unique != self._features:
raise MesonException("Cannot modify features after they have been selected or used")
self._features = value_unique

def get_build_def_files(self) -> T.List[str]:
return self.build_def_files

def load_package(self, path: str = '.') -> T.Union[WorkspaceState, PackageState]:
"""Load the root Cargo.toml package and prepare it with features and dependencies."""
pkgs: T.Iterable[PackageState]
ret: T.Union[WorkspaceState, PackageState]
if path == '.':
manifest = self._load_manifest(path)
if isinstance(manifest, Workspace):
ret = self._get_workspace(manifest, path)
pkgs = list(ret.packages[m] for m in ret.workspace.default_members)
else:
key = PackageKey(manifest.package.name, manifest.package.api)
if key not in self.packages:
self.packages[key] = PackageState(manifest, False)
ret = self.packages[key]
pkgs = [ret]
else:
ws = self.workspaces['.']
ret = ws.packages[path]
pkgs = [ret]

if self.dev_dependencies:
for pkg in pkgs:
pkg.dev_dependencies = True

for pkg in pkgs:
self._prepare_package(pkg)
for feature in self.features:
self._enable_feature(pkg, feature)
return ret

def interpret(self, subdir: str, project_root: T.Optional[str] = None) -> mparser.CodeBlockNode:
manifest = self._load_manifest(subdir)
filename = os.path.join(self.environment.source_dir, subdir, 'Cargo.toml')
Expand Down Expand Up @@ -189,6 +271,7 @@ def _process_member(member: str) -> None:
return build.block(ast)

def _load_workspace_member(self, ws: WorkspaceState, m: str) -> None:
print(m)
m = os.path.normpath(m)
# Load member's manifest
m_subdir = os.path.join(ws.subdir, m)
Expand Down Expand Up @@ -231,6 +314,26 @@ def _fetch_package(self, package_name: str, api: str) -> PackageState:
meson_depname = _dependency_name(package_name, api)
return self._fetch_package_from_subproject(package_name, meson_depname)

def _resolve_package(self, package_name: str, version_constraints: T.List[str]) -> T.Optional[raw.CargoLockPackage]:
"""From all available versions from Cargo.lock, pick the most recent
satisfying the constraints and return it."""
if self.cargolock:
cargo_lock_pkgs = self.cargolock.named(package_name)
else:
cargo_lock_pkgs = []
for cargo_pkg in cargo_lock_pkgs:
if all(version_compare(cargo_pkg.version, v) for v in version_constraints):
return cargo_pkg

if not version_constraints:
raise MesonException(f'Cannot determine version of cargo package {package_name}')
return None

def resolve_package(self, package_name: str, version_constraints: T.List[str]) -> T.Optional[PackageState]:
cargo_pkg = self._resolve_package(package_name, version_constraints)
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:
Expand Down Expand Up @@ -287,6 +390,10 @@ def _prepare_package(self, pkg: PackageState) -> None:
for depname, dep in pkg.manifest.dependencies.items():
if not dep.optional:
self._add_dependency(pkg, depname)
if pkg.dev_dependencies:
for depname, dep in pkg.manifest.dev_dependencies.items():
if not dep.optional:
self._add_dependency(pkg, depname)

def _dep_package(self, pkg: PackageState, dep: Dependency) -> PackageState:
if dep.path:
Expand All @@ -300,19 +407,8 @@ def _dep_package(self, pkg: PackageState, dep: Dependency) -> PackageState:
_, _, directory = _parse_git_url(dep.git, dep.branch)
dep_pkg = self._fetch_package_from_subproject(dep.package, directory)
else:
# From all available versions from Cargo.lock, pick the most recent
# satisfying the constraints
if self.cargolock:
cargo_lock_pkgs = self.cargolock.named(dep.package)
else:
cargo_lock_pkgs = []
for cargo_pkg in cargo_lock_pkgs:
if all(version_compare(cargo_pkg.version, v) for v in dep.meson_version):
dep.update_version(f'={cargo_pkg.version}')
break
else:
if not dep.meson_version:
raise MesonException(f'Cannot determine version of cargo package {dep.package}')
cargo_pkg = self._resolve_package(dep.package, dep.meson_version)
dep.update_version(f'={cargo_pkg.version}')
dep_pkg = self._fetch_package(dep.package, dep.api)
return dep_pkg

Expand All @@ -332,21 +428,31 @@ def _load_manifest(self, subdir: str, workspace: T.Optional[Workspace] = None, m
self.manifests[subdir] = manifest_
return manifest_

def _add_dependency(self, pkg: PackageState, depname: str) -> None:
if depname in pkg.required_deps:
return
dep = pkg.manifest.dependencies.get(depname)
if not dep:
# It could be build/dev/target dependency. Just ignore it.
return
pkg.required_deps.add(depname)
dep_pkg = self._dep_package(pkg, dep)
def _add_dependency_features(self, pkg: PackageState, dep: Dependency, dep_pkg: PackageState) -> None:
if dep.default_features:
self._enable_feature(dep_pkg, 'default')
for f in dep.features:
self._enable_feature(dep_pkg, f)
for f in pkg.optional_deps_features[depname]:
self._enable_feature(dep_pkg, f)

def _add_dependency(self, pkg: PackageState, depname: str) -> None:
if depname in pkg.visited_deps:
return
pkg.visited_deps.add(depname)

dep_pkg = None
if pkg.dev_dependencies:
dep = pkg.manifest.dev_dependencies.get(depname)
if dep:
dep_pkg = self._dep_package(pkg, dep)
self._add_dependency_features(pkg, dep, dep_pkg)
dep = pkg.manifest.dependencies.get(depname)
if dep:
dep_pkg = dep_pkg or self._dep_package(pkg, dep)
self._add_dependency_features(pkg, dep, dep_pkg)
if dep_pkg is not None:
pkg.required_deps.add(depname)
for f in pkg.optional_deps_features[depname]:
self._enable_feature(dep_pkg, f)

def _enable_feature(self, pkg: PackageState, feature: str) -> None:
if feature in pkg.features:
Expand Down
10 changes: 9 additions & 1 deletion mesonbuild/modules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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}
Expand Down
Loading
Loading