diff options
-rw-r--r-- | .github/workflows/website.yml | 35 | ||||
-rw-r--r-- | docs/meson.build | 8 | ||||
-rw-r--r-- | mesonbuild/backend/ninjabackend.py | 36 | ||||
-rw-r--r-- | mesonbuild/interpreterbase.py | 141 | ||||
-rw-r--r-- | mesonbuild/linkers.py | 2 | ||||
-rw-r--r-- | mesonbuild/mdist.py | 2 | ||||
-rw-r--r-- | mesonbuild/modules/fs.py | 18 | ||||
-rw-r--r-- | mesonbuild/scripts/scanbuild.py | 21 | ||||
-rwxr-xr-x | run_unittests.py | 135 |
9 files changed, 366 insertions, 32 deletions
diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml new file mode 100644 index 0000000..ae732e2 --- /dev/null +++ b/.github/workflows/website.yml @@ -0,0 +1,35 @@ +name: Update website + +on: + push: + branches: + - master + paths: + - docs/** + workflow_dispatch: + +jobs: + update_website: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install package + run: | + sudo apt-get -y install python3-pip ninja-build libjson-glib-dev + pip install meson hotdoc + - name: Setup SSH Keys and known_hosts + env: + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + run: | + ssh-agent -a $SSH_AUTH_SOCK > /dev/null + ssh-add - <<< "${{ secrets.WEBSITE_PRIV_KEY }}" + - name: Update website + env: + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + run: | + git config --global user.name "github-actions" + git config --global user.email "github-actions@github.com" + cd docs + meson setup _build + ninja -C _build + ninja -C _build upload diff --git a/docs/meson.build b/docs/meson.build index fa80512..7369335 100644 --- a/docs/meson.build +++ b/docs/meson.build @@ -27,11 +27,15 @@ documentation = hotdoc.generate_doc(meson.project_name(), include_paths: ['markdown', cur_bdir], default_license: 'CC-BY-SAv4.0', html_extra_theme: join_paths('theme', 'extra'), - git_upload_repository: 'git@github.com:jpakkane/jpakkane.github.io.git', + git_upload_repository: 'git@github.com:mesonbuild/mesonbuild.github.io.git', edit_on_github_repository: 'https://github.com/mesonbuild/meson', syntax_highlighting_activate: true, ) run_target('upload', - command: [find_program('hotdoc'), 'run', '--conf-file', documentation.config_path()] + command: [find_program('hotdoc'), 'run', + '--conf-file', documentation.config_path(), + '--git-upload', + '-vv', + ], ) diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index 6bd7ba6..4e826ee 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -1840,6 +1840,17 @@ int dummy; # Introspection information self.create_target_source_introspection(target, swiftc, compile_args + header_imports + module_includes, relsrc, rel_generated) + def _rsp_options(self, tool: T.Union['Compiler', 'StaticLinker', 'DynamicLinker']) -> T.Dict[str, T.Union[bool, RSPFileSyntax]]: + """Helper method to get rsp options. + + rsp_file_syntax() is only guaranteed to be implemented if + can_linker_accept_rsp() returns True. + """ + options = dict(rspable=tool.can_linker_accept_rsp()) + if options['rspable']: + options['rspfile_quote_style'] = tool.rsp_file_syntax() + return options + def generate_static_link_rules(self): num_pools = self.environment.coredata.options[OptionKey('backend_max_links')].value if 'java' in self.environment.coredata.compilers.host: @@ -1868,10 +1879,9 @@ int dummy; pool = 'pool = link_pool' else: pool = None - self.add_rule(NinjaRule(rule, cmdlist, args, description, - rspable=static_linker.can_linker_accept_rsp(), - rspfile_quote_style=static_linker.rsp_file_syntax(), - extra=pool)) + + options = self._rsp_options(static_linker) + self.add_rule(NinjaRule(rule, cmdlist, args, description, **options, extra=pool)) def generate_dynamic_link_rules(self): num_pools = self.environment.coredata.options[OptionKey('backend_max_links')].value @@ -1891,10 +1901,9 @@ int dummy; pool = 'pool = link_pool' else: pool = None - self.add_rule(NinjaRule(rule, command, args, description, - rspable=compiler.can_linker_accept_rsp(), - rspfile_quote_style=compiler.rsp_file_syntax(), - extra=pool)) + + options = self._rsp_options(compiler) + self.add_rule(NinjaRule(rule, command, args, description, **options, extra=pool)) args = self.environment.get_build_command() + \ ['--internal', @@ -1977,8 +1986,10 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) command = compiler.get_exelist() args = ['$ARGS'] + NinjaCommandArg.list(compiler.get_output_args('$out'), Quoting.none) + compiler.get_compile_only_args() + ['$in'] description = 'Compiling LLVM IR object $in' - self.add_rule(NinjaRule(rule, command, args, description, - rspable=compiler.can_linker_accept_rsp())) + + options = self._rsp_options(compiler) + + self.add_rule(NinjaRule(rule, command, args, description, **options)) self.created_llvm_ir_rule[compiler.for_machine] = True def generate_compile_rule_for(self, langname, compiler): @@ -2014,9 +2025,8 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) else: deps = 'gcc' depfile = '$DEPFILE' - self.add_rule(NinjaRule(rule, command, args, description, - rspable=compiler.can_linker_accept_rsp(), - rspfile_quote_style=compiler.rsp_file_syntax(), + options = self._rsp_options(compiler) + self.add_rule(NinjaRule(rule, command, args, description, **options, deps=deps, depfile=depfile)) def generate_pch_rule_for(self, langname, compiler): diff --git a/mesonbuild/interpreterbase.py b/mesonbuild/interpreterbase.py index 41732d6..4318023 100644 --- a/mesonbuild/interpreterbase.py +++ b/mesonbuild/interpreterbase.py @@ -357,6 +357,147 @@ def typed_pos_args(name: str, *types: T.Union[T.Type, T.Tuple[T.Type, ...]], return inner +class ContainerTypeInfo: + + """Container information for keyword arguments. + + For keyword arguments that are containers (list or dict), this class encodes + that information. + + :param container: the type of container + :param contains: the types the container holds + :param pairs: if the container is supposed to be of even length. + This is mainly used for interfaces that predate the addition of dictionaries, and use + `[key, value, key2, value2]` format. + :param allow_empty: Whether this container is allowed to be empty + There are some cases where containers not only must be passed, but must + not be empty, and other cases where an empty container is allowed. + """ + + def __init__(self, container: T.Type, contains: T.Union[T.Type, T.Tuple[T.Type, ...]], *, + pairs: bool = False, allow_empty: bool = True) : + self.container = container + self.contains = contains + self.pairs = pairs + self.allow_empty = allow_empty + + def check(self, value: T.Any) -> T.Optional[str]: + """Check that a value is valid. + + :param value: A value to check + :return: If there is an error then a string message, otherwise None + """ + if not isinstance(value, self.container): + return f'container type was "{type(value).__name__}", but should have been "{self.container.__name__}"' + iter_ = iter(value.values()) if isinstance(value, dict) else iter(value) + for each in iter_: + if not isinstance(each, self.contains): + if isinstance(self.contains, tuple): + shouldbe = 'one of: {}'.format(", ".join(f'"{t.__name__}"' for t in self.contains)) + else: + shouldbe = f'"{self.contains.__name__}"' + return f'contained a value of type "{type(each).__name__}" but should have been {shouldbe}' + if self.pairs and len(value) % 2 != 0: + return 'container should be of even length, but is not' + if not value and not self.allow_empty: + return 'container is empty, but not allowed to be' + return None + + +_T = T.TypeVar('_T') + + +class KwargInfo(T.Generic[_T]): + + """A description of a keyword argument to a meson function + + This is used to describe a value to the :func:typed_kwargs function. + + :param name: the name of the parameter + :param types: A type or tuple of types that are allowed, or a :class:ContainerType + :param required: Whether this is a required keyword argument. defaults to False + :param listify: If true, then the argument will be listified before being + checked. This is useful for cases where the Meson DSL allows a scalar or + a container, but internally we only want to work with containers + :param default: A default value to use if this isn't set. defaults to None + :param since: Meson version in which this argument has been added. defaults to None + :param deprecated: Meson version in which this argument has been deprecated. defaults to None + """ + + def __init__(self, name: str, types: T.Union[T.Type[_T], T.Tuple[T.Type[_T], ...], ContainerTypeInfo], + required: bool = False, listify: bool = False, default: T.Optional[_T] = None, + since: T.Optional[str] = None, deprecated: T.Optional[str] = None): + self.name = name + self.types = types + self.required = required + self.listify = listify + self.default = default + self.since = since + self.deprecated = deprecated + + +def typed_kwargs(name: str, *types: KwargInfo) -> T.Callable[..., T.Any]: + """Decorator for type checking keyword arguments. + + Used to wrap a meson DSL implementation function, where it checks various + things about keyword arguments, including the type, and various other + information. For non-required values it sets the value to a default, which + means the value will always be provided. + + :param name: the name of the function, including the object it's attached ot + (if applicable) + :param *types: KwargInfo entries for each keyword argument. + """ + def inner(f: TV_func) -> TV_func: + + @wraps(f) + def wrapper(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any: + kwargs, subproject = _get_callee_args(wrapped_args, want_subproject=True)[3:5] + + all_names = {t.name for t in types} + unknowns = set(kwargs).difference(all_names) + if unknowns: + # Warn about unknown argumnts, delete them and continue. This + # keeps current behavior + ustr = ', '.join([f'"{u}"' for u in sorted(unknowns)]) + mlog.warning(f'{name} got unknown keyword arguments {ustr}') + for u in unknowns: + del kwargs[u] + + for info in types: + value = kwargs.get(info.name) + if value is not None: + if info.since: + feature_name = info.name + ' arg in ' + name + FeatureNew.single_use(feature_name, info.since, subproject) + if info.deprecated: + feature_name = info.name + ' arg in ' + name + FeatureDeprecated.single_use(feature_name, info.deprecated, subproject) + if info.listify: + kwargs[info.name] = value = mesonlib.listify(value) + if isinstance(info.types, ContainerTypeInfo): + msg = info.types.check(value) + if msg is not None: + raise InvalidArguments(f'{name} keyword argument "{info.name}" {msg}') + else: + if not isinstance(value, info.types): + if isinstance(info.types, tuple): + shouldbe = 'one of: {}'.format(", ".join(f'"{t.__name__}"' for t in info.types)) + else: + shouldbe = f'"{info.types.__name__}"' + raise InvalidArguments(f'{name} keyword argument "{info.name}"" was of type "{type(value).__name__}" but should have been {shouldbe}') + elif info.required: + raise InvalidArguments(f'{name} is missing required keyword argument "{info.name}"') + else: + # set the value to the default, this ensuring all kwargs are present + # This both simplifies the typing checking and the usage + kwargs[info.name] = info.default + + return f(*wrapped_args, **wrapped_kwargs) + return T.cast(TV_func, wrapper) + return inner + + class FeatureCheckBase(metaclass=abc.ABCMeta): "Base class for feature version checks" diff --git a/mesonbuild/linkers.py b/mesonbuild/linkers.py index acb2c44..7b938ac 100644 --- a/mesonbuild/linkers.py +++ b/mesonbuild/linkers.py @@ -112,7 +112,7 @@ class StaticLinker: be implemented """ assert not self.can_linker_accept_rsp(), f'{self.id} linker accepts RSP, but doesn\' provide a supported format, this is a bug' - raise mesonlib.EnvironmentException(f'{self.id} does no implemnt rsp format, this shouldn\'t be called') + raise mesonlib.EnvironmentException(f'{self.id} does not implemnt rsp format, this shouldn\'t be called') class VisualStudioLikeLinker: diff --git a/mesonbuild/mdist.py b/mesonbuild/mdist.py index 22c5b44..397f8cd 100644 --- a/mesonbuild/mdist.py +++ b/mesonbuild/mdist.py @@ -313,5 +313,5 @@ def run(options): if rc == 0: for name in names: create_hash(name) - print('Created', os.path.relpath(name)) + print('Created', name) return rc diff --git a/mesonbuild/modules/fs.py b/mesonbuild/modules/fs.py index caa21f7..ab3aae2 100644 --- a/mesonbuild/modules/fs.py +++ b/mesonbuild/modules/fs.py @@ -25,12 +25,19 @@ from ..mesonlib import ( MesonException, path_is_in_root, ) -from ..interpreterbase import FeatureNew, typed_pos_args, noKwargs, permittedKwargs +from ..interpreterbase import FeatureNew, KwargInfo, typed_kwargs, typed_pos_args, noKwargs if T.TYPE_CHECKING: from . import ModuleState from ..interpreter import Interpreter + from typing_extensions import TypedDict + + class ReadKwArgs(TypedDict): + """Keyword Arguments for fs.read.""" + + encoding: str + class FSModule(ExtensionModule): @@ -205,9 +212,9 @@ class FSModule(ExtensionModule): return str(new) @FeatureNew('fs.read', '0.57.0') - @permittedKwargs({'encoding'}) @typed_pos_args('fs.read', (str, File)) - def read(self, state: 'ModuleState', args: T.Tuple['FileOrString'], kwargs: T.Dict[str, T.Any]) -> str: + @typed_kwargs('fs.read', KwargInfo('encoding', str, default='utf-8')) + def read(self, state: 'ModuleState', args: T.Tuple['FileOrString'], kwargs: 'ReadKwArgs') -> str: """Read a file from the source tree and return its value as a decoded string. @@ -217,10 +224,7 @@ class FSModule(ExtensionModule): loops) """ path = args[0] - encoding: str = kwargs.get('encoding', 'utf-8') - if not isinstance(encoding, str): - raise MesonException('`encoding` parameter must be a string') - + encoding = kwargs['encoding'] src_dir = self.interpreter.environment.source_dir sub_dir = self.interpreter.subdir build_dir = self.interpreter.environment.get_build_dir() diff --git a/mesonbuild/scripts/scanbuild.py b/mesonbuild/scripts/scanbuild.py index 0736b3f..bb8e30c 100644 --- a/mesonbuild/scripts/scanbuild.py +++ b/mesonbuild/scripts/scanbuild.py @@ -17,20 +17,25 @@ import shutil import tempfile from ..environment import detect_ninja, detect_scanbuild from ..coredata import get_cmd_line_file, CmdLineFileParser +from ..mesonlib import windows_proof_rmtree from pathlib import Path import typing as T from ast import literal_eval import os def scanbuild(exelist: T.List[str], srcdir: Path, blddir: Path, privdir: Path, logdir: Path, args: T.List[str]) -> int: - with tempfile.TemporaryDirectory(dir=str(privdir)) as scandir: - meson_cmd = exelist + args - build_cmd = exelist + ['-o', str(logdir)] + detect_ninja() + ['-C', scandir] - rc = subprocess.call(meson_cmd + [str(srcdir), scandir]) - if rc != 0: - return rc - return subprocess.call(build_cmd) - + # In case of problems leave the temp directory around + # so it can be debugged. + scandir = tempfile.mkdtemp(dir=str(privdir)) + meson_cmd = exelist + args + build_cmd = exelist + ['-o', str(logdir)] + detect_ninja() + ['-C', scandir] + rc = subprocess.call(meson_cmd + [str(srcdir), scandir]) + if rc != 0: + return rc + rc = subprocess.call(build_cmd) + if rc == 0: + windows_proof_rmtree(scandir) + return rc def run(args: T.List[str]) -> int: srcdir = Path(args[0]) diff --git a/run_unittests.py b/run_unittests.py index 0b18ef5..3a98368 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -52,6 +52,7 @@ import mesonbuild.coredata import mesonbuild.modules.gnome from mesonbuild.interpreter import Interpreter from mesonbuild.interpreterbase import typed_pos_args, InvalidArguments, ObjectHolder +from mesonbuild.interpreterbase import typed_pos_args, InvalidArguments, typed_kwargs, ContainerTypeInfo, KwargInfo from mesonbuild.ast import AstInterpreter from mesonbuild.mesonlib import ( BuildDirLock, LibType, MachineChoice, PerMachine, Version, is_windows, @@ -1484,6 +1485,140 @@ class InternalTests(unittest.TestCase): _(None, mock.Mock(), ['string', '1'], None) + def test_typed_kwarg_basic(self) -> None: + @typed_kwargs( + 'testfunc', + KwargInfo('input', str) + ) + def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None: + self.assertIsInstance(kwargs['input'], str) + self.assertEqual(kwargs['input'], 'foo') + + _(None, mock.Mock(), [], {'input': 'foo'}) + + def test_typed_kwarg_missing_required(self) -> None: + @typed_kwargs( + 'testfunc', + KwargInfo('input', str, required=True), + ) + def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None: + self.assertTrue(False) # should be unreachable + + with self.assertRaises(InvalidArguments) as cm: + _(None, mock.Mock(), [], {}) + self.assertEqual(str(cm.exception), 'testfunc is missing required keyword argument "input"') + + def test_typed_kwarg_missing_optional(self) -> None: + @typed_kwargs( + 'testfunc', + KwargInfo('input', str), + ) + def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.Optional[str]]) -> None: + self.assertIsNone(kwargs['input']) + + _(None, mock.Mock(), [], {}) + + def test_typed_kwarg_default(self) -> None: + @typed_kwargs( + 'testfunc', + KwargInfo('input', str, default='default'), + ) + def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None: + self.assertEqual(kwargs['input'], 'default') + + _(None, mock.Mock(), [], {}) + + def test_typed_kwarg_container_valid(self) -> None: + @typed_kwargs( + 'testfunc', + KwargInfo('input', ContainerTypeInfo(list, str), required=True), + ) + def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.List[str]]) -> None: + self.assertEqual(kwargs['input'], ['str']) + + _(None, mock.Mock(), [], {'input': ['str']}) + + def test_typed_kwarg_container_invalid(self) -> None: + @typed_kwargs( + 'testfunc', + KwargInfo('input', ContainerTypeInfo(list, str), required=True), + ) + def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.List[str]]) -> None: + self.assertTrue(False) # should be unreachable + + with self.assertRaises(InvalidArguments) as cm: + _(None, mock.Mock(), [], {'input': {}}) + self.assertEqual(str(cm.exception), 'testfunc keyword argument "input" container type was "dict", but should have been "list"') + + def test_typed_kwarg_contained_invalid(self) -> None: + @typed_kwargs( + 'testfunc', + KwargInfo('input', ContainerTypeInfo(dict, str), required=True), + ) + def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.Dict[str, str]]) -> None: + self.assertTrue(False) # should be unreachable + + with self.assertRaises(InvalidArguments) as cm: + _(None, mock.Mock(), [], {'input': {'key': 1}}) + self.assertEqual(str(cm.exception), 'testfunc keyword argument "input" contained a value of type "int" but should have been "str"') + + def test_typed_kwarg_container_listify(self) -> None: + @typed_kwargs( + 'testfunc', + KwargInfo('input', ContainerTypeInfo(list, str), listify=True), + ) + def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.List[str]]) -> None: + self.assertEqual(kwargs['input'], ['str']) + + _(None, mock.Mock(), [], {'input': 'str'}) + + def test_typed_kwarg_container_pairs(self) -> None: + @typed_kwargs( + 'testfunc', + KwargInfo('input', ContainerTypeInfo(list, str, pairs=True), listify=True), + ) + def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.List[str]]) -> None: + self.assertEqual(kwargs['input'], ['a', 'b']) + + _(None, mock.Mock(), [], {'input': ['a', 'b']}) + + with self.assertRaises(MesonException) as cm: + _(None, mock.Mock(), [], {'input': ['a']}) + self.assertEqual(str(cm.exception), "testfunc keyword argument \"input\" container should be of even length, but is not") + + def test_typed_kwarg_since(self) -> None: + @typed_kwargs( + 'testfunc', + KwargInfo('input', str, since='1.0', deprecated='2.0') + ) + def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None: + self.assertIsInstance(kwargs['input'], str) + self.assertEqual(kwargs['input'], 'foo') + + # With Meson 0.1 it should trigger the "introduced" warning but not the "deprecated" warning + mesonbuild.mesonlib.project_meson_versions[''] = '0.1' + sys.stdout = io.StringIO() + _(None, mock.Mock(subproject=''), [], {'input': 'foo'}) + self.assertRegex(sys.stdout.getvalue(), r'WARNING:.*introduced.*input arg in testfunc') + self.assertNotRegex(sys.stdout.getvalue(), r'WARNING:.*deprecated.*input arg in testfunc') + + # With Meson 1.5 it shouldn't trigger any warning + mesonbuild.mesonlib.project_meson_versions[''] = '1.5' + sys.stdout = io.StringIO() + _(None, mock.Mock(subproject=''), [], {'input': 'foo'}) + self.assertNotRegex(sys.stdout.getvalue(), r'WARNING:.*') + self.assertNotRegex(sys.stdout.getvalue(), r'WARNING:.*') + + # With Meson 2.0 it should trigger the "deprecated" warning but not the "introduced" warning + mesonbuild.mesonlib.project_meson_versions[''] = '2.0' + sys.stdout = io.StringIO() + _(None, mock.Mock(subproject=''), [], {'input': 'foo'}) + self.assertRegex(sys.stdout.getvalue(), r'WARNING:.*deprecated.*input arg in testfunc') + self.assertNotRegex(sys.stdout.getvalue(), r'WARNING:.*introduced.*input arg in testfunc') + + sys.stdout = sys.__stdout__ + + @unittest.skipIf(is_tarball(), 'Skipping because this is a tarball release') class DataTests(unittest.TestCase): |