aboutsummaryrefslogtreecommitdiff
path: root/mesonbuild/cargo
diff options
context:
space:
mode:
Diffstat (limited to 'mesonbuild/cargo')
-rw-r--r--mesonbuild/cargo/cfg.py167
-rw-r--r--mesonbuild/cargo/interpreter.py530
-rw-r--r--mesonbuild/cargo/manifest.py645
-rw-r--r--mesonbuild/cargo/raw.py192
-rw-r--r--mesonbuild/cargo/toml.py49
-rw-r--r--mesonbuild/cargo/version.py14
6 files changed, 848 insertions, 749 deletions
diff --git a/mesonbuild/cargo/cfg.py b/mesonbuild/cargo/cfg.py
index 0d49527..a0ee6e2 100644
--- a/mesonbuild/cargo/cfg.py
+++ b/mesonbuild/cargo/cfg.py
@@ -4,6 +4,7 @@
"""Rust CFG parser.
Rust uses its `cfg()` format in cargo.
+https://doc.rust-lang.org/reference/conditional-compilation.html
This may have the following functions:
- all()
@@ -22,18 +23,15 @@ so you could have examples like:
from __future__ import annotations
import dataclasses
import enum
-import functools
import typing as T
-from . import builder
-from .. import mparser
from ..mesonlib import MesonBugException
if T.TYPE_CHECKING:
_T = T.TypeVar('_T')
_LEX_TOKEN = T.Tuple['TokenType', T.Optional[str]]
- _LEX_STREAM = T.Iterable[_LEX_TOKEN]
+ _LEX_STREAM = T.Iterator[_LEX_TOKEN]
_LEX_STREAM_AH = T.Iterator[T.Tuple[_LEX_TOKEN, T.Optional[_LEX_TOKEN]]]
@@ -48,6 +46,7 @@ class TokenType(enum.Enum):
NOT = enum.auto()
COMMA = enum.auto()
EQUAL = enum.auto()
+ CFG = enum.auto()
def lexer(raw: str) -> _LEX_STREAM:
@@ -56,45 +55,41 @@ def lexer(raw: str) -> _LEX_STREAM:
:param raw: The raw cfg() expression
:return: An iterable of tokens
"""
- buffer: T.List[str] = []
+ start: int = 0
is_string: bool = False
- for s in raw:
- if s.isspace() or s in {')', '(', ',', '='} or (s == '"' and buffer):
- val = ''.join(buffer)
- buffer.clear()
- if is_string:
+ for i, s in enumerate(raw):
+ if s.isspace() or s in {')', '(', ',', '=', '"'}:
+ val = raw[start:i]
+ start = i + 1
+ if s == '"' and is_string:
yield (TokenType.STRING, val)
+ is_string = False
+ continue
elif val == 'any':
yield (TokenType.ANY, None)
elif val == 'all':
yield (TokenType.ALL, None)
elif val == 'not':
yield (TokenType.NOT, None)
+ elif val == 'cfg':
+ yield (TokenType.CFG, None)
elif val:
yield (TokenType.IDENTIFIER, val)
if s == '(':
yield (TokenType.LPAREN, None)
- continue
elif s == ')':
yield (TokenType.RPAREN, None)
- continue
elif s == ',':
yield (TokenType.COMMA, None)
- continue
elif s == '=':
yield (TokenType.EQUAL, None)
- continue
- elif s.isspace():
- continue
-
- if s == '"':
- is_string = not is_string
- else:
- buffer.append(s)
- if buffer:
+ elif s == '"':
+ is_string = True
+ val = raw[start:]
+ if val:
# This should always be an identifier
- yield (TokenType.IDENTIFIER, ''.join(buffer))
+ yield (TokenType.IDENTIFIER, val)
def lookahead(iter: T.Iterator[_T]) -> T.Iterator[T.Tuple[_T, T.Optional[_T]]]:
@@ -146,8 +141,8 @@ class Identifier(IR):
@dataclasses.dataclass
class Equal(IR):
- lhs: IR
- rhs: IR
+ lhs: Identifier
+ rhs: String
@dataclasses.dataclass
@@ -175,41 +170,40 @@ def _parse(ast: _LEX_STREAM_AH) -> IR:
else:
ntoken, _ = (None, None)
- stream: T.List[_LEX_TOKEN]
if token is TokenType.IDENTIFIER:
+ assert value
+ id_ = Identifier(value)
if ntoken is TokenType.EQUAL:
- return Equal(Identifier(value), _parse(ast))
- if token is TokenType.STRING:
- return String(value)
- if token is TokenType.EQUAL:
- # In this case the previous caller already has handled the equal
- return _parse(ast)
- if token in {TokenType.ANY, TokenType.ALL}:
+ next(ast)
+ (token, value), _ = next(ast)
+ assert token is TokenType.STRING
+ assert value is not None
+ return Equal(id_, String(value))
+ return id_
+ elif token in {TokenType.ANY, TokenType.ALL}:
type_ = All if token is TokenType.ALL else Any
- assert ntoken is TokenType.LPAREN
- next(ast) # advance the iterator to get rid of the LPAREN
- stream = []
args: T.List[IR] = []
- while token is not TokenType.RPAREN:
+ (token, value), n_stream = next(ast)
+ assert token is TokenType.LPAREN
+ if n_stream and n_stream[0] == TokenType.RPAREN:
+ return type_(args)
+ while True:
+ args.append(_parse(ast))
(token, value), _ = next(ast)
- if token is TokenType.COMMA:
- args.append(_parse(lookahead(iter(stream))))
- stream.clear()
- else:
- stream.append((token, value))
- if stream:
- args.append(_parse(lookahead(iter(stream))))
+ if token is TokenType.RPAREN:
+ break
+ assert token is TokenType.COMMA
return type_(args)
- if token is TokenType.NOT:
- next(ast) # advance the iterator to get rid of the LPAREN
- stream = []
- # Mypy can't figure out that token is overridden inside the while loop
- while token is not TokenType.RPAREN: # type: ignore
- (token, value), _ = next(ast)
- stream.append((token, value))
- return Not(_parse(lookahead(iter(stream))))
-
- raise MesonBugException(f'Unhandled Cargo token: {token}')
+ elif token in {TokenType.NOT, TokenType.CFG}:
+ is_not = token is TokenType.NOT
+ (token, value), _ = next(ast)
+ assert token is TokenType.LPAREN
+ arg = _parse(ast)
+ (token, value), _ = next(ast)
+ assert token is TokenType.RPAREN
+ return Not(arg) if is_not else arg
+ else:
+ raise MesonBugException(f'Unhandled Cargo token:{token} {value}')
def parse(ast: _LEX_STREAM) -> IR:
@@ -218,57 +212,24 @@ def parse(ast: _LEX_STREAM) -> IR:
:param ast: An iterable of Tokens
:return: An mparser Node to be used as a conditional
"""
- ast_i: _LEX_STREAM_AH = lookahead(iter(ast))
+ ast_i: _LEX_STREAM_AH = lookahead(ast)
return _parse(ast_i)
-@functools.singledispatch
-def ir_to_meson(ir: T.Any, build: builder.Builder) -> mparser.BaseNode:
- raise NotImplementedError
-
-
-@ir_to_meson.register
-def _(ir: String, build: builder.Builder) -> mparser.BaseNode:
- return build.string(ir.value)
-
-
-@ir_to_meson.register
-def _(ir: Identifier, build: builder.Builder) -> mparser.BaseNode:
- host_machine = build.identifier('host_machine')
- if ir.value == "target_arch":
- return build.method('cpu_family', host_machine)
- elif ir.value in {"target_os", "target_family"}:
- return build.method('system', host_machine)
- elif ir.value == "target_endian":
- return build.method('endian', host_machine)
- raise MesonBugException(f"Unhandled Cargo identifier: {ir.value}")
-
-
-@ir_to_meson.register
-def _(ir: Equal, build: builder.Builder) -> mparser.BaseNode:
- return build.equal(ir_to_meson(ir.lhs, build), ir_to_meson(ir.rhs, build))
-
-
-@ir_to_meson.register
-def _(ir: Not, build: builder.Builder) -> mparser.BaseNode:
- return build.not_(ir_to_meson(ir.value, build))
-
-
-@ir_to_meson.register
-def _(ir: Any, build: builder.Builder) -> mparser.BaseNode:
- args = iter(reversed(ir.args))
- last = next(args)
- cur = build.or_(ir_to_meson(next(args), build), ir_to_meson(last, build))
- for a in args:
- cur = build.or_(ir_to_meson(a, build), cur)
- return cur
+def _eval_cfg(ir: IR, cfgs: T.Dict[str, str]) -> bool:
+ if isinstance(ir, Identifier):
+ return ir.value in cfgs
+ elif isinstance(ir, Equal):
+ return cfgs.get(ir.lhs.value) == ir.rhs.value
+ elif isinstance(ir, Not):
+ return not _eval_cfg(ir.value, cfgs)
+ elif isinstance(ir, Any):
+ return any(_eval_cfg(i, cfgs) for i in ir.args)
+ elif isinstance(ir, All):
+ return all(_eval_cfg(i, cfgs) for i in ir.args)
+ else:
+ raise MesonBugException(f'Unhandled Cargo cfg IR: {ir}')
-@ir_to_meson.register
-def _(ir: All, build: builder.Builder) -> mparser.BaseNode:
- args = iter(reversed(ir.args))
- last = next(args)
- cur = build.and_(ir_to_meson(next(args), build), ir_to_meson(last, build))
- for a in args:
- cur = build.and_(ir_to_meson(a, build), cur)
- return cur
+def eval_cfg(raw: str, cfgs: T.Dict[str, str]) -> bool:
+ return _eval_cfg(parse(lexer(raw)), cfgs)
diff --git a/mesonbuild/cargo/interpreter.py b/mesonbuild/cargo/interpreter.py
index af272a8..a0d4371 100644
--- a/mesonbuild/cargo/interpreter.py
+++ b/mesonbuild/cargo/interpreter.py
@@ -11,439 +11,30 @@ port will be required.
from __future__ import annotations
import dataclasses
-import importlib
-import json
import os
-import shutil
import collections
import urllib.parse
import itertools
import typing as T
-from . import builder
-from . import version
-from ..mesonlib import MesonException, Popen_safe
+from . import builder, version, cfg
+from .toml import load_toml, TomlImplementationMissing
+from .manifest import Manifest, CargoLock, fixup_meson_varname
+from ..mesonlib import MesonException, MachineChoice
from .. import coredata, mlog
from ..wrap.wrap import PackageDefinition
if T.TYPE_CHECKING:
- from types import ModuleType
-
- from typing_extensions import Protocol, Self
-
- from . import manifest
+ from . import raw
from .. import mparser
+ from .manifest import Dependency, SystemDependency
from ..environment import Environment
from ..interpreterbase import SubProject
+ from ..compilers.rust import RustCompiler
- # Copied from typeshed. Blarg that they don't expose this
- class DataclassInstance(Protocol):
- __dataclass_fields__: T.ClassVar[dict[str, dataclasses.Field[T.Any]]]
-
- _UnknownKeysT = T.TypeVar('_UnknownKeysT', manifest.FixedPackage,
- manifest.FixedDependency, manifest.FixedLibTarget,
- manifest.FixedBuildTarget)
-
-
-# tomllib is present in python 3.11, before that it is a pypi module called tomli,
-# we try to import tomllib, then tomli,
-# TODO: add a fallback to toml2json?
-tomllib: T.Optional[ModuleType] = None
-toml2json: T.Optional[str] = None
-for t in ['tomllib', 'tomli']:
- try:
- tomllib = importlib.import_module(t)
- break
- except ImportError:
- pass
-else:
- # TODO: it would be better to use an Executable here, which could be looked
- # up in the cross file or provided by a wrap. However, that will have to be
- # passed in externally, since we don't have (and I don't think we should),
- # have access to the `Environment` for that in this module.
- toml2json = shutil.which('toml2json')
-
-
-_EXTRA_KEYS_WARNING = (
- "This may (unlikely) be an error in the cargo manifest, or may be a missing "
- "implementation in Meson. If this issue can be reproduced with the latest "
- "version of Meson, please help us by opening an issue at "
- "https://github.com/mesonbuild/meson/issues. Please include the crate and "
- "version that is generating this warning if possible."
-)
-
-class TomlImplementationMissing(MesonException):
- pass
-
-
-def load_toml(filename: str) -> T.Dict[object, object]:
- if tomllib:
- with open(filename, 'rb') as f:
- raw = tomllib.load(f)
- else:
- if toml2json is None:
- raise TomlImplementationMissing('Could not find an implementation of tomllib, nor toml2json')
-
- p, out, err = Popen_safe([toml2json, filename])
- if p.returncode != 0:
- raise MesonException('toml2json failed to decode output\n', err)
-
- raw = json.loads(out)
-
- if not isinstance(raw, dict):
- raise MesonException("Cargo.toml isn't a dictionary? How did that happen?")
-
- return raw
-
-
-def fixup_meson_varname(name: str) -> str:
- """Fixup a meson variable name
-
- :param name: The name to fix
- :return: the fixed name
- """
- return name.replace('-', '_')
-
-
-# Pylance can figure out that these do not, in fact, overlap, but mypy can't
-@T.overload
-def _fixup_raw_mappings(d: manifest.BuildTarget) -> manifest.FixedBuildTarget: ... # type: ignore
-
-@T.overload
-def _fixup_raw_mappings(d: manifest.LibTarget) -> manifest.FixedLibTarget: ... # type: ignore
-
-@T.overload
-def _fixup_raw_mappings(d: manifest.Dependency) -> manifest.FixedDependency: ...
-
-def _fixup_raw_mappings(d: T.Union[manifest.BuildTarget, manifest.LibTarget, manifest.Dependency]
- ) -> T.Union[manifest.FixedBuildTarget, manifest.FixedLibTarget,
- manifest.FixedDependency]:
- """Fixup raw cargo mappings to ones more suitable for python to consume.
-
- This does the following:
- * replaces any `-` with `_`, cargo likes the former, but python dicts make
- keys with `-` in them awkward to work with
- * Convert Dependency versions from the cargo format to something meson
- understands
-
- :param d: The mapping to fix
- :return: the fixed string
- """
- raw = {fixup_meson_varname(k): v for k, v in d.items()}
- if 'version' in raw:
- assert isinstance(raw['version'], str), 'for mypy'
- raw['version'] = version.convert(raw['version'])
- return T.cast('T.Union[manifest.FixedBuildTarget, manifest.FixedLibTarget, manifest.FixedDependency]', raw)
-
-
-def _handle_unknown_keys(data: _UnknownKeysT, cls: T.Union[DataclassInstance, T.Type[DataclassInstance]],
- msg: str) -> _UnknownKeysT:
- """Remove and warn on keys that are coming from cargo, but are unknown to
- our representations.
-
- This is intended to give users the possibility of things proceeding when a
- new key is added to Cargo.toml that we don't yet handle, but to still warn
- them that things might not work.
-
- :param data: The raw data to look at
- :param cls: The Dataclass derived type that will be created
- :param msg: the header for the error message. Usually something like "In N structure".
- :return: The original data structure, but with all unknown keys removed.
- """
- unexpected = set(data) - {x.name for x in dataclasses.fields(cls)}
- if unexpected:
- mlog.warning(msg, 'has unexpected keys', '"{}".'.format(', '.join(sorted(unexpected))),
- _EXTRA_KEYS_WARNING)
- for k in unexpected:
- # Mypy and Pyright can't prove that this is okay
- del data[k] # type: ignore[misc]
- return data
-
-
-@dataclasses.dataclass
-class Package:
-
- """Representation of a Cargo Package entry, with defaults filled in."""
-
- name: str
- version: str
- description: T.Optional[str] = None
- resolver: T.Optional[str] = None
- authors: T.List[str] = dataclasses.field(default_factory=list)
- edition: manifest.EDITION = '2015'
- rust_version: T.Optional[str] = None
- documentation: T.Optional[str] = None
- readme: T.Optional[str] = None
- homepage: T.Optional[str] = None
- repository: T.Optional[str] = None
- license: T.Optional[str] = None
- license_file: T.Optional[str] = None
- keywords: T.List[str] = dataclasses.field(default_factory=list)
- categories: T.List[str] = dataclasses.field(default_factory=list)
- workspace: T.Optional[str] = None
- build: T.Optional[str] = None
- links: T.Optional[str] = None
- exclude: T.List[str] = dataclasses.field(default_factory=list)
- include: T.List[str] = dataclasses.field(default_factory=list)
- publish: bool = True
- metadata: T.Dict[str, T.Any] = dataclasses.field(default_factory=dict)
- default_run: T.Optional[str] = None
- autolib: bool = True
- autobins: bool = True
- autoexamples: bool = True
- autotests: bool = True
- autobenches: bool = True
- api: str = dataclasses.field(init=False)
-
- def __post_init__(self) -> None:
- self.api = _version_to_api(self.version)
-
- @classmethod
- def from_raw(cls, raw: manifest.Package) -> Self:
- pkg = T.cast('manifest.FixedPackage',
- {fixup_meson_varname(k): v for k, v in raw.items()})
- pkg = _handle_unknown_keys(pkg, cls, f'Package entry {pkg["name"]}')
- return cls(**pkg)
-
-@dataclasses.dataclass
-class SystemDependency:
-
- """ Representation of a Cargo system-deps entry
- https://docs.rs/system-deps/latest/system_deps
- """
-
- name: str
- version: T.List[str]
- optional: bool = False
- feature: T.Optional[str] = None
- feature_overrides: T.Dict[str, T.Dict[str, str]] = dataclasses.field(default_factory=dict)
-
- @classmethod
- def from_raw(cls, name: str, raw: T.Any) -> SystemDependency:
- if isinstance(raw, str):
- return cls(name, SystemDependency.convert_version(raw))
- name = raw.get('name', name)
- version = SystemDependency.convert_version(raw.get('version'))
- optional = raw.get('optional', False)
- feature = raw.get('feature')
- # Everything else are overrides when certain features are enabled.
- feature_overrides = {k: v for k, v in raw.items() if k not in {'name', 'version', 'optional', 'feature'}}
- return cls(name, version, optional, feature, feature_overrides)
-
- @staticmethod
- def convert_version(version: T.Optional[str]) -> T.List[str]:
- vers = version.split(',') if version is not None else []
- result: T.List[str] = []
- for v in vers:
- v = v.strip()
- if v[0] not in '><=':
- v = f'>={v}'
- result.append(v)
- return result
-
- def enabled(self, features: T.Set[str]) -> bool:
- return self.feature is None or self.feature in features
-
-@dataclasses.dataclass
-class Dependency:
-
- """Representation of a Cargo Dependency Entry."""
-
- name: dataclasses.InitVar[str]
- version: T.List[str]
- registry: T.Optional[str] = None
- git: T.Optional[str] = None
- branch: T.Optional[str] = None
- rev: T.Optional[str] = None
- path: T.Optional[str] = None
- optional: bool = False
- package: str = ''
- default_features: bool = True
- features: T.List[str] = dataclasses.field(default_factory=list)
- api: str = dataclasses.field(init=False)
-
- def __post_init__(self, name: str) -> None:
- self.package = self.package or name
- # Extract wanted API version from version constraints.
- api = set()
- for v in self.version:
- if v.startswith(('>=', '==')):
- api.add(_version_to_api(v[2:].strip()))
- elif v.startswith('='):
- api.add(_version_to_api(v[1:].strip()))
- if not api:
- self.api = '0'
- elif len(api) == 1:
- self.api = api.pop()
- else:
- raise MesonException(f'Cannot determine minimum API version from {self.version}.')
-
- @classmethod
- def from_raw(cls, name: str, raw: manifest.DependencyV) -> Dependency:
- """Create a dependency from a raw cargo dictionary"""
- if isinstance(raw, str):
- return cls(name, version.convert(raw))
- fixed = _handle_unknown_keys(_fixup_raw_mappings(raw), cls, f'Dependency entry {name}')
- return cls(name, **fixed)
-
-
-@dataclasses.dataclass
-class BuildTarget:
-
- name: str
- crate_type: T.List[manifest.CRATE_TYPE] = dataclasses.field(default_factory=lambda: ['lib'])
- path: dataclasses.InitVar[T.Optional[str]] = None
-
- # https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-test-field
- # True for lib, bin, test
- test: bool = True
-
- # https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-doctest-field
- # True for lib
- doctest: bool = False
-
- # https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-bench-field
- # True for lib, bin, benchmark
- bench: bool = True
-
- # https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-doc-field
- # True for libraries and binaries
- doc: bool = False
-
- harness: bool = True
- edition: manifest.EDITION = '2015'
- required_features: T.List[str] = dataclasses.field(default_factory=list)
- plugin: bool = False
-
- @classmethod
- def from_raw(cls, raw: manifest.BuildTarget) -> Self:
- name = raw.get('name', '<anonymous>')
- build = _handle_unknown_keys(_fixup_raw_mappings(raw), cls, f'Binary entry {name}')
- return cls(**build)
-
-@dataclasses.dataclass
-class Library(BuildTarget):
-
- """Representation of a Cargo Library Entry."""
-
- doctest: bool = True
- doc: bool = True
- path: str = os.path.join('src', 'lib.rs')
- proc_macro: bool = False
- crate_type: T.List[manifest.CRATE_TYPE] = dataclasses.field(default_factory=lambda: ['lib'])
- doc_scrape_examples: bool = True
-
- @classmethod
- def from_raw(cls, raw: manifest.LibTarget, fallback_name: str) -> Self: # type: ignore[override]
- fixed = _fixup_raw_mappings(raw)
-
- # We need to set the name field if it's not set manually, including if
- # other fields are set in the lib section
- if 'name' not in fixed:
- fixed['name'] = fallback_name
- fixed = _handle_unknown_keys(fixed, cls, f'Library entry {fixed["name"]}')
-
- return cls(**fixed)
-
-
-@dataclasses.dataclass
-class Binary(BuildTarget):
-
- """Representation of a Cargo Bin Entry."""
-
- doc: bool = True
-
-
-@dataclasses.dataclass
-class Test(BuildTarget):
-
- """Representation of a Cargo Test Entry."""
-
- bench: bool = True
-
-
-@dataclasses.dataclass
-class Benchmark(BuildTarget):
-
- """Representation of a Cargo Benchmark Entry."""
-
- test: bool = True
-
-
-@dataclasses.dataclass
-class Example(BuildTarget):
-
- """Representation of a Cargo Example Entry."""
-
- crate_type: T.List[manifest.CRATE_TYPE] = dataclasses.field(default_factory=lambda: ['bin'])
-
-
-@dataclasses.dataclass
-class Manifest:
-
- """Cargo Manifest definition.
-
- Most of these values map up to the Cargo Manifest, but with default values
- if not provided.
-
- Cargo subprojects can contain what Meson wants to treat as multiple,
- interdependent, subprojects.
-
- :param path: the path within the cargo subproject.
- """
-
- package: Package
- dependencies: T.Dict[str, Dependency]
- dev_dependencies: T.Dict[str, Dependency]
- build_dependencies: T.Dict[str, Dependency]
- system_dependencies: T.Dict[str, SystemDependency] = dataclasses.field(init=False)
- lib: Library
- bin: T.List[Binary]
- test: T.List[Test]
- bench: T.List[Benchmark]
- example: T.List[Example]
- features: T.Dict[str, T.List[str]]
- target: T.Dict[str, T.Dict[str, Dependency]]
- path: str = ''
-
- def __post_init__(self) -> None:
- self.features.setdefault('default', [])
- self.system_dependencies = {k: SystemDependency.from_raw(k, v) for k, v in self.package.metadata.get('system-deps', {}).items()}
-
-
-def _convert_manifest(raw_manifest: manifest.Manifest, subdir: str, path: str = '') -> Manifest:
- return Manifest(
- Package.from_raw(raw_manifest['package']),
- {k: Dependency.from_raw(k, v) for k, v in raw_manifest.get('dependencies', {}).items()},
- {k: Dependency.from_raw(k, v) for k, v in raw_manifest.get('dev-dependencies', {}).items()},
- {k: Dependency.from_raw(k, v) for k, v in raw_manifest.get('build-dependencies', {}).items()},
- Library.from_raw(raw_manifest.get('lib', {}), raw_manifest['package']['name']),
- [Binary.from_raw(b) for b in raw_manifest.get('bin', {})],
- [Test.from_raw(b) for b in raw_manifest.get('test', {})],
- [Benchmark.from_raw(b) for b in raw_manifest.get('bench', {})],
- [Example.from_raw(b) for b in raw_manifest.get('example', {})],
- raw_manifest.get('features', {}),
- {k: {k2: Dependency.from_raw(k2, v2) for k2, v2 in v.get('dependencies', {}).items()}
- for k, v in raw_manifest.get('target', {}).items()},
- path,
- )
-
-
-def _version_to_api(version: str) -> str:
- # x.y.z -> x
- # 0.x.y -> 0.x
- # 0.0.x -> 0
- vers = version.split('.')
- if int(vers[0]) != 0:
- return vers[0]
- elif len(vers) >= 2 and int(vers[1]) != 0:
- return f'0.{vers[1]}'
- return '0'
-
-
-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:
@@ -458,13 +49,13 @@ def _extra_deps_varname() -> str:
return 'extra_deps'
+@dataclasses.dataclass
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))
@dataclasses.dataclass(frozen=True)
@@ -476,10 +67,16 @@ class PackageKey:
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] = {}
# Map of cargo package (name + api) to its state
self.packages: T.Dict[PackageKey, PackageState] = {}
+ # Rustc's config
+ self.cfgs = self._get_cfgs()
+
+ def get_build_def_files(self) -> T.List[str]:
+ return [os.path.join(subdir, 'Cargo.toml') for subdir in self.manifests]
def interpret(self, subdir: str) -> mparser.CodeBlockNode:
manifest = self._load_manifest(subdir)
@@ -503,9 +100,7 @@ class Interpreter:
ast += self._create_dependencies(pkg, build)
ast += self._create_meson_subdir(build)
- # 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.path, pkg.manifest.lib.path)):
+ if pkg.manifest.lib:
for crate_type in pkg.manifest.lib.crate_type:
ast.extend(self._create_lib(pkg, build, crate_type))
@@ -526,6 +121,10 @@ class Interpreter:
self.environment.wrap_resolver.wraps[meson_depname].type is not None
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:
@@ -538,11 +137,12 @@ class Interpreter:
def _load_manifest(self, subdir: str) -> Manifest:
manifest_ = self.manifests.get(subdir)
if not manifest_:
- filename = os.path.join(self.environment.source_dir, subdir, 'Cargo.toml')
- raw = load_toml(filename)
- if 'package' in raw:
- raw_manifest = T.cast('manifest.Manifest', raw)
- manifest_ = _convert_manifest(raw_manifest, subdir)
+ path = os.path.join(self.environment.source_dir, subdir)
+ filename = os.path.join(path, 'Cargo.toml')
+ toml = load_toml(filename)
+ 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')
@@ -599,6 +199,23 @@ class Interpreter:
else:
self._enable_feature(pkg, f)
+ def _get_cfgs(self) -> T.Dict[str, str]:
+ cfgs = self.host_rustc.get_cfgs().copy()
+ rustflags = self.environment.coredata.get_external_args(MachineChoice.HOST, 'rust')
+ rustflags_i = iter(rustflags)
+ for i in rustflags_i:
+ if i == '--cfg':
+ cfgs.append(next(rustflags_i))
+ return dict(self._split_cfg(i) for i in cfgs)
+
+ @staticmethod
+ def _split_cfg(cfg: str) -> T.Tuple[str, str]:
+ pair = cfg.split('=', maxsplit=1)
+ value = pair[1] if len(pair) > 1 else ''
+ if value and value[0] == '"':
+ value = value[1:-1]
+ return pair[0], value
+
def _create_project(self, pkg: PackageState, build: builder.Builder) -> T.List[mparser.BaseNode]:
"""Create the project() function call
@@ -608,6 +225,7 @@ class Interpreter:
"""
default_options: T.List[mparser.BaseNode] = []
default_options.append(build.string(f'rust_std={pkg.manifest.package.edition}'))
+ default_options.append(build.string(f'build.rust_std={pkg.manifest.package.edition}'))
if pkg.downloaded:
default_options.append(build.string('warning_level=0'))
@@ -643,8 +261,9 @@ class Interpreter:
return ast
def _create_system_dependency(self, name: str, dep: SystemDependency, build: builder.Builder) -> T.List[mparser.BaseNode]:
+ # TODO: handle feature_overrides
kw = {
- 'version': build.array([build.string(s) for s in dep.version]),
+ 'version': build.array([build.string(s) for s in dep.meson_version]),
'required': build.bool(not dep.optional),
}
varname = f'{fixup_meson_varname(name)}_system_dep'
@@ -671,7 +290,7 @@ class Interpreter:
def _create_dependency(self, dep: Dependency, build: builder.Builder) -> T.List[mparser.BaseNode]:
pkg = self._dep_package(dep)
kw = {
- 'version': build.array([build.string(s) for s in dep.version]),
+ 'version': build.array([build.string(s) for s in dep.meson_version]),
}
# Lookup for this dependency with the features we want in default_options kwarg.
#
@@ -747,7 +366,7 @@ class Interpreter:
build.block([build.function('subdir', [build.string('meson')])]))
]
- def _create_lib(self, pkg: PackageState, build: builder.Builder, crate_type: manifest.CRATE_TYPE) -> T.List[mparser.BaseNode]:
+ def _create_lib(self, pkg: PackageState, build: builder.Builder, crate_type: raw.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:
@@ -780,6 +399,9 @@ class Interpreter:
'rust_args': build.array(rust_args),
}
+ depname_suffix = '-rs' if crate_type in {'lib', 'rlib', 'proc-macro'} else f'-{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)
@@ -812,7 +434,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'
@@ -821,7 +444,7 @@ 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'),
],
),
@@ -835,24 +458,23 @@ def load_wraps(source_dir: str, subproject_dir: str) -> T.List[PackageDefinition
filename = os.path.join(source_dir, 'Cargo.lock')
if os.path.exists(filename):
try:
- cargolock = T.cast('manifest.CargoLock', load_toml(filename))
+ toml = load_toml(filename)
except TomlImplementationMissing as e:
mlog.warning('Failed to load Cargo.lock:', str(e), fatal=False)
return wraps
- for package in cargolock['package']:
- name = package['name']
- version = package['version']
- subp_name = _dependency_name(name, _version_to_api(version))
- source = package.get('source')
- if source is None:
+ 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))
+ if package.source is None:
# This is project's package, or one of its workspace members.
pass
- elif source == 'registry+https://github.com/rust-lang/crates.io-index':
- checksum = package.get('checksum')
+ elif package.source == 'registry+https://github.com/rust-lang/crates.io-index':
+ checksum = package.checksum
if checksum is None:
- checksum = cargolock['metadata'][f'checksum {name} {version} ({source})']
- url = f'https://crates.io/api/v1/crates/{name}/{version}/download'
- directory = f'{name}-{version}'
+ 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,
@@ -860,18 +482,18 @@ def load_wraps(source_dir: str, subproject_dir: str) -> T.List[PackageDefinition
'source_hash': checksum,
'method': 'cargo',
}))
- elif source.startswith('git+'):
- parts = urllib.parse.urlparse(source[4:])
+ 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': name,
+ 'directory': package.name,
'url': url,
'revision': revision,
'method': 'cargo',
}))
else:
- mlog.warning(f'Unsupported source URL in {filename}: {source}')
+ mlog.warning(f'Unsupported source URL in {filename}: {package.source}')
return wraps
diff --git a/mesonbuild/cargo/manifest.py b/mesonbuild/cargo/manifest.py
index d95df7f..ab059b0 100644
--- a/mesonbuild/cargo/manifest.py
+++ b/mesonbuild/cargo/manifest.py
@@ -4,244 +4,505 @@
"""Type definitions for cargo manifest files."""
from __future__ import annotations
+
+import dataclasses
+import os
import typing as T
-from typing_extensions import Literal, TypedDict, Required
-
-EDITION = Literal['2015', '2018', '2021']
-CRATE_TYPE = Literal['bin', 'lib', 'dylib', 'staticlib', 'cdylib', 'rlib', 'proc-macro']
-
-Package = TypedDict(
- 'Package',
- {
- 'name': Required[str],
- 'version': Required[str],
- 'authors': T.List[str],
- 'edition': EDITION,
- 'rust-version': str,
- 'description': str,
- 'readme': str,
- 'license': str,
- 'license-file': str,
- 'keywords': T.List[str],
- 'categories': T.List[str],
- 'workspace': str,
- 'build': str,
- 'links': str,
- 'include': T.List[str],
- 'exclude': T.List[str],
- 'publish': bool,
- 'metadata': T.Dict[str, T.Dict[str, str]],
- 'default-run': str,
- 'autolib': bool,
- 'autobins': bool,
- 'autoexamples': bool,
- 'autotests': bool,
- 'autobenches': bool,
- },
- total=False,
-)
-"""A description of the Package Dictionary."""
-
-class FixedPackage(TypedDict, total=False):
-
- """A description of the Package Dictionary, fixed up."""
-
- name: Required[str]
- version: Required[str]
- authors: T.List[str]
- edition: EDITION
- rust_version: str
- description: str
- readme: str
- license: str
- license_file: str
- keywords: T.List[str]
- categories: T.List[str]
- workspace: str
- build: str
- links: str
- include: T.List[str]
- exclude: T.List[str]
- publish: bool
- metadata: T.Dict[str, T.Dict[str, str]]
- default_run: str
- autolib: bool
- autobins: bool
- autoexamples: bool
- autotests: bool
- autobenches: bool
-
-
-class Badge(TypedDict):
-
- """An entry in the badge section."""
-
- status: Literal['actively-developed', 'passively-developed', 'as-is', 'experimental', 'deprecated', 'none']
-
-
-Dependency = TypedDict(
- 'Dependency',
- {
- 'version': str,
- 'registry': str,
- 'git': str,
- 'branch': str,
- 'rev': str,
- 'path': str,
- 'optional': bool,
- 'package': str,
- 'default-features': bool,
- 'features': T.List[str],
- },
- total=False,
-)
-"""An entry in the *dependencies sections."""
+from . import version
+from ..mesonlib import MesonException, lazy_property
+from .. import mlog
+if T.TYPE_CHECKING:
+ from typing_extensions import Protocol, Self
-class FixedDependency(TypedDict, total=False):
+ from . import raw
+ from .raw import EDITION, CRATE_TYPE
- """An entry in the *dependencies sections, fixed up."""
+ # Copied from typeshed. Blarg that they don't expose this
+ class DataclassInstance(Protocol):
+ __dataclass_fields__: T.ClassVar[dict[str, dataclasses.Field[T.Any]]]
- version: T.List[str]
- registry: str
- git: str
- branch: str
- rev: str
- path: str
- optional: bool
- package: str
- default_features: bool
- features: T.List[str]
-
-
-DependencyV = T.Union[Dependency, str]
-"""A Dependency entry, either a string or a Dependency Dict."""
-
-
-_BaseBuildTarget = TypedDict(
- '_BaseBuildTarget',
- {
- 'path': str,
- 'test': bool,
- 'doctest': bool,
- 'bench': bool,
- 'doc': bool,
- 'plugin': bool,
- 'proc-macro': bool,
- 'harness': bool,
- 'edition': EDITION,
- 'crate-type': T.List[CRATE_TYPE],
- 'required-features': T.List[str],
- },
- total=False,
+_DI = T.TypeVar('_DI', bound='DataclassInstance')
+_R = T.TypeVar('_R', bound='raw._BaseBuildTarget')
+
+_EXTRA_KEYS_WARNING = (
+ "This may (unlikely) be an error in the cargo manifest, or may be a missing "
+ "implementation in Meson. If this issue can be reproduced with the latest "
+ "version of Meson, please help us by opening an issue at "
+ "https://github.com/mesonbuild/meson/issues. Please include the crate and "
+ "version that is generating this warning if possible."
)
-class BuildTarget(_BaseBuildTarget, total=False):
+def fixup_meson_varname(name: str) -> str:
+ """Fixup a meson variable name
+
+ :param name: The name to fix
+ :return: the fixed name
+ """
+ return name.replace('-', '_')
+
+
+@T.overload
+def _depv_to_dep(depv: raw.FromWorkspace) -> raw.FromWorkspace: ...
+
+@T.overload
+def _depv_to_dep(depv: raw.DependencyV) -> raw.Dependency: ...
+
+def _depv_to_dep(depv: T.Union[raw.FromWorkspace, raw.DependencyV]) -> T.Union[raw.FromWorkspace, raw.Dependency]:
+ return {'version': depv} if isinstance(depv, str) else depv
+
- name: Required[str]
+def _raw_to_dataclass(raw: T.Mapping[str, object], cls: T.Type[_DI],
+ msg: str, **kwargs: T.Callable[[T.Any], object]) -> _DI:
+ """Fixup raw cargo mappings to ones more suitable for python to consume as dataclass.
-class LibTarget(_BaseBuildTarget, total=False):
+ * Replaces any `-` with `_` in the keys.
+ * Optionally pass values through the functions in kwargs, in order to do
+ recursive conversions.
+ * Remove and warn on keys that are coming from cargo, but are unknown to
+ our representations.
+
+ This is intended to give users the possibility of things proceeding when a
+ new key is added to Cargo.toml that we don't yet handle, but to still warn
+ them that things might not work.
+
+ :param data: The raw data to look at
+ :param cls: The Dataclass derived type that will be created
+ :param msg: the header for the error message. Usually something like "In N structure".
+ :return: The original data structure, but with all unknown keys removed.
+ """
+ new_dict = {}
+ unexpected = set()
+ fields = {x.name for x in dataclasses.fields(cls)}
+ for orig_k, v in raw.items():
+ k = fixup_meson_varname(orig_k)
+ if k not in fields:
+ unexpected.add(orig_k)
+ continue
+ if k in kwargs:
+ v = kwargs[k](v)
+ new_dict[k] = v
+
+ if unexpected:
+ mlog.warning(msg, 'has unexpected keys', '"{}".'.format(', '.join(sorted(unexpected))),
+ _EXTRA_KEYS_WARNING)
+ return cls(**new_dict)
+
+
+@T.overload
+def _inherit_from_workspace(raw: raw.Package,
+ raw_from_workspace: T.Optional[T.Mapping[str, object]],
+ msg: str,
+ **kwargs: T.Callable[[T.Any, T.Any], object]) -> raw.Package: ...
+
+@T.overload
+def _inherit_from_workspace(raw: T.Union[raw.FromWorkspace, raw.Dependency],
+ raw_from_workspace: T.Optional[T.Mapping[str, object]],
+ msg: str,
+ **kwargs: T.Callable[[T.Any, T.Any], object]) -> raw.Dependency: ...
+
+def _inherit_from_workspace(raw_: T.Union[raw.FromWorkspace, raw.Package, raw.Dependency], # type: ignore[misc]
+ raw_from_workspace: T.Optional[T.Mapping[str, object]],
+ msg: str,
+ **kwargs: T.Callable[[T.Any, T.Any], object]) -> T.Mapping[str, object]:
+ # allow accesses by non-literal key below
+ raw = T.cast('T.Mapping[str, object]', raw_)
+
+ if not raw_from_workspace:
+ if raw.get('workspace', False) or \
+ any(isinstance(v, dict) and v.get('workspace', False) for v in raw):
+ raise MesonException(f'Cargo.toml file requests {msg} from workspace')
+
+ return raw
+
+ result = {k: v for k, v in raw.items() if k != 'workspace'}
+ for k, v in raw.items():
+ if isinstance(v, dict) and v.get('workspace', False):
+ if k in raw_from_workspace:
+ result[k] = raw_from_workspace[k]
+ if k in kwargs:
+ result[k] = kwargs[k](v, result[k])
+ else:
+ del result[k]
+
+ if raw.get('workspace', False):
+ for k, v in raw_from_workspace.items():
+ if k not in result or k in kwargs:
+ if k in kwargs:
+ v = kwargs[k](raw.get(k), v)
+ result[k] = v
+ return result
+
+
+@dataclasses.dataclass
+class Package:
+
+ """Representation of a Cargo Package entry, with defaults filled in."""
+
+ name: str
+ version: str
+ description: T.Optional[str] = None
+ resolver: T.Optional[str] = None
+ authors: T.List[str] = dataclasses.field(default_factory=list)
+ edition: EDITION = '2015'
+ rust_version: T.Optional[str] = None
+ documentation: T.Optional[str] = None
+ readme: T.Optional[str] = None
+ homepage: T.Optional[str] = None
+ repository: T.Optional[str] = None
+ license: T.Optional[str] = None
+ license_file: T.Optional[str] = None
+ keywords: T.List[str] = dataclasses.field(default_factory=list)
+ categories: T.List[str] = dataclasses.field(default_factory=list)
+ workspace: T.Optional[str] = None
+ build: T.Optional[str] = None
+ links: T.Optional[str] = None
+ exclude: T.List[str] = dataclasses.field(default_factory=list)
+ include: T.List[str] = dataclasses.field(default_factory=list)
+ publish: bool = True
+ metadata: T.Dict[str, T.Any] = dataclasses.field(default_factory=dict)
+ default_run: T.Optional[str] = None
+ autolib: bool = True
+ autobins: bool = True
+ autoexamples: bool = True
+ autotests: bool = True
+ autobenches: bool = True
+
+ @lazy_property
+ def api(self) -> str:
+ return version.api(self.version)
+
+ @classmethod
+ def from_raw(cls, raw_pkg: raw.Package, workspace: T.Optional[Workspace] = None) -> Self:
+ raw_ws_pkg = None
+ if workspace is not None:
+ raw_ws_pkg = workspace.package
+
+ raw_pkg = _inherit_from_workspace(raw_pkg, raw_ws_pkg, f'Package entry {raw_pkg["name"]}')
+ return _raw_to_dataclass(raw_pkg, cls, f'Package entry {raw_pkg["name"]}')
+
+@dataclasses.dataclass
+class SystemDependency:
+
+ """ Representation of a Cargo system-deps entry
+ https://docs.rs/system-deps/latest/system_deps
+ """
name: str
+ version: str = ''
+ optional: bool = False
+ feature: T.Optional[str] = None
+ # TODO: convert values to dataclass
+ feature_overrides: T.Dict[str, T.Dict[str, str]] = dataclasses.field(default_factory=dict)
+
+ @classmethod
+ def from_raw(cls, name: str, raw: T.Union[T.Dict[str, T.Any], str]) -> SystemDependency:
+ if isinstance(raw, str):
+ raw = {'version': raw}
+ name = raw.get('name', name)
+ version = raw.get('version', '')
+ optional = raw.get('optional', False)
+ feature = raw.get('feature')
+ # Everything else are overrides when certain features are enabled.
+ feature_overrides = {k: v for k, v in raw.items() if k not in {'name', 'version', 'optional', 'feature'}}
+ return cls(name, version, optional, feature, feature_overrides)
+
+ @lazy_property
+ def meson_version(self) -> T.List[str]:
+ vers = self.version.split(',') if self.version else []
+ result: T.List[str] = []
+ for v in vers:
+ v = v.strip()
+ if v[0] not in '><=':
+ v = f'>={v}'
+ result.append(v)
+ return result
+
+ def enabled(self, features: T.Set[str]) -> bool:
+ return self.feature is None or self.feature in features
+
+@dataclasses.dataclass
+class Dependency:
+
+ """Representation of a Cargo Dependency Entry."""
+ package: str
+ version: str = ''
+ registry: T.Optional[str] = None
+ git: T.Optional[str] = None
+ branch: T.Optional[str] = None
+ rev: T.Optional[str] = None
+ path: T.Optional[str] = None
+ optional: bool = False
+ default_features: bool = True
+ features: T.List[str] = dataclasses.field(default_factory=list)
+
+ @lazy_property
+ def meson_version(self) -> T.List[str]:
+ return version.convert(self.version)
+
+ @lazy_property
+ def api(self) -> str:
+ # Extract wanted API version from version constraints.
+ api = set()
+ for v in self.meson_version:
+ if v.startswith(('>=', '==')):
+ api.add(version.api(v[2:].strip()))
+ elif v.startswith('='):
+ api.add(version.api(v[1:].strip()))
+ if not api:
+ return '0'
+ elif len(api) == 1:
+ return api.pop()
+ else:
+ raise MesonException(f'Cannot determine minimum API version from {self.version}.')
+
+ @classmethod
+ def from_raw_dict(cls, name: str, raw_dep: T.Union[raw.FromWorkspace, raw.Dependency], member_path: str = '', raw_ws_dep: T.Optional[raw.Dependency] = None) -> Dependency:
+ raw_dep = _inherit_from_workspace(raw_dep, raw_ws_dep,
+ f'Dependency entry {name}',
+ path=lambda pkg_path, ws_path: os.path.relpath(ws_path, member_path),
+ features=lambda pkg_path, ws_path: (pkg_path or []) + (ws_path or []))
+ raw_dep.setdefault('package', name)
+ return _raw_to_dataclass(raw_dep, cls, f'Dependency entry {name}')
+
+ @classmethod
+ def from_raw(cls, name: str, raw_depv: T.Union[raw.FromWorkspace, raw.DependencyV], member_path: str = '', workspace: T.Optional[Workspace] = None) -> Dependency:
+ """Create a dependency from a raw cargo dictionary or string"""
+ raw_ws_dep: T.Optional[raw.Dependency] = None
+ if workspace is not None:
+ raw_ws_depv = workspace.dependencies.get(name, {})
+ raw_ws_dep = _depv_to_dep(raw_ws_depv)
+
+ raw_dep = _depv_to_dep(raw_depv)
+ return cls.from_raw_dict(name, raw_dep, member_path, raw_ws_dep)
+
+
+@dataclasses.dataclass
+class BuildTarget(T.Generic[_R]):
-class _BaseFixedBuildTarget(TypedDict, total=False):
+ name: str
path: str
- test: bool
- doctest: bool
- bench: bool
- doc: bool
- plugin: bool
- harness: bool
- edition: EDITION
crate_type: T.List[CRATE_TYPE]
- required_features: T.List[str]
+ # https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-test-field
+ # True for lib, bin, test
+ test: bool = True
-class FixedBuildTarget(_BaseFixedBuildTarget, total=False):
+ # https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-doctest-field
+ # True for lib
+ doctest: bool = False
- name: str
+ # https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-bench-field
+ # True for lib, bin, benchmark
+ bench: bool = True
-class FixedLibTarget(_BaseFixedBuildTarget, total=False):
+ # https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-doc-field
+ # True for libraries and binaries
+ doc: bool = False
- name: Required[str]
- proc_macro: bool
+ harness: bool = True
+ edition: EDITION = '2015'
+ required_features: T.List[str] = dataclasses.field(default_factory=list)
+ plugin: bool = False
+ @classmethod
+ def from_raw(cls, raw: _R) -> Self:
+ name = raw.get('name', '<anonymous>')
+ return _raw_to_dataclass(raw, cls, f'Binary entry {name}')
-class Target(TypedDict):
+@dataclasses.dataclass
+class Library(BuildTarget['raw.LibTarget']):
- """Target entry in the Manifest File."""
+ """Representation of a Cargo Library Entry."""
- dependencies: T.Dict[str, DependencyV]
+ doctest: bool = True
+ doc: bool = True
+ path: str = os.path.join('src', 'lib.rs')
+ proc_macro: bool = False
+ crate_type: T.List[CRATE_TYPE] = dataclasses.field(default_factory=lambda: ['lib'])
+ doc_scrape_examples: bool = True
+ @classmethod
+ def from_raw(cls, raw: raw.LibTarget, fallback_name: str) -> Self: # type: ignore[override]
+ # We need to set the name field if it's not set manually, including if
+ # other fields are set in the lib section
+ raw.setdefault('name', fallback_name)
+ return _raw_to_dataclass(raw, cls, f'Library entry {raw["name"]}')
-class Workspace(TypedDict):
- """The representation of a workspace.
+@dataclasses.dataclass
+class Binary(BuildTarget['raw.BuildTarget']):
- In a vritual manifest the :attribute:`members` is always present, but in a
- project manifest, an empty workspace may be provided, in which case the
- workspace is implicitly filled in by values from the path based dependencies.
+ """Representation of a Cargo Bin Entry."""
- the :attribute:`exclude` is always optional
- """
+ doc: bool = True
+ crate_type: T.List[CRATE_TYPE] = dataclasses.field(default_factory=lambda: ['bin'])
- members: T.List[str]
- exclude: T.List[str]
-
-
-Manifest = TypedDict(
- 'Manifest',
- {
- 'package': Required[Package],
- 'badges': T.Dict[str, Badge],
- 'dependencies': T.Dict[str, DependencyV],
- 'dev-dependencies': T.Dict[str, DependencyV],
- 'build-dependencies': T.Dict[str, DependencyV],
- 'lib': LibTarget,
- 'bin': T.List[BuildTarget],
- 'test': T.List[BuildTarget],
- 'bench': T.List[BuildTarget],
- 'example': T.List[BuildTarget],
- 'features': T.Dict[str, T.List[str]],
- 'target': T.Dict[str, Target],
- 'workspace': Workspace,
-
- # TODO: patch?
- # TODO: replace?
- },
- total=False,
-)
-"""The Cargo Manifest format."""
+ @classmethod
+ def from_raw(cls, raw: raw.BuildTarget) -> Self:
+ if 'path' not in raw:
+ raw['path'] = os.path.join('bin', raw['name'] + '.rs')
+ return super().from_raw(raw)
-class VirtualManifest(TypedDict):
+@dataclasses.dataclass
+class Test(BuildTarget['raw.BuildTarget']):
- """The Representation of a virtual manifest.
+ """Representation of a Cargo Test Entry."""
- Cargo allows a root manifest that contains only a workspace, this is called
- a virtual manifest. This doesn't really map 1:1 with any meson concept,
- except perhaps the proposed "meta project".
+ bench: bool = True
+ crate_type: T.List[CRATE_TYPE] = dataclasses.field(default_factory=lambda: ['bin'])
+
+ @classmethod
+ def from_raw(cls, raw: raw.BuildTarget) -> Self:
+ if 'path' not in raw:
+ raw['path'] = os.path.join('tests', raw['name'] + '.rs')
+ return super().from_raw(raw)
+
+@dataclasses.dataclass
+class Benchmark(BuildTarget['raw.BuildTarget']):
+
+ """Representation of a Cargo Benchmark Entry."""
+
+ test: bool = True
+ crate_type: T.List[CRATE_TYPE] = dataclasses.field(default_factory=lambda: ['bin'])
+
+ @classmethod
+ def from_raw(cls, raw: raw.BuildTarget) -> Self:
+ if 'path' not in raw:
+ raw['path'] = os.path.join('benches', raw['name'] + '.rs')
+ return super().from_raw(raw)
+
+
+@dataclasses.dataclass
+class Example(BuildTarget['raw.BuildTarget']):
+
+ """Representation of a Cargo Example Entry."""
+
+ crate_type: T.List[CRATE_TYPE] = dataclasses.field(default_factory=lambda: ['bin'])
+
+ @classmethod
+ def from_raw(cls, raw: raw.BuildTarget) -> Self:
+ if 'path' not in raw:
+ raw['path'] = os.path.join('examples', raw['name'] + '.rs')
+ return super().from_raw(raw)
+
+
+@dataclasses.dataclass
+class Manifest:
+
+ """Cargo Manifest definition.
+
+ Most of these values map up to the Cargo Manifest, but with default values
+ if not provided.
+
+ Cargo subprojects can contain what Meson wants to treat as multiple,
+ interdependent, subprojects.
+
+ :param path: the path within the cargo subproject.
"""
- workspace: Workspace
+ package: Package
+ dependencies: T.Dict[str, Dependency] = dataclasses.field(default_factory=dict)
+ dev_dependencies: T.Dict[str, Dependency] = dataclasses.field(default_factory=dict)
+ build_dependencies: T.Dict[str, Dependency] = dataclasses.field(default_factory=dict)
+ lib: T.Optional[Library] = None
+ bin: T.List[Binary] = dataclasses.field(default_factory=list)
+ test: T.List[Test] = dataclasses.field(default_factory=list)
+ bench: T.List[Benchmark] = dataclasses.field(default_factory=list)
+ example: T.List[Example] = dataclasses.field(default_factory=list)
+ features: T.Dict[str, T.List[str]] = dataclasses.field(default_factory=dict)
+ target: T.Dict[str, T.Dict[str, Dependency]] = dataclasses.field(default_factory=dict)
+
+ path: str = ''
+
+ def __post_init__(self) -> None:
+ self.features.setdefault('default', [])
+
+ @lazy_property
+ def system_dependencies(self) -> T.Dict[str, SystemDependency]:
+ return {k: SystemDependency.from_raw(k, v) for k, v in self.package.metadata.get('system-deps', {}).items()}
+
+ @classmethod
+ def from_raw(cls, raw: raw.Manifest, path: str = '', workspace: T.Optional[Workspace] = None, member_path: str = '') -> Self:
+ # Libs are always auto-discovered and there's no other way to handle them,
+ # which is unfortunate for reproducability
+ pkg = Package.from_raw(raw['package'], workspace)
+ if pkg.autolib and 'lib' not in raw and \
+ os.path.exists(os.path.join(path, 'src/lib.rs')):
+ raw['lib'] = {}
+ fixed = _raw_to_dataclass(raw, cls, f'Cargo.toml package {raw["package"]["name"]}',
+ package=lambda x: pkg,
+ dependencies=lambda x: {k: Dependency.from_raw(k, v, member_path, workspace) for k, v in x.items()},
+ dev_dependencies=lambda x: {k: Dependency.from_raw(k, v, member_path, workspace) for k, v in x.items()},
+ build_dependencies=lambda x: {k: Dependency.from_raw(k, v, member_path, workspace) for k, v in x.items()},
+ lib=lambda x: Library.from_raw(x, raw['package']['name']),
+ bin=lambda x: [Binary.from_raw(b) for b in x],
+ test=lambda x: [Test.from_raw(b) for b in x],
+ bench=lambda x: [Benchmark.from_raw(b) for b in x],
+ example=lambda x: [Example.from_raw(b) for b in x],
+ target=lambda x: {k: {k2: Dependency.from_raw(k2, v2, member_path, workspace) for k2, v2 in v.get('dependencies', {}).items()}
+ for k, v in x.items()})
+ fixed.path = path
+ return fixed
+
+
+@dataclasses.dataclass
+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)
+
+ # inheritable settings are kept in raw format, for use with _inherit_from_workspace
+ package: T.Optional[raw.Package] = None
+ dependencies: T.Dict[str, raw.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)
-class CargoLockPackage(TypedDict, total=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_to_dataclass(ws_raw, cls, 'Workspace')
+ return fixed
+
+
+@dataclasses.dataclass
+class CargoLockPackage:
"""A description of a package in the Cargo.lock file format."""
name: str
version: str
- source: str
- checksum: str
+ source: T.Optional[str] = None
+ checksum: T.Optional[str] = None
+ dependencies: T.List[str] = dataclasses.field(default_factory=list)
+ @classmethod
+ def from_raw(cls, raw: raw.CargoLockPackage) -> CargoLockPackage:
+ return _raw_to_dataclass(raw, cls, 'Cargo.lock package')
-class CargoLock(TypedDict, total=False):
+
+@dataclasses.dataclass
+class CargoLock:
"""A description of the Cargo.lock file format."""
- version: str
- package: T.List[CargoLockPackage]
- metadata: T.Dict[str, str]
+ version: int = 1
+ package: T.List[CargoLockPackage] = dataclasses.field(default_factory=list)
+ metadata: T.Dict[str, str] = dataclasses.field(default_factory=dict)
+
+ @classmethod
+ def from_raw(cls, raw: raw.CargoLock) -> CargoLock:
+ return _raw_to_dataclass(raw, cls, 'Cargo.lock',
+ package=lambda x: [CargoLockPackage.from_raw(p) for p in x])
diff --git a/mesonbuild/cargo/raw.py b/mesonbuild/cargo/raw.py
new file mode 100644
index 0000000..67dd58a
--- /dev/null
+++ b/mesonbuild/cargo/raw.py
@@ -0,0 +1,192 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright © 2022-2024 Intel Corporation
+
+"""Type definitions for cargo manifest files."""
+
+from __future__ import annotations
+import typing as T
+
+from typing_extensions import Literal, TypedDict, Required
+
+EDITION = Literal['2015', '2018', '2021']
+CRATE_TYPE = Literal['bin', 'lib', 'dylib', 'staticlib', 'cdylib', 'rlib', 'proc-macro']
+
+
+class FromWorkspace(TypedDict):
+
+ """An entry or section that is copied from the workspace."""
+
+ workspace: bool
+
+
+Package = TypedDict(
+ 'Package',
+ {
+ 'name': Required[str],
+ 'version': Required[T.Union[FromWorkspace, str]],
+ 'authors': T.Union[FromWorkspace, T.List[str]],
+ 'edition': T.Union[FromWorkspace, EDITION],
+ 'rust-version': T.Union[FromWorkspace, str],
+ 'description': T.Union[FromWorkspace, str],
+ 'readme': T.Union[FromWorkspace, str],
+ 'license': T.Union[FromWorkspace, str],
+ 'license-file': T.Union[FromWorkspace, str],
+ 'keywords': T.Union[FromWorkspace, T.List[str]],
+ 'categories': T.Union[FromWorkspace, T.List[str]],
+ 'homepage': T.Union[FromWorkspace, str],
+ 'repository': T.Union[FromWorkspace, str],
+ 'documentation': T.Union[FromWorkspace, str],
+ 'workspace': str,
+ 'build': str,
+ 'links': str,
+ 'include': T.Union[FromWorkspace, T.List[str]],
+ 'exclude': T.Union[FromWorkspace, T.List[str]],
+ 'publish': T.Union[FromWorkspace, bool],
+ 'metadata': T.Dict[str, T.Dict[str, str]],
+ 'default-run': str,
+ 'autolib': bool,
+ 'autobins': bool,
+ 'autoexamples': bool,
+ 'autotests': bool,
+ 'autobenches': bool,
+ },
+ total=False,
+)
+"""A description of the Package Dictionary."""
+
+class Badge(TypedDict):
+
+ """An entry in the badge section."""
+
+ status: Literal['actively-developed', 'passively-developed', 'as-is', 'experimental', 'deprecated', 'none']
+
+
+Dependency = TypedDict(
+ 'Dependency',
+ {
+ 'version': str,
+ 'registry': str,
+ 'git': str,
+ 'branch': str,
+ 'rev': str,
+ 'path': str,
+ 'optional': bool,
+ 'package': str,
+ 'default-features': bool,
+ 'features': T.List[str],
+ },
+ total=False,
+)
+"""An entry in the *dependencies sections."""
+
+
+DependencyV = T.Union[Dependency, str]
+"""A Dependency entry, either a string or a Dependency Dict."""
+
+
+_BaseBuildTarget = TypedDict(
+ '_BaseBuildTarget',
+ {
+ 'path': str,
+ 'test': bool,
+ 'doctest': bool,
+ 'bench': bool,
+ 'doc': bool,
+ 'plugin': bool,
+ 'proc-macro': bool,
+ 'harness': bool,
+ 'edition': EDITION,
+ 'crate-type': T.List[CRATE_TYPE],
+ 'required-features': T.List[str],
+ },
+ total=False,
+)
+
+
+class BuildTarget(_BaseBuildTarget, total=False):
+
+ name: Required[str]
+
+
+class LibTarget(_BaseBuildTarget, total=False):
+
+ name: str
+
+
+class Target(TypedDict):
+
+ """Target entry in the Manifest File."""
+
+ dependencies: T.Dict[str, T.Union[FromWorkspace, DependencyV]]
+
+
+class Workspace(TypedDict):
+
+ """The representation of a workspace.
+
+ In a vritual manifest the :attribute:`members` is always present, but in a
+ project manifest, an empty workspace may be provided, in which case the
+ workspace is implicitly filled in by values from the path based dependencies.
+
+ the :attribute:`exclude` is always optional
+ """
+
+ members: T.List[str]
+ exclude: T.List[str]
+ package: Package
+ dependencies: T.Dict[str, DependencyV]
+
+
+Manifest = TypedDict(
+ 'Manifest',
+ {
+ 'package': Required[Package],
+ 'badges': T.Dict[str, Badge],
+ 'dependencies': T.Dict[str, T.Union[FromWorkspace, DependencyV]],
+ 'dev-dependencies': T.Dict[str, T.Union[FromWorkspace, DependencyV]],
+ 'build-dependencies': T.Dict[str, T.Union[FromWorkspace, DependencyV]],
+ 'lib': LibTarget,
+ 'bin': T.List[BuildTarget],
+ 'test': T.List[BuildTarget],
+ 'bench': T.List[BuildTarget],
+ 'example': T.List[BuildTarget],
+ 'features': T.Dict[str, T.List[str]],
+ 'target': T.Dict[str, Target],
+ 'workspace': Workspace,
+
+ # TODO: patch?
+ # TODO: replace?
+ },
+ total=False,
+)
+"""The Cargo Manifest format."""
+
+
+class VirtualManifest(TypedDict, total=False):
+
+ """The Representation of a virtual manifest.
+
+ Cargo allows a root manifest that contains only a workspace, this is called
+ a virtual manifest. This doesn't really map 1:1 with any meson concept,
+ except perhaps the proposed "meta project".
+ """
+
+ workspace: Workspace
+
+class CargoLockPackage(TypedDict, total=False):
+
+ """A description of a package in the Cargo.lock file format."""
+
+ name: str
+ version: str
+ source: str
+ checksum: str
+
+
+class CargoLock(TypedDict, total=False):
+
+ """A description of the Cargo.lock file format."""
+
+ version: int
+ package: T.List[CargoLockPackage]
+ metadata: T.Dict[str, str]
diff --git a/mesonbuild/cargo/toml.py b/mesonbuild/cargo/toml.py
new file mode 100644
index 0000000..601510e
--- /dev/null
+++ b/mesonbuild/cargo/toml.py
@@ -0,0 +1,49 @@
+from __future__ import annotations
+
+import importlib
+import shutil
+import json
+import typing as T
+
+from ..mesonlib import MesonException, Popen_safe
+if T.TYPE_CHECKING:
+ from types import ModuleType
+
+
+# tomllib is present in python 3.11, before that it is a pypi module called tomli,
+# we try to import tomllib, then tomli,
+tomllib: T.Optional[ModuleType] = None
+toml2json: T.Optional[str] = None
+for t in ['tomllib', 'tomli']:
+ try:
+ tomllib = importlib.import_module(t)
+ break
+ except ImportError:
+ pass
+else:
+ # TODO: it would be better to use an Executable here, which could be looked
+ # up in the cross file or provided by a wrap. However, that will have to be
+ # passed in externally, since we don't have (and I don't think we should),
+ # have access to the `Environment` for that in this module.
+ toml2json = shutil.which('toml2json')
+
+class TomlImplementationMissing(MesonException):
+ pass
+
+
+def load_toml(filename: str) -> T.Dict[str, object]:
+ if tomllib:
+ with open(filename, 'rb') as f:
+ raw = tomllib.load(f)
+ else:
+ if toml2json is None:
+ raise TomlImplementationMissing('Could not find an implementation of tomllib, nor toml2json')
+
+ p, out, err = Popen_safe([toml2json, filename])
+ if p.returncode != 0:
+ raise MesonException('toml2json failed to decode output\n', err)
+
+ raw = json.loads(out)
+
+ # tomllib.load() returns T.Dict[str, T.Any] but not other implementations.
+ return T.cast('T.Dict[str, object]', raw)
diff --git a/mesonbuild/cargo/version.py b/mesonbuild/cargo/version.py
index cde7a83..ce58945 100644
--- a/mesonbuild/cargo/version.py
+++ b/mesonbuild/cargo/version.py
@@ -7,6 +7,18 @@ from __future__ import annotations
import typing as T
+def api(version: str) -> str:
+ # x.y.z -> x
+ # 0.x.y -> 0.x
+ # 0.0.x -> 0
+ vers = version.split('.')
+ if int(vers[0]) != 0:
+ return vers[0]
+ elif len(vers) >= 2 and int(vers[1]) != 0:
+ return f'0.{vers[1]}'
+ return '0'
+
+
def convert(cargo_ver: str) -> T.List[str]:
"""Convert a Cargo compatible version into a Meson compatible one.
@@ -15,6 +27,8 @@ def convert(cargo_ver: str) -> T.List[str]:
"""
# Cleanup, just for safety
cargo_ver = cargo_ver.strip()
+ if not cargo_ver:
+ return []
cargo_vers = [c.strip() for c in cargo_ver.split(',')]
out: T.List[str] = []