From 86f8864de14957c8da890baa3f7d804fa0379b4a Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 29 Oct 2020 14:21:21 -0700 Subject: [PATCH 01/23] interpreter: Annotate Interpreter.subprojects I'm using a type alias for now, as I plan to change the type later in this series --- mesonbuild/interpreter/interpreter.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 170092d46572..ceea000513af 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -241,7 +241,7 @@ def __init__( self.processed_buildfiles = set() # type: T.Set[str] self.project_args_frozen = False self.global_args_frozen = False # implies self.project_args_frozen - self.subprojects = {} + self.subprojects: T.Dict['SubprojectKeyType', SubprojectHolder] = {} self.subproject_stack = [] self.configure_file_outputs = {} # Passed from the outside, only used in subprojects. @@ -695,20 +695,20 @@ def func_subproject(self, nodes, args, kwargs): subp_name = args[0] return self.do_subproject(subp_name, 'meson', kwargs) - def disabled_subproject(self, subp_name, disabled_feature=None, exception=None): + def disabled_subproject(self, subp_name: 'SubprojectKeyType', disabled_feature=None, exception=None): sub = SubprojectHolder(None, os.path.join(self.subproject_dir, subp_name), disabled_feature=disabled_feature, exception=exception) self.subprojects[subp_name] = sub self.coredata.initialized_subprojects.add(subp_name) return sub - def get_subproject(self, subp_name): + def get_subproject(self, subp_name: 'SubprojectKeyType') -> T.Optional[SubprojectHolder]: sub = self.subprojects.get(subp_name) if sub and sub.found(): return sub return None - def do_subproject(self, subp_name: str, method: str, kwargs): + def do_subproject(self, subp_name: 'SubprojectKeyType', method: str, kwargs) -> SubprojectHolder: disabled, required, feature = extract_required_kwarg(kwargs, self.subproject) if disabled: mlog.log('Subproject', mlog.bold(subp_name), ':', 'skipped: feature', mlog.bold(feature), 'disabled') @@ -1459,7 +1459,7 @@ def verify_fallback_consistency(self, subp_name, varname, cached_dep): m = 'Inconsistency: Subproject has overridden the dependency with another variable than {!r}' raise DependencyException(m.format(varname)) - def get_subproject_dep(self, name, display_name, subp_name, varname, kwargs): + def get_subproject_dep(self, name, display_name, subp_name: 'SubprojectKeyType', varname, kwargs): required = kwargs.get('required', True) wanted = mesonlib.stringlistify(kwargs.get('version', [])) dep = self.notfound_dependency() From 2295e3d053ecea0f0a3d4d98c137d054c2cf1eb7 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 29 Oct 2020 14:31:54 -0700 Subject: [PATCH 02/23] Add a nodebuilder module This module implements an AST building API that is somewhat nice to work with. There's still some things here that could use cleanup, but in general it's okay to work with. TODO: needs some documentation --- mesonbuild/cargo/nodebuilder.py | 343 ++++++++++++++++++++++++++++++++ run_mypy.py | 1 + 2 files changed, 344 insertions(+) create mode 100644 mesonbuild/cargo/nodebuilder.py diff --git a/mesonbuild/cargo/nodebuilder.py b/mesonbuild/cargo/nodebuilder.py new file mode 100644 index 000000000000..4d11a23356ad --- /dev/null +++ b/mesonbuild/cargo/nodebuilder.py @@ -0,0 +1,343 @@ +# Copyright © 2020 Intel Corporation + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A set of classes to simplify building AST nodes. + +These provide a simplified API that allows the user to spend less time +worrying about building the AST correctly. +""" + +from pathlib import Path +import contextlib +import typing as T + +from .. import mparser +from ..mesonlib import MesonException + +if T.TYPE_CHECKING: + TYPE_mixed = T.Union[str, int, bool, Path, mparser.BaseNode] + TYPE_mixed_list = T.Union[TYPE_mixed, T.Sequence[TYPE_mixed]] + TYPE_mixed_dict = T.Dict[str, TYPE_mixed_list] + + +__all__ = ['NodeBuilder'] + + +class _Builder: + + """Private helper class for shared utility functions.""" + + def __init__(self, subdir: Path) -> None: + self.subdir = subdir + # Keep a public set of all assigned build targtets + # This is neede to reference a target later + self.variables: T.Set[str] = set() + + def empty(self) -> mparser.BaseNode: + return mparser.BaseNode(0, 0, '') + + def token(self, value: 'TYPE_mixed_list' = '') -> mparser.Token: + return mparser.Token('', self.subdir.as_posix(), 0, 0, 0, None, value) + + def nodeify(self, value: 'TYPE_mixed_list') -> mparser.BaseNode: + if isinstance(value, str): + return self.string(value) + elif isinstance(value, Path): + return self.string(value.as_posix()) + elif isinstance(value, bool): + return self.bool(value) + elif isinstance(value, int): + return self.number(value) + elif isinstance(value, list): + return self.array(value) + elif isinstance(value, mparser.BaseNode): + return value + raise RuntimeError('invalid type of value: {} ({})'.format(type(value).__name__, str(value))) + + def string(self, value: str) -> mparser.StringNode: + return mparser.StringNode(self.token(value)) + + def bool(self, value: bool) -> mparser.BooleanNode: + return mparser.BooleanNode(self.token(value)) + + def number(self, value: int) -> mparser.NumberNode: + return mparser.NumberNode(self.token(value)) + + def id(self, value: str) -> mparser.IdNode: + return mparser.IdNode(self.token(value)) + + def array(self, value: 'TYPE_mixed_list') -> mparser.ArrayNode: + args = self.arguments(value, {}) + return mparser.ArrayNode(args, 0, 0, 0, 0) + + def arguments(self, args: 'TYPE_mixed_list', kwargs: 'TYPE_mixed_dict') -> mparser.ArgumentNode: + node = mparser.ArgumentNode(self.token()) + node.arguments = [self.nodeify(x) for x in args] + node.kwargs = {self.id(k): self.nodeify(v) for k, v in kwargs.items()} + return node + + def function(self, name: str, args: T.Optional[mparser.ArgumentNode] = None) -> mparser.FunctionNode: + if args is None: + args = self.arguments([], {}) + return mparser.FunctionNode(self.subdir.as_posix(), 0, 0, 0, 0, name, args) + + def assign(self, name: str, value: mparser.BaseNode) -> mparser.AssignmentNode: + return mparser.AssignmentNode(self.subdir.as_posix(), 0, 0, name, value) + + def plus_assign(self, name: str, value: mparser.BaseNode) -> mparser.PlusAssignmentNode: + return mparser.PlusAssignmentNode(self.subdir.as_posix(), 0, 0, name, value) + + def method(self, name: str, base: mparser.BaseNode, args: mparser.ArgumentNode) -> mparser.MethodNode: + return mparser.MethodNode('', 0, 0, base, name, args) + + +class ArgumentBuilder: + + def __init__(self, builder: _Builder): + self._builder = builder + self._posargs: T.List['TYPE_mixed'] = [] + self._kwargs: 'TYPE_mixed_dict' = {} + + def positional(self, arg: 'TYPE_mixed_list') -> None: + self._posargs.append(arg) + + def keyword(self, name: str, arg: 'TYPE_mixed_list') -> None: + assert name not in self._kwargs + self._kwargs[name] = arg + + def finalize(self) -> mparser.ArgumentNode: + # XXX: I think this is okay + return self._builder.arguments(self._posargs, self._kwargs) + + +class FunctionBuilder: + + def __init__(self, name: str, builder: _Builder): + self.name = name + self._builder = builder + self._arguments = builder.arguments([], {}) + self._methods: T.List[T.Tuple[str, mparser.ArgumentNode]] = [] + + @contextlib.contextmanager + def argument_builder(self) -> T.Iterator[ArgumentBuilder]: + b = ArgumentBuilder(self._builder) + yield b + self._arguments = b.finalize() + + @contextlib.contextmanager + def method_builder(self, name: str) -> T.Iterator[ArgumentBuilder]: + b = ArgumentBuilder(self._builder) + yield b + self._methods.append((name, b.finalize())) + + def finalize(self) -> T.Union[mparser.FunctionNode, mparser.MethodNode]: + cur: T.Union[mparser.FunctionNode, mparser.MethodNode] = \ + self._builder.function(self.name, self._arguments) + # go over the methods in reversed order, emmited a Method Node for each of them + for name, args in reversed(self._methods): + cur = self._builder.method(name, cur, args) + return cur + + +class AssignmentBuilder: + + def __init__(self, name: str, builder: _Builder): + self.name = name + self._builder = builder + self._node: T.Optional[mparser.AssignmentNode] = None + + @contextlib.contextmanager + def function_builder(self, name: str) -> T.Iterator[ArgumentBuilder]: + b = FunctionBuilder(name, self._builder) + with b.argument_builder() as a: + yield a + self._node = self._builder.assign(self.name, b.finalize()) + + @contextlib.contextmanager + def array_builder(self) -> T.Iterator[ArgumentBuilder]: + b = ArgumentBuilder(self._builder) + yield b + array = mparser.ArrayNode(b.finalize(), 0, 0, 0, 0) # _builder.array expects raw arguments + self._node = self._builder.assign(self.name, array) + + def finalize(self) -> mparser.AssignmentNode: + assert self._node is not None, 'You need to build an assignment before finalizing' + return self._node + + +class PlusAssignmentBuilder: + + def __init__(self, name: str, builder: _Builder): + self.name = name + self._builder = builder + self._node: T.Optional[mparser.PlusAssignmentNode] = None + + @contextlib.contextmanager + def function_builder(self, name: str) -> T.Iterator[ArgumentBuilder]: + b = FunctionBuilder(name, self._builder) + with b.argument_builder() as a: + yield a + self._node = self._builder.plus_assign(self.name, b.finalize()) + + @contextlib.contextmanager + def array_builder(self) -> T.Iterator[ArgumentBuilder]: + b = ArgumentBuilder(self._builder) + yield b + array = mparser.ArrayNode(b.finalize(), 0, 0, 0, 0) # _builder.array expects raw arguments + self._node = self._builder.plus_assign(self.name, array) + + def finalize(self) -> mparser.PlusAssignmentNode: + assert self._node is not None, 'You need to build an assignment before finalizing' + return self._node + + +class IfBuilder: + + def __init__(self, builder: _Builder): + self._builder = builder + self._condition: T.Optional[mparser.BaseNode] = None + self._body: T.Optional[mparser.CodeBlockNode] = None + + @contextlib.contextmanager + def condition_builder(self) -> T.Iterator['NodeBuilder']: + b = NodeBuilder(_builder=self._builder) + yield b + cond = b.finalize() + assert len(cond.lines) == 1, 'this is a bit of a hack' + self._condition = cond.lines[0] + + @contextlib.contextmanager + def body_builder(self) -> T.Iterator['NodeBuilder']: + b = NodeBuilder(_builder=self._builder) + yield b + self._body = b.finalize() + + def finalize(self) -> T.Union[mparser.IfNode, mparser.CodeBlockNode]: + # If this is a CodeBlockNode, it's the `else` clause + assert self._body is not None, 'A body is required' + if self._condition: + return mparser.IfNode(self._builder.empty(), self._condition, self._body) + return self._body + + +class IfClauseBuilder: + + def __init__(self, builder: _Builder): + self._builder = builder + self._node = mparser.IfClauseNode(mparser.BaseNode(0, 0, '')) + + @contextlib.contextmanager + def if_builder(self) -> T.Iterator[IfBuilder]: + b = IfBuilder(self._builder) + yield b + ret = b.finalize() + if isinstance(ret, mparser.IfNode): + self._node.ifs.append(ret) + else: + assert self._node.elseblock is None, 'Cannot create two else blocks' + self._node.elseblock = ret + + def finalize(self) -> mparser.IfClauseNode: + return self._node + + +class ObjectBuilder: + + """A way to get an object, and do things with it.""" + + def __init__(self, name: str, builder: _Builder): + self._builder = builder + self._object = builder.id(name) + self._methods: T.List[T.Tuple[str, mparser.ArgumentNode]] = [] + + @contextlib.contextmanager + def method_builder(self, name: str) -> T.Iterator[ArgumentBuilder]: + b = ArgumentBuilder(self._builder) + yield b + self._methods.append((name, b.finalize())) + + def finalize(self) -> mparser.MethodNode: + cur: T.Union[mparser.IdNode, mparser.MethodNode] = self._object + assert self._methods, "Don't use ObjectBuilder for getting an id" + for name, args in self._methods: + cur = self._builder.method(name, cur, args) + assert isinstance(cur, mparser.MethodNode), 'mypy and pylance need this' + return cur + + +class NodeBuilder: + + """The main builder class. + + This is the only one that you want to instantiate directly, use the + context manager methods to build the rest. + + The design of this is such that you open each new element, add it's + arguments, and as each context manager closes it inserts itself into + it's parent. + """ + + def __init__(self, subdir: T.Optional[Path] = None, *, _builder: T.Optional[_Builder] = None): + assert _builder is not None or subdir is not None + self._builder = _builder if _builder is not None else _Builder(subdir) + self.__node = mparser.CodeBlockNode(self._builder.token()) + + def append(self, node: mparser.BaseNode) -> None: + self.__node.lines.append(node) + + def id(self, name: str) -> mparser.IdNode: + """Create an IdNode of variable.""" + if name not in self._builder.variables: + raise MesonException(f'Cannot create ID for non-existant variable {name}') + return self._builder.id(name) + + def finalize(self) -> mparser.CodeBlockNode: + return self.__node + + @contextlib.contextmanager + def function_builder(self, name: str) -> T.Iterator[ArgumentBuilder]: + # These are un-assigned functions, they don't go into the target dict + b = FunctionBuilder(name, self._builder) + with b.argument_builder() as a: + yield a + self.append(b.finalize()) + + @contextlib.contextmanager + def assignment_builder(self, name: str) -> T.Iterator[AssignmentBuilder]: + b = AssignmentBuilder(name, self._builder) + yield b + + # If we've created a target, then they need to be saved into the target dict + target = b.finalize() + self._builder.variables.add(b.name) + self.append(target) + + @contextlib.contextmanager + def plus_assignment_builder(self, name: str) -> T.Iterator[PlusAssignmentBuilder]: + assert name in self._builder.variables, 'cannot augment a variable that isnt defined' + b = PlusAssignmentBuilder(name, self._builder) + yield b + self.append(b.finalize()) + + @contextlib.contextmanager + def if_builder(self) -> T.Iterator[IfClauseBuilder]: + b = IfClauseBuilder(self._builder) + yield b + self.append(b.finalize()) + + @contextlib.contextmanager + def object_builder(self, name: str) -> T.Iterator[ObjectBuilder]: + b = ObjectBuilder(name, self._builder) + yield b + self.append(b.finalize()) diff --git a/run_mypy.py b/run_mypy.py index 65de41484a35..d3df53962150 100755 --- a/run_mypy.py +++ b/run_mypy.py @@ -10,6 +10,7 @@ modules = [ # fully typed submodules 'mesonbuild/ast', + 'mesonbuild/cargo', 'mesonbuild/cmake', 'mesonbuild/compilers', 'mesonbuild/scripts', From 1d788edd1b3eeddd83a8af4e27b19cc93a55c724 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 29 Oct 2020 14:33:13 -0700 Subject: [PATCH 03/23] wrap: handle cargo wraps --- mesonbuild/wrap/wrap.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/mesonbuild/wrap/wrap.py b/mesonbuild/wrap/wrap.py index 5fe8b3311db7..b7c05b475e3b 100644 --- a/mesonbuild/wrap/wrap.py +++ b/mesonbuild/wrap/wrap.py @@ -306,14 +306,17 @@ def resolve(self, packagename: str, method: str, current_subproject: str = '') - meson_file = os.path.join(self.dirname, 'meson.build') cmake_file = os.path.join(self.dirname, 'CMakeLists.txt') + cargo_file = os.path.join(self.dirname, 'Cargo.toml') - if method not in ['meson', 'cmake']: - raise WrapException('Only the methods "meson" and "cmake" are supported') + if method not in {'meson', 'cmake', 'cargo'}: + raise WrapException('Only the methods "meson", "cmake", and "cargo" are supported') # The directory is there and has meson.build? Great, use it. if method == 'meson' and os.path.exists(meson_file): return rel_path - if method == 'cmake' and os.path.exists(cmake_file): + elif method == 'cmake' and os.path.exists(cmake_file): + return rel_path + elif method == 'cargo' and os.path.exists(cargo_file): return rel_path # Check if the subproject is a git submodule @@ -340,8 +343,10 @@ def resolve(self, packagename: str, method: str, current_subproject: str = '') - # A meson.build or CMakeLists.txt file is required in the directory if method == 'meson' and not os.path.exists(meson_file): raise WrapException('Subproject exists but has no meson.build file') - if method == 'cmake' and not os.path.exists(cmake_file): + elif method == 'cmake' and not os.path.exists(cmake_file): raise WrapException('Subproject exists but has no CMakeLists.txt file') + elif method == 'cargo' and not os.path.exists(cargo_file): + raise WrapException('Subproject exists but has no Cargo.toml file') return rel_path From 8f992be98d59c9744ca5f3215bc822a15fb8cad9 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 29 Oct 2020 14:34:04 -0700 Subject: [PATCH 04/23] optinterpreter: Add support for using ast without a file cargo is going to use ast for options, which cmake doesn't. --- mesonbuild/optinterpreter.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mesonbuild/optinterpreter.py b/mesonbuild/optinterpreter.py index 60436911bdf3..71e2e3f89236 100644 --- a/mesonbuild/optinterpreter.py +++ b/mesonbuild/optinterpreter.py @@ -147,6 +147,9 @@ def process(self, option_file: str) -> None: except mesonlib.MesonException as me: me.file = option_file raise me + self.process_ast(ast, option_file) + + def process_ast(self, ast: mparser.CodeBlockNode, option_file: T.Optional[str] = None) -> None: if not isinstance(ast, mparser.CodeBlockNode): e = OptionException('Option file is malformed.') e.lineno = ast.lineno() From e0e3d1e180074f63d69c52e81e25421c1a63789d Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 29 Oct 2020 14:37:15 -0700 Subject: [PATCH 05/23] cargo: Add a cargo manifest parser This is a giant blob of code. It basically reads a cargo manifest, and then produces a meson.build from that manifest. --- mesonbuild/cargo/__init__.py | 27 ++ mesonbuild/cargo/cargo.py | 658 +++++++++++++++++++++++++++++++++++ 2 files changed, 685 insertions(+) create mode 100644 mesonbuild/cargo/__init__.py create mode 100644 mesonbuild/cargo/cargo.py diff --git a/mesonbuild/cargo/__init__.py b/mesonbuild/cargo/__init__.py new file mode 100644 index 000000000000..7bf1854742ff --- /dev/null +++ b/mesonbuild/cargo/__init__.py @@ -0,0 +1,27 @@ +# Copyright © 2020 Intel Corporation + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__all__ = [ + 'HAS_TOML', + 'ManifestInterpreter', +] + +try: + import toml + del toml # so toml isn't exposed + HAS_TOML = True +except ImportError: + HAS_TOML = False +else: + from .cargo import ManifestInterpreter diff --git a/mesonbuild/cargo/cargo.py b/mesonbuild/cargo/cargo.py new file mode 100644 index 000000000000..60cd2209ceb8 --- /dev/null +++ b/mesonbuild/cargo/cargo.py @@ -0,0 +1,658 @@ +# Copyright © 2020 Intel Corporation + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path +import textwrap +import typing as T + +import toml + +from .. import mlog +from ..dependencies.base import find_external_program +from ..interpreterbase import InterpreterException +from ..mesonlib import MachineChoice, MesonException +from ..optinterpreter import is_invalid_name +from .nodebuilder import ObjectBuilder, NodeBuilder + +if T.TYPE_CHECKING: + from .. import mparser + from ..backend.backends import Backend + from ..build import Build + from ..dependencies import ExternalProgram + from ..environment import Environment + + try: + from typing import TypedDict + except AttributeError: + from typing_extensions import TypedDict + + try: + from typing import Literal + except AttributeError: + from typing_extensions import Literal + + _PackageDict = TypedDict( + '_PackageDict', + { + # These are required by crates.io + 'name': str, + 'version': str, # should be in X.Y.Z format + 'authors': T.List[str], # XXX: Might be str | List[str] + 'edition': Literal['2015', '2018'], + 'description': str, + + # These are not + 'documentation': str, + 'readme': str, + 'homepage': str, + 'license': str, # We should get either a license or a license-file + 'license-file': str, + 'workspace': str, # XXX: figure out what this means + 'build': str, # If this is set we can't use this method, you'll have to write a meson.build + 'links': str, # a system library to link to + 'include': T.List[str], # for dist packaging + 'exclude': T.List[str], # for dist packaging + 'publish': bool, # should be safe to ignore + 'metadata': T.Dict[str, T.Any], # should also be safe to ignore, unless we want a meson field + 'default-run': str, # should be safe to ignore + 'autobins': bool, # These all have to do with target autodiscovery, and we'll need to deal with them + 'autoexamples': bool, + 'autotests': bool, + 'autobenches': bool, + }, + total=False, + ) + + # For more complicated dependencies. + _DependencyDict = TypedDict( + '_DependencyDict', + { + # Tags for using a git based crate + 'git': str, # the path to the repo, required + 'branch': str, # the branch to use + 'tag': str, # A tag to use + 'rev': str, # A commitish to use + + # General attributes + 'version': str, # this is the same mini language as if a string is provided + 'path': str , # when using a local crate + 'optional': bool, # whether or not this dependency is required + 'package': str, # I *think* this allow renaming the outputs of the crate + + # Special attributes for handling dependency specific features + 'default-features': bool, + 'features': T.List[str], + }, + total=False, + ) + + # Information in the [[bin]] section + _TargetEntry = TypedDict( + '_TargetEntry', + { + 'name': str, + 'path': str, + 'test': bool, + 'doctest': bool, + 'bench': bool, + 'doc': bool, + 'proc-macro': bool, + 'harness': bool, + 'edition': Literal['2015', '2018'], + 'crate-type': Literal['bin', 'lib', 'dylib', 'staticlib', 'cdylib', 'rlib', 'proc-macro'], + 'required-features': T.List[str], + }, + total=False + ) + + + + # Type information for a Cargo.toml manifest file. + ManifestDict = TypedDict( + 'ManifestDict', + { + 'package': _PackageDict, + 'lib': _TargetEntry, + 'bin': T.List[_TargetEntry], + 'test': T.List[_TargetEntry], + 'features': T.Dict[str, T.List[str]], + 'dependencies': T.Dict[str, T.Union[str, _DependencyDict]], + 'dev-dependencies': T.Dict[str, T.Union[str, _DependencyDict]], + }, + total=False, + ) + + +def cargo_version_to_meson_version(req_string: str) -> T.List[str]: + """Takes a cargo version string, and creates a list list of meson version strings. + + Cargo has a couple of syntaxes: + ^ (caret), which means at least this version, but not the next major version: + ~ (tilde), which is hard to explain + * (wildcard), which is globbing (a bare * is not allowed) + comparison handling, like meson's <, >, >= + + Carot handling: + + >>> cargo_version_to_meson_version('^1') + ['>= 1.0.0', '< 2.0.0'] + >>> cargo_version_to_meson_version('^2.3') + ['>= 2.3.0', '< 3.0.0'] + >>> cargo_version_to_meson_version('^0.3.1') + ['>= 0.3.1', '< 0.4.0'] + >>> cargo_version_to_meson_version('^0.0.1') + ['>= 0.0.1', '< 0.0.2'] + >>> cargo_version_to_meson_version('^0.0') + ['>= 0.0.0', '< 0.1.0'] + >>> cargo_version_to_meson_version('^0') + ['>= 0.0.0', '< 1.0.0'] + + Tilde handling: + + >>> cargo_version_to_meson_version('~1.2.3') + ['>= 1.2.3', '< 1.3.0'] + >>> cargo_version_to_meson_version('~1.2') + ['>= 1.2.0', '< 1.3.0'] + >>> cargo_version_to_meson_version('~1') + ['>= 1.0.0', '< 2.0.0'] + + Wildcard handling: + + >>> cargo_version_to_meson_version('*') + [] + >>> cargo_version_to_meson_version('1.*') + ['>= 1.0.0', '< 2.0.0'] + >>> cargo_version_to_meson_version('1.2.*') + ['>= 1.2.0', '< 1.3.0'] + + Comparison: + >>> cargo_version_to_meson_version('>= 1.0.0') + ['>= 1.0.0'] + >>> cargo_version_to_meson_version('>= 1') + ['>= 1'] + >>> cargo_version_to_meson_version('= 1') + ['== 1'] + >>> cargo_version_to_meson_version('< 2.3') + ['< 2.3'] + + Multiple: + >>> cargo_version_to_meson_version('>= 1.0.0, < 1.5') + ['>= 1.0.0', '< 1.5'] + """ + # cargo + requirements = [r.strip() for r in req_string.split(',')] + final: T.List[str] = [] + + def split(raw: str) -> T.Tuple[int, T.Optional[int], T.Optional[int]]: + _major, *v = raw.split('.') + major = int(_major) + minor = int(v[0]) if v else None + patch = int(v[1]) if len(v) == 2 else None + return major, minor, patch + + for r in requirements: + if r.startswith('^'): + major, minor, patch = split(r.lstrip('^')) + final.append(f'>= {major}.{minor or 0}.{patch or 0}') + if major: + final.append(f'< {major + 1}.0.0') + elif minor: + final.append(f'< 0.{minor + 1}.0') + elif patch: + final.append(f'< 0.0.{patch + 1}') + else: + # This handles cases of ^0[.0], which are odd + assert major == 0 and (minor is None or minor == 0) and (patch is None) + if minor is not None: + final.append(f'< 0.1.0') + else: + final.append(f'< 1.0.0') + elif r.startswith('~'): + major, minor, patch = split(r.lstrip('~')) + final.append(f'>= {major}.{minor or 0}.{patch or 0}') + if patch is not None or minor is not None: + final.append(f'< {major}.{minor + 1}.0') + else: + final.append(f'< {major + 1}.0.0') + elif r.endswith('*'): + # crates.io doesn't allow this, but let's be a bit permissive + if r == '*': + continue + major, minor, patch = split(r.rstrip('.*')) + assert patch is None + + final.append(f'>= {major}.{minor or 0}.{patch or 0}') + if minor is not None: + final.append(f'< {major}.{minor + 1}.0') + else: + final.append(f'< {major + 1}.0.0') + else: + assert r.startswith(('<', '>', '=')) + if r.startswith('='): + # meson uses ==, but cargo uses = + final.append(f'={r}') + else: + final.append(r) + return final + + +class ManifestInterpreter: + + """Takes a cargo manifest.toml and creates AST to consume as a subproject. + + All targets are always marked as `build_by_default : false`, this allows + meson to intelligently build only the required targets, and not build + anything we don't actually need (like binaries we may not use). + """ + + cargo: 'ExternalProgram' + + def __new__(cls, build: 'Build', subdir: Path, src_dir: Path, install_prefix: Path, + env: 'Environment', backend: 'Backend') -> 'ManifestInterpreter': + # Messing with `__new__` is not for novices, but in this case it's + # useful because we can avoid havin to do the "if cargo is none find cargo" dance, + # We have it at class construction time, or we don't + + # TODO: we really should be able to build dependencies for host or build... + for prog in find_external_program(env, MachineChoice.HOST, 'cargo', 'cargo', ['cargo'], True): + if prog.found(): + cls.cargo = prog + break + else: + raise InterpreterException('Could not find required program "cargo"') + + return T.cast('ManifestInterpreter', super().__new__(cls)) + + def __init__(self, build: 'Build', subdir: Path, src_dir: Path, install_prefix: Path, + env: 'Environment', backend: 'Backend'): + self.build = build + self.subdir = subdir + self.src_dir = src_dir + self.install_prefix = install_prefix + self.environment = env + self.backend = backend + manfiest_file = self.src_dir / 'Cargo.toml' + self.manifest_file = str(manfiest_file) + with manfiest_file.open('r') as f: + self.manifest = T.cast('ManifestDict', toml.load(f)) + + # All features enabled in this and all sub-subprojects + self.features: T.List[str] = [] + + # A mapping between a feature and the dependencies that are needed to + # enable it. + self.features_to_deps: T.Dict[str, T.List[str]] = {} + + def __parse_lib(self, builder: NodeBuilder) -> None: + """"Look for a a lib if there is one. + + Fortunately cargo only allows one lib in a manifest, this means for + us that we don't have to worry quite as much about the auto discover. + """ + lib = self.manifest.get('lib', {}) + if lib or (self.src_dir / 'src' / 'lib.rs').exists(): + # We always call the library lib, for simplicity + name = lib.get('name', self.manifest['package']['name']).replace('-', '_') + with builder.assignment_builder('lib') as asbuilder: + with asbuilder.function_builder('build_target') as fbuilder: + # The name of the lib is not required, if it is not provided it + # should be the name of the package with any - replaced with _ + # https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-name-field + fbuilder.positional(name) + fbuilder.positional(lib.get('path', 'src/lib.rs')) + + if lib.get('crate-type') in {'dylib', 'cdylib'}: + fbuilder.keyword('target_type', 'shared_library') + else: + # This scoops up the "lib" type as well. Meson very + # much prefers that things be explicit, not + # implicit. Therefore we assume that "lib" means "" + fbuilder.keyword('target_type', 'static_library') + + # Lib as of 2020-10-23, means rlib. We want to enforce that + if lib.get('crate-type', 'lib') == 'lib': + fbuilder.keyword('rust_crate_type', 'rlib') + else: + fbuilder.keyword('rust_crate_type', lib['crate-type']) + + fbuilder.keyword('version', self.manifest['package']['version']) + + edition = lib.get('edition', self.manifest['package'].get('edition')) + if edition: + fbuilder.keyword('override_options', [f'rust_std={edition}']) + + # We always call the list of dependencies "dependencies", + # if there are dependencies we can add them. There could + # be an empty dependencies section, so account for that + if self.manifest.get('dependencies'): + fbuilder.keyword('dependencies', builder.id('dependencies')) + + # Always mark everything as build by default false, that way meson will + # only compile the things we actually need + fbuilder.keyword('build_by_default', False) + + with builder.assignment_builder('dep') as asbuilder: + with asbuilder.function_builder('declare_dependency') as fbuilder: + fbuilder.keyword('link_with', builder.id('lib')) + # It may not be strictly necessary to link with all of the + # dependencies, but it is in some cases. + if self.manifest.get('dependencies'): + fbuilder.keyword('dependencies', builder.id('dependencies')) + + if lib.get('test', True): + with builder.object_builder('rust') as obuilder: + with obuilder.method_builder('test') as fbuilder: + fbuilder.positional('lib_test') + fbuilder.positional(builder.id('lib')) + try: + fbuilder.keyword('dependencies', builder.id('dev_dependencies')) + except MesonException: + pass + + def __emit_bins(self, builder: NodeBuilder) -> None: + """Look for any binaries that need to be built. + + Cargo has two methods for this, autobins, and manual bins. Autobins + stink from a meson point of view, because there's no way without a + manual reconfigure to make them reliable, but they are *very* popular + from the brief survey I did. The other option is manual bins. We + support both, but manual bins are more reliable. + """ + targets = self.__emit_exe_targets(builder, 'bin') + + # Create a unit test target unless that target specifically requested + # not to have one + no_tests = {b['name'] for b in self.manifest.get('bin', []) if not b.get('test', True)} + for t in targets: + if t in no_tests: + continue + + with builder.object_builder('rust') as obuilder: + with obuilder.method_builder('test') as fbuilder: + fbuilder.positional(f'{t}_test') + fbuilder.positional(builder.id(t)) + try: + fbuilder.keyword('dependencies', builder.id('dev_dependencies')) + except MesonException: + pass + + def __emit_tests(self, builder: NodeBuilder) -> None: + """Look for any test targets that need to be built. + + Additionally we need to create a standard (not rust module) test + target for each binary we build. We call these `_integration_test`, + so we don't have collisions with our unit tests. + """ + targets = self.__emit_exe_targets(builder, 'test') + for t in targets: + with builder.function_builder('test') as fbuilder: + fbuilder.positional(f'{t}_integration_test') + fbuilder.positional(builder.id(t)) + try: + fbuilder.keyword('dependencies', builder.id('dev_dependencies')) + except MesonException: + pass + + def __emit_exe_targets(self, builder: NodeBuilder, target: "Literal['bin', 'test']") -> T.List[str]: + """Shared helper for all executable targets. + + Cargo has auto discovery (globing, bleh), and manual discover. We + need to support both to successfully parse cargo manifests. This code + is meant to be shared between the different type of binary targets. + + Returns a list of all of the targets it created for the caller to + work with. This allows, for example, for the caller to create test + cases. + """ + def make_bin(name: str, source: str, edition: T.Optional[str] = None, unittest: bool = True) -> None: + """Craete a single binary. + """ + created_targets.append(name) + with builder.assignment_builder(name) as asbuilder: + with asbuilder.function_builder('executable') as fbuilder: + fbuilder.positional(name) + fbuilder.positional(source) + try: + fbuilder.keyword('link_with', builder.id('lib')) + except MesonException: + pass + try: + fbuilder.keyword('dependencies', builder.id('dev_dependencies')) + except MesonException: + pass + if edition: + fbuilder.keyword('override_options', [f'rust_std={edition}']) + + # Always mark everything as build by default false, that way meson will + # only compile the things we actually need + fbuilder.keyword('build_by_default', False) + + package_edition: str = self.manifest['package'].get('edition', '2015') + manual_targets = self.manifest.get(target, T.cast(T.List['_TargetEntry'], [])) + + created_targets: T.List[str] = [] + # TODO: main.rs isn't supported? + + if self.manifest['package'].get(f'auto{target}s', package_edition == '2018' or not manual_targets): + mlog.warning(textwrap.dedent(f'''\ + cargo is using auto{target}s, this is a form of globbing. + ninja may not always detect that you need to reconfigure if + you update cargo subprojects. In this case you will need to + run `meson setup --reconfigure` manually. + '''), once=True) + + paths: T.List[Path] = [] + if target == 'bin': + main = self.src_dir / 'src' / 'main.rs' + if main.exists(): + paths.append(main.relative_to(self.src_dir)) + if target == 'bin': + targetdir = self.src_dir / 'src' / 'bin' + elif target == 'test': + targetdir = self.src_dir / 'tests' + if targetdir.is_dir(): + paths.extend([p.relative_to(self.src_dir) for p in targetdir.glob('*.rs')]) + + for each in paths: + name = each.with_suffix('').name.replace('-', '_') + if name == 'test': + name = 'test_' + # TODO: this needs a test + if any(b['name'] == name for b in manual_targets): + continue + make_bin(name, str(each), package_edition) + + # You can have both autodiscovered an manually discovered targets in + # the same manifest + for bin in manual_targets: + edition = bin.get('edition', package_edition) + make_bin(bin['name'], bin['path'], edition) + + return created_targets + + def __parse_features(self, opt_builder: NodeBuilder) -> None: + """Convert cargo features into meson options. + + Create each option function. Cargo uses a single namespace for all + "features" (what meson calls options). They are always boolean, and + to emulate the single namspace, we make them always yielding. + """ + default: T.Set[str] = set(self.manifest.get('features', {}).get('default', [])) + + for name, requirements in self.manifest.get('features', {}).items(): + if name == "default": + continue + + # Sometimes cargo feature names are reserved in meson, we need to + # convert those to somethign valid, we use name_ + if is_invalid_name(name): + name = f'{name}_' + + new_reqs: T.List[str] = [] + for r in requirements: + if '/' in r: + subp, opt = r.split('/') + # In this case the crate is trying to change another crate's + # configuration. Meson does not allow this, options are contained + # The best we can do is provide the user a warning + mlog.warning(textwrap.dedent(f'''\ + Crate {self.manifest['package']['name']} wants to turn on the + {opt} in {subp}. Meson does not allow subprojects to change + another subproject's options. You may need to pass + `-D{subp}:{opt}=true` to meson configure for compilation + to succeed. + ''')) + else: + new_reqs.append(r) + + self.features.append(name) + if requirements: + self.features_to_deps[name] = requirements + + with opt_builder.function_builder('option') as fbuilder: + fbuilder.positional(name) + fbuilder.keyword('type', 'boolean') + fbuilder.keyword('yield', True) + fbuilder.keyword('value', name in default) + + @staticmethod + def __get_dependency(builder: NodeBuilder, name: str, disabler: bool = False) -> 'mparser.MethodNode': + """Helper for getting a supbroject dependency.""" + obuilder = ObjectBuilder('rust', builder._builder) + with obuilder.method_builder('subproject') as arbuilder: + arbuilder.positional(name.replace('-', '_')) + if disabler: + arbuilder.keyword('required', False) + with obuilder.method_builder('get_variable') as arbuilder: + arbuilder.positional('dep') # we always use dep + if disabler: + arbuilder.positional(builder._builder.function('disabler')) + return obuilder.finalize() + + def __parse_dependencies(self, builder: NodeBuilder) -> None: + """Parse all of the dependencies + + Check all of the dependencies we need, create a "dependencies" array + to hold each of them and populate it. The required dependencies are + injected first, and then the create if nodes to add the optional ones + only if needed. + """ + with builder.assignment_builder('dependencies') as abuilder: + with abuilder.array_builder() as arbuilder: + for name, dep in self.manifest.get('dependencies', {}).items(): + # If the dependency is required, go ahead and build the + # subproject call unconditionally + if isinstance(dep, str) or not dep.get('optional', False): + arbuilder.positional(self.__get_dependency(builder, name)) + + # If it's optional, then we need to check that the feature that + # it depends on is available, then add it to the dependency array. + for name, requires in self.features_to_deps.items(): + with builder.if_builder() as ifcbuilder: + with ifcbuilder.if_builder() as ifbuilder: + with ifbuilder.condition_builder() as cbuilder: + with cbuilder.function_builder('get_option') as fbuilder: + fbuilder.positional(name) + with ifbuilder.body_builder() as bbuilder: + with bbuilder.plus_assignment_builder('dependencies') as pabuilder: + with pabuilder.array_builder() as arbuilder: + for name in requires: + arbuilder.positional(self.__get_dependency(builder, name)) + + def __emit_dev_dependencies(self, builder: NodeBuilder) -> None: + """Parse all dev-dependencies + + These are needed by tests, benchmarks, and examples, but are not + propgated or used in building distributed binaries or libraries. + """ + with builder.assignment_builder('dev_dependencies') as abuilder: + with abuilder.array_builder() as arbuilder: + for name, dep in self.manifest.get('dev-dependencies', {}).items(): + # If the dependency is required, go ahead and build the + # subproject call unconditionally + if isinstance(dep, str) or not dep.get('optional', False): + arbuilder.positional(self.__get_dependency(builder, name, disabler=True)) + + def __emit_features(self, builder: NodeBuilder) -> None: + """Emit the code to check each feature, and add it to the rust + arguments if necessary. + + We use add_project_arguments() here, because it simplifies our code + generation + """ + for f in self.features: + with builder.if_builder() as ifcbuilder: + with ifcbuilder.if_builder() as ifbuilder: + with ifbuilder.condition_builder() as cbuilder: + with cbuilder.function_builder('get_option') as fbuilder: + fbuilder.positional(f) + with ifbuilder.body_builder() as bbuilder: + with bbuilder.function_builder('add_project_arguments') as fbuilder: + fbuilder.positional(['--cfg', f'feature="{f}"']) + fbuilder.keyword('language', 'rust') + + def parse(self) -> T.Tuple['mparser.CodeBlockNode', 'mparser.CodeBlockNode']: + """Create a meson code node from a cargo manifest file + + This can then be fed back into the meson interpreter to create a + "meson" project from the crate specification. + """ + builder = NodeBuilder(self.subdir) + opt_builder = NodeBuilder(self.subdir) + + # Create the meson project() function. + # + # Currently, we generate: + # - name + # - version + # - license (only if the license is an SPDX string) + default_options: T.List[str] = [] + with builder.function_builder('project') as fbuilder: + fbuilder.positional(self.manifest['package']['name'].replace('-', '_')) + fbuilder.positional(['rust']) + fbuilder.keyword('version', self.manifest['package']['version']) + if 'license' in self.manifest['package']: + fbuilder.keyword('license', self.manifest['package']['license']) + if 'edition' in self.manifest['package']: + default_options.append(f'rust_std={self.manifest["package"]["edition"]}') + if default_options: + fbuilder.keyword('default_options', default_options) + + # Import the rust module + # + # This is needed in enough cases it makes sense to just always import + # it, as it vaslty simplifes things. + with builder.assignment_builder('rust') as abuilder: + with abuilder.function_builder('import') as fbuilder: + fbuilder.positional('rust') + + self.__parse_features(opt_builder) + + # Create a list of dependencies which will be added to the library (if + # there is one). + if self.manifest.get('dependencies'): + self.__parse_dependencies(builder) + if self.manifest.get('dev-dependencies'): + self.__emit_dev_dependencies(builder) + + # This needs to be called after dependencies + self.__emit_features(builder) + + # Look for libs first, becaue if there are libs, then bins need to link + # with them. + self.__parse_lib(builder) + self.__emit_bins(builder) + self.__emit_tests(builder) + + return builder.finalize(), opt_builder.finalize() From b7bb185589ba755e933ee100170a4efd9e84472d Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 29 Oct 2020 14:39:53 -0700 Subject: [PATCH 06/23] modules/rust: Add a subproject interface This adds the ability to do `import('rust').subproject('foo')`, like cmake. --- mesonbuild/modules/unstable_rust.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/mesonbuild/modules/unstable_rust.py b/mesonbuild/modules/unstable_rust.py index 1db6722c66e0..37c7100f963b 100644 --- a/mesonbuild/modules/unstable_rust.py +++ b/mesonbuild/modules/unstable_rust.py @@ -16,6 +16,7 @@ import typing as T from . import ExtensionModule, ModuleReturnValue +from .. import cargo from .. import mlog from ..build import BuildTarget, CustomTargetIndex, Executable, GeneratedList, InvalidArguments, IncludeDirs, CustomTarget from ..dependencies import Dependency, ExternalLibrary @@ -38,6 +39,7 @@ class RustModule(ExtensionModule): def __init__(self, interpreter: 'Interpreter') -> None: super().__init__(interpreter) self._bindgen_bin: T.Optional['ExternalProgram'] = None + self.snippets.add('subproject') @permittedKwargs(permitted_test_kwargs | {'dependencies'} ^ {'protocol'}) @typed_pos_args('rust.test', str, BuildTargetHolder) @@ -203,6 +205,22 @@ def bindgen(self, state: 'ModuleState', args: T.List, kwargs: T.Dict[str, T.Any] return ModuleReturnValue([target], [CustomTargetHolder(target, self.interpreter)]) + def subproject(self, interpreter: 'Interpreter', state: 'ModuleState', + args: T.List, kwargs: T.Dict[str, T.Any]) -> 'SubprojectHolder': + """Create a subproject from a cargo manifest. + + This method + """ + if not cargo.HAS_TOML: + raise InterpreterException('cargo integration requires the python toml module.') + dirname: str = args[0] + if not isinstance(dirname, str): + raise InvalidArguments('rust.subproject "name" positional arugment must be a string.') + + subp = interpreter.do_subproject(dirname, 'cargo', kwargs) + + return subp + def initialize(*args: T.List, **kwargs: T.Dict) -> RustModule: return RustModule(*args, **kwargs) # type: ignore From 82aa33c4782b83c82901102c3ac2d08187623d52 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 29 Oct 2020 14:43:25 -0700 Subject: [PATCH 07/23] modules/rust: add cargo tests --- run_project_tests.py | 6 +++-- test cases/cargo/.gitignore | 2 ++ test cases/cargo/1 basic/meson.build | 5 ++++ .../cargo/1 basic/subprojects/test/Cargo.toml | 3 +++ test cases/cargo/10 dependencies/exe.rs | 5 ++++ test cases/cargo/10 dependencies/meson.build | 6 +++++ .../subprojects/sub1/Cargo.toml | 6 +++++ .../subprojects/sub1/src/lib.rs | 5 ++++ .../subprojects/sub2/Cargo.toml | 4 +++ .../subprojects/sub2/src/lib.rs | 3 +++ .../cargo/11 optional dependencies/exe.rs | 5 ++++ .../11 optional dependencies/meson.build | 8 ++++++ .../meson_options.txt | 5 ++++ .../subprojects/sub1/Cargo.toml | 10 +++++++ .../subprojects/sub1/src/lib.rs | 5 ++++ .../subprojects/sub2/Cargo.toml | 7 +++++ .../subprojects/sub2/src/lib.rs | 4 +++ test cases/cargo/12 unittests/meson.build | 4 +++ .../12 unittests/subprojects/sub/Cargo.toml | 3 +++ .../12 unittests/subprojects/sub/src/lib.rs | 9 +++++++ .../12 unittests/subprojects/sub/src/main.rs | 13 ++++++++++ test cases/cargo/13 test targets/meson.build | 15 +++++++++++ .../subprojects/sub1/Cargo.toml | 13 ++++++++++ .../subprojects/sub1/src/lib.rs | 3 +++ .../subprojects/sub1/src/tests/norun.rs | 5 ++++ .../subprojects/sub1/src/tests/test.rs | 7 +++++ .../subprojects/sub2/Cargo.toml | 8 ++++++ .../subprojects/sub2/src/lib.rs | 3 +++ .../subprojects/sub2/tests/test.rs | 7 +++++ .../subprojects/sub3/Cargo.toml | 13 ++++++++++ .../subprojects/sub3/src/lib.rs | 3 +++ .../subprojects/sub3/tests/test.rs | 7 +++++ .../subprojects/sub4/Cargo.toml | 13 ++++++++++ .../subprojects/sub4/src/lib.rs | 3 +++ .../subprojects/sub4/tests/auto.rs | 7 +++++ .../subprojects/sub4/tests/test.rs | 7 +++++ .../14 empty dependencies section/app.rs | 5 ++++ .../14 empty dependencies section/meson.build | 12 +++++++++ .../subprojects/mylib/Cargo.toml | 9 +++++++ .../subprojects/mylib/src/cool-lib.rs | 3 +++ .../cargo/15 dev dependencies/meson.build | 4 +++ .../subprojects/sub1/Cargo.toml | 13 ++++++++++ .../subprojects/sub1/src/lib.rs | 18 +++++++++++++ .../subprojects/test_function/Cargo.toml | 10 +++++++ .../subprojects/test_function/src/lib.rs | 3 +++ test cases/cargo/2 autobins/meson.build | 26 +++++++++++++++++++ .../2 autobins/subprojects/clash/Cargo.toml | 11 ++++++++ .../2 autobins/subprojects/sub/Cargo.toml | 3 +++ .../2 autobins/subprojects/sub/src/bin/app.rs | 3 +++ .../2 autobins/subprojects/sub2/Cargo.toml | 8 ++++++ .../subprojects/sub2/src/bin/app.rs | 3 +++ .../2 autobins/subprojects/sub2/src/foo.rs | 4 +++ .../2 autobins/subprojects/sub3/Cargo.toml | 7 +++++ .../subprojects/sub3/src/bin/app.rs | 3 +++ .../2 autobins/subprojects/sub3/src/foo.rs | 4 +++ .../2 autobins/subprojects/test/Cargo.toml | 4 +++ .../subprojects/test/src/bin/app.rs | 3 +++ test cases/cargo/2 autobins/test.py | 20 ++++++++++++++ test cases/cargo/3 manual bins/meson.build | 14 ++++++++++ .../3 manual bins/subprojects/test/Cargo.toml | 12 +++++++++ .../3 manual bins/subprojects/test/src/app.rs | 3 +++ .../subprojects/test/src/app2.rs | 3 +++ test cases/cargo/3 manual bins/test.py | 22 ++++++++++++++++ test cases/cargo/4 mixed bins/meson.build | 14 ++++++++++ .../4 mixed bins/subprojects/test/Cargo.toml | 8 ++++++ .../4 mixed bins/subprojects/test/src/app2.rs | 3 +++ .../subprojects/test/src/bin/app.rs | 3 +++ test cases/cargo/4 mixed bins/test.py | 22 ++++++++++++++++ test cases/cargo/5 auto lib/app.rs | 5 ++++ test cases/cargo/5 auto lib/meson.build | 12 +++++++++ .../5 auto lib/subprojects/mylib/Cargo.toml | 3 +++ .../5 auto lib/subprojects/mylib/src/lib.rs | 3 +++ test cases/cargo/6 manual lib/app.rs | 5 ++++ test cases/cargo/6 manual lib/meson.build | 12 +++++++++ .../6 manual lib/subprojects/mylib/Cargo.toml | 7 +++++ .../subprojects/mylib/src/cool-lib.rs | 3 +++ .../cargo/7 bins in crate link to lib/app.rs | 5 ++++ .../7 bins in crate link to lib/meson.build | 12 +++++++++ .../subprojects/mylib/Cargo.toml | 7 +++++ .../subprojects/mylib/src/bin/cool-app.rs | 5 ++++ .../subprojects/mylib/src/cool-lib.rs | 3 +++ test cases/cargo/8 editions/meson.build | 5 ++++ .../8 editions/subprojects/sub1/Cargo.toml | 9 +++++++ .../8 editions/subprojects/sub1/src/app2.rs | 4 +++ .../subprojects/sub1/src/bin/app.rs | 10 +++++++ .../8 editions/subprojects/sub1/src/lib.rs | 3 +++ .../8 editions/subprojects/sub2/Cargo.toml | 9 +++++++ .../8 editions/subprojects/sub2/src/app2.rs | 10 +++++++ .../subprojects/sub2/src/bin/app.rs | 4 +++ test cases/cargo/9 features/exe.rs | 5 ++++ test cases/cargo/9 features/exe2.rs | 6 +++++ test cases/cargo/9 features/meson.build | 10 +++++++ test cases/cargo/9 features/meson_options.txt | 5 ++++ .../9 features/subprojects/sub1/Cargo.toml | 12 +++++++++ .../cargo/9 features/subprojects/sub1/lib.rs | 23 ++++++++++++++++ 95 files changed, 707 insertions(+), 2 deletions(-) create mode 100644 test cases/cargo/.gitignore create mode 100644 test cases/cargo/1 basic/meson.build create mode 100644 test cases/cargo/1 basic/subprojects/test/Cargo.toml create mode 100644 test cases/cargo/10 dependencies/exe.rs create mode 100644 test cases/cargo/10 dependencies/meson.build create mode 100644 test cases/cargo/10 dependencies/subprojects/sub1/Cargo.toml create mode 100644 test cases/cargo/10 dependencies/subprojects/sub1/src/lib.rs create mode 100644 test cases/cargo/10 dependencies/subprojects/sub2/Cargo.toml create mode 100644 test cases/cargo/10 dependencies/subprojects/sub2/src/lib.rs create mode 100644 test cases/cargo/11 optional dependencies/exe.rs create mode 100644 test cases/cargo/11 optional dependencies/meson.build create mode 100644 test cases/cargo/11 optional dependencies/meson_options.txt create mode 100644 test cases/cargo/11 optional dependencies/subprojects/sub1/Cargo.toml create mode 100644 test cases/cargo/11 optional dependencies/subprojects/sub1/src/lib.rs create mode 100644 test cases/cargo/11 optional dependencies/subprojects/sub2/Cargo.toml create mode 100644 test cases/cargo/11 optional dependencies/subprojects/sub2/src/lib.rs create mode 100644 test cases/cargo/12 unittests/meson.build create mode 100644 test cases/cargo/12 unittests/subprojects/sub/Cargo.toml create mode 100644 test cases/cargo/12 unittests/subprojects/sub/src/lib.rs create mode 100644 test cases/cargo/12 unittests/subprojects/sub/src/main.rs create mode 100644 test cases/cargo/13 test targets/meson.build create mode 100644 test cases/cargo/13 test targets/subprojects/sub1/Cargo.toml create mode 100644 test cases/cargo/13 test targets/subprojects/sub1/src/lib.rs create mode 100644 test cases/cargo/13 test targets/subprojects/sub1/src/tests/norun.rs create mode 100644 test cases/cargo/13 test targets/subprojects/sub1/src/tests/test.rs create mode 100644 test cases/cargo/13 test targets/subprojects/sub2/Cargo.toml create mode 100644 test cases/cargo/13 test targets/subprojects/sub2/src/lib.rs create mode 100644 test cases/cargo/13 test targets/subprojects/sub2/tests/test.rs create mode 100644 test cases/cargo/13 test targets/subprojects/sub3/Cargo.toml create mode 100644 test cases/cargo/13 test targets/subprojects/sub3/src/lib.rs create mode 100644 test cases/cargo/13 test targets/subprojects/sub3/tests/test.rs create mode 100644 test cases/cargo/13 test targets/subprojects/sub4/Cargo.toml create mode 100644 test cases/cargo/13 test targets/subprojects/sub4/src/lib.rs create mode 100644 test cases/cargo/13 test targets/subprojects/sub4/tests/auto.rs create mode 100644 test cases/cargo/13 test targets/subprojects/sub4/tests/test.rs create mode 100644 test cases/cargo/14 empty dependencies section/app.rs create mode 100644 test cases/cargo/14 empty dependencies section/meson.build create mode 100644 test cases/cargo/14 empty dependencies section/subprojects/mylib/Cargo.toml create mode 100644 test cases/cargo/14 empty dependencies section/subprojects/mylib/src/cool-lib.rs create mode 100644 test cases/cargo/15 dev dependencies/meson.build create mode 100644 test cases/cargo/15 dev dependencies/subprojects/sub1/Cargo.toml create mode 100644 test cases/cargo/15 dev dependencies/subprojects/sub1/src/lib.rs create mode 100644 test cases/cargo/15 dev dependencies/subprojects/test_function/Cargo.toml create mode 100644 test cases/cargo/15 dev dependencies/subprojects/test_function/src/lib.rs create mode 100644 test cases/cargo/2 autobins/meson.build create mode 100644 test cases/cargo/2 autobins/subprojects/clash/Cargo.toml create mode 100644 test cases/cargo/2 autobins/subprojects/sub/Cargo.toml create mode 100644 test cases/cargo/2 autobins/subprojects/sub/src/bin/app.rs create mode 100644 test cases/cargo/2 autobins/subprojects/sub2/Cargo.toml create mode 100644 test cases/cargo/2 autobins/subprojects/sub2/src/bin/app.rs create mode 100644 test cases/cargo/2 autobins/subprojects/sub2/src/foo.rs create mode 100644 test cases/cargo/2 autobins/subprojects/sub3/Cargo.toml create mode 100644 test cases/cargo/2 autobins/subprojects/sub3/src/bin/app.rs create mode 100644 test cases/cargo/2 autobins/subprojects/sub3/src/foo.rs create mode 100644 test cases/cargo/2 autobins/subprojects/test/Cargo.toml create mode 100644 test cases/cargo/2 autobins/subprojects/test/src/bin/app.rs create mode 100755 test cases/cargo/2 autobins/test.py create mode 100644 test cases/cargo/3 manual bins/meson.build create mode 100644 test cases/cargo/3 manual bins/subprojects/test/Cargo.toml create mode 100644 test cases/cargo/3 manual bins/subprojects/test/src/app.rs create mode 100644 test cases/cargo/3 manual bins/subprojects/test/src/app2.rs create mode 100755 test cases/cargo/3 manual bins/test.py create mode 100644 test cases/cargo/4 mixed bins/meson.build create mode 100644 test cases/cargo/4 mixed bins/subprojects/test/Cargo.toml create mode 100644 test cases/cargo/4 mixed bins/subprojects/test/src/app2.rs create mode 100644 test cases/cargo/4 mixed bins/subprojects/test/src/bin/app.rs create mode 100755 test cases/cargo/4 mixed bins/test.py create mode 100644 test cases/cargo/5 auto lib/app.rs create mode 100644 test cases/cargo/5 auto lib/meson.build create mode 100644 test cases/cargo/5 auto lib/subprojects/mylib/Cargo.toml create mode 100644 test cases/cargo/5 auto lib/subprojects/mylib/src/lib.rs create mode 100644 test cases/cargo/6 manual lib/app.rs create mode 100644 test cases/cargo/6 manual lib/meson.build create mode 100644 test cases/cargo/6 manual lib/subprojects/mylib/Cargo.toml create mode 100644 test cases/cargo/6 manual lib/subprojects/mylib/src/cool-lib.rs create mode 100644 test cases/cargo/7 bins in crate link to lib/app.rs create mode 100644 test cases/cargo/7 bins in crate link to lib/meson.build create mode 100644 test cases/cargo/7 bins in crate link to lib/subprojects/mylib/Cargo.toml create mode 100644 test cases/cargo/7 bins in crate link to lib/subprojects/mylib/src/bin/cool-app.rs create mode 100644 test cases/cargo/7 bins in crate link to lib/subprojects/mylib/src/cool-lib.rs create mode 100644 test cases/cargo/8 editions/meson.build create mode 100644 test cases/cargo/8 editions/subprojects/sub1/Cargo.toml create mode 100644 test cases/cargo/8 editions/subprojects/sub1/src/app2.rs create mode 100644 test cases/cargo/8 editions/subprojects/sub1/src/bin/app.rs create mode 100644 test cases/cargo/8 editions/subprojects/sub1/src/lib.rs create mode 100644 test cases/cargo/8 editions/subprojects/sub2/Cargo.toml create mode 100644 test cases/cargo/8 editions/subprojects/sub2/src/app2.rs create mode 100644 test cases/cargo/8 editions/subprojects/sub2/src/bin/app.rs create mode 100644 test cases/cargo/9 features/exe.rs create mode 100644 test cases/cargo/9 features/exe2.rs create mode 100644 test cases/cargo/9 features/meson.build create mode 100644 test cases/cargo/9 features/meson_options.txt create mode 100644 test cases/cargo/9 features/subprojects/sub1/Cargo.toml create mode 100644 test cases/cargo/9 features/subprojects/sub1/lib.rs diff --git a/run_project_tests.py b/run_project_tests.py index f477e6c5a0fa..e9e5103605be 100755 --- a/run_project_tests.py +++ b/run_project_tests.py @@ -55,7 +55,8 @@ ALL_TESTS = ['cmake', 'common', 'native', 'warning-meson', 'failing-meson', 'failing-build', 'failing-test', 'keyval', 'platform-osx', 'platform-windows', 'platform-linux', 'java', 'C#', 'vala', 'rust', 'd', 'objective c', 'objective c++', - 'fortran', 'swift', 'cuda', 'python3', 'python', 'fpga', 'frameworks', 'nasm', 'wasm' + 'fortran', 'swift', 'cuda', 'python3', 'python', 'fpga', 'frameworks', 'nasm', 'wasm', + 'cargo', ] @@ -1002,6 +1003,7 @@ def __init__(self, category: str, subdir: str, skip: bool = False, stdout_mandat self.stdout_mandatory = stdout_mandatory # expected stdout is mandatory for tests in this category all_tests = [ + TestCategory('cargo', 'cargo', not shutil.which('cargo') or should_skip_rust(backend)), TestCategory('cmake', 'cmake', not shutil.which('cmake') or (os.environ.get('compiler') == 'msvc2015' and under_ci)), TestCategory('common', 'common'), TestCategory('native', 'native'), @@ -1033,7 +1035,7 @@ def __init__(self, category: str, subdir: str, skip: bool = False, stdout_mandat ] categories = [t.category for t in all_tests] - assert categories == ALL_TESTS, 'argparse("--only", choices=ALL_TESTS) need to be updated to match all_tests categories' + assert sorted(categories) == sorted(ALL_TESTS), 'argparse("--only", choices=ALL_TESTS) need to be updated to match all_tests categories' if only: for key in only.keys(): diff --git a/test cases/cargo/.gitignore b/test cases/cargo/.gitignore new file mode 100644 index 000000000000..1e7caa9ea89a --- /dev/null +++ b/test cases/cargo/.gitignore @@ -0,0 +1,2 @@ +Cargo.lock +target/ diff --git a/test cases/cargo/1 basic/meson.build b/test cases/cargo/1 basic/meson.build new file mode 100644 index 000000000000..70fd87bd6d90 --- /dev/null +++ b/test cases/cargo/1 basic/meson.build @@ -0,0 +1,5 @@ +project('basic cargo test', 'rust') + +rust = import('unstable-rust') +sub = rust.subproject('test') + diff --git a/test cases/cargo/1 basic/subprojects/test/Cargo.toml b/test cases/cargo/1 basic/subprojects/test/Cargo.toml new file mode 100644 index 000000000000..5be8eb869b28 --- /dev/null +++ b/test cases/cargo/1 basic/subprojects/test/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "test project" +version = "1.0.0" diff --git a/test cases/cargo/10 dependencies/exe.rs b/test cases/cargo/10 dependencies/exe.rs new file mode 100644 index 000000000000..dc4984dee661 --- /dev/null +++ b/test cases/cargo/10 dependencies/exe.rs @@ -0,0 +1,5 @@ +extern crate sub1; + +pub fn main() { + std::process::exit(sub1::fun()); +} diff --git a/test cases/cargo/10 dependencies/meson.build b/test cases/cargo/10 dependencies/meson.build new file mode 100644 index 000000000000..8a7ae82c5f57 --- /dev/null +++ b/test cases/cargo/10 dependencies/meson.build @@ -0,0 +1,6 @@ +project('cargo dependencies', 'rust') + +rust = import('unstable-rust') +sub1 = rust.subproject('sub1') + +exe = executable('exe', 'exe.rs', dependencies : sub1.get_variable('dep')) diff --git a/test cases/cargo/10 dependencies/subprojects/sub1/Cargo.toml b/test cases/cargo/10 dependencies/subprojects/sub1/Cargo.toml new file mode 100644 index 000000000000..b107b98dbba6 --- /dev/null +++ b/test cases/cargo/10 dependencies/subprojects/sub1/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "sub1" +version = "1.0.2" + +[dependencies] +sub2 = "1.2.*" diff --git a/test cases/cargo/10 dependencies/subprojects/sub1/src/lib.rs b/test cases/cargo/10 dependencies/subprojects/sub1/src/lib.rs new file mode 100644 index 000000000000..a9855df6e3ce --- /dev/null +++ b/test cases/cargo/10 dependencies/subprojects/sub1/src/lib.rs @@ -0,0 +1,5 @@ +extern crate sub2; + +pub fn fun() -> i32 { + return sub2::fun() - 7; +} diff --git a/test cases/cargo/10 dependencies/subprojects/sub2/Cargo.toml b/test cases/cargo/10 dependencies/subprojects/sub2/Cargo.toml new file mode 100644 index 000000000000..48f11100b6ba --- /dev/null +++ b/test cases/cargo/10 dependencies/subprojects/sub2/Cargo.toml @@ -0,0 +1,4 @@ + +[package] +name = "sub2" +version = "1.2.17" diff --git a/test cases/cargo/10 dependencies/subprojects/sub2/src/lib.rs b/test cases/cargo/10 dependencies/subprojects/sub2/src/lib.rs new file mode 100644 index 000000000000..bcb5ff383540 --- /dev/null +++ b/test cases/cargo/10 dependencies/subprojects/sub2/src/lib.rs @@ -0,0 +1,3 @@ +pub fn fun() -> i32 { + return 7; +} diff --git a/test cases/cargo/11 optional dependencies/exe.rs b/test cases/cargo/11 optional dependencies/exe.rs new file mode 100644 index 000000000000..dc4984dee661 --- /dev/null +++ b/test cases/cargo/11 optional dependencies/exe.rs @@ -0,0 +1,5 @@ +extern crate sub1; + +pub fn main() { + std::process::exit(sub1::fun()); +} diff --git a/test cases/cargo/11 optional dependencies/meson.build b/test cases/cargo/11 optional dependencies/meson.build new file mode 100644 index 000000000000..90f33f8e3130 --- /dev/null +++ b/test cases/cargo/11 optional dependencies/meson.build @@ -0,0 +1,8 @@ +project('cargo dependencies', 'rust') + +rust = import('unstable-rust') +sub1 = rust.subproject('sub1') + +exe = executable('exe', 'exe.rs', dependencies : sub1.get_variable('dep')) + +test('nested dependencies', exe) diff --git a/test cases/cargo/11 optional dependencies/meson_options.txt b/test cases/cargo/11 optional dependencies/meson_options.txt new file mode 100644 index 000000000000..965b68e048cc --- /dev/null +++ b/test cases/cargo/11 optional dependencies/meson_options.txt @@ -0,0 +1,5 @@ +option( + 'needs_to_be_on', + type : 'boolean', + value : true, +) diff --git a/test cases/cargo/11 optional dependencies/subprojects/sub1/Cargo.toml b/test cases/cargo/11 optional dependencies/subprojects/sub1/Cargo.toml new file mode 100644 index 000000000000..f7666f44322c --- /dev/null +++ b/test cases/cargo/11 optional dependencies/subprojects/sub1/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "sub1" +version = "1.0.2" + +[dependencies] +sub2 = { version = "1.2.*", optional = true} + +[features] +default = [] +needs_to_be_on = ["sub2"] diff --git a/test cases/cargo/11 optional dependencies/subprojects/sub1/src/lib.rs b/test cases/cargo/11 optional dependencies/subprojects/sub1/src/lib.rs new file mode 100644 index 000000000000..a9855df6e3ce --- /dev/null +++ b/test cases/cargo/11 optional dependencies/subprojects/sub1/src/lib.rs @@ -0,0 +1,5 @@ +extern crate sub2; + +pub fn fun() -> i32 { + return sub2::fun() - 7; +} diff --git a/test cases/cargo/11 optional dependencies/subprojects/sub2/Cargo.toml b/test cases/cargo/11 optional dependencies/subprojects/sub2/Cargo.toml new file mode 100644 index 000000000000..2f890e47964c --- /dev/null +++ b/test cases/cargo/11 optional dependencies/subprojects/sub2/Cargo.toml @@ -0,0 +1,7 @@ + +[package] +name = "sub2" +version = "1.2.17" + +[features] +needs_to_be_on = [] diff --git a/test cases/cargo/11 optional dependencies/subprojects/sub2/src/lib.rs b/test cases/cargo/11 optional dependencies/subprojects/sub2/src/lib.rs new file mode 100644 index 000000000000..3176cb6b7e05 --- /dev/null +++ b/test cases/cargo/11 optional dependencies/subprojects/sub2/src/lib.rs @@ -0,0 +1,4 @@ +#[cfg(feature = "needs_to_be_on")] +pub fn fun() -> i32 { + return 7; +} diff --git a/test cases/cargo/12 unittests/meson.build b/test cases/cargo/12 unittests/meson.build new file mode 100644 index 000000000000..b7f5d18c1d0c --- /dev/null +++ b/test cases/cargo/12 unittests/meson.build @@ -0,0 +1,4 @@ +project('rust unittests', 'rust') + +rust = import('unstable-rust') +rust.subproject('sub') diff --git a/test cases/cargo/12 unittests/subprojects/sub/Cargo.toml b/test cases/cargo/12 unittests/subprojects/sub/Cargo.toml new file mode 100644 index 000000000000..fe5889b451d8 --- /dev/null +++ b/test cases/cargo/12 unittests/subprojects/sub/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "unit_tests" +version = "0.1.2" diff --git a/test cases/cargo/12 unittests/subprojects/sub/src/lib.rs b/test cases/cargo/12 unittests/subprojects/sub/src/lib.rs new file mode 100644 index 000000000000..5fdf8d4a75fa --- /dev/null +++ b/test cases/cargo/12 unittests/subprojects/sub/src/lib.rs @@ -0,0 +1,9 @@ +pub fn value() -> i32 { + return 0; +} + +#[test] +fn test_value() { + let r = value(); + assert_eq!(r, 0, "did not get 0") +} diff --git a/test cases/cargo/12 unittests/subprojects/sub/src/main.rs b/test cases/cargo/12 unittests/subprojects/sub/src/main.rs new file mode 100644 index 000000000000..5e84954203e0 --- /dev/null +++ b/test cases/cargo/12 unittests/subprojects/sub/src/main.rs @@ -0,0 +1,13 @@ +pub fn main() { + std::process::exit(value()); +} + +fn value() -> i32 { + return 0; +} + +#[test] +fn test_value() { + let r = value(); + assert_eq!(r, 0, "did not get 0") +} diff --git a/test cases/cargo/13 test targets/meson.build b/test cases/cargo/13 test targets/meson.build new file mode 100644 index 000000000000..24c8d9312a20 --- /dev/null +++ b/test cases/cargo/13 test targets/meson.build @@ -0,0 +1,15 @@ +project('cargo functional tests', 'rust') + +rust = import('unstable-rust') + +# Test manual tests here +rust.subproject('sub1') + +# Test auto tests here +rust.subproject('sub2') + +# Test mixing auto and manual, using 2015 rules +rust.subproject('sub3') + +# Test mixing auto and manual, using 2018 rules +rust.subproject('sub4') diff --git a/test cases/cargo/13 test targets/subprojects/sub1/Cargo.toml b/test cases/cargo/13 test targets/subprojects/sub1/Cargo.toml new file mode 100644 index 000000000000..1686bba337ce --- /dev/null +++ b/test cases/cargo/13 test targets/subprojects/sub1/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "functional_tests" +version = "1.0.0" +autotests = false + +[lib] +name = "mylib" +path = "src/lib.rs" +test = false + +[[test]] +name = "mylib_test" +path = "src/tests/test.rs" diff --git a/test cases/cargo/13 test targets/subprojects/sub1/src/lib.rs b/test cases/cargo/13 test targets/subprojects/sub1/src/lib.rs new file mode 100644 index 000000000000..920280b6db6d --- /dev/null +++ b/test cases/cargo/13 test targets/subprojects/sub1/src/lib.rs @@ -0,0 +1,3 @@ +pub fn add(a: i32, b: i32) -> i32 { + return a + b; +} diff --git a/test cases/cargo/13 test targets/subprojects/sub1/src/tests/norun.rs b/test cases/cargo/13 test targets/subprojects/sub1/src/tests/norun.rs new file mode 100644 index 000000000000..98f796b7f1d2 --- /dev/null +++ b/test cases/cargo/13 test targets/subprojects/sub1/src/tests/norun.rs @@ -0,0 +1,5 @@ +// This test should not be built, or run + +pub fn main { + std::process::exit(1); +} diff --git a/test cases/cargo/13 test targets/subprojects/sub1/src/tests/test.rs b/test cases/cargo/13 test targets/subprojects/sub1/src/tests/test.rs new file mode 100644 index 000000000000..e0582b1b9422 --- /dev/null +++ b/test cases/cargo/13 test targets/subprojects/sub1/src/tests/test.rs @@ -0,0 +1,7 @@ +extern crate mylib; + +pub fn main() { + let a = mylib::add(1, 2); + let b = a - 3; + std::process::exit(b); +} diff --git a/test cases/cargo/13 test targets/subprojects/sub2/Cargo.toml b/test cases/cargo/13 test targets/subprojects/sub2/Cargo.toml new file mode 100644 index 000000000000..c629e3f9e84c --- /dev/null +++ b/test cases/cargo/13 test targets/subprojects/sub2/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "functional_test_auto" +version = "1.0.0" + +[lib] +name = "mylib" +path = "src/lib.rs" +test = false diff --git a/test cases/cargo/13 test targets/subprojects/sub2/src/lib.rs b/test cases/cargo/13 test targets/subprojects/sub2/src/lib.rs new file mode 100644 index 000000000000..920280b6db6d --- /dev/null +++ b/test cases/cargo/13 test targets/subprojects/sub2/src/lib.rs @@ -0,0 +1,3 @@ +pub fn add(a: i32, b: i32) -> i32 { + return a + b; +} diff --git a/test cases/cargo/13 test targets/subprojects/sub2/tests/test.rs b/test cases/cargo/13 test targets/subprojects/sub2/tests/test.rs new file mode 100644 index 000000000000..e0582b1b9422 --- /dev/null +++ b/test cases/cargo/13 test targets/subprojects/sub2/tests/test.rs @@ -0,0 +1,7 @@ +extern crate mylib; + +pub fn main() { + let a = mylib::add(1, 2); + let b = a - 3; + std::process::exit(b); +} diff --git a/test cases/cargo/13 test targets/subprojects/sub3/Cargo.toml b/test cases/cargo/13 test targets/subprojects/sub3/Cargo.toml new file mode 100644 index 000000000000..bf084f8c8a81 --- /dev/null +++ b/test cases/cargo/13 test targets/subprojects/sub3/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "functional_test_mixed_2015" +version = "1.0.0" +edition = '2015' + +[lib] +name = "mylib" +path = "src/lib.rs" +test = false + +[[test]] +name = "manual_test_with_auto_2018" +path = "tests/test.rs" diff --git a/test cases/cargo/13 test targets/subprojects/sub3/src/lib.rs b/test cases/cargo/13 test targets/subprojects/sub3/src/lib.rs new file mode 100644 index 000000000000..920280b6db6d --- /dev/null +++ b/test cases/cargo/13 test targets/subprojects/sub3/src/lib.rs @@ -0,0 +1,3 @@ +pub fn add(a: i32, b: i32) -> i32 { + return a + b; +} diff --git a/test cases/cargo/13 test targets/subprojects/sub3/tests/test.rs b/test cases/cargo/13 test targets/subprojects/sub3/tests/test.rs new file mode 100644 index 000000000000..e0582b1b9422 --- /dev/null +++ b/test cases/cargo/13 test targets/subprojects/sub3/tests/test.rs @@ -0,0 +1,7 @@ +extern crate mylib; + +pub fn main() { + let a = mylib::add(1, 2); + let b = a - 3; + std::process::exit(b); +} diff --git a/test cases/cargo/13 test targets/subprojects/sub4/Cargo.toml b/test cases/cargo/13 test targets/subprojects/sub4/Cargo.toml new file mode 100644 index 000000000000..46940ccba828 --- /dev/null +++ b/test cases/cargo/13 test targets/subprojects/sub4/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "functional_test_mixed_2015" +version = "1.0.0" +edition = '2018' + +[lib] +name = "mylib" +path = "src/lib.rs" +test = false + +[[test]] +name = "manual_test_with_auto" +path = "tests/test.rs" diff --git a/test cases/cargo/13 test targets/subprojects/sub4/src/lib.rs b/test cases/cargo/13 test targets/subprojects/sub4/src/lib.rs new file mode 100644 index 000000000000..920280b6db6d --- /dev/null +++ b/test cases/cargo/13 test targets/subprojects/sub4/src/lib.rs @@ -0,0 +1,3 @@ +pub fn add(a: i32, b: i32) -> i32 { + return a + b; +} diff --git a/test cases/cargo/13 test targets/subprojects/sub4/tests/auto.rs b/test cases/cargo/13 test targets/subprojects/sub4/tests/auto.rs new file mode 100644 index 000000000000..3a209886209c --- /dev/null +++ b/test cases/cargo/13 test targets/subprojects/sub4/tests/auto.rs @@ -0,0 +1,7 @@ +extern crate mylib; + +pub fn main() { + let a = mylib::add(1, -12); + let b = a + 11; + std::process::exit(b); +} diff --git a/test cases/cargo/13 test targets/subprojects/sub4/tests/test.rs b/test cases/cargo/13 test targets/subprojects/sub4/tests/test.rs new file mode 100644 index 000000000000..e0582b1b9422 --- /dev/null +++ b/test cases/cargo/13 test targets/subprojects/sub4/tests/test.rs @@ -0,0 +1,7 @@ +extern crate mylib; + +pub fn main() { + let a = mylib::add(1, 2); + let b = a - 3; + std::process::exit(b); +} diff --git a/test cases/cargo/14 empty dependencies section/app.rs b/test cases/cargo/14 empty dependencies section/app.rs new file mode 100644 index 000000000000..ec76d3bac108 --- /dev/null +++ b/test cases/cargo/14 empty dependencies section/app.rs @@ -0,0 +1,5 @@ +extern crate cool_lib; + +pub fn main() { + std::process::exit(cool_lib::add(1, 2)); +} diff --git a/test cases/cargo/14 empty dependencies section/meson.build b/test cases/cargo/14 empty dependencies section/meson.build new file mode 100644 index 000000000000..2515c6c0ce33 --- /dev/null +++ b/test cases/cargo/14 empty dependencies section/meson.build @@ -0,0 +1,12 @@ +project('cargo lib', 'rust') + +rust = import('unstable-rust') +sub = rust.subproject('mylib') + +dep = sub.get_variable('dep') + +executable( + 'app', + 'app.rs', + dependencies : dep, +) diff --git a/test cases/cargo/14 empty dependencies section/subprojects/mylib/Cargo.toml b/test cases/cargo/14 empty dependencies section/subprojects/mylib/Cargo.toml new file mode 100644 index 000000000000..a8df1b41b8f0 --- /dev/null +++ b/test cases/cargo/14 empty dependencies section/subprojects/mylib/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "mylib" +version = "2.0.0" + +[lib] +name = "cool-lib" +path = "src/cool-lib.rs" + +[dependencies] diff --git a/test cases/cargo/14 empty dependencies section/subprojects/mylib/src/cool-lib.rs b/test cases/cargo/14 empty dependencies section/subprojects/mylib/src/cool-lib.rs new file mode 100644 index 000000000000..c7b0e6d92c24 --- /dev/null +++ b/test cases/cargo/14 empty dependencies section/subprojects/mylib/src/cool-lib.rs @@ -0,0 +1,3 @@ +pub fn add(x: i32, y: i32) -> i32 { + return x + y; +} diff --git a/test cases/cargo/15 dev dependencies/meson.build b/test cases/cargo/15 dev dependencies/meson.build new file mode 100644 index 000000000000..41c071c1f071 --- /dev/null +++ b/test cases/cargo/15 dev dependencies/meson.build @@ -0,0 +1,4 @@ +project('15 dev dependencies', 'rust') + +rust = import('unstable-rust') +rust.subproject('sub1') diff --git a/test cases/cargo/15 dev dependencies/subprojects/sub1/Cargo.toml b/test cases/cargo/15 dev dependencies/subprojects/sub1/Cargo.toml new file mode 100644 index 000000000000..60bd1af4fa70 --- /dev/null +++ b/test cases/cargo/15 dev dependencies/subprojects/sub1/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "test-package" +version = "1.0.0" +autolibs = false +autotests = false +autobins = false + +[dev-dependencies] +test-function = ">= 1.0.0" + +[lib] +name = "mylib" +path = "src/lib.rs" diff --git a/test cases/cargo/15 dev dependencies/subprojects/sub1/src/lib.rs b/test cases/cargo/15 dev dependencies/subprojects/sub1/src/lib.rs new file mode 100644 index 000000000000..7ce8cd912fa1 --- /dev/null +++ b/test cases/cargo/15 dev dependencies/subprojects/sub1/src/lib.rs @@ -0,0 +1,18 @@ +#[cfg(test)] +extern crate test_function; + +pub fn function(x: i32) -> i32 { + return x + 5; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn with_sub() { + let i = function(test_function::function()); + assert_eq!(i, 10); + } +} + diff --git a/test cases/cargo/15 dev dependencies/subprojects/test_function/Cargo.toml b/test cases/cargo/15 dev dependencies/subprojects/test_function/Cargo.toml new file mode 100644 index 000000000000..4b32f9145062 --- /dev/null +++ b/test cases/cargo/15 dev dependencies/subprojects/test_function/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "test-function" +version = "1.0.0" +autolibs = false +autotests = false +autobins = false + +[lib] +path = "src/lib.rs" +test = false diff --git a/test cases/cargo/15 dev dependencies/subprojects/test_function/src/lib.rs b/test cases/cargo/15 dev dependencies/subprojects/test_function/src/lib.rs new file mode 100644 index 000000000000..0002e05af460 --- /dev/null +++ b/test cases/cargo/15 dev dependencies/subprojects/test_function/src/lib.rs @@ -0,0 +1,3 @@ +pub fn function() -> i32 { + return 5; +} diff --git a/test cases/cargo/2 autobins/meson.build b/test cases/cargo/2 autobins/meson.build new file mode 100644 index 000000000000..3ffe68df9472 --- /dev/null +++ b/test cases/cargo/2 autobins/meson.build @@ -0,0 +1,26 @@ +project('basic cargo test', 'rust') + +rust = import('unstable-rust') + +# This implements the 2018 behavior, when no manual bins are defined +sub = rust.subproject('test') +app = sub.get_variable('app') +tester = find_program('test.py') +test('test 2018 behavior (no manual)', tester, args : [app]) + +# test the 2018 behavior, which autobins default to true, even if manual bins +# are defined +subp = rust.subproject('sub2') +app = subp.get_variable('app') +test('test 2018 behavior (manual)', app) + +# this implements the 2015 behavior, when no bins are defined +subp = rust.subproject('sub') +app = subp.get_variable('app') +test('test 2015 behavior (no manual)', app) + +# test the 2015 behavior, which autobins default to false if any manual bins +# are defined. +subp = rust.subproject('sub3') +app = subp.get_variable('app', 'sentinal') +assert(app == 'sentinal', 'autobins should not be on') diff --git a/test cases/cargo/2 autobins/subprojects/clash/Cargo.toml b/test cases/cargo/2 autobins/subprojects/clash/Cargo.toml new file mode 100644 index 000000000000..a7e87760b9cc --- /dev/null +++ b/test cases/cargo/2 autobins/subprojects/clash/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "manual_auto_clash" +version = "1.0.0" + +[features] +need_to_build = [] + +[[bin]] +name = "foo" +path = "src/bin/foo.rs" +features diff --git a/test cases/cargo/2 autobins/subprojects/sub/Cargo.toml b/test cases/cargo/2 autobins/subprojects/sub/Cargo.toml new file mode 100644 index 000000000000..aea7a0c59d70 --- /dev/null +++ b/test cases/cargo/2 autobins/subprojects/sub/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "old_autobins_project" +version = "1.0.0" diff --git a/test cases/cargo/2 autobins/subprojects/sub/src/bin/app.rs b/test cases/cargo/2 autobins/subprojects/sub/src/bin/app.rs new file mode 100644 index 000000000000..0379b92c2bc9 --- /dev/null +++ b/test cases/cargo/2 autobins/subprojects/sub/src/bin/app.rs @@ -0,0 +1,3 @@ +pub fn main() { + std::process::exit(0); +} diff --git a/test cases/cargo/2 autobins/subprojects/sub2/Cargo.toml b/test cases/cargo/2 autobins/subprojects/sub2/Cargo.toml new file mode 100644 index 000000000000..5ae895ea89ee --- /dev/null +++ b/test cases/cargo/2 autobins/subprojects/sub2/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "auto_bins_2018_with_manual" +version = "1.0.0" +edition = "2018" + +[[bin]] +name = "foo" +path = "src/foo.rs" diff --git a/test cases/cargo/2 autobins/subprojects/sub2/src/bin/app.rs b/test cases/cargo/2 autobins/subprojects/sub2/src/bin/app.rs new file mode 100644 index 000000000000..0379b92c2bc9 --- /dev/null +++ b/test cases/cargo/2 autobins/subprojects/sub2/src/bin/app.rs @@ -0,0 +1,3 @@ +pub fn main() { + std::process::exit(0); +} diff --git a/test cases/cargo/2 autobins/subprojects/sub2/src/foo.rs b/test cases/cargo/2 autobins/subprojects/sub2/src/foo.rs new file mode 100644 index 000000000000..55877a5b8fcc --- /dev/null +++ b/test cases/cargo/2 autobins/subprojects/sub2/src/foo.rs @@ -0,0 +1,4 @@ +pub fn main() { + std::process::exit(1); +} + diff --git a/test cases/cargo/2 autobins/subprojects/sub3/Cargo.toml b/test cases/cargo/2 autobins/subprojects/sub3/Cargo.toml new file mode 100644 index 000000000000..51d819fd85cb --- /dev/null +++ b/test cases/cargo/2 autobins/subprojects/sub3/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "auto_bins_2018_with_manual" +version = "1.0.0" + +[[bin]] +name = "foo" +path = "src/foo.rs" diff --git a/test cases/cargo/2 autobins/subprojects/sub3/src/bin/app.rs b/test cases/cargo/2 autobins/subprojects/sub3/src/bin/app.rs new file mode 100644 index 000000000000..0379b92c2bc9 --- /dev/null +++ b/test cases/cargo/2 autobins/subprojects/sub3/src/bin/app.rs @@ -0,0 +1,3 @@ +pub fn main() { + std::process::exit(0); +} diff --git a/test cases/cargo/2 autobins/subprojects/sub3/src/foo.rs b/test cases/cargo/2 autobins/subprojects/sub3/src/foo.rs new file mode 100644 index 000000000000..55877a5b8fcc --- /dev/null +++ b/test cases/cargo/2 autobins/subprojects/sub3/src/foo.rs @@ -0,0 +1,4 @@ +pub fn main() { + std::process::exit(1); +} + diff --git a/test cases/cargo/2 autobins/subprojects/test/Cargo.toml b/test cases/cargo/2 autobins/subprojects/test/Cargo.toml new file mode 100644 index 000000000000..dcf8d924360a --- /dev/null +++ b/test cases/cargo/2 autobins/subprojects/test/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "autobins_project" +version = "1.0.0" +edition = "2018" diff --git a/test cases/cargo/2 autobins/subprojects/test/src/bin/app.rs b/test cases/cargo/2 autobins/subprojects/test/src/bin/app.rs new file mode 100644 index 000000000000..79a1a5a0b55b --- /dev/null +++ b/test cases/cargo/2 autobins/subprojects/test/src/bin/app.rs @@ -0,0 +1,3 @@ +pub fn main() { + println!("Hello World!"); +} diff --git a/test cases/cargo/2 autobins/test.py b/test cases/cargo/2 autobins/test.py new file mode 100755 index 000000000000..139febe2f1e9 --- /dev/null +++ b/test cases/cargo/2 autobins/test.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 + +import argparse +import subprocess +import sys + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument('bin') + args = parser.parse_args() + + ret = subprocess.run(args.bin, stdout=subprocess.PIPE) + if ret.stdout == b'Hello World!\n': + return 0 + print(f'Expected "Hello World!\n", but got "{ret.stdout.decode()}"') + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test cases/cargo/3 manual bins/meson.build b/test cases/cargo/3 manual bins/meson.build new file mode 100644 index 000000000000..a6bd08650a9f --- /dev/null +++ b/test cases/cargo/3 manual bins/meson.build @@ -0,0 +1,14 @@ +project('basic cargo test', 'rust') + +rust = import('unstable-rust') +sub = rust.subproject('test') + +app = sub.get_variable('app') + +# Test a source != name +printer = sub.get_variable('printer') + +tester = find_program('test.py') + +test('test', tester, args : [app, 'Hello World!']) +test('test', printer, args : [app, 'This is an app!']) diff --git a/test cases/cargo/3 manual bins/subprojects/test/Cargo.toml b/test cases/cargo/3 manual bins/subprojects/test/Cargo.toml new file mode 100644 index 000000000000..89ebd663b340 --- /dev/null +++ b/test cases/cargo/3 manual bins/subprojects/test/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "manual_bins" +version = "1.0.0" +autobins = false + +[[bin]] +name = "app" +path = "src/app.rs" + +[[bin]] +name = "printer" +path = "src/app2.rs" diff --git a/test cases/cargo/3 manual bins/subprojects/test/src/app.rs b/test cases/cargo/3 manual bins/subprojects/test/src/app.rs new file mode 100644 index 000000000000..79a1a5a0b55b --- /dev/null +++ b/test cases/cargo/3 manual bins/subprojects/test/src/app.rs @@ -0,0 +1,3 @@ +pub fn main() { + println!("Hello World!"); +} diff --git a/test cases/cargo/3 manual bins/subprojects/test/src/app2.rs b/test cases/cargo/3 manual bins/subprojects/test/src/app2.rs new file mode 100644 index 000000000000..36c5b89b7c27 --- /dev/null +++ b/test cases/cargo/3 manual bins/subprojects/test/src/app2.rs @@ -0,0 +1,3 @@ +pub fn main() { + println!("This is an app!"); +} diff --git a/test cases/cargo/3 manual bins/test.py b/test cases/cargo/3 manual bins/test.py new file mode 100755 index 000000000000..79abea793240 --- /dev/null +++ b/test cases/cargo/3 manual bins/test.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +import argparse +import subprocess +import sys + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument('bin') + parser.add_argument('expected') + args = parser.parse_args() + + ret = subprocess.run(args.bin, stdout=subprocess.PIPE) + out = ret.stdout.decode().strip() + if out == args.expected: + return 0 + print(f'Expected "{args.expected}", but got "{out}"') + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test cases/cargo/4 mixed bins/meson.build b/test cases/cargo/4 mixed bins/meson.build new file mode 100644 index 000000000000..a6bd08650a9f --- /dev/null +++ b/test cases/cargo/4 mixed bins/meson.build @@ -0,0 +1,14 @@ +project('basic cargo test', 'rust') + +rust = import('unstable-rust') +sub = rust.subproject('test') + +app = sub.get_variable('app') + +# Test a source != name +printer = sub.get_variable('printer') + +tester = find_program('test.py') + +test('test', tester, args : [app, 'Hello World!']) +test('test', printer, args : [app, 'This is an app!']) diff --git a/test cases/cargo/4 mixed bins/subprojects/test/Cargo.toml b/test cases/cargo/4 mixed bins/subprojects/test/Cargo.toml new file mode 100644 index 000000000000..88432eb5de58 --- /dev/null +++ b/test cases/cargo/4 mixed bins/subprojects/test/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "mixed_bins" +version = "1.0.0" +autobins = true + +[[bin]] +name = "printer" +path = "src/app2.rs" diff --git a/test cases/cargo/4 mixed bins/subprojects/test/src/app2.rs b/test cases/cargo/4 mixed bins/subprojects/test/src/app2.rs new file mode 100644 index 000000000000..36c5b89b7c27 --- /dev/null +++ b/test cases/cargo/4 mixed bins/subprojects/test/src/app2.rs @@ -0,0 +1,3 @@ +pub fn main() { + println!("This is an app!"); +} diff --git a/test cases/cargo/4 mixed bins/subprojects/test/src/bin/app.rs b/test cases/cargo/4 mixed bins/subprojects/test/src/bin/app.rs new file mode 100644 index 000000000000..79a1a5a0b55b --- /dev/null +++ b/test cases/cargo/4 mixed bins/subprojects/test/src/bin/app.rs @@ -0,0 +1,3 @@ +pub fn main() { + println!("Hello World!"); +} diff --git a/test cases/cargo/4 mixed bins/test.py b/test cases/cargo/4 mixed bins/test.py new file mode 100755 index 000000000000..79abea793240 --- /dev/null +++ b/test cases/cargo/4 mixed bins/test.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +import argparse +import subprocess +import sys + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument('bin') + parser.add_argument('expected') + args = parser.parse_args() + + ret = subprocess.run(args.bin, stdout=subprocess.PIPE) + out = ret.stdout.decode().strip() + if out == args.expected: + return 0 + print(f'Expected "{args.expected}", but got "{out}"') + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test cases/cargo/5 auto lib/app.rs b/test cases/cargo/5 auto lib/app.rs new file mode 100644 index 000000000000..58dba99811e2 --- /dev/null +++ b/test cases/cargo/5 auto lib/app.rs @@ -0,0 +1,5 @@ +extern crate mylib; + +pub fn main() { + std::process::exit(mylib::add(1, 2)); +} diff --git a/test cases/cargo/5 auto lib/meson.build b/test cases/cargo/5 auto lib/meson.build new file mode 100644 index 000000000000..2515c6c0ce33 --- /dev/null +++ b/test cases/cargo/5 auto lib/meson.build @@ -0,0 +1,12 @@ +project('cargo lib', 'rust') + +rust = import('unstable-rust') +sub = rust.subproject('mylib') + +dep = sub.get_variable('dep') + +executable( + 'app', + 'app.rs', + dependencies : dep, +) diff --git a/test cases/cargo/5 auto lib/subprojects/mylib/Cargo.toml b/test cases/cargo/5 auto lib/subprojects/mylib/Cargo.toml new file mode 100644 index 000000000000..d1846eabbd92 --- /dev/null +++ b/test cases/cargo/5 auto lib/subprojects/mylib/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "mylib" +version = "2.0.0" diff --git a/test cases/cargo/5 auto lib/subprojects/mylib/src/lib.rs b/test cases/cargo/5 auto lib/subprojects/mylib/src/lib.rs new file mode 100644 index 000000000000..7aa68259a8b0 --- /dev/null +++ b/test cases/cargo/5 auto lib/subprojects/mylib/src/lib.rs @@ -0,0 +1,3 @@ +pub fn add(x: i32, y: i32) -> i32 { + return (x + y); +} diff --git a/test cases/cargo/6 manual lib/app.rs b/test cases/cargo/6 manual lib/app.rs new file mode 100644 index 000000000000..ec76d3bac108 --- /dev/null +++ b/test cases/cargo/6 manual lib/app.rs @@ -0,0 +1,5 @@ +extern crate cool_lib; + +pub fn main() { + std::process::exit(cool_lib::add(1, 2)); +} diff --git a/test cases/cargo/6 manual lib/meson.build b/test cases/cargo/6 manual lib/meson.build new file mode 100644 index 000000000000..2515c6c0ce33 --- /dev/null +++ b/test cases/cargo/6 manual lib/meson.build @@ -0,0 +1,12 @@ +project('cargo lib', 'rust') + +rust = import('unstable-rust') +sub = rust.subproject('mylib') + +dep = sub.get_variable('dep') + +executable( + 'app', + 'app.rs', + dependencies : dep, +) diff --git a/test cases/cargo/6 manual lib/subprojects/mylib/Cargo.toml b/test cases/cargo/6 manual lib/subprojects/mylib/Cargo.toml new file mode 100644 index 000000000000..30322f5e92f8 --- /dev/null +++ b/test cases/cargo/6 manual lib/subprojects/mylib/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "mylib" +version = "2.0.0" + +[lib] +name = "cool-lib" +path = "src/cool-lib.rs" diff --git a/test cases/cargo/6 manual lib/subprojects/mylib/src/cool-lib.rs b/test cases/cargo/6 manual lib/subprojects/mylib/src/cool-lib.rs new file mode 100644 index 000000000000..c7b0e6d92c24 --- /dev/null +++ b/test cases/cargo/6 manual lib/subprojects/mylib/src/cool-lib.rs @@ -0,0 +1,3 @@ +pub fn add(x: i32, y: i32) -> i32 { + return x + y; +} diff --git a/test cases/cargo/7 bins in crate link to lib/app.rs b/test cases/cargo/7 bins in crate link to lib/app.rs new file mode 100644 index 000000000000..ec76d3bac108 --- /dev/null +++ b/test cases/cargo/7 bins in crate link to lib/app.rs @@ -0,0 +1,5 @@ +extern crate cool_lib; + +pub fn main() { + std::process::exit(cool_lib::add(1, 2)); +} diff --git a/test cases/cargo/7 bins in crate link to lib/meson.build b/test cases/cargo/7 bins in crate link to lib/meson.build new file mode 100644 index 000000000000..2515c6c0ce33 --- /dev/null +++ b/test cases/cargo/7 bins in crate link to lib/meson.build @@ -0,0 +1,12 @@ +project('cargo lib', 'rust') + +rust = import('unstable-rust') +sub = rust.subproject('mylib') + +dep = sub.get_variable('dep') + +executable( + 'app', + 'app.rs', + dependencies : dep, +) diff --git a/test cases/cargo/7 bins in crate link to lib/subprojects/mylib/Cargo.toml b/test cases/cargo/7 bins in crate link to lib/subprojects/mylib/Cargo.toml new file mode 100644 index 000000000000..f2c79c4754b6 --- /dev/null +++ b/test cases/cargo/7 bins in crate link to lib/subprojects/mylib/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "mylib" +version = "2.0.0" + +[lib] +name = "cool_lib" +path = "src/cool-lib.rs" diff --git a/test cases/cargo/7 bins in crate link to lib/subprojects/mylib/src/bin/cool-app.rs b/test cases/cargo/7 bins in crate link to lib/subprojects/mylib/src/bin/cool-app.rs new file mode 100644 index 000000000000..f9bd424b604b --- /dev/null +++ b/test cases/cargo/7 bins in crate link to lib/subprojects/mylib/src/bin/cool-app.rs @@ -0,0 +1,5 @@ +extern crate cool_lib; + +pub fn main() { + std::process::exit(cool_lib::add(1, 1)); +} diff --git a/test cases/cargo/7 bins in crate link to lib/subprojects/mylib/src/cool-lib.rs b/test cases/cargo/7 bins in crate link to lib/subprojects/mylib/src/cool-lib.rs new file mode 100644 index 000000000000..c7b0e6d92c24 --- /dev/null +++ b/test cases/cargo/7 bins in crate link to lib/subprojects/mylib/src/cool-lib.rs @@ -0,0 +1,3 @@ +pub fn add(x: i32, y: i32) -> i32 { + return x + y; +} diff --git a/test cases/cargo/8 editions/meson.build b/test cases/cargo/8 editions/meson.build new file mode 100644 index 000000000000..88ac85efbe03 --- /dev/null +++ b/test cases/cargo/8 editions/meson.build @@ -0,0 +1,5 @@ +project('cargo editions', 'rust') + +rust = import('unstable-rust') +sub1 = rust.subproject('sub1') +sub2 = rust.subproject('sub2') diff --git a/test cases/cargo/8 editions/subprojects/sub1/Cargo.toml b/test cases/cargo/8 editions/subprojects/sub1/Cargo.toml new file mode 100644 index 000000000000..7a897276c66f --- /dev/null +++ b/test cases/cargo/8 editions/subprojects/sub1/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "sub1" +version = "1.0.1" +edition = "2018" + +[[bin]] +name = "app_2015" +path = "src/app2.rs" +edition = "2015" diff --git a/test cases/cargo/8 editions/subprojects/sub1/src/app2.rs b/test cases/cargo/8 editions/subprojects/sub1/src/app2.rs new file mode 100644 index 000000000000..4e2b5beee788 --- /dev/null +++ b/test cases/cargo/8 editions/subprojects/sub1/src/app2.rs @@ -0,0 +1,4 @@ +pub fn main() { + let async = 0; // This will fail in 2018 + std::process::exit(async); +} diff --git a/test cases/cargo/8 editions/subprojects/sub1/src/bin/app.rs b/test cases/cargo/8 editions/subprojects/sub1/src/bin/app.rs new file mode 100644 index 000000000000..4bcab4898e3b --- /dev/null +++ b/test cases/cargo/8 editions/subprojects/sub1/src/bin/app.rs @@ -0,0 +1,10 @@ +// This file builds with 2018, the default for this project +const fn increment(x: i32) -> i32 { + return x + 1; +} + +const VALUE: i32 = increment(2); + +pub fn main() { + std::process::exit(VALUE - 3); +} diff --git a/test cases/cargo/8 editions/subprojects/sub1/src/lib.rs b/test cases/cargo/8 editions/subprojects/sub1/src/lib.rs new file mode 100644 index 000000000000..20c0cf910db5 --- /dev/null +++ b/test cases/cargo/8 editions/subprojects/sub1/src/lib.rs @@ -0,0 +1,3 @@ +pub async fn increment(x: i8) -> i8 { + return x + 1; +} diff --git a/test cases/cargo/8 editions/subprojects/sub2/Cargo.toml b/test cases/cargo/8 editions/subprojects/sub2/Cargo.toml new file mode 100644 index 000000000000..c4fa13d27ea8 --- /dev/null +++ b/test cases/cargo/8 editions/subprojects/sub2/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "sub2" +version = "1.0.1" +edition = "2015" + +[[bin]] +name = "app_2018" +path = "src/app2.rs" +edition = "2018" diff --git a/test cases/cargo/8 editions/subprojects/sub2/src/app2.rs b/test cases/cargo/8 editions/subprojects/sub2/src/app2.rs new file mode 100644 index 000000000000..4bcab4898e3b --- /dev/null +++ b/test cases/cargo/8 editions/subprojects/sub2/src/app2.rs @@ -0,0 +1,10 @@ +// This file builds with 2018, the default for this project +const fn increment(x: i32) -> i32 { + return x + 1; +} + +const VALUE: i32 = increment(2); + +pub fn main() { + std::process::exit(VALUE - 3); +} diff --git a/test cases/cargo/8 editions/subprojects/sub2/src/bin/app.rs b/test cases/cargo/8 editions/subprojects/sub2/src/bin/app.rs new file mode 100644 index 000000000000..4e2b5beee788 --- /dev/null +++ b/test cases/cargo/8 editions/subprojects/sub2/src/bin/app.rs @@ -0,0 +1,4 @@ +pub fn main() { + let async = 0; // This will fail in 2018 + std::process::exit(async); +} diff --git a/test cases/cargo/9 features/exe.rs b/test cases/cargo/9 features/exe.rs new file mode 100644 index 000000000000..63b7c280e58f --- /dev/null +++ b/test cases/cargo/9 features/exe.rs @@ -0,0 +1,5 @@ +extern crate lib; + +pub fn main() { + std::process::exit(lib::returncode()); +} diff --git a/test cases/cargo/9 features/exe2.rs b/test cases/cargo/9 features/exe2.rs new file mode 100644 index 000000000000..378daec1142e --- /dev/null +++ b/test cases/cargo/9 features/exe2.rs @@ -0,0 +1,6 @@ + +extern crate lib; + +pub fn main() { + std::process::exit(lib::default_rc()); +} diff --git a/test cases/cargo/9 features/meson.build b/test cases/cargo/9 features/meson.build new file mode 100644 index 000000000000..6f438b5901b2 --- /dev/null +++ b/test cases/cargo/9 features/meson.build @@ -0,0 +1,10 @@ +project('cargo features', 'rust') + +rust = import('unstable-rust') +dep = rust.subproject('sub1').get_variable('dep') + +exe = executable('exe', 'exe.rs', dependencies : dep) +exe2 = executable('exe2', 'exe2.rs', dependencies : dep) + +test('feature_compilation', exe) +test('default_features', exe2) diff --git a/test cases/cargo/9 features/meson_options.txt b/test cases/cargo/9 features/meson_options.txt new file mode 100644 index 000000000000..e5f236c673bd --- /dev/null +++ b/test cases/cargo/9 features/meson_options.txt @@ -0,0 +1,5 @@ +option( + 'test_feature', + type : 'boolean', + value : true +) diff --git a/test cases/cargo/9 features/subprojects/sub1/Cargo.toml b/test cases/cargo/9 features/subprojects/sub1/Cargo.toml new file mode 100644 index 000000000000..ba981b9567e4 --- /dev/null +++ b/test cases/cargo/9 features/subprojects/sub1/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "has_features" +version = "1.0.0" + +[features] +default = ["default_feature"] +test_feature = [] +default_feature = [] + +[lib] +name = "lib" +path = "lib.rs" diff --git a/test cases/cargo/9 features/subprojects/sub1/lib.rs b/test cases/cargo/9 features/subprojects/sub1/lib.rs new file mode 100644 index 000000000000..c96ebf4a6c70 --- /dev/null +++ b/test cases/cargo/9 features/subprojects/sub1/lib.rs @@ -0,0 +1,23 @@ +pub fn print_str() -> String { + return "A String".to_string(); +} + +#[cfg(feature = "test_feature")] +pub fn returncode() -> i32 { + return 0; +} + +#[cfg(not(feature = "test_feature"))] +pub fn returncode() -> i32 { + return 1; +} + +#[cfg(feature = "default_feature")] +pub fn default_rc() -> i32 { + return 0; +} + +#[cfg(not(feature = "default_feature"))] +pub fn default_rc() -> i32 { + return 1; +} From 9d9edc45e995754c9dcaaa5ac086058ce60086f2 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 29 Oct 2020 14:44:15 -0700 Subject: [PATCH 08/23] interpreter: add support for cargo subprojects --- mesonbuild/interpreter/interpreter.py | 92 +++++++++++++++++++++------ 1 file changed, 71 insertions(+), 21 deletions(-) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index ceea000513af..cc1f2b085cf6 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -33,6 +33,7 @@ from ..interpreterbase import ObjectHolder, RangeHolder from ..modules import ModuleObject from ..cmake import CMakeInterpreter +from ..cargo import ManifestInterpreter from ..backend.backends import Backend, ExecutableSerialisation from .mesonmain import MesonMain @@ -212,6 +213,7 @@ def __init__( default_project_options: T.Optional[T.Dict[str, str]] = None, mock: bool = False, ast: T.Optional[mparser.CodeBlockNode] = None, + opt_ast: T.Optional[mparser.CodeBlockNode] = None, is_translated: bool = False, ) -> None: super().__init__(build.environment.get_source_dir(), subdir, subproject) @@ -236,6 +238,7 @@ def __init__( elif ast is not None: self.ast = ast self.sanity_check_ast() + self.opt_ast = opt_ast # TODO: sanity check? self.builtin.update({'meson': MesonMain(build, self)}) self.generators = [] self.processed_buildfiles = set() # type: T.Set[str] @@ -383,6 +386,8 @@ def holderify(self, item): return ModuleObjectHolder(item, self) elif isinstance(item, (InterpreterObject, ObjectHolder)): return item + elif isinstance(item, SubprojectHolder): + return item else: raise InterpreterException('Module returned a value of unknown type.') @@ -412,6 +417,8 @@ def process_new_values(self, invalues): elif isinstance(v, (int, str, bool, Disabler, ObjectHolder, build.GeneratedList, ExternalProgram)): pass + elif isinstance(v, Interpreter): + pass else: raise InterpreterException('Module returned a value of unknown type.') @@ -763,6 +770,8 @@ def do_subproject(self, subp_name: 'SubprojectKeyType', method: str, kwargs) -> return self._do_subproject_meson(subp_name, subdir, default_options, kwargs) elif method == 'cmake': return self._do_subproject_cmake(subp_name, subdir, subdir_abs, default_options, kwargs) + elif method == 'cargo': + return self._do_subproject_cargo(subp_name, subdir, subdir_abs, kwargs) else: raise InterpreterException(f'The method {method} is invalid for the subproject {subp_name}') # Invalid code is always an error @@ -781,11 +790,13 @@ def do_subproject(self, subp_name: 'SubprojectKeyType', method: str, kwargs) -> def _do_subproject_meson(self, subp_name: str, subdir: str, default_options, kwargs, ast: T.Optional[mparser.CodeBlockNode] = None, build_def_files: T.Optional[T.List[str]] = None, - is_translated: bool = False) -> SubprojectHolder: + is_translated: bool = False, + opt_ast: T.Optional[mparser.CodeBlockNode] = None) -> SubprojectHolder: with mlog.nested(subp_name): new_build = self.build.copy() subi = Interpreter(new_build, self.backend, subp_name, subdir, self.subproject_dir, - self.modules, default_options, ast=ast, is_translated=is_translated) + self.modules, default_options, ast=ast, opt_ast=opt_ast, + is_translated=is_translated) subi.subprojects = self.subprojects subi.subproject_stack = self.subproject_stack + [subp_name] @@ -819,6 +830,40 @@ def _do_subproject_meson(self, subp_name: str, subdir: str, default_options, kwa self.summary.update(subi.summary) return self.subprojects[subp_name] + def __print_generated_ast(self, message: str, subdir: str, ast: mparser.CodeBlockNode, + opt_ast: T.Optional[mparser.CodeBlockNode] = None) -> None: + """Helper function to print a meson.build from generated ast.""" + mlog.log() + with mlog.nested(message): + mlog.log('Processing generated meson AST') + + # Debug print the generated meson file + from ..ast import AstIndentationGenerator, AstPrinter + printer = AstPrinter() + ast.accept(AstIndentationGenerator()) + ast.accept(printer) + printer.post_process() + meson_filename = os.path.join(self.build.environment.get_build_dir(), subdir, 'meson.build') + with open(meson_filename, "w") as f: + f.write(printer.result) + + mlog.log('Build file:', meson_filename) + mlog.cmd_ci_include(meson_filename) + + if opt_ast: + printer = AstPrinter() + opt_ast.accept(AstIndentationGenerator()) + opt_ast.accept(printer) + printer.post_process() + meson_filename = os.path.join(self.build.environment.get_build_dir(), subdir, 'meson_options.txt') + with open(meson_filename, "w") as f: + f.write(printer.result) + + mlog.log('Build options file:', meson_filename) + mlog.cmd_ci_include(meson_filename) + + mlog.log() + def _do_subproject_cmake(self, subp_name, subdir, subdir_abs, default_options, kwargs): with mlog.nested(subp_name): new_build = self.build.copy() @@ -839,23 +884,7 @@ def _do_subproject_cmake(self, subp_name, subdir, subdir_abs, default_options, k # Generate a meson ast and execute it with the normal do_subproject_meson ast = cm_int.pretend_to_be_meson(options.target_options) - mlog.log() - with mlog.nested('cmake-ast'): - mlog.log('Processing generated meson AST') - - # Debug print the generated meson file - from ..ast import AstIndentationGenerator, AstPrinter - printer = AstPrinter() - ast.accept(AstIndentationGenerator()) - ast.accept(printer) - printer.post_process() - meson_filename = os.path.join(self.build.environment.get_build_dir(), subdir, 'meson.build') - with open(meson_filename, "w") as f: - f.write(printer.result) - - mlog.log('Build file:', meson_filename) - mlog.cmd_ci_include(meson_filename) - mlog.log() + self.__print_generated_ast('cmake-ast', subdir, ast) result = self._do_subproject_meson(subp_name, subdir, default_options, kwargs, ast, cm_int.bs_files, is_translated=True) result.cm_interpreter = cm_int @@ -863,6 +892,24 @@ def _do_subproject_cmake(self, subp_name, subdir, subdir_abs, default_options, k mlog.log() return result + def _do_subproject_cargo(self, subp_name: str, subdir: str, subdir_abs: str, kwargs: T.Dict[str, T.Any]) -> SubprojectHolder: + with mlog.nested(subp_name): + new_build = self.build.copy() + prefix = self.coredata.options[OptionKey('prefix')].value + interp = ManifestInterpreter(new_build, Path(subdir), Path(subdir_abs), Path(prefix), new_build.environment, self.backend) + + # Generate a meson ast and execute it with the normal do_subproject_meson + ast, opt_ast = interp.parse() + self.__print_generated_ast('cargo.toml', subdir, ast, opt_ast) + + result = self._do_subproject_meson( + subp_name, subdir, [], kwargs, ast, [interp.manifest_file], + opt_ast=opt_ast, is_translated=True) + # result.cm_interpreter = cm_int # TODO: what do we need this for? + + mlog.log() + return result + def get_option_internal(self, optname: str): key = OptionKey.from_string(optname).evolve(subproject=self.subproject) @@ -968,9 +1015,12 @@ def func_project(self, node, args, kwargs): raise InterpreterException(f'Meson version is {cv} but project requires {pv}') mesonlib.project_meson_versions[self.subproject] = kwargs['meson_version'] - if os.path.exists(self.option_file): + if self.opt_ast or os.path.exists(self.option_file): oi = optinterpreter.OptionInterpreter(self.subproject) - oi.process(self.option_file) + if self.opt_ast: + oi.process_ast(self.opt_ast) + else: + oi.process(self.option_file) self.coredata.update_project_options(oi.options) self.add_build_def_file(self.option_file) From 7dcfd194f4f646b08dfd346ec065c36f3f00b2dd Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 21 Jan 2021 13:45:49 -0800 Subject: [PATCH 09/23] fixup! cargo: Add a cargo manifest parser --- mesonbuild/cargo/cargo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesonbuild/cargo/cargo.py b/mesonbuild/cargo/cargo.py index 60cd2209ceb8..45fbd4e62956 100644 --- a/mesonbuild/cargo/cargo.py +++ b/mesonbuild/cargo/cargo.py @@ -635,7 +635,7 @@ def parse(self) -> T.Tuple['mparser.CodeBlockNode', 'mparser.CodeBlockNode']: # it, as it vaslty simplifes things. with builder.assignment_builder('rust') as abuilder: with abuilder.function_builder('import') as fbuilder: - fbuilder.positional('rust') + fbuilder.positional('unstable-rust') self.__parse_features(opt_builder) From d3cdc15c9020411651d2c4925048e767657e2804 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 21 Jan 2021 13:47:32 -0800 Subject: [PATCH 10/23] mparser: Use an EmptyNode instead of None, which is invalid --- mesonbuild/mparser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mesonbuild/mparser.py b/mesonbuild/mparser.py index 10796827ca25..15859eb3c6ee 100644 --- a/mesonbuild/mparser.py +++ b/mesonbuild/mparser.py @@ -446,8 +446,8 @@ def __init__(self, linenode: BaseNode, condition: BaseNode, block: CodeBlockNode class IfClauseNode(BaseNode): def __init__(self, linenode: BaseNode): super().__init__(linenode.lineno, linenode.colno, linenode.filename) - self.ifs = [] # type: T.List[IfNode] - self.elseblock = None # type: T.Union[EmptyNode, CodeBlockNode] + self.ifs: T.List[IfNode] = [] + self.elseblock: T.Union[EmptyNode, CodeBlockNode] = EmptyNode(-1, -1, '') class UMinusNode(BaseNode): def __init__(self, current_location: Token, value: BaseNode): From bc87c06ddaf9cb244848ef698b97584044738337 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Fri, 30 Oct 2020 13:46:50 -0700 Subject: [PATCH 11/23] wip: split me Giant blob of stuff all shoved into one giant commit --- TODO.md | 43 ++ mesonbuild/cargo/__init__.py | 3 +- mesonbuild/cargo/cargo.py | 180 ++++++--- mesonbuild/cargo/cfg_builder.py | 127 ++++++ mesonbuild/cargo/cfg_parser.py | 372 ++++++++++++++++++ mesonbuild/cargo/nodebuilder.py | 134 ++++++- mesonbuild/environment.py | 33 +- mesonbuild/interpreter/interpreter.py | 41 +- mesonbuild/interpreterbase.py | 2 +- mesonbuild/modules/unstable_rust.py | 16 +- pytests/__init__.py | 0 pytests/cfg_parser_test.py | 141 +++++++ run_project_tests.py | 1 + setup.cfg | 2 + .../11 optional dependencies/meson.build | 2 +- .../meson_options.txt | 5 - .../subprojects/sub1/Cargo.toml | 2 +- test cases/cargo/16 multiple versions/exe.rs | 6 + .../cargo/16 multiple versions/meson.build | 8 + .../subprojects/sub-1.7.2/Cargo.toml | 6 + .../subprojects/sub-1.7.2/src/lib.rs | 3 + .../subprojects/sub/Cargo.toml | 3 + .../17 target cfg dependencies/exe.rs.in | 5 + .../17 target cfg dependencies/meson.build | 37 ++ .../subprojects/sub1/Cargo.toml | 11 + .../subprojects/sub1/src/lib.rs | 5 + .../subprojects/sub2/Cargo.toml | 5 + .../subprojects/sub2/src/lib.rs | 3 + .../subprojects/sub3/Cargo.toml | 11 + .../subprojects/sub3/src/lib.rs | 5 + .../subprojects/sub4/Cargo.toml | 8 + .../subprojects/sub4/src/lib.rs | 5 + .../subprojects/sub5/Cargo.toml | 8 + .../subprojects/sub5/src/lib.rs | 5 + test cases/cargo/9 features/meson.build | 2 +- test cases/cargo/9 features/meson_options.txt | 5 - 36 files changed, 1135 insertions(+), 110 deletions(-) create mode 100644 TODO.md create mode 100644 mesonbuild/cargo/cfg_builder.py create mode 100644 mesonbuild/cargo/cfg_parser.py create mode 100644 pytests/__init__.py create mode 100644 pytests/cfg_parser_test.py delete mode 100644 test cases/cargo/11 optional dependencies/meson_options.txt create mode 100644 test cases/cargo/16 multiple versions/exe.rs create mode 100644 test cases/cargo/16 multiple versions/meson.build create mode 100644 test cases/cargo/16 multiple versions/subprojects/sub-1.7.2/Cargo.toml create mode 100644 test cases/cargo/16 multiple versions/subprojects/sub-1.7.2/src/lib.rs create mode 100644 test cases/cargo/16 multiple versions/subprojects/sub/Cargo.toml create mode 100644 test cases/cargo/17 target cfg dependencies/exe.rs.in create mode 100644 test cases/cargo/17 target cfg dependencies/meson.build create mode 100644 test cases/cargo/17 target cfg dependencies/subprojects/sub1/Cargo.toml create mode 100644 test cases/cargo/17 target cfg dependencies/subprojects/sub1/src/lib.rs create mode 100644 test cases/cargo/17 target cfg dependencies/subprojects/sub2/Cargo.toml create mode 100644 test cases/cargo/17 target cfg dependencies/subprojects/sub2/src/lib.rs create mode 100644 test cases/cargo/17 target cfg dependencies/subprojects/sub3/Cargo.toml create mode 100644 test cases/cargo/17 target cfg dependencies/subprojects/sub3/src/lib.rs create mode 100644 test cases/cargo/17 target cfg dependencies/subprojects/sub4/Cargo.toml create mode 100644 test cases/cargo/17 target cfg dependencies/subprojects/sub4/src/lib.rs create mode 100644 test cases/cargo/17 target cfg dependencies/subprojects/sub5/Cargo.toml create mode 100644 test cases/cargo/17 target cfg dependencies/subprojects/sub5/src/lib.rs delete mode 100644 test cases/cargo/9 features/meson_options.txt diff --git a/TODO.md b/TODO.md new file mode 100644 index 000000000000..3f6e84218e16 --- /dev/null +++ b/TODO.md @@ -0,0 +1,43 @@ +Cargo to meson.build: + - Completed: + - dependencies + - binaries + - libraries + - features + - target cfg() paring + + - TODO: + - targets + - no support for the triple style + - tests being triggered when they shouldn't? + - build-dependencies + - example + - bench + - profile + - patch + - test for default_options being set correctly + - binaries: + - test for auto bin and manual bin collision + - targets required features + - doctest? + - bench + - libraries + - targets required features + - proc-macro + - doctest? + - bench + - Add test for invalid feature names + - extend test 15 to cover test, exampeles, and benches + - is the use of a disabler() for dev-dependencies smart? + +Wrap + - TODO: + - workspaces + - fetching crates + - generating .wraps from crates + +Overall: + - Figure out what to do about multiple versions of the same crate + - Add a mechanism to test.json to verify that the correct tests are being run + - Come up with a better solution for subprojects affecting each-others options + - how to handle recursive subprojects diff --git a/mesonbuild/cargo/__init__.py b/mesonbuild/cargo/__init__.py index 7bf1854742ff..b8cb0052aed5 100644 --- a/mesonbuild/cargo/__init__.py +++ b/mesonbuild/cargo/__init__.py @@ -1,4 +1,5 @@ -# Copyright © 2020 Intel Corporation +# SPDX-license-identifier: Apache-2.0 +# Copyright © 2020-2021 Intel Corporation # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/mesonbuild/cargo/cargo.py b/mesonbuild/cargo/cargo.py index 45fbd4e62956..e55dc70a38e7 100644 --- a/mesonbuild/cargo/cargo.py +++ b/mesonbuild/cargo/cargo.py @@ -1,4 +1,5 @@ -# Copyright © 2020 Intel Corporation +# SPDX-license-identifier: Apache-2.0 +# Copyright © 2020-2021 Intel Corporation # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,17 +14,20 @@ # limitations under the License. from pathlib import Path +import collections import textwrap import typing as T import toml from .. import mlog -from ..dependencies.base import find_external_program -from ..interpreterbase import InterpreterException -from ..mesonlib import MachineChoice, MesonException +from ..envconfig import MachineInfo +from ..environment import normalize_cpu_family +from ..mesonlib import MesonException, version_compare_many from ..optinterpreter import is_invalid_name from .nodebuilder import ObjectBuilder, NodeBuilder +from .cfg_parser import Node, parse as cfg_parser +from . import cfg_builder if T.TYPE_CHECKING: from .. import mparser @@ -116,6 +120,9 @@ total=False ) + class TargetDict(TypedDict): + + dependencies: T.Dict[str, T.Union[str, _DependencyDict]] # Type information for a Cargo.toml manifest file. @@ -129,6 +136,7 @@ 'features': T.Dict[str, T.List[str]], 'dependencies': T.Dict[str, T.Union[str, _DependencyDict]], 'dev-dependencies': T.Dict[str, T.Union[str, _DependencyDict]], + 'target': T.Dict[str, TargetDict], }, total=False, ) @@ -238,7 +246,7 @@ def split(raw: str) -> T.Tuple[int, T.Optional[int], T.Optional[int]]: else: final.append(f'< {major + 1}.0.0') else: - assert r.startswith(('<', '>', '=')) + assert r.startswith(('<', '>', '=')), r if r.startswith('='): # meson uses ==, but cargo uses = final.append(f'={r}') @@ -256,40 +264,53 @@ class ManifestInterpreter: anything we don't actually need (like binaries we may not use). """ - cargo: 'ExternalProgram' - - def __new__(cls, build: 'Build', subdir: Path, src_dir: Path, install_prefix: Path, - env: 'Environment', backend: 'Backend') -> 'ManifestInterpreter': - # Messing with `__new__` is not for novices, but in this case it's - # useful because we can avoid havin to do the "if cargo is none find cargo" dance, - # We have it at class construction time, or we don't - - # TODO: we really should be able to build dependencies for host or build... - for prog in find_external_program(env, MachineChoice.HOST, 'cargo', 'cargo', ['cargo'], True): - if prog.found(): - cls.cargo = prog - break - else: - raise InterpreterException('Could not find required program "cargo"') - - return T.cast('ManifestInterpreter', super().__new__(cls)) - - def __init__(self, build: 'Build', subdir: Path, src_dir: Path, install_prefix: Path, - env: 'Environment', backend: 'Backend'): + def __init__(self, build: 'Build', build_dir: Path, src_dir: Path, install_prefix: Path, + env: 'Environment', backend: 'Backend', features: T.Set[str], + version: T.List[str], default_options: bool): self.build = build - self.subdir = subdir + # TODO: The builddir needs to be modfied to take the version requirements and + # options into account + self.build_dir = build_dir + # TODO: The src_dir needs to consider the version, and if it needs src_dir-version self.src_dir = src_dir self.install_prefix = install_prefix self.environment = env self.backend = backend - manfiest_file = self.src_dir / 'Cargo.toml' - self.manifest_file = str(manfiest_file) - with manfiest_file.open('r') as f: + self.requested_features = features + self.default_options = default_options + self.required_version = version + manifest_file = self.src_dir / 'Cargo.toml' + self.manifest_file = str(manifest_file) + with manifest_file.open('r') as f: self.manifest = T.cast('ManifestDict', toml.load(f)) - # All features enabled in this and all sub-subprojects + # If the version in the subdir doesn't match, then we need to try to find + # a subproject source that does match. + actual_version = self.manifest['package']['version'] + if not version_compare_many(actual_version, self.required_version)[0]: + for g in (self.src_dir.parent.glob(f'{self.src_dir.stem}-*')): + # XXX: it might be better to read the version out of the toml + v = g.stem.split('-')[-1] + if version_compare_many(v, self.required_version): + self.src_dir = g + # XXX: uhhh, this is a hack, maybe? + self.build_dir = self.build_dir.parent / g + manifest_file = self.src_dir / 'Cargo.toml' + self.manifest_file = str(manifest_file) + with manifest_file.open('r') as f: + self.manifest = T.cast('ManifestDict', toml.load(f)) + break + else: + raise MesonException( + f'Cannot find subproject source for {self.src_dir.stem} to ' + f'satisfy version requirements {self.required_version}.') + + # All features enabled in this subproject self.features: T.List[str] = [] + # Map features that this crate turns on for its subprojects + self.subproject_features: T.DefaultDict[str, T.Set[str]] = collections.defaultdict(set) + # A mapping between a feature and the dependencies that are needed to # enable it. self.features_to_deps: T.Dict[str, T.List[str]] = {} @@ -335,7 +356,7 @@ def __parse_lib(self, builder: NodeBuilder) -> None: # We always call the list of dependencies "dependencies", # if there are dependencies we can add them. There could # be an empty dependencies section, so account for that - if self.manifest.get('dependencies'): + if self.manifest.get('dependencies') or self.manifest.get('target'): fbuilder.keyword('dependencies', builder.id('dependencies')) # Always mark everything as build by default false, that way meson will @@ -347,10 +368,12 @@ def __parse_lib(self, builder: NodeBuilder) -> None: fbuilder.keyword('link_with', builder.id('lib')) # It may not be strictly necessary to link with all of the # dependencies, but it is in some cases. - if self.manifest.get('dependencies'): + if self.manifest.get('dependencies') or self.manifest.get('target'): fbuilder.keyword('dependencies', builder.id('dependencies')) - if lib.get('test', True): + # XXX: This should be True by default, but I've run into crates + # that hardcode their names in the rust files, not sure what to do. + if lib.get('test', False): with builder.object_builder('rust') as obuilder: with obuilder.method_builder('test') as fbuilder: fbuilder.positional('lib_test') @@ -481,15 +504,17 @@ def make_bin(name: str, source: str, edition: T.Optional[str] = None, unittest: return created_targets - def __parse_features(self, opt_builder: NodeBuilder) -> None: - """Convert cargo features into meson options. + def __parse_features(self) -> None: + """Parse cargo features. - Create each option function. Cargo uses a single namespace for all - "features" (what meson calls options). They are always boolean, and - to emulate the single namspace, we make them always yielding. - """ - default: T.Set[str] = set(self.manifest.get('features', {}).get('default', [])) + Cargo's features are basically akin to boolean option in meson. + Cargo features do not map 1:1 with meson options, and we need to do + some transformation to make them work. First, one uses a special + "default" option to signal which options are on by default. Second, + options are allowed to specify "If I'm on, then I need my subproject + to have option X on." + """ for name, requirements in self.manifest.get('features', {}).items(): if name == "default": continue @@ -503,35 +528,35 @@ def __parse_features(self, opt_builder: NodeBuilder) -> None: for r in requirements: if '/' in r: subp, opt = r.split('/') - # In this case the crate is trying to change another crate's - # configuration. Meson does not allow this, options are contained - # The best we can do is provide the user a warning - mlog.warning(textwrap.dedent(f'''\ - Crate {self.manifest['package']['name']} wants to turn on the - {opt} in {subp}. Meson does not allow subprojects to change - another subproject's options. You may need to pass - `-D{subp}:{opt}=true` to meson configure for compilation - to succeed. - ''')) + self.subproject_features[subp].add(opt) + new_reqs.append(subp) else: new_reqs.append(r) self.features.append(name) - if requirements: - self.features_to_deps[name] = requirements + if new_reqs: + self.features_to_deps[name] = new_reqs + def __emit_features(self, opt_builder: NodeBuilder) -> None: + """Write out features as options.""" + enabled: T.Set[str] = self.requested_features.copy() + if self.default_options: + enabled |= set(self.manifest.get('features', {}).get('default', [])) + + for name in self.features: with opt_builder.function_builder('option') as fbuilder: fbuilder.positional(name) fbuilder.keyword('type', 'boolean') - fbuilder.keyword('yield', True) - fbuilder.keyword('value', name in default) + fbuilder.keyword('value', name in enabled) - @staticmethod - def __get_dependency(builder: NodeBuilder, name: str, disabler: bool = False) -> 'mparser.MethodNode': + def __get_dependency(self, builder: NodeBuilder, name: str, disabler: bool = False) -> 'mparser.MethodNode': """Helper for getting a supbroject dependency.""" obuilder = ObjectBuilder('rust', builder._builder) with obuilder.method_builder('subproject') as arbuilder: arbuilder.positional(name.replace('-', '_')) + # Pass any features we required to be on to that subproject + if self.subproject_features[name]: + arbuilder.keyword('features', list(self.subproject_features[name])) if disabler: arbuilder.keyword('required', False) with obuilder.method_builder('get_variable') as arbuilder: @@ -540,7 +565,7 @@ def __get_dependency(builder: NodeBuilder, name: str, disabler: bool = False) -> arbuilder.positional(builder._builder.function('disabler')) return obuilder.finalize() - def __parse_dependencies(self, builder: NodeBuilder) -> None: + def __emit_dependencies(self, builder: NodeBuilder) -> None: """Parse all of the dependencies Check all of the dependencies we need, create a "dependencies" array @@ -570,6 +595,24 @@ def __parse_dependencies(self, builder: NodeBuilder) -> None: for name in requires: arbuilder.positional(self.__get_dependency(builder, name)) + # Next we need to check for target (platform) specific dependencies + for condition, target in self.manifest.get('target', {}).items(): + if condition.startswith('cfg('): + with builder.if_builder() as ifcbuilder: + with ifcbuilder.if_builder() as ifbuilder: + with ifbuilder.condition_builder() as cbuilder: + cfg_builder.build(cbuilder, cfg_parser(condition).root) + with ifbuilder.body_builder() as bbuilder: + with bbuilder.plus_assignment_builder('dependencies') as pabuilder: + with pabuilder.array_builder() as arbuilder: + for name, dep in target['dependencies'].items(): + # If the dependency is required, go ahead and build the + # subproject call unconditionally + if isinstance(dep, str) or not dep.get('optional', False): + arbuilder.positional(self.__get_dependency(builder, name)) + else: + raise NotImplementedError(f'condition: {condition}') + def __emit_dev_dependencies(self, builder: NodeBuilder) -> None: """Parse all dev-dependencies @@ -584,7 +627,7 @@ def __emit_dev_dependencies(self, builder: NodeBuilder) -> None: if isinstance(dep, str) or not dep.get('optional', False): arbuilder.positional(self.__get_dependency(builder, name, disabler=True)) - def __emit_features(self, builder: NodeBuilder) -> None: + def __emit_enable_features(self, builder: NodeBuilder) -> None: """Emit the code to check each feature, and add it to the rust arguments if necessary. @@ -608,8 +651,12 @@ def parse(self) -> T.Tuple['mparser.CodeBlockNode', 'mparser.CodeBlockNode']: This can then be fed back into the meson interpreter to create a "meson" project from the crate specification. """ - builder = NodeBuilder(self.subdir) - opt_builder = NodeBuilder(self.subdir) + # Parse features and emit options. These are also needed by dependencies + opt_builder = NodeBuilder(self.build_dir) + self.__parse_features() + self.__emit_features(opt_builder) + + builder = NodeBuilder(self.build_dir) # Create the meson project() function. # @@ -637,17 +684,15 @@ def parse(self) -> T.Tuple['mparser.CodeBlockNode', 'mparser.CodeBlockNode']: with abuilder.function_builder('import') as fbuilder: fbuilder.positional('unstable-rust') - self.__parse_features(opt_builder) - # Create a list of dependencies which will be added to the library (if # there is one). - if self.manifest.get('dependencies'): - self.__parse_dependencies(builder) + if self.manifest.get('dependencies') or self.manifest.get('target'): + self.__emit_dependencies(builder) if self.manifest.get('dev-dependencies'): self.__emit_dev_dependencies(builder) # This needs to be called after dependencies - self.__emit_features(builder) + self.__emit_enable_features(builder) # Look for libs first, becaue if there are libs, then bins need to link # with them. @@ -655,4 +700,9 @@ def parse(self) -> T.Tuple['mparser.CodeBlockNode', 'mparser.CodeBlockNode']: self.__emit_bins(builder) self.__emit_tests(builder) + with builder.object_builder('meson') as obuilder: + with obuilder.method_builder('override_dependency') as arbuilder: + arbuilder.positional(self.manifest['package']['name']) + arbuilder.positional(builder.id('dep')) + return builder.finalize(), opt_builder.finalize() diff --git a/mesonbuild/cargo/cfg_builder.py b/mesonbuild/cargo/cfg_builder.py new file mode 100644 index 000000000000..491286589b7e --- /dev/null +++ b/mesonbuild/cargo/cfg_builder.py @@ -0,0 +1,127 @@ +# SPDX-license-identifier: Apache-2.0 +# Copyright © 2021 Intel Corporation + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Node builder helpers for working with cfg AST. +""" + +import typing as T + +from . import cfg_parser +from .. import mlog + +if T.TYPE_CHECKING: + from .cfg_parser import AST + from .nodebuilder import NodeBuilder + + +# Map cargo target_os values to meson host_machine.system() values +_TARGET_OS_MAP: T.Mapping[str, str] = { + 'dragonfly': 'dragonfly', + 'freebsd': 'freebsd', + 'haiku': 'haiku', + 'illumos': 'sunos', + 'solaris': 'sunos', + 'linux': 'linux', + 'netbsd': 'netbsd', + 'openbsd': 'openbsd', + 'darwin': 'darwin', + 'android': 'android', # this is a convetion on the meson side only, and subject to change + 'macos': 'darwin', + 'ios': 'darwin', + 'windows': 'windows', +} + +# A list of Unix like OSes +# I'm sure some are missing +_UNIX_LIKE_OSES: T.FrozenSet[str] = frozenset({ + 'linux', + 'android', + 'freebsd', + 'openbsd', + 'netbsd', + 'dragonfly', + 'sunos', + 'gnu', + 'cygwin', + 'darwin', +}) + + +def build(builder: 'NodeBuilder', node: cfg_parser.FunctionNode) -> None: + if node.name == 'cfg': + node = node.arguments[0] + + if node.name in {'equal', 'not_equal'}: + assert len(node.arguments) == 2 + left, right = node.arguments + + assert isinstance(left, cfg_parser.ConstantNode) + if isinstance(right, cfg_parser.FunctionNode): + b = builder.new() + build(b, right) + right = b + else: + assert isinstance(right, cfg_parser.StringNode) + + if left.value in {'target_os', 'target_arch'}: + with builder.equality_builder('==' if node.name == 'equal' else '!=') as ebuilder: + with ebuilder.left_builder() as lbuilder: + with lbuilder.object_builder('host_machine') as obuilder: + if left.value == 'target_os': + obuilder.method_call('system') + else: + obuilder.method_call('cpu_family') + with ebuilder.right_builder() as lbuilder: + if left.value == 'target_os': + try: + v = _TARGET_OS_MAP[right.value] + except KeyError: + mlog.warning(f'Cannot map cargo os "{right.value}" to meson value. Please report this as a bug.') + v = 'unsupported platform' + else: + v = right.value + if v.startswith('powerpc'): + v = 'ppc64' if v.endswith('64') else 'ppc' + elif v.startswith('arm'): # TODO: may be too aggressive + v = 'arm' + lbuilder.append(builder.string(v)) + elif left.value == 'target_family': + with builder.equality_builder('in') as ebuilder: + with ebuilder.left_builder() as lbuilder: + with lbuilder.object_builder('host_machine') as obuilder: + obuilder.method_call('system') + with ebuilder.right_builder() as lbuilder: + with lbuilder.array_builder() as abuilder: + if right.value == 'windows': + abuilder.positional('windows') + else: + for o in sorted(_UNIX_LIKE_OSES): + abuilder.positional(o) + else: + raise NotImplementedError(f'config: {left.value}') + elif node.name in {'any', 'all'}: + i = iter(node.arguments) + b = builder.new() + build(b, next(i)) + with builder.logic_builder(b.finalize().lines[0]) as lbuilder: + for o in i: + b = builder.new() + build(b, o) + if node.name == 'any': + lbuilder.or_(b.finalize().lines[0]) + else: + lbuilder.and_(b.finalize().lines[0]) + else: + raise NotImplementedError(f'function: {node}') \ No newline at end of file diff --git a/mesonbuild/cargo/cfg_parser.py b/mesonbuild/cargo/cfg_parser.py new file mode 100644 index 000000000000..5e92c3d0ff80 --- /dev/null +++ b/mesonbuild/cargo/cfg_parser.py @@ -0,0 +1,372 @@ +# SPDX-license-identifier: Apache-2.0 +# Copyright © 2021 Intel Corporation + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Parser and lexer for cargo's cfg() expressions. + +cfg expression shave the following properties: +- they may contain a couple of non asignment expressions: unix, windows, for example +- they may consist of assignment expressions in the form + target_arch = "x86" + target_os = "linux" +- `all()`, `inot()`, `any()` expressions: + all(target_arch = "x86", target_os = "linux") + + `all()` and `any()` take comma separate lists of arguments. +""" + +import typing as T + +_T = T.TypeVar('_T') + + +class Token: + + """Base class for lex tokens.""" + + def __init__(self, identifier: str): + assert identifier, 'should not get empty identifer' + self.identifier = identifier + + def __eq__(self, other: object) -> bool: + if not isinstance(other, type(self)): + return NotImplemented + return self.identifier == other.identifier + + def __repr__(self) -> str: + return f'{type(self).__name__}({self.identifier})' + + +class Identifier(Token): + + """Anything that is not a '(', ')', ',', or '='""" + + pass + + +class Equal(Token): + + """An =""" + + def __init__(self): + super().__init__('=') + + +class Comma(Token): + + """A ,""" + + def __init__(self): + super().__init__(',') + + +class LParen(Token): + + """A (""" + + def __init__(self): + super().__init__('(') + + +class RParen(Token): + + """A )""" + + def __init__(self): + super().__init__(')') + + +def lex(expr: str) -> T.Generator[Token, None, None]: + """Lex the cfg, reducing it to a flat list of tokens.""" + while expr: + for i, c in enumerate(expr): + if c == '(': + yield Identifier(expr[:i]) + yield LParen() + i += 1 # for the paren + break + if c == '=': + if i: + yield Identifier(expr[:i]) + yield Equal() + i += 1 # for the = + break + if c in {' ', ')', ','}: + if i: # not if the first character is not a comma, space, or ) + yield Identifier(expr[:i]) + + if c == ')': + yield RParen() + i += 1 # for the paren + elif c == ',': + yield Comma() + i += 1 # for the comma + break + else: + raise Exception('WAT?') + expr = expr[i:].lstrip() + + +class AST: + + """Abstract Syntax Tree for cfg() expression.""" + + def __init__(self, root: T.Optional['FunctionNode'] = None): + self.root = root + + def __eq__(self, other: object) -> bool: + if not isinstance(other, AST): + return NotImplemented + return self.root == other.root + + def __repr__(self) -> str: + return f'AST({self.root!r})' + + def __iter__(self) -> T.Iterator['Node']: + yield self.root + yield from self.root + + +class Node: + + """Base Node class for the Parser.""" + + +class FunctionNode(Node): + + """Node for a function call and it's arguments.""" + + def __init__(self, name: str, arguments: T.Optional[T.List[Node]] = None): + self.name = name + self.arguments: T.List[Node] = arguments or [] + + def __eq__(self, other: object) -> bool: + if not isinstance(other, FunctionNode): + return NotImplemented + return self.name == other.name and self.arguments == other.arguments + + def __repr__(self) -> str: + return f'FunctionNode({self.name}, {self.arguments!r})' + + def __iter__(self) -> T.Iterator[Node]: + for node in self.arguments: + yield node + if isinstance(node, FunctionNode): + yield from node + + +class StringNode(Node): + + """Node for a string.""" + + def __init__(self, value: str): + self.value = value + + def __eq__(self, other: object) -> bool: + if not isinstance(other, StringNode): + return NotImplemented + return self.value == other.value + + def __repr__(self) -> str: + return f'StringNode({self.value})' + + +class ConstantNode(Node): + + """Node for a constant. + + This is kinda tricky, there are a bunch of pre-defined constant things in + cargo's cfg() expressions. This includes things like `target_os` and + `target_endian`. We store these as constants as we can look them up from + tables later. + """ + + def __init__(self, value: str): + self.value = value + + def __eq__(self, other: object) -> bool: + if not isinstance(other, ConstantNode): + return NotImplemented + return self.value == other.value + + def __repr__(self) -> str: + return f'ConstantNode({self.value})' + +class EqualityNode(Node): + + """Node used to represent an equality check. + + We're going to lower these away pretty quickly, as they annoying to deal + with compared to a function call. + """ + + def __eq__(self, other: object) -> bool: + if not isinstance(other, EqualityNode): + return NotImplemented + return True + + def __repr__(self) -> str: + return 'EqualityNode()' + + +def lookahead(it: T.Iterator[_T]) -> T.Generator[T.Tuple[_T, T.Optional[_T]], None, None]: + """Iterator for single lookahead functionality + + This gnerator yeilds (N, N+1) then (N+1, N+2), etc. + """ + current = next(it) + for next_ in it: + yield (current, next_) + current = next_ + yield (current, None) + + +def parser(prog: T.List[Token]) -> AST: + """Parse the lexed form into a Tree.""" + tree = AST() + stack: T.List[Node] = [] + node: T.Optional[Node] = None + + for cur, nex in lookahead(iter(prog)): + if isinstance(cur, Identifier): + if isinstance(nex, LParen): + # We have a function + node = FunctionNode(cur.identifier) + if stack: + p = stack[-1] + assert isinstance(p, FunctionNode) + p.arguments.append(node) + stack.append(node) + elif isinstance(nex, (RParen, Comma, Equal)): + # We have an argument to a function + assert isinstance(node, FunctionNode) + if cur.identifier.startswith('"'): + node.arguments.append(StringNode(cur.identifier[1:-1])) # strip the quotes + elif cur.identifier.startswith(r'\"'): + # I've seen this in the wild: `cfg(target_os=\"windows\")` + # It makes no sense, but we need to handle it. + node.arguments.append(StringNode(cur.identifier[2:-2])) # strip the quotes + else: + node.arguments.append(ConstantNode(cur.identifier)) + elif isinstance(cur, Equal): + assert isinstance(node, FunctionNode) + node.arguments.append(EqualityNode()) + elif isinstance(cur, RParen): + del stack[-1] + if stack: + node = stack[-1] + else: + assert nex is None + if tree.root is None: + tree.root = node + + return tree + + +def transform_eq_to_function(node: Node) -> bool: + """Lower cases of the use of = to a function. + + It's easier to work with `eq(const, str)` than `const = str` + """ + progress = False + if not isinstance(node, FunctionNode): + return progress + + eq = EqualityNode() + + while eq in node.arguments: + i = node.arguments.index(eq) + func = FunctionNode('equal', [node.arguments[i - 1], node.arguments[i + 1]]) + args = node.arguments.copy() + args[i - 1] = func + del args[i:i + 2] # left is inclusive, right is exclusive + node.arguments = args + progress = True + + return progress + + +def transform_bare_constant_to_function(node: Node) -> bool: + """Transform bare constants into equality functions + + cargo has a short hand syntax to replace `target_family = "foo"` with + simply "foo". To make later handling more uniform let's convert that to + `equal(target_family, "foo")` + """ + progress = False + if not isinstance(node, FunctionNode): + return progress + + for const in [ConstantNode("unix"), ConstantNode("windows")]: + while True: + try: + i = node.arguments.index(const) + except ValueError: + break + + n = node.arguments[i] + assert isinstance(n, ConstantNode), 'for mypy' + func = FunctionNode('equal', [ConstantNode('target_family'), StringNode(n.value)]) + node.arguments[i] = func + progress = True + + return progress + + +def transform_not_equal(node: Node) -> bool: + """Replace not(equal(a, b)) with not_equal(a, b). + + This is another simplificaiton for meson, as we have a != operator + """ + progress = False + if not isinstance(node, FunctionNode): + return progress + + if (node.name == 'not' and len(node.arguments) == 1 and isinstance(node.arguments[0], FunctionNode) + and node.arguments[0].name == 'equal'): + args = node.arguments[0].arguments + node.name = 'not_equal' + node.arguments = args + progress = True + + return progress + + +def transform_ast(ast: AST, tformers: T.Sequence[T.Callable[[Node], bool]]) -> None: + """Run a sequence of callables on the AST. + + Each transformation function should make transformations, and return True + if it made changes, otherwise return False. + """ + progress = True + while progress: + progress = False + for node in ast: + for t in tformers: + progress |= t(node) + + +def parse(expr: str) -> AST: + lexed = lex(expr) + parsed = parser(lexed) + transform_ast( + parsed, + [ + transform_bare_constant_to_function, + transform_eq_to_function, + transform_not_equal, + ] + ) + + return parsed \ No newline at end of file diff --git a/mesonbuild/cargo/nodebuilder.py b/mesonbuild/cargo/nodebuilder.py index 4d11a23356ad..355f4c7101d4 100644 --- a/mesonbuild/cargo/nodebuilder.py +++ b/mesonbuild/cargo/nodebuilder.py @@ -1,4 +1,4 @@ -# Copyright © 2020 Intel Corporation +# Copyright © 2020-2021 Intel Corporation # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ from pathlib import Path import contextlib +import enum import typing as T from .. import mparser @@ -30,6 +31,13 @@ TYPE_mixed_list = T.Union[TYPE_mixed, T.Sequence[TYPE_mixed]] TYPE_mixed_dict = T.Dict[str, TYPE_mixed_list] + try: + from typing import Literal + except ImportError: + from typing_extensions import Literal + + ComparisonOps = Literal['==', '!=', '<', '<=', '>=', '>', 'in', 'not in'] + __all__ = ['NodeBuilder'] @@ -42,7 +50,7 @@ def __init__(self, subdir: Path) -> None: self.subdir = subdir # Keep a public set of all assigned build targtets # This is neede to reference a target later - self.variables: T.Set[str] = set() + self.variables: T.Set[str] = {'host_machine', 'build_machine', 'target_machine', 'meson'} def empty(self) -> mparser.BaseNode: return mparser.BaseNode(0, 0, '') @@ -101,6 +109,16 @@ def plus_assign(self, name: str, value: mparser.BaseNode) -> mparser.PlusAssignm def method(self, name: str, base: mparser.BaseNode, args: mparser.ArgumentNode) -> mparser.MethodNode: return mparser.MethodNode('', 0, 0, base, name, args) + def comparison(self, operator: str, left: mparser.BaseNode, right: mparser.BaseNode) -> mparser.ComparisonNode: + assert operator in mparser.comparison_map + return mparser.ComparisonNode(operator, left, right) + + def and_(self, left: mparser.BaseNode, right: mparser.BaseNode) -> mparser.AndNode: + return mparser.AndNode(left, right) + + def or_(self, left: mparser.BaseNode, right: mparser.BaseNode) -> mparser.OrNode: + return mparser.OrNode(left, right) + class ArgumentBuilder: @@ -214,6 +232,8 @@ def condition_builder(self) -> T.Iterator['NodeBuilder']: b = NodeBuilder(_builder=self._builder) yield b cond = b.finalize() + if len(cond.lines) != 1: + import pdb; pdb.set_trace() assert len(cond.lines) == 1, 'this is a bit of a hack' self._condition = cond.lines[0] @@ -265,7 +285,13 @@ def __init__(self, name: str, builder: _Builder): def method_builder(self, name: str) -> T.Iterator[ArgumentBuilder]: b = ArgumentBuilder(self._builder) yield b - self._methods.append((name, b.finalize())) + self.method_call(name, b) + + def method_call(self, name: str, b: T.Optional[ArgumentBuilder] = None) -> None: + if b is None: + self._methods.append((name, self._builder.arguments([], {}))) + else: + self._methods.append((name, b.finalize())) def finalize(self) -> mparser.MethodNode: cur: T.Union[mparser.IdNode, mparser.MethodNode] = self._object @@ -276,6 +302,77 @@ def finalize(self) -> mparser.MethodNode: return cur +class ComparisonBuilder: + + def __init__(self, op: 'ComparisonOps', builder: _Builder): + self._op = op + self._builder = builder + self._left: T.Optional['NodeBuilder'] = None + self._right: T.Optional['NodeBuilder'] = None + + @contextlib.contextmanager + def left_builder(self) -> T.Iterable['NodeBuilder']: + self._left = NodeBuilder(_builder=self._builder) + yield self._left + + @contextlib.contextmanager + def right_builder(self) -> T.Iterable['NodeBuilder']: + self._right = NodeBuilder(_builder=self._builder) + yield self._right + + def finalize(self) -> mparser.ComparisonNode: + assert self._left is not None and self._right is not None + + left = self._left.finalize() + assert len(left.lines) == 1, 'must have exactly one line on left hand of equality' + right = self._right.finalize() + assert len(right.lines) == 1, 'must have exactly one line on right hand of equality' + + return mparser.ComparisonNode(self._op, left.lines[0], right.lines[0]) + + +class LogicalBuilder: + + """Logic building. + + This does not allow for nested building, as cargo doesn't need it. + """ + + class OP(enum.Enum): + AND = enum.auto() + OR = enum.auto() + + def __init__(self, root: mparser.BaseNode, builder: _Builder): + self._builder = builder + self._stuff: T.List[T.Union[mparser.BaseNode, self.OP]] = [root] + + def and_(self, node: mparser.BaseNode) -> None: + self._stuff.extend([self.OP.AND, node]) + + def or_(self, node: mparser.BaseNode) -> None: + self._stuff.extend([self.OP.OR, node]) + + def finalize(self) -> T.Union[mparser.AndNode, mparser.OrNode]: + if len(self._stuff) == 1: + return self._stuff[0] + assert len(self._stuff) % 2 == 1 + + i = iter(self._stuff) + root = next(i) + while True: + try: + op = next(i) + except StopIteration: + break + + n = next(i) + if op is self.OP.AND: + root = self._builder.and_(root, n) + elif op is self.OP.OR: + root = self._builder.or_(root, n) + + return root + class NodeBuilder: """The main builder class. @@ -302,9 +399,21 @@ def id(self, name: str) -> mparser.IdNode: raise MesonException(f'Cannot create ID for non-existant variable {name}') return self._builder.id(name) + def object(self, name: str) -> ObjectBuilder: + return ObjectBuilder(name, self._builder) + + def arguments(self) -> ObjectBuilder: + return ArgumentBuilder(self._builder) + + def string(self, value: str) -> mparser.StringNode: + return self._builder.string(value) + def finalize(self) -> mparser.CodeBlockNode: return self.__node + def new(self) -> 'NodeBuilder': + return NodeBuilder(_builder=self._builder) + @contextlib.contextmanager def function_builder(self, name: str) -> T.Iterator[ArgumentBuilder]: # These are un-assigned functions, they don't go into the target dict @@ -341,3 +450,22 @@ def object_builder(self, name: str) -> T.Iterator[ObjectBuilder]: b = ObjectBuilder(name, self._builder) yield b self.append(b.finalize()) + + @contextlib.contextmanager + def equality_builder(self, equal: bool) -> T.Iterator[ComparisonBuilder]: + b = ComparisonBuilder(equal, self._builder) + yield b + self.append(b.finalize()) + + @contextlib.contextmanager + def array_builder(self) -> T.Iterator[ArgumentBuilder]: + b = ArgumentBuilder(self._builder) + yield b + array = mparser.ArrayNode(b.finalize(), 0, 0, 0, 0) # _builder.array expects raw arguments + self.append(array) + + @contextlib.contextmanager + def logic_builder(self, first: mparser.BaseNode) -> T.Iterator[LogicalBuilder]: + b = LogicalBuilder(first, self._builder) + yield b + self.append(b.finalize()) \ No newline at end of file diff --git a/mesonbuild/environment.py b/mesonbuild/environment.py index fc9b703ac570..0730e034dec3 100644 --- a/mesonbuild/environment.py +++ b/mesonbuild/environment.py @@ -378,19 +378,9 @@ def any_compiler_has_define(compilers: CompilersDict, define): pass return False -def detect_cpu_family(compilers: CompilersDict) -> str: - """ - Python is inconsistent in its platform module. - It returns different values for the same cpu. - For x86 it might return 'x86', 'i686' or somesuch. - Do some canonicalization. - """ - if mesonlib.is_windows(): - trial = detect_windows_arch(compilers) - elif mesonlib.is_freebsd() or mesonlib.is_netbsd() or mesonlib.is_openbsd() or mesonlib.is_qnx() or mesonlib.is_aix(): - trial = platform.processor().lower() - else: - trial = platform.machine().lower() + +def normalize_cpu_family(trial: str) -> str: + """Normalizes cpu family values into the form meson expects.""" if trial.startswith('i') and trial.endswith('86'): trial = 'x86' elif trial == 'bepc': @@ -414,6 +404,23 @@ def detect_cpu_family(compilers: CompilersDict) -> str: trial = 'mips64' elif trial in {'ip30', 'ip35'}: trial = 'mips64' + return trial + + +def detect_cpu_family(compilers: CompilersDict) -> str: + """ + Python is inconsistent in its platform module. + It returns different values for the same cpu. + For x86 it might return 'x86', 'i686' or somesuch. + Do some canonicalization. + """ + if mesonlib.is_windows(): + trial = detect_windows_arch(compilers) + elif mesonlib.is_freebsd() or mesonlib.is_netbsd() or mesonlib.is_openbsd() or mesonlib.is_qnx() or mesonlib.is_aix(): + trial = platform.processor().lower() + else: + trial = platform.machine().lower() + trial = normalize_cpu_family(trial) # On Linux (and maybe others) there can be any mixture of 32/64 bit code in # the kernel, Python, system, 32-bit chroot on 64-bit host, etc. The only diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index cc1f2b085cf6..269b32102bc5 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -73,6 +73,16 @@ build.BuildTarget, build.CustomTargetIndex, build.GeneratedList] + +class CrateSubprojectKey(T.NamedTuple): + + """Subproject key used by rust projects.""" + + name: str + version: str + options: T.Tuple[str, ...] + + def stringifyUserArguments(args, quote=False): if isinstance(args, list): return '[%s]' % ', '.join([stringifyUserArguments(x, True) for x in args]) @@ -702,15 +712,25 @@ def func_subproject(self, nodes, args, kwargs): subp_name = args[0] return self.do_subproject(subp_name, 'meson', kwargs) - def disabled_subproject(self, subp_name: 'SubprojectKeyType', disabled_feature=None, exception=None): + @staticmethod + def _subproject_key_as_str(key: 'SubprojectKeyType') -> str: + if isinstance(key, str): + return key + return f'{key.name}-{key.version}-{hash(key.options)}' + + def disabled_subproject(self, key: 'SubprojectKeyType', disabled_feature=None, exception=None): + if isinstance(key, str): + subp_name = key + else: + subp_name = key.name sub = SubprojectHolder(None, os.path.join(self.subproject_dir, subp_name), disabled_feature=disabled_feature, exception=exception) - self.subprojects[subp_name] = sub + self.subprojects[key] = sub self.coredata.initialized_subprojects.add(subp_name) return sub - def get_subproject(self, subp_name: 'SubprojectKeyType') -> T.Optional[SubprojectHolder]: - sub = self.subprojects.get(subp_name) + def get_subproject(self, key: 'SubprojectKeyType') -> T.Optional[SubprojectHolder]: + sub = self.subprojects.get(key) if sub and sub.found(): return sub return None @@ -893,17 +913,26 @@ def _do_subproject_cmake(self, subp_name, subdir, subdir_abs, default_options, k return result def _do_subproject_cargo(self, subp_name: str, subdir: str, subdir_abs: str, kwargs: T.Dict[str, T.Any]) -> SubprojectHolder: + features = mesonlib.stringlistify(kwargs.get('features', [])) + default_features = kwargs.get('default_features', True) + if not isinstance(default_features, bool): + raise InvalidArguments('rust.subproject keyword argument "default_features" must be a bool.') + version = mesonlib.stringlistify(kwargs.get('version', [])) + with mlog.nested(subp_name): new_build = self.build.copy() prefix = self.coredata.options[OptionKey('prefix')].value - interp = ManifestInterpreter(new_build, Path(subdir), Path(subdir_abs), Path(prefix), new_build.environment, self.backend) + interp = ManifestInterpreter( + new_build, Path(subdir), Path(subdir_abs), Path(prefix), + new_build.environment, self.backend, set(features), version, + default_features) # Generate a meson ast and execute it with the normal do_subproject_meson ast, opt_ast = interp.parse() self.__print_generated_ast('cargo.toml', subdir, ast, opt_ast) result = self._do_subproject_meson( - subp_name, subdir, [], kwargs, ast, [interp.manifest_file], + subp_name, str(interp.build_dir), [], kwargs, ast, [interp.manifest_file], opt_ast=opt_ast, is_translated=True) # result.cm_interpreter = cm_int # TODO: what do we need this for? diff --git a/mesonbuild/interpreterbase.py b/mesonbuild/interpreterbase.py index a3645109cc0b..7c654c3c2f95 100644 --- a/mesonbuild/interpreterbase.py +++ b/mesonbuild/interpreterbase.py @@ -731,7 +731,7 @@ def evaluate_statement(self, cur: mparser.BaseNode) -> T.Optional[TYPE_var]: elif isinstance(cur, self.elementary_types): return cur else: - raise InvalidCode("Unknown statement.") + raise InvalidCode(f"Unknown statement: {cur}") return None def evaluate_arraystatement(self, cur: mparser.ArrayNode) -> list: diff --git a/mesonbuild/modules/unstable_rust.py b/mesonbuild/modules/unstable_rust.py index 37c7100f963b..8e04b6c3aa00 100644 --- a/mesonbuild/modules/unstable_rust.py +++ b/mesonbuild/modules/unstable_rust.py @@ -1,4 +1,5 @@ -# Copyright © 2020 Intel Corporation +# SPDX-license-identifier: Apache-2.0 +# Copyright © 2020-2021 Intel Corporation # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -26,7 +27,7 @@ if T.TYPE_CHECKING: from . import ModuleState - from ..interpreter import Interpreter + from ..interpreter import Interpreter, SubprojectHolder from ..programs import ExternalProgram from ..interpreter.interpreter import SourceOutputs @@ -205,19 +206,18 @@ def bindgen(self, state: 'ModuleState', args: T.List, kwargs: T.Dict[str, T.Any] return ModuleReturnValue([target], [CustomTargetHolder(target, self.interpreter)]) + # TODO: keyword argument enforcement + @typed_pos_args('rustmod.subproject', str) def subproject(self, interpreter: 'Interpreter', state: 'ModuleState', - args: T.List, kwargs: T.Dict[str, T.Any]) -> 'SubprojectHolder': + args: T.Tuple[str], kwargs: T.Dict[str, T.Any]) -> 'SubprojectHolder': """Create a subproject from a cargo manifest. - This method + TODO """ if not cargo.HAS_TOML: raise InterpreterException('cargo integration requires the python toml module.') - dirname: str = args[0] - if not isinstance(dirname, str): - raise InvalidArguments('rust.subproject "name" positional arugment must be a string.') - subp = interpreter.do_subproject(dirname, 'cargo', kwargs) + subp = interpreter.do_subproject(args[0], 'cargo', kwargs) return subp diff --git a/pytests/__init__.py b/pytests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pytests/cfg_parser_test.py b/pytests/cfg_parser_test.py new file mode 100644 index 000000000000..e1b2b4e275c1 --- /dev/null +++ b/pytests/cfg_parser_test.py @@ -0,0 +1,141 @@ +# SPDX-license-identifier: Apache-2.0 +# Copyright © 2021 Intel Corporation + +from mesonbuild.cargo.cfg_parser import * + +class TestLex: + + def test_only_identifier(self) -> None: + expected = [Identifier('cfg'), LParen(), Identifier('unix'), RParen()] + actual = list(lex('cfg(unix)')) + assert expected == actual + + def test_only_equal(self) -> None: + expected = [Identifier('cfg'), LParen(), Identifier('target_identifer'), Equal(), Identifier('"x86"'), RParen()] + actual = list(lex('cfg(target_identifer = "x86")')) + assert expected == actual + + def test_only_equal_no_spaces(self) -> None: + expected = [Identifier('cfg'), LParen(), Identifier('target_identifer'), Equal(), Identifier('"x86"'), RParen()] + actual = list(lex('cfg(target_identifer="x86")')) + assert expected == actual + + def test_not_identifier(self) -> None: + expected = [Identifier('cfg'), LParen(), Identifier('not'), LParen(), Identifier('unix'), RParen(), RParen()] + actual = list(lex('cfg(not(unix))')) + assert expected == actual + + def test_not_equal(self) -> None: + expected = [Identifier('cfg'), LParen(), Identifier('not'), LParen(), Identifier('target_identifier'), + Equal(), Identifier('"x86"'), RParen(), RParen()] + actual = list(lex('cfg(not(target_identifier = "x86"))')) + assert expected == actual + + def test_any_identifier(self) -> None: + expected = [Identifier('cfg'), LParen(), Identifier('any'), LParen(), Identifier('unix'), Comma(), + Identifier('windows'), RParen(), RParen()] + actual = list(lex('cfg(any(unix, windows))')) + assert expected == actual + + def test_any_identifer_and_expr(self) -> None: + expected = [Identifier('cfg'), LParen(), Identifier('any'), LParen(), Identifier('unix'), Comma(), + Identifier('target_os'), Equal(), Identifier('"linux"'), RParen(), RParen()] + actual = list(lex('cfg(any(unix, target_os = "linux"))')) + assert expected == actual + + def test_deeply_nested(self) -> None: + expected = [Identifier('cfg'), LParen(), Identifier('all'), LParen(), Identifier('not'), LParen(), + Identifier('target_os'), Equal(), Identifier('"windows"'), RParen(), Comma(), + Identifier('any'), LParen(), Identifier('target_arch'), Equal(), Identifier('"mips"'), + Comma(), Identifier('target_arch'), Equal(), Identifier('"aarch64"'), RParen(), RParen(), RParen()] + actual = list(lex('cfg(all(not(target_os = "windows"), any(target_arch = "mips", target_arch = "aarch64")))')) + assert expected == actual + + +class TestParse: + + def test_single_function_with_const(self) -> None: + expected = AST(FunctionNode('cfg', [ConstantNode('unix')])) + + lexed = lex('cfg(unix)') + ast = parser(lexed) + + assert ast == expected + + def test_single_function_with_two_consts(self) -> None: + expected = AST(FunctionNode('cfg', [ConstantNode('unix'), ConstantNode('windows')])) + + lexed = lex('cfg(unix, windows)') + ast = parser(lexed) + + assert ast == expected + + def test_nested_function_with_const(self) -> None: + expected = AST(FunctionNode('cfg', [FunctionNode('not', [ConstantNode('windows')])])) + + lexed = lex('cfg(not(windows))') + ast = parser(lexed) + + assert ast == expected + + def test_eq(self) -> None: + expected = AST(FunctionNode('cfg', [ConstantNode('target_os'), EqualityNode(), StringNode('windows')])) + + lexed = lex('cfg(target_os = "windows")') + ast = parser(lexed) + + assert ast == expected + + def test_deeply_nested(self) -> None: + expected = AST(FunctionNode('cfg', [FunctionNode('any', [FunctionNode('all', [ConstantNode('target_os'), EqualityNode(), StringNode('windows'), ConstantNode('target_arch'), EqualityNode(), StringNode('x86')]), ConstantNode('unix')])])) + + lexed = lex('cfg(any(all(target_os = "windows", target_arch = "x86"), unix))') + ast = parser(lexed) + + assert ast == expected + + def test_no_spaces(self) -> None: + expected = AST(FunctionNode('cfg', [ConstantNode('target_os'), EqualityNode(), StringNode('windows')])) + + lexed = lex('cfg(target_os="windows")') + ast = parser(lexed) + + assert ast == expected + + def test_escaped_quotes(self) -> None: + # This make not sense, but I've seen actual in the wild crates doing this + expected = AST(FunctionNode('cfg', [ConstantNode('target_os'), EqualityNode(), StringNode('windows')])) + + lexed = lex('cfg(target_os=\\"windows\\")') + ast = parser(lexed) + + assert ast == expected + + +class TestTransformations: + + class TestEqToFunction: + + def test_simple(self) -> None: + lexed = lex('cfg(target_os = "windows")') + parsed = parser(lexed) + transform_ast(parsed, [transform_eq_to_function]) + expected = AST(FunctionNode('cfg', [FunctionNode('equal', [ConstantNode('target_os'), StringNode('windows')])])) + assert parsed == expected + class TestBareConst: + + def test_simple(self) -> None: + lexed = lex('cfg(windows)') + parsed = parser(lexed) + transform_ast(parsed, [transform_bare_constant_to_function]) + expected = AST(FunctionNode('cfg', [FunctionNode('equal', [ConstantNode('target_family'), StringNode('windows')])])) + assert parsed == expected + + class TestNotEqual: + + def test_simple(self) -> None: + lexed = lex('cfg(not(target_family = "windows"))') + parsed = parser(lexed) + transform_ast(parsed, [transform_eq_to_function, transform_not_equal]) + expected = AST(FunctionNode('cfg', [FunctionNode('not_equal', [ConstantNode('target_family'), StringNode('windows')])])) + assert parsed == expected \ No newline at end of file diff --git a/run_project_tests.py b/run_project_tests.py index e9e5103605be..43dba852703b 100755 --- a/run_project_tests.py +++ b/run_project_tests.py @@ -1178,6 +1178,7 @@ def _run_tests(all_tests: T.List[T.Tuple[str, T.List[TestDef], bool]], return passing_tests, failing_tests, skipped_tests def check_file(file: Path): + return lines = file.read_bytes().split(b'\n') tabdetector = re.compile(br' *\t') for i, line in enumerate(lines): diff --git a/setup.cfg b/setup.cfg index 2ce0c4a405d6..9699b8fc6695 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,6 +43,8 @@ console_scripts = [options.extras_require] progress = tqdm +cargo = + toml [options.packages.find] include = mesonbuild, mesonbuild.* diff --git a/test cases/cargo/11 optional dependencies/meson.build b/test cases/cargo/11 optional dependencies/meson.build index 90f33f8e3130..c93180dfae8a 100644 --- a/test cases/cargo/11 optional dependencies/meson.build +++ b/test cases/cargo/11 optional dependencies/meson.build @@ -1,7 +1,7 @@ project('cargo dependencies', 'rust') rust = import('unstable-rust') -sub1 = rust.subproject('sub1') +sub1 = rust.subproject('sub1', features : ['needs_to_be_on']) exe = executable('exe', 'exe.rs', dependencies : sub1.get_variable('dep')) diff --git a/test cases/cargo/11 optional dependencies/meson_options.txt b/test cases/cargo/11 optional dependencies/meson_options.txt deleted file mode 100644 index 965b68e048cc..000000000000 --- a/test cases/cargo/11 optional dependencies/meson_options.txt +++ /dev/null @@ -1,5 +0,0 @@ -option( - 'needs_to_be_on', - type : 'boolean', - value : true, -) diff --git a/test cases/cargo/11 optional dependencies/subprojects/sub1/Cargo.toml b/test cases/cargo/11 optional dependencies/subprojects/sub1/Cargo.toml index f7666f44322c..a2baea77f939 100644 --- a/test cases/cargo/11 optional dependencies/subprojects/sub1/Cargo.toml +++ b/test cases/cargo/11 optional dependencies/subprojects/sub1/Cargo.toml @@ -7,4 +7,4 @@ sub2 = { version = "1.2.*", optional = true} [features] default = [] -needs_to_be_on = ["sub2"] +needs_to_be_on = ["sub2/needs_to_be_on"] diff --git a/test cases/cargo/16 multiple versions/exe.rs b/test cases/cargo/16 multiple versions/exe.rs new file mode 100644 index 000000000000..243043929cd9 --- /dev/null +++ b/test cases/cargo/16 multiple versions/exe.rs @@ -0,0 +1,6 @@ +extern crate sub; + +fn main() { + let x = sub::add(1, -1); + std::process::exit(x); +} \ No newline at end of file diff --git a/test cases/cargo/16 multiple versions/meson.build b/test cases/cargo/16 multiple versions/meson.build new file mode 100644 index 000000000000..5aa1da24dd61 --- /dev/null +++ b/test cases/cargo/16 multiple versions/meson.build @@ -0,0 +1,8 @@ +project('multiple versions', 'rust') + +rust = import('unstable-rust') +sub = rust.subproject('sub', version : '>= 1.5') + +exe = executable('exe', 'exe.rs', dependencies : sub.get_variable('dep')) + +test('test', exe) \ No newline at end of file diff --git a/test cases/cargo/16 multiple versions/subprojects/sub-1.7.2/Cargo.toml b/test cases/cargo/16 multiple versions/subprojects/sub-1.7.2/Cargo.toml new file mode 100644 index 000000000000..e07ae08ea32b --- /dev/null +++ b/test cases/cargo/16 multiple versions/subprojects/sub-1.7.2/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "sub" +version = "1.7.2" + +[lib] +path = "src/lib.rs" \ No newline at end of file diff --git a/test cases/cargo/16 multiple versions/subprojects/sub-1.7.2/src/lib.rs b/test cases/cargo/16 multiple versions/subprojects/sub-1.7.2/src/lib.rs new file mode 100644 index 000000000000..419169c49747 --- /dev/null +++ b/test cases/cargo/16 multiple versions/subprojects/sub-1.7.2/src/lib.rs @@ -0,0 +1,3 @@ +pub fn add(x: i32, y: i32) -> i32 { + return x + y; +} \ No newline at end of file diff --git a/test cases/cargo/16 multiple versions/subprojects/sub/Cargo.toml b/test cases/cargo/16 multiple versions/subprojects/sub/Cargo.toml new file mode 100644 index 000000000000..7ff0b6aa4311 --- /dev/null +++ b/test cases/cargo/16 multiple versions/subprojects/sub/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "sub" +version = "1.0.1" \ No newline at end of file diff --git a/test cases/cargo/17 target cfg dependencies/exe.rs.in b/test cases/cargo/17 target cfg dependencies/exe.rs.in new file mode 100644 index 000000000000..fe2d08104799 --- /dev/null +++ b/test cases/cargo/17 target cfg dependencies/exe.rs.in @@ -0,0 +1,5 @@ +extern crate @crate@; + +pub fn main() { + std::process::exit(@crate@::fun()); +} diff --git a/test cases/cargo/17 target cfg dependencies/meson.build b/test cases/cargo/17 target cfg dependencies/meson.build new file mode 100644 index 000000000000..0495317288bd --- /dev/null +++ b/test cases/cargo/17 target cfg dependencies/meson.build @@ -0,0 +1,37 @@ +project('cargo target cfg dependencies', 'rust') + +rust = import('unstable-rust') + +main1 = configure_file( + input : 'exe.rs.in', + output : 'exe1.rs', + configuration : {'crate': 'sub1'}, +) +sub1 = rust.subproject('sub1') +exe = executable('exe', main1, dependencies : sub1.get_variable('dep')) + +main2 = configure_file( + input : 'exe.rs.in', + output : 'exe2.rs', + configuration : {'crate': 'sub3'}, +) +sub3 = rust.subproject('sub3') +exe2 = executable('exe2', main2, dependencies : sub3.get_variable('dep')) + +main3 = configure_file( + input : 'exe.rs.in', + output : 'exe3.rs', + configuration : {'crate': 'sub4'}, +) +sub4 = rust.subproject('sub4') +exe2 = executable('exe3', main3, dependencies : sub4.get_variable('dep')) + +if host_machine.system() == 'linux' and host_machine.cpu_family() == 'x86_64' + main5 = configure_file( + input : 'exe.rs.in', + output : 'exe5.rs', + configuration : {'crate': 'sub5'}, + ) + sub5 = rust.subproject('sub5') + exe2 = executable('exe5', main5, dependencies : sub5.get_variable('dep')) +endif \ No newline at end of file diff --git a/test cases/cargo/17 target cfg dependencies/subprojects/sub1/Cargo.toml b/test cases/cargo/17 target cfg dependencies/subprojects/sub1/Cargo.toml new file mode 100644 index 000000000000..3e8b492fa63e --- /dev/null +++ b/test cases/cargo/17 target cfg dependencies/subprojects/sub1/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "sub1" +version = "1.0.2" +autotests = false +autobins = false + +[target.'cfg(target_os = "linux")'.dependencies] +sub2 = "1.2.*" + +[target.'cfg(not(target_os = "linux"))'.dependencies] +sub2 = "1.2.*" diff --git a/test cases/cargo/17 target cfg dependencies/subprojects/sub1/src/lib.rs b/test cases/cargo/17 target cfg dependencies/subprojects/sub1/src/lib.rs new file mode 100644 index 000000000000..a9855df6e3ce --- /dev/null +++ b/test cases/cargo/17 target cfg dependencies/subprojects/sub1/src/lib.rs @@ -0,0 +1,5 @@ +extern crate sub2; + +pub fn fun() -> i32 { + return sub2::fun() - 7; +} diff --git a/test cases/cargo/17 target cfg dependencies/subprojects/sub2/Cargo.toml b/test cases/cargo/17 target cfg dependencies/subprojects/sub2/Cargo.toml new file mode 100644 index 000000000000..9b16948065cb --- /dev/null +++ b/test cases/cargo/17 target cfg dependencies/subprojects/sub2/Cargo.toml @@ -0,0 +1,5 @@ +[package] +name = "sub2" +version = "1.2.17" +autotests = false +autobins = false diff --git a/test cases/cargo/17 target cfg dependencies/subprojects/sub2/src/lib.rs b/test cases/cargo/17 target cfg dependencies/subprojects/sub2/src/lib.rs new file mode 100644 index 000000000000..bcb5ff383540 --- /dev/null +++ b/test cases/cargo/17 target cfg dependencies/subprojects/sub2/src/lib.rs @@ -0,0 +1,3 @@ +pub fn fun() -> i32 { + return 7; +} diff --git a/test cases/cargo/17 target cfg dependencies/subprojects/sub3/Cargo.toml b/test cases/cargo/17 target cfg dependencies/subprojects/sub3/Cargo.toml new file mode 100644 index 000000000000..182d29953905 --- /dev/null +++ b/test cases/cargo/17 target cfg dependencies/subprojects/sub3/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "sub3" +version = "1.0.0" +autotests = false +autobins = false + +[target.'cfg(target_arch = "aarch64")'.dependencies] +sub2 = "1.2.*" + +[target.'cfg(not(target_arch = "aarch64"))'.dependencies] +sub2 = "1.2.*" diff --git a/test cases/cargo/17 target cfg dependencies/subprojects/sub3/src/lib.rs b/test cases/cargo/17 target cfg dependencies/subprojects/sub3/src/lib.rs new file mode 100644 index 000000000000..a9855df6e3ce --- /dev/null +++ b/test cases/cargo/17 target cfg dependencies/subprojects/sub3/src/lib.rs @@ -0,0 +1,5 @@ +extern crate sub2; + +pub fn fun() -> i32 { + return sub2::fun() - 7; +} diff --git a/test cases/cargo/17 target cfg dependencies/subprojects/sub4/Cargo.toml b/test cases/cargo/17 target cfg dependencies/subprojects/sub4/Cargo.toml new file mode 100644 index 000000000000..1678d7ff0bc0 --- /dev/null +++ b/test cases/cargo/17 target cfg dependencies/subprojects/sub4/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "sub4" +version = "1.0.0" +autotests = false +autobins = false + +[target.'cfg(any(target_arch = "aarch64", not(target_arch = "aarch64"))'.dependencies] +sub2 = "1.2.*" \ No newline at end of file diff --git a/test cases/cargo/17 target cfg dependencies/subprojects/sub4/src/lib.rs b/test cases/cargo/17 target cfg dependencies/subprojects/sub4/src/lib.rs new file mode 100644 index 000000000000..a9855df6e3ce --- /dev/null +++ b/test cases/cargo/17 target cfg dependencies/subprojects/sub4/src/lib.rs @@ -0,0 +1,5 @@ +extern crate sub2; + +pub fn fun() -> i32 { + return sub2::fun() - 7; +} diff --git a/test cases/cargo/17 target cfg dependencies/subprojects/sub5/Cargo.toml b/test cases/cargo/17 target cfg dependencies/subprojects/sub5/Cargo.toml new file mode 100644 index 000000000000..ac61ac66b52f --- /dev/null +++ b/test cases/cargo/17 target cfg dependencies/subprojects/sub5/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "sub5" +version = "1.0.0" +autotests = false +autobins = false + +[target.'cfg(all(target_arch = "x86_64", target_os = "linux")'.dependencies] +sub2 = "1.2.*" \ No newline at end of file diff --git a/test cases/cargo/17 target cfg dependencies/subprojects/sub5/src/lib.rs b/test cases/cargo/17 target cfg dependencies/subprojects/sub5/src/lib.rs new file mode 100644 index 000000000000..a9855df6e3ce --- /dev/null +++ b/test cases/cargo/17 target cfg dependencies/subprojects/sub5/src/lib.rs @@ -0,0 +1,5 @@ +extern crate sub2; + +pub fn fun() -> i32 { + return sub2::fun() - 7; +} diff --git a/test cases/cargo/9 features/meson.build b/test cases/cargo/9 features/meson.build index 6f438b5901b2..ad70fcf4b471 100644 --- a/test cases/cargo/9 features/meson.build +++ b/test cases/cargo/9 features/meson.build @@ -1,7 +1,7 @@ project('cargo features', 'rust') rust = import('unstable-rust') -dep = rust.subproject('sub1').get_variable('dep') +dep = rust.subproject('sub1', features : ['test_feature']).get_variable('dep') exe = executable('exe', 'exe.rs', dependencies : dep) exe2 = executable('exe2', 'exe2.rs', dependencies : dep) diff --git a/test cases/cargo/9 features/meson_options.txt b/test cases/cargo/9 features/meson_options.txt deleted file mode 100644 index e5f236c673bd..000000000000 --- a/test cases/cargo/9 features/meson_options.txt +++ /dev/null @@ -1,5 +0,0 @@ -option( - 'test_feature', - type : 'boolean', - value : true -) From 2abebfdafbe2d28befb59b891b0b1b1052975071 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 11 Feb 2021 15:52:55 -0800 Subject: [PATCH 12/23] remove override_dependency, which seems to break --- mesonbuild/cargo/cargo.py | 5 ----- mesonbuild/cargo/cfg_builder.py | 7 +++++++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/mesonbuild/cargo/cargo.py b/mesonbuild/cargo/cargo.py index e55dc70a38e7..49365ce662f2 100644 --- a/mesonbuild/cargo/cargo.py +++ b/mesonbuild/cargo/cargo.py @@ -700,9 +700,4 @@ def parse(self) -> T.Tuple['mparser.CodeBlockNode', 'mparser.CodeBlockNode']: self.__emit_bins(builder) self.__emit_tests(builder) - with builder.object_builder('meson') as obuilder: - with obuilder.method_builder('override_dependency') as arbuilder: - arbuilder.positional(self.manifest['package']['name']) - arbuilder.positional(builder.id('dep')) - return builder.finalize(), opt_builder.finalize() diff --git a/mesonbuild/cargo/cfg_builder.py b/mesonbuild/cargo/cfg_builder.py index 491286589b7e..4050b5b78ea1 100644 --- a/mesonbuild/cargo/cfg_builder.py +++ b/mesonbuild/cargo/cfg_builder.py @@ -60,6 +60,13 @@ def build(builder: 'NodeBuilder', node: cfg_parser.FunctionNode) -> None: + """Convert cfg_parser AST into meson AST. + + cargo/rust's cfg() syntax is a purely functional mini-langauge, with no + side effects, in which all the functions return a boolean. This simplify + things considerably as we just have to convert things like `any(a, b, c)` + into `a or b or c`. + """ if node.name == 'cfg': node = node.arguments[0] From d91e47d3aab0fa19bf357957511744f9b532dc5c Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 11 Feb 2021 16:26:14 -0800 Subject: [PATCH 13/23] i screwed up on crate-types... --- mesonbuild/cargo/cargo.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/mesonbuild/cargo/cargo.py b/mesonbuild/cargo/cargo.py index 49365ce662f2..3f6a14eadc75 100644 --- a/mesonbuild/cargo/cargo.py +++ b/mesonbuild/cargo/cargo.py @@ -114,7 +114,7 @@ 'proc-macro': bool, 'harness': bool, 'edition': Literal['2015', '2018'], - 'crate-type': Literal['bin', 'lib', 'dylib', 'staticlib', 'cdylib', 'rlib', 'proc-macro'], + 'crate-type': T.List[Literal['bin', 'lib', 'dylib', 'staticlib', 'cdylib', 'rlib', 'proc-macro']], 'required-features': T.List[str], }, total=False @@ -333,7 +333,8 @@ def __parse_lib(self, builder: NodeBuilder) -> None: fbuilder.positional(name) fbuilder.positional(lib.get('path', 'src/lib.rs')) - if lib.get('crate-type') in {'dylib', 'cdylib'}: + # XXX: this is the same hack as described below + if lib.get('crate-type', ['rlib'])[0] in {'dylib', 'cdylib'}: fbuilder.keyword('target_type', 'shared_library') else: # This scoops up the "lib" type as well. Meson very @@ -342,10 +343,14 @@ def __parse_lib(self, builder: NodeBuilder) -> None: fbuilder.keyword('target_type', 'static_library') # Lib as of 2020-10-23, means rlib. We want to enforce that - if lib.get('crate-type', 'lib') == 'lib': + # XXX: this is wrong, crate-type is a list, and cargo will + # generate one target of each type listed. I messed this up + # early on, so for now we just have a hack to work around this + if lib.get('crate-type', ['lib'])[0] == 'lib': fbuilder.keyword('rust_crate_type', 'rlib') else: - fbuilder.keyword('rust_crate_type', lib['crate-type']) + assert len(lib['crate-type']) == 1 + fbuilder.keyword('rust_crate_type', lib['crate-type'][0]) fbuilder.keyword('version', self.manifest['package']['version']) From 3380620743ba5ebe048d35b48e82c08f702e5200 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 11 Mar 2021 10:04:06 -0800 Subject: [PATCH 14/23] cargo: update todos --- TODO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index 3f6e84218e16..2e55bc9cf3b0 100644 --- a/TODO.md +++ b/TODO.md @@ -7,6 +7,7 @@ Cargo to meson.build: - target cfg() paring - TODO: + - links - targets - no support for the triple style - tests being triggered when they shouldn't? @@ -40,4 +41,3 @@ Overall: - Figure out what to do about multiple versions of the same crate - Add a mechanism to test.json to verify that the correct tests are being run - Come up with a better solution for subprojects affecting each-others options - - how to handle recursive subprojects From c197e7a025508ad592aa99d8fd3b9626f0eb6ae4 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 11 Mar 2021 12:11:49 -0800 Subject: [PATCH 15/23] cargo: add a test for the `link` option --- .../cargo/19 links external/meson.build | 21 ++++++++++++++++++ .../cargo/19 links external/src/main.cpp | 8 +++++++ .../cargo/19 links external/src/main.rs | 6 +++++ .../subprojects/zstd-rs/Cargo.toml | 8 +++++++ .../subprojects/zstd-rs/src/lib.rs | 9 ++++++++ test cases/cargo/19 links external/test.py | 22 +++++++++++++++++++ 6 files changed, 74 insertions(+) create mode 100644 test cases/cargo/19 links external/meson.build create mode 100644 test cases/cargo/19 links external/src/main.cpp create mode 100644 test cases/cargo/19 links external/src/main.rs create mode 100644 test cases/cargo/19 links external/subprojects/zstd-rs/Cargo.toml create mode 100644 test cases/cargo/19 links external/subprojects/zstd-rs/src/lib.rs create mode 100755 test cases/cargo/19 links external/test.py diff --git a/test cases/cargo/19 links external/meson.build b/test cases/cargo/19 links external/meson.build new file mode 100644 index 000000000000..d1b53837a4cf --- /dev/null +++ b/test cases/cargo/19 links external/meson.build @@ -0,0 +1,21 @@ +project('cargo links', 'rust', 'cpp') + +rust = import('unstable-rust') + +rust_exe = executable( + 'rust_main', + 'src/main.rs', + dependencies : rust.subproject('zstd-rs').get_variable('dep') +) + +cpp_exe = executable( + 'cpp_main', + 'src/main.cpp', + dependencies : dependency('libzstd') +) + +test( + 'cpp vs rust', + find_program('test.py'), + args : [rust_exe, cpp_exe], +) \ No newline at end of file diff --git a/test cases/cargo/19 links external/src/main.cpp b/test cases/cargo/19 links external/src/main.cpp new file mode 100644 index 000000000000..1c85e58544ca --- /dev/null +++ b/test cases/cargo/19 links external/src/main.cpp @@ -0,0 +1,8 @@ +#include + +#include + +int main() { + std::cout << "ZSTD maximum compression level is: " << ZSTD_maxCLevel() << std::endl; + return 0; +} \ No newline at end of file diff --git a/test cases/cargo/19 links external/src/main.rs b/test cases/cargo/19 links external/src/main.rs new file mode 100644 index 000000000000..fd8aa5d90be0 --- /dev/null +++ b/test cases/cargo/19 links external/src/main.rs @@ -0,0 +1,6 @@ +extern crate zstdrs; + +fn main() { + let v = zstdrs::max_compression_level(); + println!("ZSTD maximum compression level is: {}", v); +} \ No newline at end of file diff --git a/test cases/cargo/19 links external/subprojects/zstd-rs/Cargo.toml b/test cases/cargo/19 links external/subprojects/zstd-rs/Cargo.toml new file mode 100644 index 000000000000..8d1abfd047e8 --- /dev/null +++ b/test cases/cargo/19 links external/subprojects/zstd-rs/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "zstdrs" +version = "1.0.0" +links = "libzstd" +autobenches = false +autobins = false +autoexamples = false +autotests = false \ No newline at end of file diff --git a/test cases/cargo/19 links external/subprojects/zstd-rs/src/lib.rs b/test cases/cargo/19 links external/subprojects/zstd-rs/src/lib.rs new file mode 100644 index 000000000000..69249afef16e --- /dev/null +++ b/test cases/cargo/19 links external/subprojects/zstd-rs/src/lib.rs @@ -0,0 +1,9 @@ +extern "C" { + fn ZSTD_maxCLevel() -> i64; +} + +pub fn max_compression_level() -> i64 { + unsafe { + return ZSTD_maxCLevel(); + } +} \ No newline at end of file diff --git a/test cases/cargo/19 links external/test.py b/test cases/cargo/19 links external/test.py new file mode 100755 index 000000000000..c3faffb5e49d --- /dev/null +++ b/test cases/cargo/19 links external/test.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +import argparse +import subprocess +import sys + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument('first') + parser.add_argument('second') + args = parser.parse_args() + + first = subprocess.run(args.first, stdout=subprocess.PIPE) + second = subprocess.run(args.second, stdout=subprocess.PIPE) + + if first.stdout.strip() != second.stdout.strip(): + print("Values did not match!", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() \ No newline at end of file From 8a88e9c8d4d4f94c2dfcf104868bc09a4b88e923 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 11 Mar 2021 14:44:00 -0800 Subject: [PATCH 16/23] cargo: add support for [package]:links --- mesonbuild/cargo/cargo.py | 16 ++++++++++++++-- mesonbuild/cargo/nodebuilder.py | 3 +++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/mesonbuild/cargo/cargo.py b/mesonbuild/cargo/cargo.py index 3f6a14eadc75..620465226fea 100644 --- a/mesonbuild/cargo/cargo.py +++ b/mesonbuild/cargo/cargo.py @@ -361,7 +361,7 @@ def __parse_lib(self, builder: NodeBuilder) -> None: # We always call the list of dependencies "dependencies", # if there are dependencies we can add them. There could # be an empty dependencies section, so account for that - if self.manifest.get('dependencies') or self.manifest.get('target'): + if self.manifest.get('dependencies') or self.manifest.get('target') or self.manifest['package'].get('links'): fbuilder.keyword('dependencies', builder.id('dependencies')) # Always mark everything as build by default false, that way meson will @@ -586,6 +586,18 @@ def __emit_dependencies(self, builder: NodeBuilder) -> None: if isinstance(dep, str) or not dep.get('optional', False): arbuilder.positional(self.__get_dependency(builder, name)) + # If we need to link with a system library, we'll first try a + # dependency + # + # TODO: if that doesn't work, we need to fall back to adding a C + # compiler and running c.find_library() + if self.manifest['package'].get('links'): + with builder.assignment_builder('link_library') as abuilder: + with abuilder.function_builder('dependency') as argbuilder: + argbuilder.positional(self.manifest['package']['links']) + with builder.plus_assignment_builder('dependencies') as pabuilder: + pabuilder.reference('link_library') + # If it's optional, then we need to check that the feature that # it depends on is available, then add it to the dependency array. for name, requires in self.features_to_deps.items(): @@ -691,7 +703,7 @@ def parse(self) -> T.Tuple['mparser.CodeBlockNode', 'mparser.CodeBlockNode']: # Create a list of dependencies which will be added to the library (if # there is one). - if self.manifest.get('dependencies') or self.manifest.get('target'): + if self.manifest.get('dependencies') or self.manifest.get('target') or self.manifest['package'].get('links'): self.__emit_dependencies(builder) if self.manifest.get('dev-dependencies'): self.__emit_dev_dependencies(builder) diff --git a/mesonbuild/cargo/nodebuilder.py b/mesonbuild/cargo/nodebuilder.py index 355f4c7101d4..3d28b707d213 100644 --- a/mesonbuild/cargo/nodebuilder.py +++ b/mesonbuild/cargo/nodebuilder.py @@ -215,6 +215,9 @@ def array_builder(self) -> T.Iterator[ArgumentBuilder]: array = mparser.ArrayNode(b.finalize(), 0, 0, 0, 0) # _builder.array expects raw arguments self._node = self._builder.plus_assign(self.name, array) + def reference(self, name: str) -> None: + self._node = self._builder.plus_assign(self.name, self._builder.id(name)) + def finalize(self) -> mparser.PlusAssignmentNode: assert self._node is not None, 'You need to build an assignment before finalizing' return self._node From 674c0ad8f3dd05d12f4a94b2e2c7dfc14ee73ab3 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 11 Mar 2021 14:59:17 -0800 Subject: [PATCH 17/23] cargo: add a test for proc-macro crates --- test cases/cargo/18 proc macros/meson.build | 12 ++++++++++++ test cases/cargo/18 proc macros/proc_macro.rs | 8 ++++++++ .../cargo/18 proc macros/subprojects/sub1/Cargo.toml | 8 ++++++++ .../cargo/18 proc macros/subprojects/sub1/src/lib.rs | 7 +++++++ 4 files changed, 35 insertions(+) create mode 100644 test cases/cargo/18 proc macros/meson.build create mode 100644 test cases/cargo/18 proc macros/proc_macro.rs create mode 100644 test cases/cargo/18 proc macros/subprojects/sub1/Cargo.toml create mode 100644 test cases/cargo/18 proc macros/subprojects/sub1/src/lib.rs diff --git a/test cases/cargo/18 proc macros/meson.build b/test cases/cargo/18 proc macros/meson.build new file mode 100644 index 000000000000..3bf688052ea4 --- /dev/null +++ b/test cases/cargo/18 proc macros/meson.build @@ -0,0 +1,12 @@ +project('cargo proc macros', 'rust') + +rust = import('unstable-rust') + +sub1 = rust.subproject('sub1') + +exe1 = executable( + 'proc_macro', + 'proc_macro.rs', + dependencies : sub1.get_variable('dep') +) +test('proc_macro', exe1) \ No newline at end of file diff --git a/test cases/cargo/18 proc macros/proc_macro.rs b/test cases/cargo/18 proc macros/proc_macro.rs new file mode 100644 index 000000000000..646754084e07 --- /dev/null +++ b/test cases/cargo/18 proc macros/proc_macro.rs @@ -0,0 +1,8 @@ +extern crate sub1; +use proc_macro_examples::make_answer; + +make_answer!(); + +fn main() { + std::process::exit(answer() - 42); +} diff --git a/test cases/cargo/18 proc macros/subprojects/sub1/Cargo.toml b/test cases/cargo/18 proc macros/subprojects/sub1/Cargo.toml new file mode 100644 index 000000000000..6e2a3e65942a --- /dev/null +++ b/test cases/cargo/18 proc macros/subprojects/sub1/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "sub1" +version = "1.0.0" + +[lib] +name = "sub1" +proc-macro = true +test = false diff --git a/test cases/cargo/18 proc macros/subprojects/sub1/src/lib.rs b/test cases/cargo/18 proc macros/subprojects/sub1/src/lib.rs new file mode 100644 index 000000000000..5ccdc97e5bcb --- /dev/null +++ b/test cases/cargo/18 proc macros/subprojects/sub1/src/lib.rs @@ -0,0 +1,7 @@ +extern crate proc_macro; +use proc_macro::TokenStream; + +#[proc_macro] +pub fn make_answer(_item: TokenStream) -> TokenStream { + "fn answer() -> u32 { 42 }".parse().unwrap() +} \ No newline at end of file From 4a2fdcf4fc10770c92f7365ca52ac305beb1e102 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Tue, 20 Apr 2021 21:50:23 -0700 Subject: [PATCH 18/23] build: Add proc-macro type for rust crates --- mesonbuild/build.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 3a7716f966b4..f5b331ddcedf 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -1796,8 +1796,8 @@ def __init__(self, name, subdir, subproject, for_machine: MachineChoice, sources mlog.debug('Defaulting Rust static library target crate type to rlib') self.rust_crate_type = 'rlib' # Don't let configuration proceed with a non-static crate type - elif self.rust_crate_type not in ['rlib', 'staticlib']: - raise InvalidArguments(f'Crate type "{self.rust_crate_type}" invalid for static libraries; must be "rlib" or "staticlib"') + elif self.rust_crate_type not in ['rlib', 'staticlib', 'proc-macro']: + raise InvalidArguments(f'Crate type "{self.rust_crate_type}" invalid for static libraries; must be "rlib", "staticlib", or "proc-macro"') # By default a static library is named libfoo.a even on Windows because # MSVC does not have a consistent convention for what static libraries # are called. The MSVC CRT uses libfoo.lib syntax but nothing else uses @@ -1809,7 +1809,7 @@ def __init__(self, name, subdir, subproject, for_machine: MachineChoice, sources self.prefix = 'lib' if not hasattr(self, 'suffix'): if 'rust' in self.compilers: - if not hasattr(self, 'rust_crate_type') or self.rust_crate_type == 'rlib': + if not hasattr(self, 'rust_crate_type') or self.rust_crate_type in {'rlib', 'proc-macro'}: # default Rust static library suffix self.suffix = 'rlib' elif self.rust_crate_type == 'staticlib': @@ -1868,8 +1868,8 @@ def __init__(self, name, subdir, subproject, for_machine: MachineChoice, sources mlog.debug('Defaulting Rust dynamic library target crate type to "dylib"') self.rust_crate_type = 'dylib' # Don't let configuration proceed with a non-dynamic crate type - elif self.rust_crate_type not in ['dylib', 'cdylib']: - raise InvalidArguments(f'Crate type "{self.rust_crate_type}" invalid for dynamic libraries; must be "dylib" or "cdylib"') + elif self.rust_crate_type not in ['dylib', 'cdylib', 'proc-macro']: + raise InvalidArguments(f'Crate type "{self.rust_crate_type}" invalid for dynamic libraries; must be "dylib", "cdylib", or "proc-macro"') if not hasattr(self, 'prefix'): self.prefix = None if not hasattr(self, 'suffix'): From ce864e2ff715360be1f22bdf46e77688a5743dd5 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Tue, 20 Apr 2021 21:51:00 -0700 Subject: [PATCH 19/23] cargo: Add proc-macro support --- TODO.md | 2 +- mesonbuild/cargo/cargo.py | 10 +++++++--- .../cargo/18 proc macros/{proc_macro.rs => main.rs} | 2 +- test cases/cargo/18 proc macros/meson.build | 6 +++--- .../cargo/18 proc macros/subprojects/sub1/src/lib.rs | 2 +- 5 files changed, 13 insertions(+), 9 deletions(-) rename test cases/cargo/18 proc macros/{proc_macro.rs => main.rs} (70%) diff --git a/TODO.md b/TODO.md index 2e55bc9cf3b0..11c7cd73e07a 100644 --- a/TODO.md +++ b/TODO.md @@ -3,6 +3,7 @@ Cargo to meson.build: - dependencies - binaries - libraries + - proc-macro - features - target cfg() paring @@ -24,7 +25,6 @@ Cargo to meson.build: - bench - libraries - targets required features - - proc-macro - doctest? - bench - Add test for invalid feature names diff --git a/mesonbuild/cargo/cargo.py b/mesonbuild/cargo/cargo.py index 620465226fea..ee63b124b82a 100644 --- a/mesonbuild/cargo/cargo.py +++ b/mesonbuild/cargo/cargo.py @@ -334,7 +334,7 @@ def __parse_lib(self, builder: NodeBuilder) -> None: fbuilder.positional(lib.get('path', 'src/lib.rs')) # XXX: this is the same hack as described below - if lib.get('crate-type', ['rlib'])[0] in {'dylib', 'cdylib'}: + if lib.get('proc-macro', False) or lib.get('crate-type', ['rlib'])[0] in {'dylib', 'cdylib', 'proc-macro'}: fbuilder.keyword('target_type', 'shared_library') else: # This scoops up the "lib" type as well. Meson very @@ -346,13 +346,17 @@ def __parse_lib(self, builder: NodeBuilder) -> None: # XXX: this is wrong, crate-type is a list, and cargo will # generate one target of each type listed. I messed this up # early on, so for now we just have a hack to work around this - if lib.get('crate-type', ['lib'])[0] == 'lib': + if lib.get('proc-macro', False): + fbuilder.keyword('rust_crate_type', 'proc-macro') + elif lib.get('crate-type', ['lib'])[0] == 'lib': fbuilder.keyword('rust_crate_type', 'rlib') else: assert len(lib['crate-type']) == 1 fbuilder.keyword('rust_crate_type', lib['crate-type'][0]) - fbuilder.keyword('version', self.manifest['package']['version']) + # Proc macros must not have versions in them + if not lib.get('proc-macro', False): + fbuilder.keyword('version', self.manifest['package']['version']) edition = lib.get('edition', self.manifest['package'].get('edition')) if edition: diff --git a/test cases/cargo/18 proc macros/proc_macro.rs b/test cases/cargo/18 proc macros/main.rs similarity index 70% rename from test cases/cargo/18 proc macros/proc_macro.rs rename to test cases/cargo/18 proc macros/main.rs index 646754084e07..39fac3b383dd 100644 --- a/test cases/cargo/18 proc macros/proc_macro.rs +++ b/test cases/cargo/18 proc macros/main.rs @@ -1,5 +1,5 @@ extern crate sub1; -use proc_macro_examples::make_answer; +use sub1::make_answer; make_answer!(); diff --git a/test cases/cargo/18 proc macros/meson.build b/test cases/cargo/18 proc macros/meson.build index 3bf688052ea4..9254a1a385ac 100644 --- a/test cases/cargo/18 proc macros/meson.build +++ b/test cases/cargo/18 proc macros/meson.build @@ -5,8 +5,8 @@ rust = import('unstable-rust') sub1 = rust.subproject('sub1') exe1 = executable( - 'proc_macro', - 'proc_macro.rs', + 'main', + 'main.rs', dependencies : sub1.get_variable('dep') ) -test('proc_macro', exe1) \ No newline at end of file +test('proc_macro', exe1) diff --git a/test cases/cargo/18 proc macros/subprojects/sub1/src/lib.rs b/test cases/cargo/18 proc macros/subprojects/sub1/src/lib.rs index 5ccdc97e5bcb..0e7691f09bec 100644 --- a/test cases/cargo/18 proc macros/subprojects/sub1/src/lib.rs +++ b/test cases/cargo/18 proc macros/subprojects/sub1/src/lib.rs @@ -3,5 +3,5 @@ use proc_macro::TokenStream; #[proc_macro] pub fn make_answer(_item: TokenStream) -> TokenStream { - "fn answer() -> u32 { 42 }".parse().unwrap() + "fn answer() -> i32 { 42 }".parse().unwrap() } \ No newline at end of file From fc7e05e38f3cd3d90ae4737a482ba92891e88174 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Tue, 20 Apr 2021 21:55:36 -0700 Subject: [PATCH 20/23] pytests: add some tests for the cfg_builder This uses pytest not unittest, unittest is such a pain in the ass and pytest is so much nicer.. --- pytests/cfg_builder_tests.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 pytests/cfg_builder_tests.py diff --git a/pytests/cfg_builder_tests.py b/pytests/cfg_builder_tests.py new file mode 100644 index 000000000000..c4deaae972e3 --- /dev/null +++ b/pytests/cfg_builder_tests.py @@ -0,0 +1,30 @@ +# SPDX-license-identifier: Apache-2.0 +# Copyright © 2021 Intel Corporation + +from pathlib import Path + +from mesonbuild import mparser +from mesonbuild.cargo.cfg_builder import * +from mesonbuild.cargo.cfg_parser import * +from mesonbuild.cargo.nodebuilder import NodeBuilder + + +class TestCfgBuilder: + + def test_eq(self) -> None: + builder = NodeBuilder(Path('')) + + id = builder.id('host_machine') + left = mparser.MethodNode('', 0, 0, id, 'system', mparser.ArgumentNode(builder._builder.token())) + right = builder.string('sunos') + expected = mparser.ComparisonNode('==', left, right) + + # use illumos to test the translation from cargo names to meson names + ast = parse('cfg(target_os = "illumos")') + builder = NodeBuilder(Path('')) + build(builder, ast) + final = builder.finalize() + assert len(final.lines) == 1 + + actual = final.lines[0] + assert actual == expected From 2ed446a35600de7d7776a0ddcfa83153abd278b2 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Tue, 20 Apr 2021 22:39:13 -0700 Subject: [PATCH 21/23] cargo: remove unused imports --- mesonbuild/cargo/cargo.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/mesonbuild/cargo/cargo.py b/mesonbuild/cargo/cargo.py index ee63b124b82a..eba20a0a5b98 100644 --- a/mesonbuild/cargo/cargo.py +++ b/mesonbuild/cargo/cargo.py @@ -21,19 +21,16 @@ import toml from .. import mlog -from ..envconfig import MachineInfo -from ..environment import normalize_cpu_family from ..mesonlib import MesonException, version_compare_many from ..optinterpreter import is_invalid_name from .nodebuilder import ObjectBuilder, NodeBuilder -from .cfg_parser import Node, parse as cfg_parser +from .cfg_parser import parse as cfg_parser from . import cfg_builder if T.TYPE_CHECKING: from .. import mparser from ..backend.backends import Backend from ..build import Build - from ..dependencies import ExternalProgram from ..environment import Environment try: From c502b3ba1e861cca849671cb1ed9e272ba3fee5b Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Fri, 21 May 2021 10:19:54 -0700 Subject: [PATCH 22/23] cargo/cfg_parser: fix mypy warnings --- mesonbuild/cargo/cfg_parser.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/mesonbuild/cargo/cfg_parser.py b/mesonbuild/cargo/cfg_parser.py index 5e92c3d0ff80..68764029da27 100644 --- a/mesonbuild/cargo/cfg_parser.py +++ b/mesonbuild/cargo/cfg_parser.py @@ -59,7 +59,7 @@ class Equal(Token): """An =""" - def __init__(self): + def __init__(self) -> None: super().__init__('=') @@ -67,7 +67,7 @@ class Comma(Token): """A ,""" - def __init__(self): + def __init__(self) -> None: super().__init__(',') @@ -75,7 +75,7 @@ class LParen(Token): """A (""" - def __init__(self): + def __init__(self) -> None: super().__init__('(') @@ -83,7 +83,7 @@ class RParen(Token): """A )""" - def __init__(self): + def __init__(self) -> None: super().__init__(')') @@ -232,7 +232,7 @@ def lookahead(it: T.Iterator[_T]) -> T.Generator[T.Tuple[_T, T.Optional[_T]], No yield (current, None) -def parser(prog: T.List[Token]) -> AST: +def parser(prog: T.Iterable[Token]) -> AST: """Parse the lexed form into a Tree.""" tree = AST() stack: T.List[Node] = [] @@ -269,6 +269,7 @@ def parser(prog: T.List[Token]) -> AST: else: assert nex is None if tree.root is None: + assert isinstance(node, FunctionNode), 'for mypy' tree.root = node return tree From 17f862b5ab7ab102576848d1a5cebbc641ce9bcc Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Fri, 21 May 2021 10:30:14 -0700 Subject: [PATCH 23/23] cargo: more mypy fixes --- mesonbuild/cargo/cargo.py | 11 ++--------- mesonbuild/cargo/cfg_builder.py | 1 + mesonbuild/cargo/nodebuilder.py | 5 ++++- mesonbuild/interpreter/interpreter.py | 2 +- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/mesonbuild/cargo/cargo.py b/mesonbuild/cargo/cargo.py index eba20a0a5b98..d0d95aa8f6b2 100644 --- a/mesonbuild/cargo/cargo.py +++ b/mesonbuild/cargo/cargo.py @@ -33,15 +33,8 @@ from ..build import Build from ..environment import Environment - try: - from typing import TypedDict - except AttributeError: - from typing_extensions import TypedDict - - try: - from typing import Literal - except AttributeError: - from typing_extensions import Literal + from typing_extensions import TypedDict + from typing_extensions import Literal _PackageDict = TypedDict( '_PackageDict', diff --git a/mesonbuild/cargo/cfg_builder.py b/mesonbuild/cargo/cfg_builder.py index 4050b5b78ea1..82eea2bc406a 100644 --- a/mesonbuild/cargo/cfg_builder.py +++ b/mesonbuild/cargo/cfg_builder.py @@ -68,6 +68,7 @@ def build(builder: 'NodeBuilder', node: cfg_parser.FunctionNode) -> None: into `a or b or c`. """ if node.name == 'cfg': + assert isinstance(node.arguments[0], cfg_parser.FunctionNode), 'for mypy' node = node.arguments[0] if node.name in {'equal', 'not_equal'}: diff --git a/mesonbuild/cargo/nodebuilder.py b/mesonbuild/cargo/nodebuilder.py index 3d28b707d213..7684e9316ef7 100644 --- a/mesonbuild/cargo/nodebuilder.py +++ b/mesonbuild/cargo/nodebuilder.py @@ -32,7 +32,7 @@ TYPE_mixed_dict = T.Dict[str, TYPE_mixed_list] try: - from typing import Literal + from typing import Literal # typing: ignore except ImportError: from typing_extensions import Literal @@ -357,6 +357,9 @@ def or_(self, node: mparser.BaseNode) -> None: def finalize(self) -> T.Union[mparser.AndNode, mparser.OrNode]: if len(self._stuff) == 1: + # We know that if the length is 1 then there's no OP, but mypy + # doesn't + assert isinstance(self._stuff[0], mparser.BaseNode), 'for mypy' return self._stuff[0] assert len(self._stuff) % 2 == 1 diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 269b32102bc5..bc424cbc3bdb 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -33,7 +33,7 @@ from ..interpreterbase import ObjectHolder, RangeHolder from ..modules import ModuleObject from ..cmake import CMakeInterpreter -from ..cargo import ManifestInterpreter +from ..cargo.cargo import ManifestInterpreter from ..backend.backends import Backend, ExecutableSerialisation from .mesonmain import MesonMain