Skip to content

Commit 66657f0

Browse files
committed
cargo: support workspaces
Extracted from a patch by Xavier Claessens <xclaessens@netflix.com>
1 parent 5f0d569 commit 66657f0

File tree

2 files changed

+134
-14
lines changed

2 files changed

+134
-14
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
## Support for Cargo workspaces
2+
3+
When parsing `Cargo.toml` files, Meson now recognizes workspaces
4+
and will process all the required members and any requested optional
5+
members of the workspace.

mesonbuild/cargo/interpreter.py

Lines changed: 129 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
from . import builder, version, cfg
2121
from .toml import load_toml
22-
from .manifest import Manifest, CargoLock, fixup_meson_varname
22+
from .manifest import Manifest, CargoLock, Workspace, fixup_meson_varname
2323
from ..mesonlib import MesonException, MachineChoice, version_compare
2424
from .. import coredata, mlog
2525
from ..wrap.wrap import PackageDefinition
@@ -56,6 +56,9 @@ class PackageState:
5656
features: T.Set[str] = dataclasses.field(default_factory=set)
5757
required_deps: T.Set[str] = dataclasses.field(default_factory=set)
5858
optional_deps_features: T.Dict[str, T.Set[str]] = dataclasses.field(default_factory=lambda: collections.defaultdict(set))
59+
# If this package is member of a workspace.
60+
ws_subdir: T.Optional[str] = None
61+
ws_member: T.Optional[str] = None
5962

6063

6164
@dataclasses.dataclass(frozen=True)
@@ -64,13 +67,27 @@ class PackageKey:
6467
api: str
6568

6669

70+
@dataclasses.dataclass
71+
class WorkspaceState:
72+
workspace: Workspace
73+
subdir: str
74+
# member path -> PackageState, for all members of this workspace
75+
packages: T.Dict[str, PackageState] = dataclasses.field(default_factory=dict)
76+
# package name to member path, for all members of this workspace
77+
packages_to_member: T.Dict[str, str] = dataclasses.field(default_factory=dict)
78+
# member paths that are required to be built
79+
required_members: T.List[str] = dataclasses.field(default_factory=list)
80+
81+
6782
class Interpreter:
6883
def __init__(self, env: Environment, subdir: str, subprojects_dir: str) -> None:
6984
self.environment = env
7085
# Map Cargo.toml's subdir to loaded manifest.
71-
self.manifests: T.Dict[str, Manifest] = {}
86+
self.manifests: T.Dict[str, T.Union[Manifest, Workspace]] = {}
7287
# Map of cargo package (name + api) to its state
7388
self.packages: T.Dict[PackageKey, PackageState] = {}
89+
# Map subdir to workspace
90+
self.workspaces: T.Dict[str, WorkspaceState] = {}
7491
# Cargo packages
7592
filename = os.path.join(self.environment.get_source_dir(), subdir, 'Cargo.lock')
7693
subprojects_dir = os.path.join(self.environment.get_source_dir(), subprojects_dir)
@@ -90,17 +107,23 @@ def interpret(self, subdir: str, project_root: T.Optional[str] = None) -> mparse
90107
manifest = self._load_manifest(subdir)
91108
filename = os.path.join(self.environment.source_dir, subdir, 'Cargo.toml')
92109
build = builder.Builder(filename)
110+
if isinstance(manifest, Workspace):
111+
return self.interpret_workspace(manifest, build, subdir, project_root)
93112
return self.interpret_package(manifest, build, subdir, project_root)
94113

95114
def interpret_package(self, manifest: Manifest, build: builder.Builder, subdir: str, project_root: T.Optional[str]) -> mparser.CodeBlockNode:
96115
# Build an AST for this package
97116
ast: T.List[mparser.BaseNode] = []
98-
pkg, cached = self._fetch_package(manifest.package.name, manifest.package.api)
99-
if not cached:
100-
# This is an entry point, always enable the 'default' feature.
101-
# FIXME: We should have a Meson option similar to `cargo build --no-default-features`
102-
self._enable_feature(pkg, 'default')
103-
if not project_root:
117+
if project_root:
118+
ws = self.workspaces[project_root]
119+
member = ws.packages_to_member[manifest.package.name]
120+
pkg = ws.packages[member]
121+
else:
122+
pkg, cached = self._fetch_package(manifest.package.name, manifest.package.api)
123+
if not cached:
124+
# This is an entry point, always enable the 'default' feature.
125+
# FIXME: We should have a Meson option similar to `cargo build --no-default-features`
126+
self._enable_feature(pkg, 'default')
104127
ast += self._create_project(pkg.manifest.package.name, pkg, build)
105128
ast.append(build.assign(build.function('import', [build.string('rust')]), 'rust'))
106129
ast += self._create_package(pkg, build, subdir)
@@ -122,6 +145,74 @@ def _create_package(self, pkg: PackageState, build: builder.Builder, subdir: str
122145

123146
return ast
124147

148+
def interpret_workspace(self, workspace: Workspace, build: builder.Builder, subdir: str, project_root: T.Optional[str]) -> mparser.CodeBlockNode:
149+
ws = self._get_workspace(workspace, subdir)
150+
name = os.path.dirname(subdir)
151+
subprojects_dir = os.path.join(subdir, 'subprojects')
152+
self.environment.wrap_resolver.load_and_merge(subprojects_dir, T.cast('SubProject', name))
153+
ast: T.List[mparser.BaseNode] = []
154+
if not ws.required_members:
155+
for member in ws.workspace.default_members:
156+
self._require_workspace_member(ws, member)
157+
158+
# Call subdir() for each required member of the workspace. The order is
159+
# important, if a member depends on another member, that member must be
160+
# processed first.
161+
processed_members: T.Dict[str, PackageState] = {}
162+
163+
def _process_member(member: str) -> None:
164+
if member in processed_members:
165+
return
166+
pkg = ws.packages[member]
167+
for depname in pkg.required_deps:
168+
dep = pkg.manifest.dependencies[depname]
169+
if dep.path:
170+
dep_member = os.path.normpath(os.path.join(pkg.ws_member, dep.path))
171+
_process_member(dep_member)
172+
ast.append(build.function('subdir', [build.string(member)]))
173+
processed_members[member] = pkg
174+
175+
ast.append(build.assign(build.function('import', [build.string('rust')]), 'rust'))
176+
for member in ws.required_members:
177+
_process_member(member)
178+
if not project_root:
179+
ast = self._create_project(name, None, build) + ast
180+
181+
return build.block(ast)
182+
183+
def _load_workspace_member(self, ws: WorkspaceState, m: str) -> None:
184+
m = os.path.normpath(m)
185+
# Load member's manifest
186+
m_subdir = os.path.join(ws.subdir, m)
187+
manifest_ = self._load_manifest(m_subdir, ws.workspace, m)
188+
assert isinstance(manifest_, Manifest)
189+
self._add_workspace_member(manifest_, ws, m)
190+
191+
def _add_workspace_member(self, manifest_: Manifest, ws: WorkspaceState, m: str) -> None:
192+
if m in ws.packages:
193+
return
194+
pkg = PackageState(manifest_, ws_subdir=ws.subdir, ws_member=m)
195+
ws.packages[m] = pkg
196+
ws.packages_to_member[manifest_.package.name] = m
197+
198+
def _get_workspace(self, workspace: Workspace, subdir: str) -> WorkspaceState:
199+
ws = self.workspaces.get(subdir)
200+
if ws:
201+
return ws
202+
ws = WorkspaceState(workspace, subdir)
203+
for m in workspace.members:
204+
self._load_workspace_member(ws, m)
205+
self.workspaces[subdir] = ws
206+
return ws
207+
208+
def _require_workspace_member(self, ws: WorkspaceState, member: str) -> PackageState:
209+
member = os.path.normpath(member)
210+
pkg = ws.packages[member]
211+
if member not in ws.required_members:
212+
self._prepare_package(pkg)
213+
ws.required_members.append(member)
214+
return pkg
215+
125216
def _fetch_package(self, package_name: str, api: str) -> T.Tuple[PackageState, bool]:
126217
key = PackageKey(package_name, api)
127218
pkg = self.packages.get(key)
@@ -150,6 +241,11 @@ def _fetch_package_from_subproject(self, package_name: str, meson_depname: str)
150241
downloaded = \
151242
subp_name in self.environment.wrap_resolver.wraps and \
152243
self.environment.wrap_resolver.wraps[subp_name].type is not None
244+
if isinstance(manifest, Workspace):
245+
ws = self._get_workspace(manifest, subdir)
246+
member = ws.packages_to_member[package_name]
247+
pkg = self._require_workspace_member(ws, member)
248+
return pkg, False
153249
key = PackageKey(package_name, version.api(manifest.package.version))
154250

155251
pkg = self.packages.get(key)
@@ -172,7 +268,14 @@ def _prepare_package(self, pkg: PackageState) -> None:
172268
self._add_dependency(pkg, depname)
173269

174270
def _dep_package(self, pkg: PackageState, dep: Dependency) -> PackageState:
175-
if dep.git:
271+
if dep.path:
272+
if not pkg.ws_subdir:
273+
raise MesonException("path dependencies only supported inside workspaces")
274+
ws = self.workspaces[pkg.ws_subdir]
275+
dep_member = os.path.normpath(os.path.join(pkg.ws_member, dep.path))
276+
self._load_workspace_member(ws, dep_member)
277+
dep_pkg = self._require_workspace_member(ws, dep_member)
278+
elif dep.git:
176279
_, _, directory = _parse_git_url(dep.git, dep.branch)
177280
dep_pkg, _ = self._fetch_package_from_subproject(dep.package, directory)
178281
else:
@@ -192,18 +295,30 @@ def _dep_package(self, pkg: PackageState, dep: Dependency) -> PackageState:
192295
dep_pkg, _ = self._fetch_package(dep.package, dep.api)
193296
return dep_pkg
194297

195-
def _load_manifest(self, subdir: str) -> Manifest:
298+
def _load_manifest(self, subdir: str, workspace: T.Optional[Workspace] = None, member_path: str = '') -> T.Union[Manifest, Workspace]:
196299
manifest_ = self.manifests.get(subdir)
197300
if not manifest_:
198301
path = os.path.join(self.environment.source_dir, subdir)
199302
filename = os.path.join(path, 'Cargo.toml')
200303
toml = load_toml(filename)
304+
workspace_ = None
305+
if 'workspace' in toml:
306+
raw_workspace = T.cast('raw.VirtualManifest', toml)
307+
workspace_ = Workspace.from_raw(raw_workspace)
308+
manifest_ = None
201309
if 'package' in toml:
202310
raw_manifest = T.cast('raw.Manifest', toml)
203-
manifest_ = Manifest.from_raw(raw_manifest, path)
204-
self.manifests[subdir] = manifest_
205-
else:
206-
raise MesonException(f'{subdir}/Cargo.toml does not have [package] section')
311+
manifest_ = Manifest.from_raw(raw_manifest, path, workspace, member_path)
312+
if not manifest_ and not workspace_:
313+
raise MesonException(f'{subdir}/Cargo.toml does not have [package] or [workspace] section')
314+
315+
if workspace_:
316+
if manifest_:
317+
raise NotImplementedError
318+
self.manifests[subdir] = workspace_
319+
return workspace_
320+
321+
self.manifests[subdir] = manifest_
207322
return manifest_
208323

209324
def _add_dependency(self, pkg: PackageState, depname: str) -> None:

0 commit comments

Comments
 (0)