diff options
33 files changed, 582 insertions, 126 deletions
@@ -3,6 +3,7 @@ /mesonbuild/modules/cmake.py @mensinda /mesonbuild/modules/unstable_external_project.py @xclaesse /mesonbuild/modules/unstable_rust.py @dcbaker +/mesonbuild/modules/unstable_wayland.py @markbolhuis /mesonbuild/ast/ @mensinda /mesonbuild/cmake/ @mensinda /mesonbuild/compilers/ @dcbaker diff --git a/ci/ciimage/arch/install.sh b/ci/ciimage/arch/install.sh index 72816ab..9628635 100755 --- a/ci/ciimage/arch/install.sh +++ b/ci/ciimage/arch/install.sh @@ -14,7 +14,7 @@ pkgs=( itstool gtk3 java-environment=8 gtk-doc llvm clang sdl2 graphviz doxygen vulkan-validation-layers openssh mercurial gtk-sharp-2 qt5-tools libwmf valgrind cmake netcdf-fortran openmpi nasm gnustep-base gettext - python-lxml hotdoc rust-bindgen qt6-base qt6-tools + python-lxml hotdoc rust-bindgen qt6-base qt6-tools wayland wayland-protocols # cuda ) diff --git a/ci/ciimage/fedora/install.sh b/ci/ciimage/fedora/install.sh index df1d853..2f1ae7d 100755 --- a/ci/ciimage/fedora/install.sh +++ b/ci/ciimage/fedora/install.sh @@ -15,7 +15,7 @@ pkgs=( doxygen vulkan-devel vulkan-validation-layers-devel openssh mercurial gtk-sharp2-devel libpcap-devel gpgme-devel qt5-qtbase-devel qt5-qttools-devel qt5-linguist qt5-qtbase-private-devel libwmf-devel valgrind cmake openmpi-devel nasm gnustep-base-devel gettext-devel ncurses-devel - libxml2-devel libxslt-devel libyaml-devel glib2-devel json-glib-devel libgcrypt-devel + libxml2-devel libxslt-devel libyaml-devel glib2-devel json-glib-devel libgcrypt-devel wayland-devel wayland-protocols-devel ) # Sys update diff --git a/docs/markdown/Builtin-options.md b/docs/markdown/Builtin-options.md index db3c3e8..c8e98dd 100644 --- a/docs/markdown/Builtin-options.md +++ b/docs/markdown/Builtin-options.md @@ -271,10 +271,11 @@ name with the module name: `-D<module>.<option>=<value>` (e.g. `-Dpython.platlib ### Python module -| Option | Default value | Possible values | Description | -| ------ | ------------- | --------------- | ----------- | -| platlibdir | | Directory path | Directory for site-specific, platform-specific files (Since 0.60.0) | -| purelibdir | | Directory path | Directory for site-specific, non-platform-specific files (Since 0.60.0) | +| Option | Default value | Possible values | Description | +| ------ | ------------- | ----------------- | ----------- | +| install_env | prefix | {auto,prefix,system,venv} | Which python environment to install to (Since 0.62.0) | +| platlibdir | | Directory path | Directory for site-specific, platform-specific files (Since 0.60.0) | +| purelibdir | | Directory path | Directory for site-specific, non-platform-specific files (Since 0.60.0) | *Since 0.60.0* `python.platlibdir` and `python.purelibdir` options are used by python module methods `python.install_sources()` and `python.get_install_dir()`. @@ -283,3 +284,14 @@ relative to the installation `prefix`, which will often result in installed pyth modules to not be found by the interpreter unless `prefix` is `/usr` on Linux, or for example `C:\Python39` on Windows. These options can be absolute paths outside of `prefix`. + +*Since 0.62.0* The `python.install_env` option is used to detect the correct +installation path. Setting to `system` will avoid making the paths relative to +`prefix` and instead use the global site-packages of the selected python +interpreter directly, even if it is a venv. Setting to `venv` will instead use +the paths for the virtualenv the python found installation comes from (or fail +if it is not a virtualenv). Setting to `auto` will check if the found +installation is a virtualenv, and use `venv` or `system` as appropriate (but +never `prefix`). This option is mutually exclusive with the `platlibdir`/`purelibdir`. + +For backwards compatibility purposes, the default `install_env` is `prefix`. diff --git a/docs/markdown/CMake-module.md b/docs/markdown/CMake-module.md index 8e6c4e9..a5c0c7e 100644 --- a/docs/markdown/CMake-module.md +++ b/docs/markdown/CMake-module.md @@ -262,6 +262,7 @@ the `configuration` parameter. * `input`: the template file where that will be treated for variable substitutions contained in `configuration`. * `install_dir`: optional installation directory, it defaults to `$(libdir)/cmake/$(name)`. * `configuration`: a `configuration_data` object that will be used for variable substitution in the template file. + *Since 0.62.0* it can take a dictionary instead. Example: diff --git a/docs/markdown/Wayland-module.md b/docs/markdown/Wayland-module.md new file mode 100644 index 0000000..d30627c --- /dev/null +++ b/docs/markdown/Wayland-module.md @@ -0,0 +1,64 @@ +# Unstable Wayland Module + +This module is available since version 0.62.0. + +This module provides helper functions to find wayland protocol +xmls and to generate .c and .h files using wayland-scanner + +**Note**: this module is unstable. It is only provided as a technology +preview. Its API may change in arbitrary ways between releases or it +might be removed from Meson altogether. + +## Quick Usage + +```meson +project('hello-wayland', 'c') + +wl_dep = dependency('wayland-client') +wl_mod = import('unstable-wayland') + +xml = wl_mod.find_protocol('xdg-shell') +xdg_shell = wl_mod.scan_xml(xml) + +executable('hw', 'main.c', xdg_shell, dependencies : wl_dep) +``` + +## Methods + +### find_protocol + +```meson +xml = wl_mod.find_protocol( + 'xdg-decoration', + state : 'unstable', + version : 1, +) +``` +This function requires one positional argument: the protocol base name. +- `state` Optional arg that specifies the current state of the protocol. +Either stable, staging, or unstable. +The default is stable. +- `version` The backwards incompatible version number. +Required for staging or unstable. An error is raised for stable. + +### scan_xml +```meson +generated = wl_mod.scan_xml( + 'my-protocol.xml', + side : 'client', + scope : 'private', +) +``` +This function accepts one or more arguments of either string or file type. + +- `side` Optional arg that specifies if client or server side code is generated. +The default is client side. +- `scope` Optional arg that specifies the scope of the generated code. +Either public or private. +The default is private. + + +## Links +- [Official Wayland Documentation](https://wayland.freedesktop.org/docs/html/) +- [Wayland GitLab](https://gitlab.freedesktop.org/wayland) +- [Wayland Book](https://wayland-book.com/) diff --git a/docs/markdown/_Sidebar.md b/docs/markdown/_Sidebar.md index 0ca1762..ce73d5a 100644 --- a/docs/markdown/_Sidebar.md +++ b/docs/markdown/_Sidebar.md @@ -13,3 +13,4 @@ * [i18n](i18n-module.md) * [pkgconfig](Pkgconfig-module.md) * [rust](Rust-module.md) +* [wayland](Wayland-module.md) diff --git a/docs/markdown/snippets/cmake_configure_package_config_dict.md b/docs/markdown/snippets/cmake_configure_package_config_dict.md new file mode 100644 index 0000000..253a887 --- /dev/null +++ b/docs/markdown/snippets/cmake_configure_package_config_dict.md @@ -0,0 +1,5 @@ +## cmake.configure_package_config_file can now take a dict + +The `configuration` kwarg of the `configure_package_config_file()` function +from the `cmake` module can now take a dict object, just like the regular +`configure_file()` function. diff --git a/docs/markdown/snippets/python_module_env.md b/docs/markdown/snippets/python_module_env.md new file mode 100644 index 0000000..87a156d --- /dev/null +++ b/docs/markdown/snippets/python_module_env.md @@ -0,0 +1,9 @@ +## New option to choose python installation environment + +It is now possible to specify `-Dpython.install_env` and choose how python modules are installed. + +- `venv`: assume that a virtualenv is active and install to that +- `system`: install to the global site-packages of the selected interpreter + (the one that the venv module calls --system-site-packages) +- `prefix`: preserve existing behavior +- `auto`: autodetect whether to use venv or system diff --git a/docs/markdown/snippets/wayland-module.md b/docs/markdown/snippets/wayland-module.md new file mode 100644 index 0000000..cd5e5dc --- /dev/null +++ b/docs/markdown/snippets/wayland-module.md @@ -0,0 +1,4 @@ +## New unstable wayland module + +This module can search for protocol xml files from the wayland-protocols +package, and generate .c and .h files using wayland-scanner. diff --git a/docs/sitemap.txt b/docs/sitemap.txt index 82e0a7b..11b64e0 100644 --- a/docs/sitemap.txt +++ b/docs/sitemap.txt @@ -58,6 +58,7 @@ index.md SourceSet-module.md Windows-module.md i18n-module.md + Wayland-module.md Java.md Vala.md D.md diff --git a/docs/theme/extra/templates/navbar_links.html b/docs/theme/extra/templates/navbar_links.html index c518de5..65a21a2 100644 --- a/docs/theme/extra/templates/navbar_links.html +++ b/docs/theme/extra/templates/navbar_links.html @@ -26,6 +26,7 @@ ("Rust-module.html","Rust"), \ ("Simd-module.html","Simd"), \ ("SourceSet-module.html","SourceSet"), \ + ("Wayland-module.html","Wayland"), \ ("Windows-module.html","Windows")]: <li> <a href="@tup[0]">@tup[1]</a> diff --git a/docs/yaml/functions/summary.yaml b/docs/yaml/functions/summary.yaml index 3e7d463..29bff0b 100644 --- a/docs/yaml/functions/summary.yaml +++ b/docs/yaml/functions/summary.yaml @@ -10,7 +10,7 @@ description: | the section keyword argument is omitted, those key/value pairs are implicitly grouped into a section with no title. key/value pairs can optionally be grouped into a dictionary, but keep in mind that - dictionaries does not guarantee ordering. `key` must be string, + dictionaries do not guarantee ordering. `key` must be string, `value` can be: - an integer, boolean or string @@ -62,10 +62,33 @@ example: | ``` posargs: - key: - type: str - description: The name of the new entry + key_or_dict: + type: str | dict[str | bool | int | dep | external_program | list[str | bool | int | dep | external_program]] + description: | + The name of the new entry, or a dict containing multiple entries. If a + dict is passed it is equivalent to calling summary() once for each + key-value pair. Keep in mind that dictionaries do not guarantee + ordering. +optargs: value: type: str | bool | int | dep | external_program | list[str | bool | int | dep | external_program] - description: The value to print for the `key` + description: | + The value to print for the `key`. Only valid if `key_or_dict` is a str. + +kwargs: + bool_yn: + type: bool + default: false + description: Convert bool values to yes and no + section: + type: str + description: The section to put this summary information under. If the + section keyword argument is omitted, key/value pairs are implicitly + grouped into a section with no title. + list_sep: + type: str + since: 0.54.0 + description: | + The separator to use when printing list values in this summary. If no + separator is given, each list item will be printed on its own line. diff --git a/mesonbuild/coredata.py b/mesonbuild/coredata.py index 4a84467..e5c0287 100644 --- a/mesonbuild/coredata.py +++ b/mesonbuild/coredata.py @@ -1218,6 +1218,8 @@ BUILTIN_CORE_OPTIONS: 'KeyedOptionDictType' = OrderedDict([ (OptionKey('force_fallback_for'), BuiltinOption(UserArrayOption, 'Force fallback for those subprojects', [])), # Python module + (OptionKey('install_env', module='python'), + BuiltinOption(UserComboOption, 'Which python environment to install to', 'prefix', choices=['auto', 'prefix', 'system', 'venv'])), (OptionKey('platlibdir', module='python'), BuiltinOption(UserStringOption, 'Directory for site-specific, platform-specific files.', '')), (OptionKey('purelibdir', module='python'), diff --git a/mesonbuild/mdevenv.py b/mesonbuild/mdevenv.py index eb05020..3c522a6 100644 --- a/mesonbuild/mdevenv.py +++ b/mesonbuild/mdevenv.py @@ -48,7 +48,11 @@ def run(options: argparse.Namespace) -> int: args = options.command if not args: prompt_prefix = f'[{b.project_name}]' - if is_windows(): + shell_env = os.environ.get("SHELL") + # Prefer $SHELL in a MSYS2 bash despite it being Windows + if shell_env and os.path.exists(shell_env): + args = [shell_env] + elif is_windows(): shell = get_windows_shell() if shell == 'powershell.exe': args = ['powershell.exe'] @@ -62,9 +66,7 @@ def run(options: argparse.Namespace) -> int: args = [os.environ.get("SHELL", os.path.realpath("/bin/sh"))] if "bash" in args[0] and not os.environ.get("MESON_DISABLE_PS1_OVERRIDE"): tmprc = tempfile.NamedTemporaryFile(mode='w') - bashrc = os.path.expanduser('~/.bashrc') - if os.path.exists(bashrc): - tmprc.write(f'. {bashrc}\n') + tmprc.write('[ -e ~/.bashrc ] && . ~/.bashrc\n') tmprc.write(f'export PS1="{prompt_prefix} $PS1"') tmprc.flush() # Let the GC remove the tmp file diff --git a/mesonbuild/mesonlib/universal.py b/mesonbuild/mesonlib/universal.py index e86fb99..1bbc2c6 100644 --- a/mesonbuild/mesonlib/universal.py +++ b/mesonbuild/mesonlib/universal.py @@ -245,7 +245,7 @@ def is_ascii_string(astring: T.Union[str, bytes]) -> bool: return True -def check_direntry_issues(direntry_array: T.Union[T.List[T.Union[str, bytes]], str, bytes]) -> None: +def check_direntry_issues(direntry_array: T.Union[T.Iterable[T.Union[str, bytes]], str, bytes]) -> None: import locale # Warn if the locale is not UTF-8. This can cause various unfixable issues # such as os.stat not being able to decode filenames with unicode in them. @@ -253,7 +253,7 @@ def check_direntry_issues(direntry_array: T.Union[T.List[T.Union[str, bytes]], s # encoding, so we can just warn about it. e = locale.getpreferredencoding() if e.upper() != 'UTF-8' and not is_windows(): - if not isinstance(direntry_array, list): + if isinstance(direntry_array, (str, bytes)): direntry_array = [direntry_array] for de in direntry_array: if is_ascii_string(de): diff --git a/mesonbuild/modules/__init__.py b/mesonbuild/modules/__init__.py index 73e22ff..85bec0b 100644 --- a/mesonbuild/modules/__init__.py +++ b/mesonbuild/modules/__init__.py @@ -80,8 +80,10 @@ class ModuleState: def find_program(self, prog: T.Union[str, T.List[str]], required: bool = True, version_func: T.Optional[T.Callable[['ExternalProgram'], str]] = None, - wanted: T.Optional[str] = None, silent: bool = False) -> 'ExternalProgram': - return self._interpreter.find_program_impl(prog, required=required, version_func=version_func, wanted=wanted, silent=silent) + wanted: T.Optional[str] = None, silent: bool = False, + for_machine: MachineChoice = MachineChoice.HOST) -> 'ExternalProgram': + return self._interpreter.find_program_impl(prog, required=required, version_func=version_func, + wanted=wanted, silent=silent, for_machine=for_machine) def test(self, args: T.Tuple[str, T.Union[build.Executable, build.Jar, 'ExternalProgram', mesonlib.File]], workdir: T.Optional[str] = None, @@ -103,6 +105,13 @@ class ModuleState: module: T.Optional[str] = None) -> T.Union[str, int, bool, 'WrapMode']: return self.environment.coredata.get_option(mesonlib.OptionKey(name, subproject, machine, lang, module)) + def is_user_defined_option(self, name: str, subproject: str = '', + machine: MachineChoice = MachineChoice.HOST, + lang: T.Optional[str] = None, + module: T.Optional[str] = None) -> bool: + key = mesonlib.OptionKey(name, subproject, machine, lang, module) + return key in self._interpreter.user_defined_options.cmd_line_options + class ModuleObject(HoldableObject): """Base class for all objects returned by modules diff --git a/mesonbuild/modules/cmake.py b/mesonbuild/modules/cmake.py index 73882c7..b046371 100644 --- a/mesonbuild/modules/cmake.py +++ b/mesonbuild/modules/cmake.py @@ -37,10 +37,13 @@ from ..interpreterbase import ( typed_kwargs, KwargInfo, + ContainerTypeInfo, ) if T.TYPE_CHECKING: - class WriteBasicPackageVersionFile(T.TypedDict): + from typing_extensions import TypedDict + + class WriteBasicPackageVersionFile(TypedDict): arch_independent: bool compatibility: str @@ -48,6 +51,13 @@ if T.TYPE_CHECKING: name: str version: str + class ConfigurePackageConfigFile(TypedDict): + + configuration: T.Union[build.ConfigurationData, dict] + input: T.Union[str, mesonlib.File] + install_dir: T.Optional[str] + name: str + COMPATIBILITIES = ['AnyNewerVersion', 'SameMajorVersion', 'SameMinorVersion', 'ExactVersion'] # Taken from https://github.com/Kitware/CMake/blob/master/Modules/CMakePackageConfigHelpers.cmake @@ -334,42 +344,37 @@ class CmakeModule(ExtensionModule): shutil.copymode(infile, outfile_tmp) mesonlib.replace_if_different(outfile, outfile_tmp) - @permittedKwargs({'input', 'name', 'install_dir', 'configuration'}) - def configure_package_config_file(self, state, args, kwargs): - if args: - raise mesonlib.MesonException('configure_package_config_file takes only keyword arguments.') - - if 'input' not in kwargs: - raise mesonlib.MesonException('configure_package_config_file requires "input" keyword.') + @noPosargs + @typed_kwargs( + 'cmake.configure_package_config_file', + KwargInfo('configuration', (build.ConfigurationData, dict), required=True), + KwargInfo('input', + (str, mesonlib.File, ContainerTypeInfo(list, mesonlib.File)), required=True, + validator=lambda x: 'requires exactly one file' if isinstance(x, list) and len(x) != 1 else None, + convertor=lambda x: x[0] if isinstance(x, list) else x), + KwargInfo('install_dir', (str, NoneType), default=None), + KwargInfo('name', str, required=True), + ) + def configure_package_config_file(self, state, args, kwargs: 'ConfigurePackageConfigFile'): inputfile = kwargs['input'] - if isinstance(inputfile, list): - if len(inputfile) != 1: - m = "Keyword argument 'input' requires exactly one file" - raise mesonlib.MesonException(m) - inputfile = inputfile[0] - if not isinstance(inputfile, (str, mesonlib.File)): - raise mesonlib.MesonException("input must be a string or a file") if isinstance(inputfile, str): inputfile = mesonlib.File.from_source_file(state.environment.source_dir, state.subdir, inputfile) ifile_abs = inputfile.absolute_path(state.environment.source_dir, state.environment.build_dir) - if 'name' not in kwargs: - raise mesonlib.MesonException('"name" not specified.') name = kwargs['name'] (ofile_path, ofile_fname) = os.path.split(os.path.join(state.subdir, f'{name}Config.cmake')) ofile_abs = os.path.join(state.environment.build_dir, ofile_path, ofile_fname) - install_dir = kwargs.get('install_dir', os.path.join(state.environment.coredata.get_option(mesonlib.OptionKey('libdir')), 'cmake', name)) - if not isinstance(install_dir, str): - raise mesonlib.MesonException('"install_dir" must be a string.') + install_dir = kwargs['install_dir'] + if install_dir is None: + install_dir = os.path.join(state.environment.coredata.get_option(mesonlib.OptionKey('libdir')), 'cmake', name) - if 'configuration' not in kwargs: - raise mesonlib.MesonException('"configuration" not specified.') conf = kwargs['configuration'] - if not isinstance(conf, build.ConfigurationData): - raise mesonlib.MesonException('Argument "configuration" is not of type configuration_data') + if isinstance(conf, dict): + FeatureNew.single_use('cmake.configure_package_config_file dict as configuration', '0.62.0', state.subproject, location=state.current_node) + conf = build.ConfigurationData(conf) prefix = state.environment.coredata.get_option(mesonlib.OptionKey('prefix')) abs_install_dir = install_dir diff --git a/mesonbuild/modules/python.py b/mesonbuild/modules/python.py index 2b62ea3..a91ec58 100644 --- a/mesonbuild/modules/python.py +++ b/mesonbuild/modules/python.py @@ -338,11 +338,13 @@ variables.update({'base_prefix': getattr(sys, 'base_prefix', sys.prefix)}) print(json.dumps({ 'variables': variables, 'paths': paths, + 'sysconfig_paths': sysconfig.get_paths(), 'install_paths': install_paths, 'sys_paths': sys.path, 'version': sysconfig.get_python_version(), 'platform': sysconfig.get_platform(), 'is_pypy': '__pypy__' in sys.builtin_module_names, + 'is_venv': sys.prefix != variables['base_prefix'], 'link_libpython': links_against_libpython(), })) ''' @@ -352,7 +354,9 @@ if T.TYPE_CHECKING: install_paths: T.Dict[str, str] is_pypy: bool + is_venv: bool link_libpython: bool + sysconfig_paths: T.Dict[str, str] paths: T.Dict[str, str] platform: str suffix: str @@ -377,7 +381,9 @@ class PythonExternalProgram(ExternalProgram): self.info: 'PythonIntrospectionDict' = { 'install_paths': {}, 'is_pypy': False, + 'is_venv': False, 'link_libpython': False, + 'sysconfig_paths': {}, 'paths': {}, 'platform': 'sentinal', 'variables': {}, @@ -422,7 +428,22 @@ class PythonExternalProgram(ExternalProgram): return rel_path value = state.get_option(f'{key}dir', module='python') if value: + if state.is_user_defined_option('install_env', module='python'): + raise mesonlib.MesonException(f'python.{key}dir and python.install_env are mutually exclusive') return value + + install_env = state.get_option('install_env', module='python') + if install_env == 'auto': + install_env = 'venv' if self.info['is_venv'] else 'system' + + if install_env == 'system': + rel_path = os.path.join(self.info['variables']['prefix'], rel_path) + elif install_env == 'venv': + if not self.info['is_venv']: + raise mesonlib.MesonException('python.install_env cannot be set to "venv" unless you are in a venv!') + # inside a venv, deb_system is *never* active hence info['paths'] may be wrong + rel_path = self.info['sysconfig_paths'][key] + # Use python's path relative to prefix, and warn if that's not a location # python will lookup for modules. abs_path = Path(state.get_option('prefix'), rel_path) diff --git a/mesonbuild/modules/sourceset.py b/mesonbuild/modules/sourceset.py index 515e670..dd78c45 100644 --- a/mesonbuild/modules/sourceset.py +++ b/mesonbuild/modules/sourceset.py @@ -12,23 +12,76 @@ # See the License for the specific language governing permissions and # limitations under the License. -from collections import namedtuple -from .. import mesonlib -from .. import build -from ..mesonlib import listify, OrderedSet +from __future__ import annotations +import typing as T + from . import ExtensionModule, ModuleObject, MutableModuleObject +from .. import build +from .. import dependencies +from .. import mesonlib from ..interpreterbase import ( - noPosargs, noKwargs, permittedKwargs, + noPosargs, noKwargs, InterpreterException, InvalidArguments, InvalidCode, FeatureNew, ) +from ..interpreterbase.decorators import ContainerTypeInfo, KwargInfo, typed_kwargs, typed_pos_args +from ..mesonlib import OrderedSet + +if T.TYPE_CHECKING: + from typing_extensions import TypedDict + + from . import ModuleState + from ..interpreter import Interpreter + from ..interpreterbase import TYPE_var, TYPE_kwargs + + class AddKwargs(TypedDict): -SourceSetRule = namedtuple('SourceSetRule', 'keys sources if_false sourcesets dependencies extra_deps') -SourceFiles = namedtuple('SourceFiles', 'sources dependencies') + when: T.List[T.Union[str, dependencies.Dependency]] + if_true: T.List[T.Union[mesonlib.FileOrString, build.GeneratedTypes, dependencies.Dependency]] + if_false: T.List[T.Union[mesonlib.FileOrString, build.GeneratedTypes]] + + class AddAllKw(TypedDict): + + when: T.List[T.Union[str, dependencies.Dependency]] + if_true: T.List[SourceSetImpl] + + class ApplyKw(TypedDict): + + strict: bool + + +_WHEN_KW: KwargInfo[T.List[T.Union[str, dependencies.Dependency]]] = KwargInfo( + 'when', + ContainerTypeInfo(list, (str, dependencies.Dependency)), + listify=True, + default=[], +) -class SourceSet(MutableModuleObject): - def __init__(self, interpreter): + +class SourceSetRule(T.NamedTuple): + keys: T.List[str] + sources: T.Any + if_false: T.Any + sourcesets: T.List[SourceSetImpl] + dependencies: T.List[dependencies.Dependency] + + +class SourceFiles(T.NamedTuple): + sources: OrderedSet[T.Union[mesonlib.FileOrString, build.GeneratedTypes]] + dependencies: OrderedSet[dependencies.Dependency] + + +class SourceSet: + """Base class to avoid circular references. + + Because of error messages, this class is called SourceSet, and the actual + implementation is an Impl. + """ + + +class SourceSetImpl(SourceSet, MutableModuleObject): + def __init__(self, interpreter: Interpreter): super().__init__() - self.rules = [] + self.rules: T.List[SourceSetRule] = [] self.subproject = interpreter.subproject self.environment = interpreter.environment self.subdir = interpreter.subdir @@ -41,71 +94,104 @@ class SourceSet(MutableModuleObject): 'apply': self.apply_method, }) - def check_source_files(self, arg, allow_deps): - sources = [] - deps = [] - for x in arg: - if isinstance(x, (str, mesonlib.File, - build.GeneratedList, build.CustomTarget, - build.CustomTargetIndex)): - sources.append(x) - elif hasattr(x, 'found'): - if not allow_deps: - msg = 'Dependencies are not allowed in the if_false argument.' - raise InvalidArguments(msg) + def check_source_files(self, args: T.Sequence[T.Union[mesonlib.FileOrString, build.GeneratedTypes, dependencies.Dependency]], + ) -> T.Tuple[T.List[T.Union[mesonlib.FileOrString, build.GeneratedTypes]], T.List[dependencies.Dependency]]: + sources: T.List[T.Union[mesonlib.FileOrString, build.GeneratedTypes]] = [] + deps: T.List[dependencies.Dependency] = [] + for x in args: + if isinstance(x, dependencies.Dependency): deps.append(x) else: - msg = 'Sources must be strings or file-like objects.' - raise InvalidArguments(msg) - mesonlib.check_direntry_issues(sources) + sources.append(x) + to_check: T.List[str] = [] + + # Get the actual output names to check + for s in sources: + if isinstance(s, str): + to_check.append(s) + elif isinstance(s, mesonlib.File): + to_check.append(s.fname) + else: + to_check.extend(s.get_outputs()) + mesonlib.check_direntry_issues(to_check) return sources, deps - def check_conditions(self, arg): - keys = [] - deps = [] - for x in listify(arg): + def check_conditions(self, args: T.Sequence[T.Union[str, dependencies.Dependency]] + ) -> T.Tuple[T.List[str], T.List[dependencies.Dependency]]: + keys: T.List[str] = [] + deps: T.List[dependencies.Dependency] = [] + for x in args: if isinstance(x, str): keys.append(x) - elif hasattr(x, 'found'): - deps.append(x) else: - raise InvalidArguments('Conditions must be strings or dependency object') + deps.append(x) return keys, deps - @permittedKwargs(['when', 'if_false', 'if_true']) - def add_method(self, state, args, kwargs): + @typed_pos_args('sourceset.add', varargs=(str, mesonlib.File, build.GeneratedList, build.CustomTarget, build.CustomTargetIndex, dependencies.Dependency)) + @typed_kwargs( + 'sourceset.add', + _WHEN_KW, + KwargInfo( + 'if_true', + ContainerTypeInfo(list, (str, mesonlib.File, build.GeneratedList, build.CustomTarget, build.CustomTargetIndex, dependencies.Dependency)), + listify=True, + default=[], + ), + KwargInfo( + 'if_false', + ContainerTypeInfo(list, (str, mesonlib.File, build.GeneratedList, build.CustomTarget, build.CustomTargetIndex)), + listify=True, + default=[], + ), + ) + def add_method(self, state: ModuleState, + args: T.Tuple[T.List[T.Union[mesonlib.FileOrString, build.GeneratedTypes, dependencies.Dependency]]], + kwargs: AddKwargs) -> None: if self.frozen: raise InvalidCode('Tried to use \'add\' after querying the source set') - when = listify(kwargs.get('when', [])) - if_true = listify(kwargs.get('if_true', [])) - if_false = listify(kwargs.get('if_false', [])) - if not when and not if_true and not if_false: - if_true = args - elif args: + when = kwargs['when'] + if_true = kwargs['if_true'] + if_false = kwargs['if_false'] + if not any([when, if_true, if_false]): + if_true = args[0] + elif args[0]: raise InterpreterException('add called with both positional and keyword arguments') keys, dependencies = self.check_conditions(when) - sources, extra_deps = self.check_source_files(if_true, True) - if_false, _ = self.check_source_files(if_false, False) - self.rules.append(SourceSetRule(keys, sources, if_false, [], dependencies, extra_deps)) + sources, extra_deps = self.check_source_files(if_true) + if_false, _ = self.check_source_files(if_false) + self.rules.append(SourceSetRule(keys, sources, if_false, [], dependencies + extra_deps)) - @permittedKwargs(['when', 'if_true']) - def add_all_method(self, state, args, kwargs): + @typed_pos_args('sourceset.add_all', varargs=SourceSet) + @typed_kwargs( + 'sourceset.add_all', + _WHEN_KW, + KwargInfo( + 'if_true', + ContainerTypeInfo(list, SourceSet), + listify=True, + default=[], + ) + ) + def add_all_method(self, state: ModuleState, args: T.Tuple[T.List[SourceSetImpl]], + kwargs: AddAllKw) -> None: if self.frozen: raise InvalidCode('Tried to use \'add_all\' after querying the source set') - when = listify(kwargs.get('when', [])) - if_true = listify(kwargs.get('if_true', [])) + when = kwargs['when'] + if_true = kwargs['if_true'] if not when and not if_true: - if_true = args - elif args: + if_true = args[0] + elif args[0]: raise InterpreterException('add_all called with both positional and keyword arguments') keys, dependencies = self.check_conditions(when) for s in if_true: - if not isinstance(s, SourceSet): + if not isinstance(s, SourceSetImpl): raise InvalidCode('Arguments to \'add_all\' after the first must be source sets') - s.frozen = True - self.rules.append(SourceSetRule(keys, [], [], if_true, dependencies, [])) + s.frozen = True + self.rules.append(SourceSetRule(keys, [], [], if_true, dependencies)) - def collect(self, enabled_fn, all_sources, into=None): + def collect(self, enabled_fn: T.Callable[[str], bool], + all_sources: bool, + into: T.Optional['SourceFiles'] = None) -> SourceFiles: if not into: into = SourceFiles(OrderedSet(), OrderedSet()) for entry in self.rules: @@ -113,7 +199,6 @@ class SourceSet(MutableModuleObject): all(enabled_fn(key) for key in entry.keys): into.sources.update(entry.sources) into.dependencies.update(entry.dependencies) - into.dependencies.update(entry.extra_deps) for ss in entry.sourcesets: ss.collect(enabled_fn, all_sources, into) if not all_sources: @@ -123,7 +208,8 @@ class SourceSet(MutableModuleObject): @noKwargs @noPosargs - def all_sources_method(self, state, args, kwargs): + def all_sources_method(self, state: ModuleState, args: T.List[TYPE_var], kwargs: TYPE_kwargs + ) -> T.List[T.Union[mesonlib.FileOrString, build.GeneratedTypes]]: self.frozen = True files = self.collect(lambda x: True, True) return list(files.sources) @@ -131,31 +217,32 @@ class SourceSet(MutableModuleObject): @noKwargs @noPosargs @FeatureNew('source_set.all_dependencies() method', '0.52.0') - def all_dependencies_method(self, state, args, kwargs): + def all_dependencies_method(self, state: ModuleState, args: T.List[TYPE_var], kwargs: TYPE_kwargs + ) -> T.List[dependencies.Dependency]: self.frozen = True files = self.collect(lambda x: True, True) return list(files.dependencies) - @permittedKwargs(['strict']) - def apply_method(self, state, args, kwargs): - if len(args) != 1: - raise InterpreterException('Apply takes exactly one argument') + @typed_pos_args('sourceset.apply', (build.ConfigurationData, dict)) + @typed_kwargs('sourceset.apply', KwargInfo('strict', bool, default=True)) + def apply_method(self, state: ModuleState, args: T.Tuple[T.Union[build.ConfigurationData, T.Dict[str, TYPE_var]]], kwargs: ApplyKw) -> SourceFilesObject: config_data = args[0] self.frozen = True - strict = kwargs.get('strict', True) + strict = kwargs['strict'] if isinstance(config_data, dict): - def _get_from_config_data(key): + def _get_from_config_data(key: str) -> bool: + assert isinstance(config_data, dict), 'for mypy' if strict and key not in config_data: raise InterpreterException(f'Entry {key} not in configuration dictionary.') - return config_data.get(key, False) + return bool(config_data.get(key, False)) else: - config_cache = dict() + config_cache: T.Dict[str, bool] = {} - def _get_from_config_data(key): - nonlocal config_cache + def _get_from_config_data(key: str) -> bool: + assert isinstance(config_data, build.ConfigurationData), 'for mypy' if key not in config_cache: if key in config_data: - config_cache[key] = config_data.get(key)[0] + config_cache[key] = bool(config_data.get(key)[0]) elif strict: raise InvalidArguments(f'sourceset.apply: key "{key}" not in passed configuration, and strict set.') else: @@ -167,7 +254,7 @@ class SourceSet(MutableModuleObject): return res class SourceFilesObject(ModuleObject): - def __init__(self, files): + def __init__(self, files: SourceFiles): super().__init__() self.files = files self.methods.update({ @@ -177,26 +264,28 @@ class SourceFilesObject(ModuleObject): @noPosargs @noKwargs - def sources_method(self, state, args, kwargs): + def sources_method(self, state: ModuleState, args: T.List[TYPE_var], kwargs: TYPE_kwargs + ) -> T.List[T.Union[mesonlib.FileOrString, build.GeneratedTypes]]: return list(self.files.sources) @noPosargs @noKwargs - def dependencies_method(self, state, args, kwargs): + def dependencies_method(self, state: ModuleState, args: T.List[TYPE_var], kwargs: TYPE_kwargs + ) -> T.List[dependencies.Dependency]: return list(self.files.dependencies) class SourceSetModule(ExtensionModule): @FeatureNew('SourceSet module', '0.51.0') - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, interpreter: Interpreter): + super().__init__(interpreter) self.methods.update({ 'source_set': self.source_set, }) @noKwargs @noPosargs - def source_set(self, state, args, kwargs): - return SourceSet(self.interpreter) + def source_set(self, state: ModuleState, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> SourceSetImpl: + return SourceSetImpl(self.interpreter) -def initialize(*args, **kwargs): - return SourceSetModule(*args, **kwargs) +def initialize(interp: Interpreter) -> SourceSetModule: + return SourceSetModule(interp) diff --git a/mesonbuild/modules/unstable_wayland.py b/mesonbuild/modules/unstable_wayland.py new file mode 100644 index 0000000..85da2b7 --- /dev/null +++ b/mesonbuild/modules/unstable_wayland.py @@ -0,0 +1,120 @@ +# Copyright 2022 Mark Bolhuis <mark@bolhuis.dev> + +# 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. + +import os + +from . import ExtensionModule, ModuleReturnValue +from ..build import CustomTarget +from ..interpreter.type_checking import NoneType, in_set_validator +from ..interpreterbase import FeatureNew, typed_pos_args, typed_kwargs, KwargInfo +from ..mesonlib import File, MesonException, MachineChoice + + +class WaylandModule(ExtensionModule): + + @FeatureNew('wayland module', '0.62.0') + def __init__(self, interpreter): + super().__init__(interpreter) + + self.protocols_dep = None + self.pkgdatadir = None + self.scanner_bin = None + + self.methods.update({ + 'scan_xml': self.scan_xml, + 'find_protocol': self.find_protocol, + }) + + @typed_pos_args('wayland.scan_xml', varargs=(str, File), min_varargs=1) + @typed_kwargs( + 'wayland.scan_xml', + KwargInfo('side', str, default='client', validator=in_set_validator({'client', 'server'})), + KwargInfo('scope', str, default='private', validator=in_set_validator({'private', 'public'})), + ) + def scan_xml(self, state, args, kwargs): + if self.scanner_bin is None: + self.scanner_bin = state.find_program('wayland-scanner', for_machine=MachineChoice.BUILD) + + scope = kwargs['scope'] + side = kwargs['side'] + + xml_files = self.interpreter.source_strings_to_files(args[0]) + targets = [] + for xml_file in xml_files: + name = os.path.splitext(os.path.basename(xml_file.fname))[0] + + code = CustomTarget( + f'{name}-protocol', + state.subdir, + state.subproject, + [self.scanner_bin, f'{scope}-code', '@INPUT@', '@OUTPUT@'], + [xml_file], + [f'{name}-protocol.c'], + backend=state.backend, + ) + targets.append(code) + + header = CustomTarget( + f'{name}-{side}-protocol', + state.subdir, + state.subproject, + [self.scanner_bin, f'{side}-header', '@INPUT@', '@OUTPUT@'], + [xml_file], + [f'{name}-{side}-protocol.h'], + backend=state.backend, + ) + targets.append(header) + + return ModuleReturnValue(targets, targets) + + @typed_pos_args('wayland.find_protocol', str) + @typed_kwargs( + 'wayland.find_protocol', + KwargInfo('state', str, default='stable', validator=in_set_validator({'stable', 'staging', 'unstable'})), + KwargInfo('version', (int, NoneType)), + ) + def find_protocol(self, state, args, kwargs): + base_name = args[0] + xml_state = kwargs['state'] + version = kwargs['version'] + + if xml_state != 'stable' and version is None: + raise MesonException(f'{xml_state} protocols require a version number.') + + if xml_state == 'stable' and version is not None: + raise MesonException('stable protocols do not require a version number.') + + if self.protocols_dep is None: + self.protocols_dep = self.interpreter.func_dependency(state.current_node, ['wayland-protocols'], {}) + + if self.pkgdatadir is None: + self.pkgdatadir = self.protocols_dep.get_variable(pkgconfig='pkgdatadir', internal='pkgdatadir') + + if xml_state == 'stable': + xml_name = f'{base_name}.xml' + elif xml_state == 'staging': + xml_name = f'{base_name}-v{version}.xml' + else: + xml_name = f'{base_name}-unstable-v{version}.xml' + + path = os.path.join(self.pkgdatadir, xml_state, base_name, xml_name) + + if not os.path.exists(path): + raise MesonException(f'The file {path} does not exist.') + + return File.from_absolute_file(path) + + +def initialize(interpreter): + return WaylandModule(interpreter) diff --git a/mesonbuild/mtest.py b/mesonbuild/mtest.py index c844645..02b0cba 100644 --- a/mesonbuild/mtest.py +++ b/mesonbuild/mtest.py @@ -1348,9 +1348,12 @@ class SingleTestRunner: self.console_mode = ConsoleUser.LOGGER def _get_test_cmd(self) -> T.Optional[T.List[str]]: - if self.test.fname[0].endswith('.jar'): + testentry = self.test.fname[0] + if self.options.no_rebuild and not os.path.isfile(testentry): + raise TestException(f'The test program {testentry!r} does not exist. Cannot run tests before building them.') + if testentry.endswith('.jar'): return ['java', '-jar'] + self.test.fname - elif not self.test.is_cross_built and run_with_mono(self.test.fname[0]): + elif not self.test.is_cross_built and run_with_mono(testentry): return ['mono'] + self.test.fname elif self.test.cmd_is_built and self.test.is_cross_built and self.test.needs_exe_wrapper: if self.test.exe_wrapper is None: diff --git a/run_mypy.py b/run_mypy.py index 00d490b..2d7232b 100755 --- a/run_mypy.py +++ b/run_mypy.py @@ -46,6 +46,7 @@ modules = [ 'mesonbuild/modules/java.py', 'mesonbuild/modules/keyval.py', 'mesonbuild/modules/qt.py', + 'mesonbuild/modules/sourceset.py', 'mesonbuild/modules/unstable_external_project.py', 'mesonbuild/modules/unstable_rust.py', 'mesonbuild/modules/windows.py', diff --git a/run_project_tests.py b/run_project_tests.py index 926f4ef..ea8f901 100755 --- a/run_project_tests.py +++ b/run_project_tests.py @@ -84,7 +84,7 @@ if T.TYPE_CHECKING: ALL_TESTS = ['cmake', 'common', 'native', 'warning-meson', 'failing-meson', 'failing-build', 'failing-test', 'keyval', 'platform-osx', 'platform-windows', 'platform-linux', 'java', 'C#', 'vala', 'cython', 'rust', 'd', 'objective c', 'objective c++', - 'fortran', 'swift', 'cuda', 'python3', 'python', 'fpga', 'frameworks', 'nasm', 'wasm', + 'fortran', 'swift', 'cuda', 'python3', 'python', 'fpga', 'frameworks', 'nasm', 'wasm', 'wayland' ] @@ -1033,6 +1033,13 @@ def should_skip_rust(backend: Backend) -> bool: return True return False +def should_skip_wayland() -> bool: + if mesonlib.is_windows() or mesonlib.is_osx(): + return True + if not shutil.which('wayland-scanner'): + return True + return False + def detect_tests_to_run(only: T.Dict[str, T.List[str]], use_tmp: bool) -> T.List[T.Tuple[str, T.List[TestDef], bool]]: """ Parameters @@ -1089,6 +1096,7 @@ def detect_tests_to_run(only: T.Dict[str, T.List[str]], use_tmp: bool) -> T.List TestCategory('frameworks', 'frameworks'), TestCategory('nasm', 'nasm'), TestCategory('wasm', 'wasm', shutil.which('emcc') is None or backend is not Backend.ninja), + TestCategory('wayland', 'wayland', should_skip_wayland()), ] categories = [t.category for t in all_tests] diff --git a/test cases/cmake/20 cmake file/meson.build b/test cases/cmake/20 cmake file/meson.build index 758bbee..5c45d66 100644 --- a/test cases/cmake/20 cmake file/meson.build +++ b/test cases/cmake/20 cmake file/meson.build @@ -4,11 +4,9 @@ project( cmake = import('cmake') -cmake_conf = configuration_data() -cmake_conf.set_quoted('foo', 'bar') cmake.configure_package_config_file( name : 'foolib', input : 'foolib.cmake.in', install_dir : get_option('libdir') / 'cmake', - configuration : cmake_conf, + configuration : {'foo': '"bar"'}, ) diff --git a/test cases/vala/14 target glib version and gresources/test.vala b/test cases/vala/14 target glib version and gresources/test.vala index 79ef47d..f10bbe4 100644 --- a/test cases/vala/14 target glib version and gresources/test.vala +++ b/test cases/vala/14 target glib version and gresources/test.vala @@ -9,7 +9,7 @@ public class TestWidget : Box { } [GtkChild] - private Entry entry; + private unowned Entry entry; public TestWidget (string text) { this.text = text; diff --git a/test cases/wayland/1 client/main.c b/test cases/wayland/1 client/main.c new file mode 100644 index 0000000..6aca80d --- /dev/null +++ b/test cases/wayland/1 client/main.c @@ -0,0 +1,9 @@ +#include "xdg-shell-client-protocol.h" + +int main() { +#ifdef XDG_SHELL_CLIENT_PROTOCOL_H + return 0; +#else + return 1; +#endif +} diff --git a/test cases/wayland/1 client/meson.build b/test cases/wayland/1 client/meson.build new file mode 100644 index 0000000..7ca868b --- /dev/null +++ b/test cases/wayland/1 client/meson.build @@ -0,0 +1,16 @@ +project('wayland-test-client', 'c') + +wl_protocols_dep = dependency('wayland-protocols', required : false) +if not wl_protocols_dep.found() + error('MESON_SKIP_TEST: wayland-protocols not installed') +endif + +wl_dep = dependency('wayland-client') +wl_mod = import('unstable-wayland') + +xdg_shell_xml = wl_mod.find_protocol('xdg-shell') +xdg_shell = wl_mod.scan_xml(xdg_shell_xml) + +exe = executable('client', 'main.c', xdg_shell, dependencies : wl_dep) + +test('client', exe) diff --git a/test cases/wayland/2 server/main.c b/test cases/wayland/2 server/main.c new file mode 100644 index 0000000..3307499 --- /dev/null +++ b/test cases/wayland/2 server/main.c @@ -0,0 +1,9 @@ +#include "xdg-shell-server-protocol.h" + +int main() { +#ifdef XDG_SHELL_SERVER_PROTOCOL_H + return 0; +#else + return 1; +#endif +} diff --git a/test cases/wayland/2 server/meson.build b/test cases/wayland/2 server/meson.build new file mode 100644 index 0000000..c93ff11 --- /dev/null +++ b/test cases/wayland/2 server/meson.build @@ -0,0 +1,16 @@ +project('wayland-test-server', 'c') + +wl_protocols_dep = dependency('wayland-protocols', required : false) +if not wl_protocols_dep.found() + error('MESON_SKIP_TEST: wayland-protocols not installed') +endif + +wl_dep = dependency('wayland-server') +wl_mod = import('unstable-wayland') + +xdg_shell_xml = wl_mod.find_protocol('xdg-shell') +xdg_shell = wl_mod.scan_xml(xdg_shell_xml, side : 'server') + +exe = executable('server', 'main.c', xdg_shell, dependencies : wl_dep) + +test('client', exe) diff --git a/test cases/wayland/3 local/main.c b/test cases/wayland/3 local/main.c new file mode 100644 index 0000000..97bfa56 --- /dev/null +++ b/test cases/wayland/3 local/main.c @@ -0,0 +1,9 @@ +#include "test-client-protocol.h" + +int main() { +#ifdef TEST_CLIENT_PROTOCOL_H + return 0; +#else + return 1; +#endif +} diff --git a/test cases/wayland/3 local/meson.build b/test cases/wayland/3 local/meson.build new file mode 100644 index 0000000..7a470d6 --- /dev/null +++ b/test cases/wayland/3 local/meson.build @@ -0,0 +1,11 @@ +project('wayland-test-local', 'c') + +wl_dep = dependency('wayland-client') +wl_mod = import('unstable-wayland') + +xmls = files('test.xml') +gen = wl_mod.scan_xml(xmls) + +exe = executable('local', 'main.c', gen, dependencies : wl_dep) + +test('local', exe) diff --git a/test cases/wayland/3 local/test.xml b/test cases/wayland/3 local/test.xml new file mode 100644 index 0000000..f3c6db1 --- /dev/null +++ b/test cases/wayland/3 local/test.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<protocol name="test"> + <interface name="ext_test" version="1"> + <request name="destroy" type="destructor"/> + </interface> +</protocol> |