Skip to content

Instantly share code, notes, and snippets.

@bonzini
Last active June 6, 2025 16:50
Show Gist options
  • Save bonzini/4552ee5636a956ee365f29be76ffd1b1 to your computer and use it in GitHub Desktop.
Save bonzini/4552ee5636a956ee365f29be76ffd1b1 to your computer and use it in GitHub Desktop.
ws.diff
commit 184ef2eeaa6d5fadf98eba99b32bd6695467bc1b
Author: Paolo Bonzini <[email protected]>
Date: Fri Jun 6 09:05:55 2025 +0200
wip
diff --git a/mesonbuild/cargo/interpreter.py b/mesonbuild/cargo/interpreter.py
index 933d1c603..af652a2f1 100644
--- a/mesonbuild/cargo/interpreter.py
+++ b/mesonbuild/cargo/interpreter.py
@@ -14,13 +14,12 @@ import dataclasses
import os
import collections
import urllib.parse
-import itertools
-import re
import typing as T
+from functools import lru_cache
from . import builder, version, cfg
from .toml import load_toml, TomlImplementationMissing
-from .manifest import Manifest, CargoLock, fixup_meson_varname
+from .manifest import Manifest, CargoLock, Workspace, fixup_meson_varname
from ..mesonlib import MesonException, MachineChoice
from .. import coredata, mlog
from ..wrap.wrap import PackageDefinition
@@ -29,13 +28,15 @@ if T.TYPE_CHECKING:
from . import raw
from .. import mparser
from .manifest import Dependency, SystemDependency
+ from .raw import CRATE_TYPE
from ..environment import Environment
from ..interpreterbase import SubProject
from ..compilers.rust import RustCompiler
-def _dependency_name(package_name: str, api: str) -> str:
- basename = package_name[:-3] if package_name.endswith('-rs') else package_name
- return f'{basename}-{api}-rs'
+
+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}'
def _dependency_varname(package_name: str) -> str:
@@ -50,13 +51,16 @@ def _extra_deps_varname() -> str:
return 'extra_deps'
[email protected]
class PackageState:
- def __init__(self, manifest: Manifest, downloaded: bool) -> None:
- self.manifest = manifest
- self.downloaded = downloaded
- self.features: T.Set[str] = set()
- self.required_deps: T.Set[str] = set()
- self.optional_deps_features: T.Dict[str, T.Set[str]] = collections.defaultdict(set)
+ manifest: Manifest
+ downloaded: bool = False
+ features: T.Set[str] = dataclasses.field(default_factory=set)
+ required_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))
+ # If this package is member of a workspace.
+ ws_subdir: T.Optional[str] = None
+ ws_member: T.Optional[str] = None
@dataclasses.dataclass(frozen=True)
@@ -65,31 +69,61 @@ class PackageKey:
api: str
[email protected]
+class WorkspaceState:
+ workspace: Workspace
+ # member path -> PackageState, for all members of this workspace
+ packages: T.Dict[str, PackageState] = dataclasses.field(default_factory=dict)
+ # package name to member path, for all members of this workspace
+ packages_to_member: T.Dict[str, str] = dataclasses.field(default_factory=dict)
+ # member paths that are required to be built
+ required_members: T.List[str] = dataclasses.field(default_factory=list)
+
+
class Interpreter:
def __init__(self, env: Environment) -> None:
self.environment = env
self.host_rustc = T.cast('RustCompiler', self.environment.coredata.compilers[MachineChoice.HOST]['rust'])
# Map Cargo.toml's subdir to loaded manifest.
- self.manifests: T.Dict[str, Manifest] = {}
+ self.manifests: T.Dict[str, T.Union[Manifest, Workspace]] = {}
# Map of cargo package (name + api) to its state
self.packages: T.Dict[PackageKey, PackageState] = {}
+ # Map subdir to workspace
+ self.workspaces: T.Dict[str, WorkspaceState] = {}
# Rustc's config
self.cfgs = self._get_cfgs()
- def interpret(self, subdir: str) -> mparser.CodeBlockNode:
+ def interpret(self, subdir: str, project_root: T.Optional[str] = None) -> mparser.CodeBlockNode:
manifest = self._load_manifest(subdir)
- pkg, cached = self._fetch_package(manifest.package.name, manifest.package.api)
- if not cached:
- # This is an entry point, always enable the 'default' feature.
- # FIXME: We should have a Meson option similar to `cargo build --no-default-features`
- self._enable_feature(pkg, 'default')
-
- # Build an AST for this package
filename = os.path.join(self.environment.source_dir, subdir, 'Cargo.toml')
build = builder.Builder(filename)
- ast = self._create_project(pkg, build)
- ast += [
- build.assign(build.function('import', [build.string('rust')]), 'rust'),
+ if isinstance(manifest, Workspace):
+ return self.interpret_workspace(manifest, build, subdir, project_root)
+ return self.interpret_package(manifest, build, subdir, project_root)
+
+ def interpret_package(self, manifest: Manifest, build: builder.Builder, subdir: str, project_root: T.Optional[str]) -> mparser.CodeBlockNode:
+ if project_root:
+ ws = self.workspaces[project_root]
+ member = ws.packages_to_member[manifest.package.name]
+ pkg = ws.packages[member]
+ else:
+ pkg, cached = self._fetch_package(manifest.package.name, manifest.package.api)
+ assert isinstance(pkg, PackageState)
+ if not cached:
+ # This is an entry point, always enable the 'default' feature.
+ # FIXME: We should have a Meson option similar to `cargo build --no-default-features`
+ self._enable_feature(pkg, 'default')
+
+ # Build an AST for this package
+ ast: T.List[mparser.BaseNode] = []
+ if not project_root:
+ ast += self._create_project(pkg, build)
+ ast.append(build.assign(build.function('import', [build.string('rust')]), 'rust'))
+ ast += self._create_package(pkg, build, subdir)
+ return build.block(ast)
+
+ def _create_package(self, pkg: PackageState, build: builder.Builder, subdir: str) -> T.List[mparser.BaseNode]:
+ ast: T.List[mparser.BaseNode] = [
build.function('message', [
build.string('Enabled features:'),
build.array([build.string(f) for f in pkg.features]),
@@ -98,52 +132,169 @@ class Interpreter:
ast += self._create_dependencies(pkg, build)
ast += self._create_meson_subdir(build)
- if pkg.manifest.lib:
+ # Libs are always auto-discovered and there's no other way to handle them,
+ # which is unfortunate for reproducability
+ if os.path.exists(os.path.join(self.environment.source_dir, subdir, pkg.manifest.lib.path)):
for crate_type in pkg.manifest.lib.crate_type:
- ast.extend(self._create_lib(pkg, build, crate_type))
+ ast += self._create_lib(pkg, build, crate_type)
+ return ast
+
+ def interpret_workspace(self, workspace: Workspace, build: builder.Builder, subdir: str, project_root: T.Optional[str]) -> mparser.CodeBlockNode:
+ ws = self._get_workspace(workspace, subdir)
+ name = os.path.dirname(subdir)
+ subprojects_dir = os.path.join(subdir, 'subprojects')
+ self.environment.wrap_resolver.load_and_merge(subprojects_dir, T.cast('SubProject', name))
+ ast: T.List[mparser.BaseNode] = []
+ if not project_root:
+ args: T.List[mparser.BaseNode] = [
+ build.string(name),
+ build.string('rust'),
+ ]
+ kwargs: T.Dict[str, mparser.BaseNode] = {
+ 'meson_version': build.string(f'>= {coredata.stable_version}'),
+ }
+ ast += [
+ build.function('project', args, kwargs),
+ build.assign(build.function('import', [build.string('rust')]), 'rust'),
+ ]
+ if not ws.required_members:
+ for member in ws.workspace.default_members:
+ self._add_workspace_member(ws, member)
+
+ # Call subdir() for each required member of the workspace. The order is
+ # important, if a member depends on another member, that member must be
+ # processed first.
+ processed_members: T.Set[str] = set()
+ def _process_member(member: str) -> None:
+ if member in processed_members:
+ return
+ pkg = ws.packages[member]
+ for depname in pkg.required_deps:
+ dep = pkg.manifest.dependencies[depname]
+ if dep.path:
+ dep_member = os.path.normpath(os.path.join(pkg.ws_member, dep.path))
+ _process_member(dep_member)
+ if member == '.':
+ ast.extend(self._create_package(pkg, build, subdir))
+ else:
+ ast.append(build.function('subdir', [build.string(member)]))
+ processed_members.add(member)
+ for member in ws.required_members:
+ _process_member(member)
return build.block(ast)
+ def _get_workspace(self, workspace: Workspace, subdir: str) -> WorkspaceState:
+ ws = self.workspaces.get(subdir)
+ if ws:
+ return ws
+ ws = WorkspaceState(workspace)
+ all_members = set(workspace.members)
+ all_members.update(d.path for d in workspace.dependencies.values() if d.path)
+ all_members.difference_update(workspace.exclude)
+ for m in all_members:
+ m = os.path.normpath(m)
+ # Load member's manifest
+ m_subdir = os.path.join(subdir, m)
+ manifest_ = self._load_manifest(m_subdir, ws.workspace.ws_raw, m)
+ assert isinstance(manifest_, Manifest)
+ pkg = PackageState(manifest_, ws_subdir=subdir, ws_member=m)
+ ws.packages[m] = pkg
+ ws.packages_to_member[manifest_.package.name] = m
+ if workspace.root_package:
+ m = '.'
+ pkg = PackageState(workspace.root_package, ws_subdir=subdir, ws_member=m)
+ ws.packages[m] = pkg
+ ws.packages_to_member[workspace.root_package.package.name] = m
+ self.workspaces[subdir] = ws
+ return ws
+
+ def _add_workspace_member(self, ws: WorkspaceState, member: str) -> PackageState:
+ pkg = ws.packages[member]
+ if member not in ws.required_members:
+ self._prepare_package(pkg)
+ ws.required_members.append(member)
+ return pkg
+
def _fetch_package(self, package_name: str, api: str) -> T.Tuple[PackageState, bool]:
key = PackageKey(package_name, api)
pkg = self.packages.get(key)
if pkg:
return pkg, True
meson_depname = _dependency_name(package_name, api)
- subdir, _ = self.environment.wrap_resolver.resolve(meson_depname)
+ return self._fetch_package_from_subproject(package_name, meson_depname)
+
+ @lru_cache(maxsize=None)
+ def _fetch_package_from_subproject(self, package_name: str, meson_depname: str) -> T.Tuple[PackageState, bool]:
+ subp_name, _ = self.environment.wrap_resolver.find_dep_provider(meson_depname)
+ subdir, _ = self.environment.wrap_resolver.resolve(subp_name)
subprojects_dir = os.path.join(subdir, 'subprojects')
- self.environment.wrap_resolver.load_and_merge(subprojects_dir, T.cast('SubProject', meson_depname))
+ self.environment.wrap_resolver.load_and_merge(subprojects_dir, T.cast('SubProject', subp_name))
manifest = self._load_manifest(subdir)
downloaded = \
- meson_depname in self.environment.wrap_resolver.wraps and \
- self.environment.wrap_resolver.wraps[meson_depname].type is not None
+ subp_name in self.environment.wrap_resolver.wraps and \
+ self.environment.wrap_resolver.wraps[subp_name].type is not None
+ if isinstance(manifest, Workspace):
+ ws = self._get_workspace(manifest, subdir)
+ member = ws.packages_to_member[package_name]
+ pkg = self._add_workspace_member(ws, member)
+ return pkg, False
+ key = PackageKey(package_name, version.api(manifest.package.version))
+ pkg = self.packages.get(key)
+ if pkg:
+ return pkg, True
pkg = PackageState(manifest, downloaded)
self.packages[key] = pkg
- # Merge target specific dependencies that are enabled
- for condition, dependencies in manifest.target.items():
- if cfg.eval_cfg(condition, self.cfgs):
- manifest.dependencies.update(dependencies)
- # Fetch required dependencies recursively.
- for depname, dep in manifest.dependencies.items():
- if not dep.optional:
- self._add_dependency(pkg, depname)
+ self._prepare_package(pkg)
return pkg, False
- def _dep_package(self, dep: Dependency) -> PackageState:
- return self.packages[PackageKey(dep.package, dep.api)]
+ def _prepare_package(self, pkg: PackageState) -> None:
+ # Merge target specific dependencies that are enabled
+ for condition, dependencies in pkg.manifest.target.items():
+ if cfg.eval_cfg(condition, self.cfgs):
+ pkg.manifest.dependencies.update(dependencies)
+ # Fetch required dependencies recursively.
+ for depname, dep in pkg.manifest.dependencies.items():
+ if not dep.optional:
+ self._add_dependency(pkg, depname)
- def _load_manifest(self, subdir: str) -> Manifest:
+ def _dep_package(self, pkg: PackageState, dep: Dependency) -> PackageState:
+ if dep.path:
+ ws = self.workspaces[pkg.ws_subdir]
+ dep_member = os.path.normpath(os.path.join(pkg.ws_member, dep.path))
+ dep_pkg = self._add_workspace_member(ws, dep_member)
+ elif dep.git:
+ _, _, directory = _parse_git_url(dep.git, dep.branch)
+ dep_pkg, _ = self._fetch_package_from_subproject(dep.package, directory)
+ elif dep.version:
+ dep_pkg, _ = self._fetch_package(dep.package, dep.api)
+ else:
+ raise MesonException(f'Cannot determine version of cargo package {dep.package}')
+ return dep_pkg
+
+ def _load_manifest(self, subdir: str, workspace: T.Optional[raw.Workspace] = None, member_path: str = '') -> T.Union[Manifest, Workspace]:
manifest_ = self.manifests.get(subdir)
if not manifest_:
path = os.path.join(self.environment.source_dir, subdir)
filename = os.path.join(path, 'Cargo.toml')
toml = load_toml(filename)
+ workspace_ = None
+ if 'workspace' in toml:
+ raw_workspace = T.cast('raw.VirtualManifest', toml)
+ workspace_ = Workspace.from_raw(raw_workspace)
+ manifest_ = None
if 'package' in toml:
raw_manifest = T.cast('raw.Manifest', toml)
- manifest_ = Manifest.from_raw(raw_manifest, path)
- self.manifests[subdir] = manifest_
- else:
- raise MesonException(f'{subdir}/Cargo.toml does not have [package] section')
+ manifest_ = Manifest.from_raw(raw_manifest, path, workspace, member_path)
+ if not manifest_ and not workspace_:
+ raise MesonException(f'{subdir}/Cargo.toml does not have [package] or [workspace] section')
+
+ if workspace_:
+ workspace_.root_package = manifest_
+ self.manifests[subdir] = workspace_
+ return workspace_
+
+ self.manifests[subdir] = manifest_
return manifest_
def _add_dependency(self, pkg: PackageState, depname: str) -> None:
@@ -151,22 +302,10 @@ class Interpreter:
return
dep = pkg.manifest.dependencies.get(depname)
if not dep:
- if depname in itertools.chain(pkg.manifest.dev_dependencies, pkg.manifest.build_dependencies):
- # FIXME: Not supported yet
- return
- raise MesonException(f'Dependency {depname} not defined in {pkg.manifest.package.name} manifest')
+ # It could be build/dev/target dependency. Just ignore it.
+ return
+ dep_pkg = self._dep_package(pkg, dep)
pkg.required_deps.add(depname)
- if not dep.version:
- # The manifest did not specify a version, but we probably have one
- # from Cargo.lock. See if any of our subprojects matches.
- for subp_name in self.environment.wrap_resolver.wraps.keys():
- m = re.fullmatch(rf'{dep.package}-([\d\.]+)-rs', subp_name)
- if m is not None:
- dep.update_version(m[1])
- break
- else:
- raise MesonException(f'Cannot determine version of cargo package {depname}')
- dep_pkg, _ = self._fetch_package(dep.package, dep.api)
if dep.default_features:
self._enable_feature(dep_pkg, 'default')
for f in dep.features:
@@ -190,7 +329,7 @@ class Interpreter:
depname = depname[:-1]
if depname in pkg.required_deps:
dep = pkg.manifest.dependencies[depname]
- dep_pkg = self._dep_package(dep)
+ dep_pkg = self._dep_package(pkg, dep)
self._enable_feature(dep_pkg, dep_f)
else:
# This feature will be enabled only if that dependency
@@ -200,7 +339,7 @@ class Interpreter:
self._add_dependency(pkg, depname)
dep = pkg.manifest.dependencies.get(depname)
if dep:
- dep_pkg = self._dep_package(dep)
+ dep_pkg = self._dep_package(pkg, dep)
self._enable_feature(dep_pkg, dep_f)
elif f.startswith('dep:'):
self._add_dependency(pkg, f[4:])
@@ -261,7 +400,8 @@ class Interpreter:
ast: T.List[mparser.BaseNode] = []
for depname in pkg.required_deps:
dep = pkg.manifest.dependencies[depname]
- ast += self._create_dependency(dep, build)
+ dep_pkg = self._dep_package(pkg, dep)
+ 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(pkg.features):
@@ -295,10 +435,11 @@ class Interpreter:
),
]
- def _create_dependency(self, dep: Dependency, build: builder.Builder) -> T.List[mparser.BaseNode]:
- pkg = self._dep_package(dep)
+ def _create_dependency(self, pkg: PackageState, dep: Dependency, build: builder.Builder) -> T.List[mparser.BaseNode]:
+ version_ = dep.version or [pkg.manifest.package.version]
+ api = dep.api or pkg.manifest.package.api
kw = {
- 'version': build.array([build.string(s) for s in dep.version]),
+ 'version': build.array([build.string(s) for s in version_]),
}
# Lookup for this dependency with the features we want in default_options kwarg.
#
@@ -316,7 +457,7 @@ class Interpreter:
build.assign(
build.function(
'dependency',
- [build.string(_dependency_name(dep.package, dep.api))],
+ [build.string(_dependency_name(dep.package, api))],
kw,
),
_dependency_varname(dep.package),
@@ -346,7 +487,7 @@ class Interpreter:
build.if_(build.not_in(build.identifier('f'), build.identifier('actual_features')), build.block([
build.function('error', [
build.string('Dependency'),
- build.string(_dependency_name(dep.package, dep.api)),
+ build.string(_dependency_name(dep.package, api)),
build.string('previously configured with features'),
build.identifier('actual_features'),
build.string('but need'),
@@ -374,14 +515,14 @@ class Interpreter:
build.block([build.function('subdir', [build.string('meson')])]))
]
- def _create_lib(self, pkg: PackageState, build: builder.Builder, crate_type: raw.CRATE_TYPE) -> T.List[mparser.BaseNode]:
+ def _create_lib(self, pkg: PackageState, build: builder.Builder, crate_type: CRATE_TYPE) -> T.List[mparser.BaseNode]:
dependencies: T.List[mparser.BaseNode] = []
dependency_map: T.Dict[mparser.BaseNode, mparser.BaseNode] = {}
for name in pkg.required_deps:
dep = pkg.manifest.dependencies[name]
dependencies.append(build.identifier(_dependency_varname(dep.package)))
if name != dep.package:
- dep_pkg = self._dep_package(dep)
+ dep_pkg = self._dep_package(pkg, dep)
dep_lib_name = dep_pkg.manifest.lib.name
dependency_map[build.string(fixup_meson_varname(dep_lib_name))] = build.string(name)
for name, sys_dep in pkg.manifest.system_dependencies.items():
@@ -407,6 +548,9 @@ class Interpreter:
'rust_args': build.array(rust_args),
}
+ depname_suffix = '-rs' if crate_type in {'lib', 'rlib', 'proc-macro'} else crate_type
+ depname = _dependency_name(pkg.manifest.package.name, pkg.manifest.package.api, depname_suffix)
+
lib: mparser.BaseNode
if pkg.manifest.lib.proc_macro or crate_type == 'proc-macro':
lib = build.method('proc_macro', build.identifier('rust'), posargs, kwargs)
@@ -439,7 +583,8 @@ class Interpreter:
'link_with': build.identifier('lib'),
'variables': build.dict({
build.string('features'): build.string(','.join(pkg.features)),
- })
+ }),
+ 'version': build.string(pkg.manifest.package.version),
},
),
'dep'
@@ -448,28 +593,48 @@ class Interpreter:
'override_dependency',
build.identifier('meson'),
[
- build.string(_dependency_name(pkg.manifest.package.name, pkg.manifest.package.api)),
+ build.string(depname),
build.identifier('dep'),
],
),
]
+def _parse_git_url(url: str, branch: T.Optional[str] = None) -> T.Tuple[str, str, str]:
+ if url.startswith('git+'):
+ url = url[4:]
+ parts = urllib.parse.urlparse(url)
+ query = urllib.parse.parse_qs(parts.query)
+ query_branch = query['branch'][0] if 'branch' in query else ''
+ branch = branch or query_branch
+ revision = parts.fragment or branch
+ directory = os.path.basename(parts.path)
+ if directory.endswith('.git'):
+ directory = directory[:-4]
+ if branch:
+ directory += f'-{branch}'
+ url = urllib.parse.urlunparse(parts._replace(params='', query='', fragment=''))
+ return url, revision, directory
+
+
def load_wraps(source_dir: str, subproject_dir: str) -> T.List[PackageDefinition]:
""" Convert Cargo.lock into a list of wraps """
- wraps: T.List[PackageDefinition] = []
+ # Map directory -> PackageDefinition, to avoid duplicates. Multiple packages
+ # can have the same source URL, in that case we have a single wrap that
+ # provides multiple dependency names.
+ wraps: T.Dict[str, PackageDefinition] = {}
filename = os.path.join(source_dir, 'Cargo.lock')
if os.path.exists(filename):
try:
toml = load_toml(filename)
except TomlImplementationMissing as e:
mlog.warning('Failed to load Cargo.lock:', str(e), fatal=False)
- return wraps
+ return []
raw_cargolock = T.cast('raw.CargoLock', toml)
cargolock = CargoLock.from_raw(raw_cargolock)
for package in cargolock.package:
- subp_name = _dependency_name(package.name, version.api(package.version))
+ meson_depname = _dependency_name(package.name, version.api(package.version))
if package.source is None:
# This is project's package, or one of its workspace members.
pass
@@ -479,25 +644,24 @@ def load_wraps(source_dir: str, subproject_dir: str) -> T.List[PackageDefinition
checksum = cargolock.metadata[f'checksum {package.name} {package.version} ({package.source})']
url = f'https://crates.io/api/v1/crates/{package.name}/{package.version}/download'
directory = f'{package.name}-{package.version}'
- wraps.append(PackageDefinition.from_values(subp_name, subproject_dir, 'file', {
- 'directory': directory,
- 'source_url': url,
- 'source_filename': f'{directory}.tar.gz',
- 'source_hash': checksum,
- 'method': 'cargo',
- }))
+ if directory not in wraps:
+ wraps[directory] = PackageDefinition.from_values(meson_depname, subproject_dir, 'file', {
+ 'directory': directory,
+ 'source_url': url,
+ 'source_filename': f'{directory}.tar.gz',
+ 'source_hash': checksum,
+ 'method': 'cargo',
+ })
+ wraps[directory].add_provided_dep(meson_depname)
elif package.source.startswith('git+'):
- parts = urllib.parse.urlparse(package.source[4:])
- query = urllib.parse.parse_qs(parts.query)
- branch = query['branch'][0] if 'branch' in query else ''
- revision = parts.fragment or branch
- url = urllib.parse.urlunparse(parts._replace(params='', query='', fragment=''))
- wraps.append(PackageDefinition.from_values(subp_name, subproject_dir, 'git', {
- 'directory': package.name,
- 'url': url,
- 'revision': revision,
- 'method': 'cargo',
- }))
+ url, revision, directory = _parse_git_url(package.source)
+ if directory not in wraps:
+ wraps[directory] = PackageDefinition.from_values(directory, subproject_dir, 'git', {
+ 'url': url,
+ 'revision': revision,
+ 'method': 'cargo',
+ })
+ wraps[directory].add_provided_dep(meson_depname)
else:
mlog.warning(f'Unsupported source URL in {filename}: {package.source}')
- return wraps
+ return list(wraps.values())
diff --git a/mesonbuild/cargo/manifest.py b/mesonbuild/cargo/manifest.py
index 50bb5d251..50c0a8f41 100644
--- a/mesonbuild/cargo/manifest.py
+++ b/mesonbuild/cargo/manifest.py
@@ -249,7 +249,7 @@ class Dependency:
elif v.startswith('='):
api.add(version.api(v[1:].strip()))
if not api:
- return '0'
+ return ''
elif len(api) == 1:
return api.pop()
else:
@@ -275,13 +275,6 @@ class Dependency:
raw_dep = _depv_to_dep(raw_depv)
return cls.from_raw_dict(name, raw_dep, member_path, raw_ws_dep)
- def update_version(self, v: str) -> None:
- self.version = version.convert(v)
- try:
- delattr(self, 'api')
- except AttributeError:
- pass
-
@dataclasses.dataclass
class BuildTarget(T.Generic[_R]):
@@ -453,6 +446,37 @@ class Manifest:
return fixed
[email protected]
+class Workspace:
+
+ """Cargo Workspace definition.
+ """
+
+ resolver: str = dataclasses.field(default_factory=lambda: '2')
+ members: T.List[str] = dataclasses.field(default_factory=list)
+ exclude: T.List[str] = dataclasses.field(default_factory=list)
+ default_members: T.List[str] = dataclasses.field(default_factory=list)
+
+ package: T.Optional[raw.Package] = None
+ dependencies: T.Dict[str, Dependency] = dataclasses.field(default_factory=dict)
+ lints: T.Dict[str, T.Any] = dataclasses.field(default_factory=dict)
+ metadata: T.Dict[str, T.Any] = dataclasses.field(default_factory=dict)
+
+ # Keep the raw data for members to inherit from.
+ ws_raw: T.Optional[raw.Workspace] = dataclasses.field(init=False)
+
+ # A workspace can also have a root package.
+ root_package: T.Optional[Manifest] = dataclasses.field(init=False)
+
+ @classmethod
+ def from_raw(cls, raw: raw.VirtualManifest) -> Workspace:
+ ws_raw = raw['workspace']
+ fixed = _raw_mapping_to_attributes(ws_raw, cls, 'Workspace',
+ dependencies=lambda x: {k: Dependency.from_raw(k, v) for k, v in x.items()})
+ fixed.ws_raw = ws_raw
+ return fixed
+
+
@dataclasses.dataclass
class CargoLockPackage:
diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py
index cd3318b45..e7efe2ada 100644
--- a/mesonbuild/interpreter/interpreter.py
+++ b/mesonbuild/interpreter/interpreter.py
@@ -235,6 +235,7 @@ class InterpreterRuleRelaxation(Enum):
'''
ALLOW_BUILD_DIR_FILE_REFERENCES = 1
+ CARGO_SUBDIR = 2
permitted_dependency_kwargs = {
'allow_fallback',
@@ -955,6 +956,19 @@ class Interpreter(InterpreterBase, HoldableObject):
return self.disabled_subproject(subp_name, exception=e)
raise e
+ def _save_ast(self, subdir: str, ast: mparser.CodeBlockNode) -> None:
+ # Debug print the generated meson file
+ from ..ast import AstIndentationGenerator, AstPrinter
+ printer = AstPrinter(update_ast_line_nos=True)
+ ast.accept(AstIndentationGenerator())
+ ast.accept(printer)
+ printer.post_process()
+ meson_filename = os.path.join(self.build.environment.get_build_dir(), subdir, 'meson.build')
+ with open(meson_filename, "w", encoding='utf-8') as f:
+ f.write(printer.result)
+ mlog.log('Generated Meson AST:', meson_filename)
+ mlog.cmd_ci_include(meson_filename)
+
def _do_subproject_meson(self, subp_name: str, subdir: str,
default_options: T.Dict[str, options.ElementaryOptionValues],
kwargs: kwtypes.DoSubproject,
@@ -963,18 +977,7 @@ class Interpreter(InterpreterBase, HoldableObject):
relaxations: T.Optional[T.Set[InterpreterRuleRelaxation]] = None) -> SubprojectHolder:
with mlog.nested(subp_name):
if ast:
- # Debug print the generated meson file
- from ..ast import AstIndentationGenerator, AstPrinter
- printer = AstPrinter(update_ast_line_nos=True)
- ast.accept(AstIndentationGenerator())
- ast.accept(printer)
- printer.post_process()
- meson_filename = os.path.join(self.build.environment.get_build_dir(), subdir, 'meson.build')
- with open(meson_filename, "w", encoding='utf-8') as f:
- f.write(printer.result)
- mlog.log('Generated Meson AST:', meson_filename)
- mlog.cmd_ci_include(meson_filename)
-
+ self._save_ast(subdir, ast)
new_build = self.build.copy()
subi = Interpreter(new_build, self.backend, subp_name, subdir, self.subproject_dir,
default_options, ast=ast, relaxations=relaxations,
@@ -1057,7 +1060,8 @@ class Interpreter(InterpreterBase, HoldableObject):
return self._do_subproject_meson(
subp_name, subdir, default_options, kwargs, ast,
# FIXME: Are there other files used by cargo interpreter?
- [os.path.join(subdir, 'Cargo.toml')])
+ [os.path.join(subdir, 'Cargo.toml')],
+ relaxations={InterpreterRuleRelaxation.CARGO_SUBDIR})
@typed_pos_args('get_option', str)
@noKwargs
@@ -2460,7 +2464,11 @@ class Interpreter(InterpreterBase, HoldableObject):
os.makedirs(os.path.join(self.environment.build_dir, subdir), exist_ok=True)
- if not self._evaluate_subdir(self.environment.get_source_dir(), subdir):
+ if InterpreterRuleRelaxation.CARGO_SUBDIR in self.relaxations:
+ codeblock = self.environment.cargo.interpret(subdir, self.root_subdir)
+ self._save_ast(subdir, codeblock)
+ self._evaluate_codeblock(codeblock, subdir)
+ elif not self._evaluate_subdir(self.environment.get_source_dir(), subdir):
buildfilename = os.path.join(subdir, environment.build_filename)
raise InterpreterException(f"Nonexistent build file '{buildfilename!s}'")
diff --git a/mesonbuild/interpreterbase/interpreterbase.py b/mesonbuild/interpreterbase/interpreterbase.py
index b13bbae1a..9ba596da7 100644
--- a/mesonbuild/interpreterbase/interpreterbase.py
+++ b/mesonbuild/interpreterbase/interpreterbase.py
@@ -733,15 +733,18 @@ class InterpreterBase:
except mesonlib.MesonException as me:
me.file = absname
raise me
+ if visitors:
+ for visitor in visitors:
+ codeblock.accept(visitor)
+ self._evaluate_codeblock(codeblock, subdir)
+ return True
+
+ def _evaluate_codeblock(self, codeblock: mparser.CodeBlockNode, subdir: str) -> None:
try:
prev_subdir = self.subdir
self.subdir = subdir
- if visitors:
- for visitor in visitors:
- codeblock.accept(visitor)
self.evaluate_codeblock(codeblock)
except SubdirDoneRequest:
pass
finally:
self.subdir = prev_subdir
- return True
diff --git a/mesonbuild/wrap/wrap.py b/mesonbuild/wrap/wrap.py
index c8eff6988..404fcb8bd 100644
--- a/mesonbuild/wrap/wrap.py
+++ b/mesonbuild/wrap/wrap.py
@@ -295,6 +295,9 @@ class PackageDefinition:
with open(self.get_hashfile(subproject_directory), 'w', encoding='utf-8') as file:
file.write(self.wrapfile_hash + '\n')
+ def add_provided_dep(self, name: str) -> None:
+ self.provided_deps[name] = None
+
def get_directory(subdir_root: str, packagename: str) -> str:
fname = os.path.join(subdir_root, packagename + '.wrap')
if os.path.isfile(fname):
@@ -331,6 +334,7 @@ class Resolver:
self.wrapdb: T.Dict[str, T.Any] = {}
self.wrapdb_provided_deps: T.Dict[str, str] = {}
self.wrapdb_provided_programs: T.Dict[str, str] = {}
+ self.loaded_dirs: T.Set[str] = set()
self.load_wraps()
self.load_netrc()
self.load_wrapdb()
@@ -372,13 +376,15 @@ class Resolver:
# Add provided deps and programs into our lookup tables
for wrap in self.wraps.values():
self.add_wrap(wrap)
+ self.loaded_dirs.add(self.subdir)
def add_wrap(self, wrap: PackageDefinition) -> None:
for k in wrap.provided_deps.keys():
if k in self.provided_deps:
prev_wrap = self.provided_deps[k]
- m = f'Multiple wrap files provide {k!r} dependency: {wrap.name} and {prev_wrap.name}'
- raise WrapException(m)
+ if prev_wrap.type is not None:
+ m = f'Multiple wrap files provide {k!r} dependency: {wrap.name} and {prev_wrap.name}'
+ raise WrapException(m)
self.provided_deps[k] = wrap
for k in wrap.provided_programs:
if k in self.provided_programs:
@@ -416,16 +422,16 @@ class Resolver:
def _merge_wraps(self, other_resolver: 'Resolver') -> None:
for k, v in other_resolver.wraps.items():
- self.wraps.setdefault(k, v)
- for k, v in other_resolver.provided_deps.items():
- self.provided_deps.setdefault(k, v)
- for k, v in other_resolver.provided_programs.items():
- self.provided_programs.setdefault(k, v)
+ # Allow replacing wraps that were created for a bare directory.
+ if k not in self.wraps or self.wraps[k].type is None:
+ self.wraps[k] = v
+ self.add_wrap(v)
def load_and_merge(self, subdir: str, subproject: SubProject) -> None:
- if self.wrap_mode != WrapMode.nopromote:
+ if self.wrap_mode != WrapMode.nopromote and subdir not in self.loaded_dirs:
other_resolver = Resolver(self.source_dir, subdir, subproject, self.wrap_mode, self.wrap_frontend, self.allow_insecure, self.silent)
self._merge_wraps(other_resolver)
+ self.loaded_dirs.add(subdir)
def find_dep_provider(self, packagename: str) -> T.Tuple[T.Optional[str], T.Optional[str]]:
# Python's ini parser converts all key values to lowercase.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment