aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/website.yml35
-rw-r--r--docs/meson.build8
-rw-r--r--mesonbuild/backend/ninjabackend.py36
-rw-r--r--mesonbuild/interpreterbase.py141
-rw-r--r--mesonbuild/linkers.py2
-rw-r--r--mesonbuild/mdist.py2
-rw-r--r--mesonbuild/modules/fs.py18
-rw-r--r--mesonbuild/scripts/scanbuild.py21
-rwxr-xr-xrun_unittests.py135
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):