Last active
June 6, 2025 16:50
-
-
Save bonzini/4552ee5636a956ee365f29be76ffd1b1 to your computer and use it in GitHub Desktop.
ws.diff
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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