Skip to content

Commit 596dae8

Browse files
committed
WIP: cargo: Add workspace support
1 parent 8afac2e commit 596dae8

File tree

2 files changed

+249
-51
lines changed

2 files changed

+249
-51
lines changed

mesonbuild/cargo/interpreter.py

Lines changed: 183 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@
1414
import os
1515
import collections
1616
import urllib.parse
17-
import itertools
1817
import typing as T
18+
from functools import lru_cache
1919

2020
from . import builder, cfg, version
2121
from .toml import load_toml, TomlImplementationMissing
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
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,31 +67,61 @@ class PackageKey:
6467
api: str
6568

6669

70+
@dataclasses.dataclass
71+
class WorkspaceState:
72+
workspace: Workspace
73+
# member path -> PackageState, for all members of this workspace
74+
packages: T.Dict[str, PackageState] = dataclasses.field(default_factory=dict)
75+
# package name to member path, for all members of this workspace
76+
packages_to_member: T.Dict[str, str] = dataclasses.field(default_factory=dict)
77+
# member paths that are required to be built
78+
required_members: T.List[str] = dataclasses.field(default_factory=list)
79+
80+
6781
class Interpreter:
6882
def __init__(self, env: Environment) -> None:
6983
self.environment = env
7084
self.host_rustc = T.cast('RustCompiler', self.environment.coredata.compilers[MachineChoice.HOST]['rust'])
7185
# Map Cargo.toml's subdir to loaded manifest.
72-
self.manifests: T.Dict[str, Manifest] = {}
86+
self.manifests: T.Dict[str, T.Union[Manifest, Workspace]] = {}
7387
# Map of cargo package (name + api) to its state
7488
self.packages: T.Dict[PackageKey, PackageState] = {}
89+
# Map subdir to workspace
90+
self.workspaces: T.Dict[str, WorkspaceState] = {}
7591
# Rustc's config
7692
self.cfgs = self._get_cfgs()
7793

78-
def interpret(self, subdir: str) -> mparser.CodeBlockNode:
94+
def interpret(self, subdir: str, project_root: T.Optional[str] = None) -> mparser.CodeBlockNode:
7995
manifest = self._load_manifest(subdir)
80-
pkg, cached = self._fetch_package(manifest.package.name, manifest.package.api)
81-
if not cached:
82-
# This is an entry point, always enable the 'default' feature.
83-
# FIXME: We should have a Meson option similar to `cargo build --no-default-features`
84-
self._enable_feature(pkg, 'default')
85-
86-
# Build an AST for this package
8796
filename = os.path.join(self.environment.source_dir, subdir, 'Cargo.toml')
8897
build = builder.Builder(filename)
89-
ast = self._create_project(pkg, build)
90-
ast += [
91-
build.assign(build.function('import', [build.string('rust')]), 'rust'),
98+
if isinstance(manifest, Workspace):
99+
return self.interpret_workspace(manifest, build, subdir, project_root)
100+
return self.interpret_package(manifest, build, subdir, project_root)
101+
102+
def interpret_package(self, manifest: Manifest, build: builder.Builder, subdir: str, project_root: T.Optional[str]) -> mparser.CodeBlockNode:
103+
if project_root:
104+
ws = self.workspaces[project_root]
105+
member = ws.packages_to_member[manifest.package.name]
106+
pkg = ws.packages[member]
107+
else:
108+
pkg, cached = self._fetch_package(manifest.package.name, manifest.package.api)
109+
assert isinstance(pkg, PackageState)
110+
if not cached:
111+
# This is an entry point, always enable the 'default' feature.
112+
# FIXME: We should have a Meson option similar to `cargo build --no-default-features`
113+
self._enable_feature(pkg, 'default')
114+
115+
# Build an AST for this package
116+
ast: T.List[mparser.BaseNode] = []
117+
if not project_root:
118+
ast += self._create_project(pkg, build)
119+
ast.append(build.assign(build.function('import', [build.string('rust')]), 'rust'))
120+
ast += self._create_package(pkg, build, subdir)
121+
return build.block(ast)
122+
123+
def _create_package(self, pkg: PackageState, build: builder.Builder, subdir: str) -> T.List[mparser.BaseNode]:
124+
ast: T.List[mparser.BaseNode] = [
92125
build.function('message', [
93126
build.string('Enabled features:'),
94127
build.array([build.string(f) for f in pkg.features]),
@@ -97,48 +130,155 @@ def interpret(self, subdir: str) -> mparser.CodeBlockNode:
97130
ast += self._create_dependencies(pkg, build)
98131
ast += self._create_meson_subdir(build)
99132

100-
# Libs are always auto-discovered and there's no other way to handle them,
101-
# which is unfortunate for reproducability
102133
if os.path.exists(os.path.join(self.environment.source_dir, subdir, pkg.manifest.lib.path)):
103134
for crate_type in pkg.manifest.lib.crate_type:
104-
ast.extend(self._create_lib(pkg, build, crate_type))
135+
ast += self._create_lib(pkg, build, crate_type)
136+
137+
return ast
138+
139+
def interpret_workspace(self, workspace: Workspace, build: builder.Builder, subdir: str, project_root: T.Optional[str]) -> mparser.CodeBlockNode:
140+
ws = self._get_workspace(workspace, subdir)
141+
name = os.path.dirname(subdir)
142+
subprojects_dir = os.path.join(subdir, 'subprojects')
143+
self.environment.wrap_resolver.load_and_merge(subprojects_dir, T.cast('SubProject', name))
144+
ast: T.List[mparser.BaseNode] = []
145+
if not project_root:
146+
args: T.List[mparser.BaseNode] = [
147+
build.string(name),
148+
build.string('rust'),
149+
]
150+
kwargs: T.Dict[str, mparser.BaseNode] = {
151+
'meson_version': build.string(f'>= {coredata.stable_version}'),
152+
}
153+
ast += [
154+
build.function('project', args, kwargs),
155+
build.assign(build.function('import', [build.string('rust')]), 'rust'),
156+
]
157+
if not ws.required_members:
158+
for member in ws.workspace.default_members:
159+
self._add_workspace_member(ws, member)
160+
161+
# Call subdir() for each required member of the workspace. The order is
162+
# important, if a member depends on another member, that member must be
163+
# processed first.
164+
processed_members: T.Set[str] = set()
165+
166+
def _process_member(member: str) -> None:
167+
if member in processed_members:
168+
return
169+
pkg = ws.packages[member]
170+
for depname in pkg.required_deps:
171+
dep = pkg.manifest.dependencies[depname]
172+
if dep.path:
173+
dep_member = os.path.normpath(os.path.join(pkg.ws_member, dep.path))
174+
_process_member(dep_member)
175+
if member == '.':
176+
ast.extend(self._create_package(pkg, build, subdir))
177+
else:
178+
ast.append(build.function('subdir', [build.string(member)]))
179+
processed_members.add(member)
180+
181+
for member in ws.required_members:
182+
_process_member(member)
105183

106184
return build.block(ast)
107185

186+
def _get_workspace(self, workspace: Workspace, subdir: str) -> WorkspaceState:
187+
ws = self.workspaces.get(subdir)
188+
if ws:
189+
return ws
190+
ws = WorkspaceState(workspace)
191+
for m in workspace.members:
192+
m = os.path.normpath(m)
193+
# Load member's manifest
194+
m_subdir = os.path.join(subdir, m)
195+
manifest_ = self._load_manifest(m_subdir, ws.workspace, m)
196+
assert isinstance(manifest_, Manifest)
197+
pkg = PackageState(manifest_, ws_subdir=subdir, ws_member=m)
198+
ws.packages[m] = pkg
199+
ws.packages_to_member[manifest_.package.name] = m
200+
if workspace.root_package:
201+
m = '.'
202+
pkg = PackageState(workspace.root_package, ws_subdir=subdir, ws_member=m)
203+
ws.packages[m] = pkg
204+
ws.packages_to_member[workspace.root_package.package.name] = m
205+
self.workspaces[subdir] = ws
206+
return ws
207+
208+
def _add_workspace_member(self, ws: WorkspaceState, member: str) -> PackageState:
209+
pkg = ws.packages[member]
210+
if member not in ws.required_members:
211+
self._prepare_package(pkg)
212+
ws.required_members.append(member)
213+
return pkg
214+
108215
def _fetch_package(self, package_name: str, api: str) -> T.Tuple[PackageState, bool]:
109216
key = PackageKey(package_name, api)
110217
pkg = self.packages.get(key)
111218
if pkg:
112219
return pkg, True
113220
meson_depname = _dependency_name(package_name, api)
114-
subdir, _ = self.environment.wrap_resolver.resolve(meson_depname)
221+
return self._fetch_package_from_subproject(package_name, meson_depname)
222+
223+
@lru_cache(maxsize=None)
224+
def _fetch_package_from_subproject(self, package_name: str, meson_depname: str) -> T.Tuple[PackageState, bool]:
225+
subp_name, _ = self.environment.wrap_resolver.find_dep_provider(meson_depname)
226+
subdir, _ = self.environment.wrap_resolver.resolve(subp_name)
115227
subprojects_dir = os.path.join(subdir, 'subprojects')
116-
self.environment.wrap_resolver.load_and_merge(subprojects_dir, T.cast('SubProject', meson_depname))
228+
self.environment.wrap_resolver.load_and_merge(subprojects_dir, T.cast('SubProject', subp_name))
117229
manifest = self._load_manifest(subdir)
118230
downloaded = \
119-
meson_depname in self.environment.wrap_resolver.wraps and \
120-
self.environment.wrap_resolver.wraps[meson_depname].type is not None
231+
subp_name in self.environment.wrap_resolver.wraps and \
232+
self.environment.wrap_resolver.wraps[subp_name].type is not None
233+
if isinstance(manifest, Workspace):
234+
ws = self._get_workspace(manifest, subdir)
235+
member = ws.packages_to_member[package_name]
236+
pkg = self._add_workspace_member(ws, member)
237+
return pkg, False
238+
key = PackageKey(package_name, version.api(manifest.package.version))
239+
pkg = self.packages.get(key)
240+
if pkg:
241+
return pkg, True
121242
pkg = PackageState(manifest, downloaded)
122243
self.packages[key] = pkg
244+
self._prepare_package(pkg)
245+
return pkg, False
246+
247+
def _prepare_package(self, pkg: PackageState) -> None:
123248
# Merge target specific dependencies that are enabled
124-
for condition, dependencies in manifest.target.items():
249+
for condition, dependencies in pkg.manifest.target.items():
125250
if cfg.eval_cfg(condition, self.cfgs):
126-
manifest.dependencies.update(dependencies)
251+
pkg.manifest.dependencies.update(dependencies)
127252
# Fetch required dependencies recursively.
128-
for depname, dep in manifest.dependencies.items():
253+
for depname, dep in pkg.manifest.dependencies.items():
129254
if not dep.optional:
130255
self._add_dependency(pkg, depname)
131-
return pkg, False
132256

133-
def _dep_package(self, dep: Dependency) -> PackageState:
134-
return self.packages[PackageKey(dep.package, dep.api)]
257+
def _dep_package(self, pkg: PackageState, dep: Dependency) -> PackageState:
258+
if dep.path:
259+
ws = self.workspaces[pkg.ws_subdir]
260+
dep_member = os.path.normpath(os.path.join(pkg.ws_member, dep.path))
261+
dep_pkg = self._add_workspace_member(ws, dep_member)
262+
elif dep.git:
263+
_, _, directory = _parse_git_url(dep.git, dep.branch)
264+
dep_pkg, _ = self._fetch_package_from_subproject(dep.package, directory)
265+
elif dep.version:
266+
dep_pkg, _ = self._fetch_package(dep.package, dep.api)
267+
else:
268+
raise MesonException(f'Cannot determine version of cargo package {dep.package}')
269+
return dep_pkg
135270

136-
def _load_manifest(self, subdir: str) -> Manifest:
271+
def _load_manifest(self, subdir: str, workspace: T.Optional[Workspace] = None, member_path: str = '') -> T.Union[Manifest, Workspace]:
137272
manifest_ = self.manifests.get(subdir)
138273
if not manifest_:
139274
filename = os.path.join(self.environment.source_dir, subdir, 'Cargo.toml')
140275
raw = load_toml(filename)
141-
manifest_ = Manifest.from_raw(raw)
276+
if 'workspace' in raw:
277+
manifest_ = Workspace.from_raw(raw)
278+
elif 'package' in raw:
279+
manifest_ = Manifest.from_raw(raw, workspace, member_path)
280+
else:
281+
raise MesonException(f'{subdir}/Cargo.toml does not have [package] or [workspace] section')
142282
self.manifests[subdir] = manifest_
143283
return manifest_
144284

@@ -147,12 +287,10 @@ def _add_dependency(self, pkg: PackageState, depname: str) -> None:
147287
return
148288
dep = pkg.manifest.dependencies.get(depname)
149289
if not dep:
150-
if depname in itertools.chain(pkg.manifest.dev_dependencies, pkg.manifest.build_dependencies):
151-
# FIXME: Not supported yet
152-
return
153-
raise MesonException(f'Dependency {depname} not defined in {pkg.manifest.package.name} manifest')
290+
# It could be build/dev/target dependency. Just ignore it.
291+
return
292+
dep_pkg = self._dep_package(pkg, dep)
154293
pkg.required_deps.add(depname)
155-
dep_pkg, _ = self._fetch_package(dep.package, dep.api)
156294
if dep.default_features:
157295
self._enable_feature(dep_pkg, 'default')
158296
for f in dep.features:
@@ -176,7 +314,7 @@ def _enable_feature(self, pkg: PackageState, feature: str) -> None:
176314
depname = depname[:-1]
177315
if depname in pkg.required_deps:
178316
dep = pkg.manifest.dependencies[depname]
179-
dep_pkg = self._dep_package(dep)
317+
dep_pkg = self._dep_package(pkg, dep)
180318
self._enable_feature(dep_pkg, dep_f)
181319
else:
182320
# This feature will be enabled only if that dependency
@@ -186,7 +324,7 @@ def _enable_feature(self, pkg: PackageState, feature: str) -> None:
186324
self._add_dependency(pkg, depname)
187325
dep = pkg.manifest.dependencies.get(depname)
188326
if dep:
189-
dep_pkg = self._dep_package(dep)
327+
dep_pkg = self._dep_package(pkg, dep)
190328
self._enable_feature(dep_pkg, dep_f)
191329
elif f.startswith('dep:'):
192330
self._add_dependency(pkg, f[4:])
@@ -247,7 +385,8 @@ def _create_dependencies(self, pkg: PackageState, build: builder.Builder) -> T.L
247385
ast: T.List[mparser.BaseNode] = []
248386
for depname in pkg.required_deps:
249387
dep = pkg.manifest.dependencies[depname]
250-
ast += self._create_dependency(dep, build)
388+
dep_pkg = self._dep_package(pkg, dep)
389+
ast += self._create_dependency(dep_pkg, dep, build)
251390
ast.append(build.assign(build.array([]), 'system_deps_args'))
252391
for name, sys_dep in pkg.manifest.system_dependencies.items():
253392
if sys_dep.enabled(pkg.features):
@@ -280,10 +419,11 @@ def _create_system_dependency(self, name: str, dep: SystemDependency, build: bui
280419
),
281420
]
282421

283-
def _create_dependency(self, dep: Dependency, build: builder.Builder) -> T.List[mparser.BaseNode]:
284-
pkg = self._dep_package(dep)
422+
def _create_dependency(self, pkg: PackageState, dep: Dependency, build: builder.Builder) -> T.List[mparser.BaseNode]:
423+
version_ = dep.meson_version or [pkg.manifest.package.version]
424+
api = dep.api or pkg.manifest.package.api
285425
kw = {
286-
'version': build.array([build.string(s) for s in dep.meson_version]),
426+
'version': build.array([build.string(s) for s in version_]),
287427
}
288428
# Lookup for this dependency with the features we want in default_options kwarg.
289429
#
@@ -301,7 +441,7 @@ def _create_dependency(self, dep: Dependency, build: builder.Builder) -> T.List[
301441
build.assign(
302442
build.function(
303443
'dependency',
304-
[build.string(_dependency_name(dep.package, dep.api))],
444+
[build.string(_dependency_name(dep.package, api))],
305445
kw,
306446
),
307447
_dependency_varname(dep.package),
@@ -331,7 +471,7 @@ def _create_dependency(self, dep: Dependency, build: builder.Builder) -> T.List[
331471
build.if_(build.not_in(build.identifier('f'), build.identifier('actual_features')), build.block([
332472
build.function('error', [
333473
build.string('Dependency'),
334-
build.string(_dependency_name(dep.package, dep.api)),
474+
build.string(_dependency_name(dep.package, api)),
335475
build.string('previously configured with features'),
336476
build.identifier('actual_features'),
337477
build.string('but need'),
@@ -366,7 +506,7 @@ def _create_lib(self, pkg: PackageState, build: builder.Builder, crate_type: CRA
366506
dep = pkg.manifest.dependencies[name]
367507
dependencies.append(build.identifier(_dependency_varname(dep.package)))
368508
if name != dep.package:
369-
dep_pkg = self._dep_package(dep)
509+
dep_pkg = self._dep_package(pkg, dep)
370510
dep_lib_name = dep_pkg.manifest.lib.name
371511
dependency_map[build.string(fixup_meson_varname(dep_lib_name))] = build.string(name)
372512
for name, sys_dep in pkg.manifest.system_dependencies.items():

0 commit comments

Comments
 (0)