| # SPDX-License-Identifier: Apache-2.0 |
| # Copyright © 2022-2024 Intel Corporation |
| |
| """Interpreter for converting Cargo Toml definitions to Meson AST |
| |
| There are some notable limits here. We don't even try to convert something with |
| a build.rs: there's so few limits on what Cargo allows a build.rs (basically |
| none), and no good way for us to convert them. In that case, an actual meson |
| 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, version, cfg |
| from ..mesonlib import MesonException, Popen_safe, 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 mparser |
| 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_varname(package_name: str) -> str: |
| return f'{fixup_meson_varname(package_name)}_dep' |
| |
| |
| def _extra_args_varname() -> str: |
| return 'extra_args' |
| |
| |
| def _extra_deps_varname() -> str: |
| return 'extra_deps' |
| |
| |
| 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) |
| |
| |
| @dataclasses.dataclass(frozen=True) |
| class PackageKey: |
| package_name: str |
| api: str |
| |
| |
| 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 interpret(self, subdir: str) -> 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'), |
| build.function('message', [ |
| build.string('Enabled features:'), |
| build.array([build.string(f) for f in pkg.features]), |
| ]), |
| ] |
| 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)): |
| for crate_type in pkg.manifest.lib.crate_type: |
| ast.extend(self._create_lib(pkg, build, crate_type)) |
| |
| return build.block(ast) |
| |
| 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) |
| subprojects_dir = os.path.join(subdir, 'subprojects') |
| self.environment.wrap_resolver.load_and_merge(subprojects_dir, T.cast('SubProject', meson_depname)) |
| 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 |
| 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) |
| return pkg, False |
| |
| def _dep_package(self, dep: Dependency) -> PackageState: |
| return self.packages[PackageKey(dep.package, dep.api)] |
| |
| 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) |
| self.manifests[subdir] = manifest_ |
| else: |
| raise MesonException(f'{subdir}/Cargo.toml does not have [package] section') |
| return manifest_ |
| |
| def _add_dependency(self, pkg: PackageState, depname: str) -> None: |
| if depname in pkg.required_deps: |
| 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') |
| pkg.required_deps.add(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: |
| self._enable_feature(dep_pkg, f) |
| for f in pkg.optional_deps_features[depname]: |
| self._enable_feature(dep_pkg, f) |
| |
| def _enable_feature(self, pkg: PackageState, feature: str) -> None: |
| if feature in pkg.features: |
| return |
| pkg.features.add(feature) |
| # A feature can also be a dependency. |
| if feature in pkg.manifest.dependencies: |
| self._add_dependency(pkg, feature) |
| # Recurse on extra features and dependencies this feature pulls. |
| # https://doc.rust-lang.org/cargo/reference/features.html#the-features-section |
| for f in pkg.manifest.features.get(feature, []): |
| if '/' in f: |
| depname, dep_f = f.split('/', 1) |
| if depname[-1] == '?': |
| depname = depname[:-1] |
| if depname in pkg.required_deps: |
| dep = pkg.manifest.dependencies[depname] |
| dep_pkg = self._dep_package(dep) |
| self._enable_feature(dep_pkg, dep_f) |
| else: |
| # This feature will be enabled only if that dependency |
| # is later added. |
| pkg.optional_deps_features[depname].add(dep_f) |
| else: |
| self._add_dependency(pkg, depname) |
| dep = pkg.manifest.dependencies.get(depname) |
| if dep: |
| dep_pkg = self._dep_package(dep) |
| self._enable_feature(dep_pkg, dep_f) |
| elif f.startswith('dep:'): |
| self._add_dependency(pkg, f[4:]) |
| 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 |
| |
| :param pkg: The package to generate from |
| :param build: The AST builder |
| :return: a list nodes |
| """ |
| 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')) |
| |
| args: T.List[mparser.BaseNode] = [] |
| args.extend([ |
| build.string(pkg.manifest.package.name), |
| build.string('rust'), |
| ]) |
| kwargs: T.Dict[str, mparser.BaseNode] = { |
| 'version': build.string(pkg.manifest.package.version), |
| # Always assume that the generated meson is using the latest features |
| # This will warn when when we generate deprecated code, which is helpful |
| # for the upkeep of the module |
| 'meson_version': build.string(f'>= {coredata.stable_version}'), |
| 'default_options': build.array(default_options), |
| } |
| if pkg.manifest.package.license: |
| kwargs['license'] = build.string(pkg.manifest.package.license) |
| elif pkg.manifest.package.license_file: |
| kwargs['license_files'] = build.string(pkg.manifest.package.license_file) |
| |
| return [build.function('project', args, kwargs)] |
| |
| def _create_dependencies(self, pkg: PackageState, build: builder.Builder) -> T.List[mparser.BaseNode]: |
| ast: T.List[mparser.BaseNode] = [] |
| for depname in pkg.required_deps: |
| dep = pkg.manifest.dependencies[depname] |
| ast += self._create_dependency(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): |
| ast += self._create_system_dependency(name, sys_dep, build) |
| return ast |
| |
| def _create_system_dependency(self, name: str, dep: SystemDependency, build: builder.Builder) -> T.List[mparser.BaseNode]: |
| kw = { |
| 'version': build.array([build.string(s) for s in dep.version]), |
| 'required': build.bool(not dep.optional), |
| } |
| varname = f'{fixup_meson_varname(name)}_system_dep' |
| cfg = f'system_deps_have_{fixup_meson_varname(name)}' |
| return [ |
| build.assign( |
| build.function( |
| 'dependency', |
| [build.string(dep.name)], |
| kw, |
| ), |
| varname, |
| ), |
| build.if_( |
| build.method('found', build.identifier(varname)), build.block([ |
| build.plusassign( |
| build.array([build.string('--cfg'), build.string(cfg)]), |
| 'system_deps_args' |
| ), |
| ]) |
| ), |
| ] |
| |
| 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]), |
| } |
| # Lookup for this dependency with the features we want in default_options kwarg. |
| # |
| # However, this subproject could have been previously configured with a |
| # different set of features. Cargo collects the set of features globally |
| # but Meson can only use features enabled by the first call that triggered |
| # the configuration of that subproject. |
| # |
| # Verify all features that we need are actually enabled for that dependency, |
| # otherwise abort with an error message. The user has to set the corresponding |
| # option manually with -Dxxx-rs:feature-yyy=true, or the main project can do |
| # that in its project(..., default_options: ['xxx-rs:feature-yyy=true']). |
| return [ |
| # xxx_dep = dependency('xxx', version : ...) |
| build.assign( |
| build.function( |
| 'dependency', |
| [build.string(_dependency_name(dep.package, dep.api))], |
| kw, |
| ), |
| _dependency_varname(dep.package), |
| ), |
| # actual_features = xxx_dep.get_variable('features', default_value : '').split(',') |
| build.assign( |
| build.method( |
| 'split', |
| build.method( |
| 'get_variable', |
| build.identifier(_dependency_varname(dep.package)), |
| [build.string('features')], |
| {'default_value': build.string('')} |
| ), |
| [build.string(',')], |
| ), |
| 'actual_features' |
| ), |
| # needed_features = [f1, f2, ...] |
| # foreach f : needed_features |
| # if f not in actual_features |
| # error() |
| # endif |
| # endforeach |
| build.assign(build.array([build.string(f) for f in pkg.features]), 'needed_features'), |
| build.foreach(['f'], build.identifier('needed_features'), build.block([ |
| 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('previously configured with features'), |
| build.identifier('actual_features'), |
| build.string('but need'), |
| build.identifier('needed_features'), |
| ]) |
| ])) |
| ])), |
| ] |
| |
| def _create_meson_subdir(self, build: builder.Builder) -> T.List[mparser.BaseNode]: |
| # Allow Cargo subprojects to add extra Rust args in meson/meson.build file. |
| # This is used to replace build.rs logic. |
| |
| # extra_args = [] |
| # extra_deps = [] |
| # fs = import('fs') |
| # if fs.is_dir('meson') |
| # subdir('meson') |
| # endif |
| return [ |
| build.assign(build.array([]), _extra_args_varname()), |
| build.assign(build.array([]), _extra_deps_varname()), |
| build.assign(build.function('import', [build.string('fs')]), 'fs'), |
| build.if_(build.method('is_dir', build.identifier('fs'), [build.string('meson')]), |
| 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]: |
| 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_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(): |
| if sys_dep.enabled(pkg.features): |
| dependencies.append(build.identifier(f'{fixup_meson_varname(name)}_system_dep')) |
| |
| rust_args: T.List[mparser.BaseNode] = [ |
| build.identifier('features_args'), |
| build.identifier(_extra_args_varname()), |
| build.identifier('system_deps_args'), |
| ] |
| |
| dependencies.append(build.identifier(_extra_deps_varname())) |
| |
| posargs: T.List[mparser.BaseNode] = [ |
| build.string(fixup_meson_varname(pkg.manifest.lib.name)), |
| build.string(pkg.manifest.lib.path), |
| ] |
| |
| kwargs: T.Dict[str, mparser.BaseNode] = { |
| 'dependencies': build.array(dependencies), |
| 'rust_dependency_map': build.dict(dependency_map), |
| 'rust_args': build.array(rust_args), |
| } |
| |
| lib: mparser.BaseNode |
| if pkg.manifest.lib.proc_macro or crate_type == 'proc-macro': |
| lib = build.method('proc_macro', build.identifier('rust'), posargs, kwargs) |
| else: |
| if crate_type in {'lib', 'rlib', 'staticlib'}: |
| target_type = 'static_library' |
| elif crate_type in {'dylib', 'cdylib'}: |
| target_type = 'shared_library' |
| else: |
| raise MesonException(f'Unsupported crate type {crate_type}') |
| if crate_type in {'staticlib', 'cdylib'}: |
| kwargs['rust_abi'] = build.string('c') |
| lib = build.function(target_type, posargs, kwargs) |
| |
| features_args: T.List[mparser.BaseNode] = [] |
| for f in pkg.features: |
| features_args += [build.string('--cfg'), build.string(f'feature="{f}"')] |
| |
| # features_args = ['--cfg', 'feature="f1"', ...] |
| # lib = xxx_library() |
| # dep = declare_dependency() |
| # meson.override_dependency() |
| return [ |
| build.assign(build.array(features_args), 'features_args'), |
| build.assign(lib, 'lib'), |
| build.assign( |
| build.function( |
| 'declare_dependency', |
| kw={ |
| 'link_with': build.identifier('lib'), |
| 'variables': build.dict({ |
| build.string('features'): build.string(','.join(pkg.features)), |
| }) |
| }, |
| ), |
| 'dep' |
| ), |
| build.method( |
| 'override_dependency', |
| build.identifier('meson'), |
| [ |
| build.string(_dependency_name(pkg.manifest.package.name, pkg.manifest.package.api)), |
| build.identifier('dep'), |
| ], |
| ), |
| ] |
| |
| |
| def load_wraps(source_dir: str, subproject_dir: str) -> T.List[PackageDefinition]: |
| """ Convert Cargo.lock into a list of wraps """ |
| |
| wraps: 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)) |
| 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: |
| # 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') |
| 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}' |
| 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', |
| })) |
| elif source.startswith('git+'): |
| parts = urllib.parse.urlparse(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, |
| 'url': url, |
| 'revision': revision, |
| 'method': 'cargo', |
| })) |
| else: |
| mlog.warning(f'Unsupported source URL in {filename}: {source}') |
| return wraps |