Skip to content
Draft
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
30 changes: 30 additions & 0 deletions docs/markdown/Rust-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,33 @@ 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.
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
89 changes: 77 additions & 12 deletions mesonbuild/cargo/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
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

Expand Down Expand Up @@ -55,7 +55,9 @@ 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
Expand All @@ -80,6 +82,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 +94,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 +103,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 +239,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 @@ -287,6 +338,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 Down Expand Up @@ -332,21 +387,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
65 changes: 63 additions & 2 deletions mesonbuild/modules/rust.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from mesonbuild.interpreterbase.decorators import FeatureNew

from . import ExtensionModule, ModuleReturnValue, ModuleInfo
from . import ExtensionModule, ModuleReturnValue, ModuleInfo, MutableModuleObject
from .. import mesonlib, mlog
from ..build import (BothLibraries, BuildTarget, CustomTargetIndex, Executable, ExtractedObjects, GeneratedList,
CustomTarget, InvalidArguments, Jar, StructuredSources, SharedLibrary, StaticLibrary)
Expand All @@ -21,12 +21,13 @@
)
from ..interpreterbase import ContainerTypeInfo, InterpreterException, KwargInfo, typed_kwargs, typed_pos_args, noPosargs, permittedKwargs
from ..interpreter.interpreterobjects import Doctest
from ..mesonlib import File, MesonException, PerMachine
from ..mesonlib import File, MachineChoice, MesonException, PerMachine
from ..programs import ExternalProgram, NonExistingExternalProgram

if T.TYPE_CHECKING:
from . import ModuleState
from ..build import BuildTargetTypes, ExecutableKeywordArguments, IncludeDirs, LibTypes
from .. import cargo
from ..compilers.rust import RustCompiler
from ..dependencies import Dependency, ExternalLibrary
from ..interpreter import Interpreter
Expand Down Expand Up @@ -63,6 +64,10 @@ 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]
dev_dependencies: bool

RUST_TEST_KWS: T.List[KwargInfo] = [
KwargInfo(
Expand All @@ -81,13 +86,24 @@ def no_spaces_validator(arg: T.Optional[T.Union[str, T.List]]) -> T.Optional[str
return None


class RustWorkspace(MutableModuleObject):
"""Represents a Rust workspace, controlling the build of packages
recorded in a Cargo.lock file."""

def __init__(self, interpreter: Interpreter, root_package: T.Union[cargo.WorkspaceState, cargo.PackageState]) -> None:
super().__init__()
self.interpreter = interpreter
self.root_package = root_package


class RustModule(ExtensionModule):

"""A module that holds helper functions for rust."""

INFO = ModuleInfo('rust', '0.57.0', stabilized='1.0.0')
_bindgen_rust_target: T.Optional[str]
rustdoc: PerMachine[T.Optional[ExternalProgram]] = PerMachine(None, None)
_workspace_cache: T.Dict[cargo.Interpreter, RustWorkspace] = {}

def __init__(self, interpreter: Interpreter) -> None:
super().__init__(interpreter)
Expand All @@ -103,6 +119,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]:
Expand Down Expand Up @@ -500,6 +517,50 @@ def proc_macro(self, state: ModuleState, args: T.Tuple[str, SourcesVarargsType],
target = state._interpreter.build_target(state.current_node, args, kwargs, SharedLibrary)
return target

@noPosargs
@typed_kwargs(
'rust.workspace',
KwargInfo('default_features', (bool, NoneType), default=None),
KwargInfo('dev_dependencies', (bool), default=True),
KwargInfo(
'features',
(ContainerTypeInfo(list, str), NoneType),
default=None,
listify=True,
),
)
def workspace(self, state: ModuleState, args: T.List, kwargs: FuncWorkspace) -> RustWorkspace:
"""Creates a Rust workspace object, controlling the build of
all the packages in a Cargo.lock file."""
if self.interpreter.cargo is None:
raise MesonException("rust.workspace() requires a Cargo project (Cargo.toml and Cargo.lock)")

self.interpreter.add_languages(['rust'], True, MachineChoice.HOST)
self.interpreter.add_languages(['rust'], True, MachineChoice.BUILD)

default_features = kwargs['default_features']
features = kwargs['features']
if default_features is not None or features is not None:
# If custom features are provided, default_features = None should be treated as True
if default_features is None:
default_features = True

cargo_features = ['default'] if default_features else []
if features is not None:
cargo_features.extend(features)
self.interpreter.cargo.features = cargo_features
self.interpreter.cargo.dev_dependencies = kwargs['dev_dependencies']

# Check if we already have a cached workspace for this cargo interpreter
# TODO: this should be per-subproject
ws_obj = self._workspace_cache.get(self.interpreter.cargo)
if ws_obj is None:
root_pkg = self.interpreter.cargo.load_package()
ws_obj = RustWorkspace(self.interpreter, root_pkg)
self._workspace_cache[self.interpreter.cargo] = ws_obj

return ws_obj


def initialize(interp: Interpreter) -> RustModule:
return RustModule(interp)
Loading