diff options
-rw-r--r-- | docs/markdown/Qt4-module.md | 2 | ||||
-rw-r--r-- | docs/markdown/Qt5-module.md | 92 | ||||
-rw-r--r-- | docs/markdown/Qt6-module.md | 8 | ||||
-rw-r--r-- | docs/markdown/_include_qt_base.md | 160 | ||||
-rw-r--r-- | docs/markdown/snippets/qt_preprocess_separate.md | 5 | ||||
-rw-r--r-- | docs/markdown/snippets/qt_preprocessed_varargs_deprecated.md | 33 | ||||
-rw-r--r-- | docs/sitemap.txt | 1 | ||||
-rw-r--r-- | mesonbuild/build.py | 137 | ||||
-rw-r--r-- | mesonbuild/interpreter/interpreter.py | 34 | ||||
-rw-r--r-- | mesonbuild/interpreter/interpreterobjects.py | 95 | ||||
-rw-r--r-- | mesonbuild/interpreter/kwargs.py | 35 | ||||
-rw-r--r-- | mesonbuild/modules/__init__.py | 8 | ||||
-rw-r--r-- | mesonbuild/modules/qt.py | 436 | ||||
-rw-r--r-- | mesonbuild/modules/unstable_rust.py | 2 | ||||
-rwxr-xr-x | run_mypy.py | 1 | ||||
-rwxr-xr-x | run_unittests.py | 2 | ||||
-rw-r--r-- | test cases/frameworks/4 qt/meson.build | 17 |
17 files changed, 708 insertions, 360 deletions
diff --git a/docs/markdown/Qt4-module.md b/docs/markdown/Qt4-module.md index 4be1be5..6b62415 100644 --- a/docs/markdown/Qt4-module.md +++ b/docs/markdown/Qt4-module.md @@ -2,3 +2,5 @@ This module provides support for Qt4's `moc`, `uic` and `rcc` tools. It is used identically to the [Qt 5 module](Qt5-module.md). + +{{ _include_qt_base.md }} diff --git a/docs/markdown/Qt5-module.md b/docs/markdown/Qt5-module.md index 32c34ea..8a5eb00 100644 --- a/docs/markdown/Qt5-module.md +++ b/docs/markdown/Qt5-module.md @@ -1,94 +1,6 @@ # Qt5 module The Qt5 module provides tools to automatically deal with the various -tools and steps required for Qt. The module has two methods. +tools and steps required for Qt. -## preprocess - -This method takes the following keyword arguments: - - `moc_headers`, `moc_sources`, `ui_files`, `qresources`, which define the files that require preprocessing with `moc`, `uic` and `rcc` - - `include_directories`, the directories to add to header search path for `moc` (optional) - - `moc_extra_arguments`, any additional arguments to `moc` (optional). Available since v0.44.0. - - `uic_extra_arguments`, any additional arguments to `uic` (optional). Available since v0.49.0. - - `rcc_extra_arguments`, any additional arguments to `rcc` (optional). Available since v0.49.0. - - `dependencies`, dependency objects needed by moc. Available since v0.48.0. - -It returns an opaque object that should be passed to a main build target. - -## compile_translations (since v0.44.0) - -This method generates the necessary targets to build translation files with lrelease, it takes the following keyword arguments: - - `ts_files`, the list of input translation files produced by Qt's lupdate tool. - - `install` when true, this target is installed during the install step (optional). - - `install_dir` directory to install to (optional). - - `build_by_default` when set to true, to have this target be built by default, that is, when invoking `meson compile`; the default value is false (optional). - - `qresource` rcc source file to extract ts_files from; cannot be used with ts_files kwarg. Available since v0.56.0. - - `rcc_extra_arguments`, any additional arguments to `rcc` (optional), when used with `qresource. Available since v0.56.0. - -Returns either: a list of custom targets for the compiled -translations, or, if using a `qresource` file, a single custom target -containing the processed source file, which should be passed to a main -build target. - -## has_tools - -This method returns `true` if all tools used by this module are found, -`false` otherwise. - -It should be used to compile optional Qt code: -```meson -qt5 = import('qt5') -if qt5.has_tools(required: get_option('qt_feature')) - moc_files = qt5.preprocess(...) - ... -endif -``` - -This method takes the following keyword arguments: -- `required`: by default, `required` is set to `false`. If `required` is set to - `true` or an enabled [`feature`](Build-options.md#features) and some tools are - missing Meson will abort. -- `method`: method used to find the Qt dependency (`auto` by default). - -*Since: 0.54.0* - -## Dependencies - -See [Qt dependencies](Dependencies.md#qt4-qt5) - -The 'modules' argument is used to include Qt modules in the project. -See the Qt documentation for the [list of -modules](http://doc.qt.io/qt-5/qtmodules.html). - -The 'private_headers' argument allows usage of Qt's modules private -headers. (since v0.47.0) - -## Example -A simple example would look like this: - -```meson -qt5 = import('qt5') -qt5_dep = dependency('qt5', modules: ['Core', 'Gui']) -inc = include_directories('includes') -moc_files = qt5.preprocess(moc_headers : 'myclass.h', - moc_extra_arguments: ['-DMAKES_MY_MOC_HEADER_COMPILE'], - include_directories: inc, - dependencies: qt5_dep) -translations = qt5.compile_translations(ts_files : 'myTranslation_fr.ts', build_by_default : true) -executable('myprog', 'main.cpp', 'myclass.cpp', moc_files, - include_directories: inc, - dependencies : qt5_dep) -``` - -Sometimes, translations are embedded inside the binary using qresource -files. In this case the ts files do not need to be explicitly listed, -but will be inferred from the built qm files listed in the qresource -file. For example: - -```meson -qt5 = import('qt5') -qt5_dep = dependency('qt5', modules: ['Core', 'Gui']) -lang_cpp = qt5.compile_translations(qresource: 'lang.qrc') -executable('myprog', 'main.cpp', lang_cpp, - dependencies: qt5_dep) -``` +{{ _include_qt_base.md }} diff --git a/docs/markdown/Qt6-module.md b/docs/markdown/Qt6-module.md new file mode 100644 index 0000000..4d40423 --- /dev/null +++ b/docs/markdown/Qt6-module.md @@ -0,0 +1,8 @@ +# Qt6 module + +*New in Meson 0.57.0* + +The Qt5 module provides tools to automatically deal with the various +tools and steps required for Qt. + +{{ _include_qt_base.md }} diff --git a/docs/markdown/_include_qt_base.md b/docs/markdown/_include_qt_base.md new file mode 100644 index 0000000..4c50abc --- /dev/null +++ b/docs/markdown/_include_qt_base.md @@ -0,0 +1,160 @@ +## compile_resources + +*New in 0.59.0* + +Compiles Qt's resources collection files (.qrc) into c++ files for compilation. + +It takes no positional arguments, and the following keyword arguments: + - `name` (string | empty): if provided a single .cpp file will be generated, + and the output of all qrc files will be combined in this file, otherwise + each qrc file be written to it's own cpp file. + - `sources` (File | string)[]: A list of sources to be transpiled. Required, + must have at least one source + - `extra_args` string[]: Extra arguments to pass directly to `qt-rcc` + - `method` string: The method to use to detect qt, see `dependency()` for more + information. + +## compile_ui + +*New in 0.59.0* + +Compiles Qt's ui files (.ui) into header files. + +It takes no positional arguments, and the following keyword arguments: + - `sources` (File | string)[]: A list of sources to be transpiled. Required, + must have at least one source + - `extra_args` string[]: Extra arguments to pass directly to `qt-uic` + - `method` string: The method to use to detect qt, see `dependency()` for more + information. + +## compile_moc + +*New in 0.59.0* + +Compiles Qt's moc files (.moc) into header and/or source files. At least one of +the keyword arguments `headers` and `sources` must be provided. + +It takes no positional arguments, and the following keyword arguments: + - `sources` (File | string)[]: A list of sources to be transpiled into .moc + files for manual inclusion. + - `headers` (File | string)[]: A list of headers to be transpiled into .cpp files + - `extra_args` string[]: Extra arguments to pass directly to `qt-moc` + - `method` string: The method to use to detect qt, see `dependency()` for more + information. + - `include_directories` IncludeDirectory[]: A list of `include_directory()` + objects used when transpiling the .moc files + +## preprocess + +Consider using `compile_resources`, `compile_ui`, and `compile_moc` instead. + +Takes sources for moc, uic, and rcc, and converts them into c++ files for +compilation. + +Has the following signature: `qt.preprocess(name: str | None, *sources: str)` + +If the `name` parameter is passed then all of the rcc files will be wirtten to a single output file + +The variadic `sources` arguments have been deprecated since Meson 0.59.0, as has the `sources` keyword argument. These passed files unmodified through the preprocessor, don't do this, just add the output of the generator to another sources list: +```meson +sources = files('a.cpp', 'main.cpp', 'bar.c') +sources += qt.preprocess(qresources : ['resources']) +``` + +This method takes the following keyword arguments: + - `qresources`: a list of strings, Files, Custom Targets, or Build Targets to pass the `rcc` compiler + - `ui_files`: a list of strings, Files, Custom Targets, or Build Targets to pass the `uic` compiler + - `moc_sources`: a list of strings, Files, Custom Targets, or Build Targets to pass the `moc` compiler the + - `moc_headers`: a list of strings, Files, Custom Targets, or Build Targets to pass the `moc` compiler. These will be converted into .cpp files + - `include_directories`, the directories to add to header search path for `moc` (optional) + - `moc_extra_arguments`, any additional arguments to `moc` (optional). Available since v0.44.0. + - `uic_extra_arguments`, any additional arguments to `uic` (optional). Available since v0.49.0. + - `rcc_extra_arguments`, any additional arguments to `rcc` (optional). Available since v0.49.0. + - `dependencies`, dependency objects needed by moc. Available since v0.48.0. + - `sources`, a list of extra sources, which are added to the output unchaged. Deprecated in 0.59.0 + +It returns an array of targets and sources to pass to a compilation target. + +## compile_translations (since v0.44.0) + +This method generates the necessary targets to build translation files with +lrelease, it takes no positional arguments, and the following keyword arguments: + + - `ts_files` (str | File)[], the list of input translation files produced by Qt's lupdate tool. + - `install` bool: when true, this target is installed during the install step (optional). + - `install_dir` string: directory to install to (optional). + - `build_by_default` bool: when set to true, to have this target be built by + default, that is, when invoking `meson compile`; the default value is false + (optional). + - `qresource` string: rcc source file to extract ts_files from; cannot be used + with ts_files kwarg. Available since v0.56.0. + - `rcc_extra_arguments` string[]: any additional arguments to `rcc` (optional), + when used with `qresource. Available since v0.56.0. + +Returns either: a list of custom targets for the compiled +translations, or, if using a `qresource` file, a single custom target +containing the processed source file, which should be passed to a main +build target. + +## has_tools + +This method returns `true` if all tools used by this module are found, +`false` otherwise. + +It should be used to compile optional Qt code: +```meson +qt5 = import('qt5') +if qt5.has_tools(required: get_option('qt_feature')) + moc_files = qt5.preprocess(...) + ... +endif +``` + +This method takes the following keyword arguments: +- `required` bool | FeatureOption: by default, `required` is set to `false`. If `required` is set to + `true` or an enabled [`feature`](Build-options.md#features) and some tools are + missing Meson will abort. +- `method` string: method used to find the Qt dependency (`auto` by default). + +*Since: 0.54.0* + +## Dependencies + +See [Qt dependencies](Dependencies.md#qt4-qt5) + +The 'modules' argument is used to include Qt modules in the project. +See the Qt documentation for the [list of +modules](http://doc.qt.io/qt-5/qtmodules.html). + +The 'private_headers' argument allows usage of Qt's modules private +headers. (since v0.47.0) + +## Example +A simple example would look like this: + +```meson +qt5 = import('qt5') +qt5_dep = dependency('qt5', modules: ['Core', 'Gui']) +inc = include_directories('includes') +moc_files = qt5.compile_moc(headers : 'myclass.h', + extra_arguments: ['-DMAKES_MY_MOC_HEADER_COMPILE'], + include_directories: inc, + dependencies: qt5_dep) +translations = qt5.compile_translations(ts_files : 'myTranslation_fr.ts', build_by_default : true) +executable('myprog', 'main.cpp', 'myclass.cpp', moc_files, + include_directories: inc, + dependencies : qt5_dep) +``` + +Sometimes, translations are embedded inside the binary using qresource +files. In this case the ts files do not need to be explicitly listed, +but will be inferred from the built qm files listed in the qresource +file. For example: + +```meson +qt5 = import('qt5') +qt5_dep = dependency('qt5', modules: ['Core', 'Gui']) +lang_cpp = qt5.compile_translations(qresource: 'lang.qrc') +executable('myprog', 'main.cpp', lang_cpp, + dependencies: qt5_dep) +``` diff --git a/docs/markdown/snippets/qt_preprocess_separate.md b/docs/markdown/snippets/qt_preprocess_separate.md new file mode 100644 index 0000000..1035f9a --- /dev/null +++ b/docs/markdown/snippets/qt_preprocess_separate.md @@ -0,0 +1,5 @@ +## Separate functions for qt preprocess + +`qt.preprocess` is a large, complicated function that does a lot of things, +a new set of `compile_*` functions have been provided as well. These are +conceptually simpler, as they do a single thing. diff --git a/docs/markdown/snippets/qt_preprocessed_varargs_deprecated.md b/docs/markdown/snippets/qt_preprocessed_varargs_deprecated.md new file mode 100644 index 0000000..5418eb3 --- /dev/null +++ b/docs/markdown/snippets/qt_preprocessed_varargs_deprecated.md @@ -0,0 +1,33 @@ +## Qt.preprocess source arguments deprecated + +The `qt.preprocess` method currently has this signature: +`qt.preprocess(name: str | None, *srcs: str)`, this is not a nice signature +because it's confusing, and there's a `sources` keyword argument as well. +Both of these pass sources through unmodified, this is a bit of a historical +accident, and not the way that any other module works. These have been +deprecated, so instead of: +```meson +sources = qt.preprocess( + name, + list, of, sources, + sources : [more, sources], + ... # things to process, +) + +executable( + 'foo', + sources, +) +``` +use +```meson +processed = qt.preprocess( + name, + ... # thins to process +) + +executable( + 'foo', + 'list', 'of', 'sources', 'more', 'sources', processed, +) +``` diff --git a/docs/sitemap.txt b/docs/sitemap.txt index 8014668..e1efe65 100644 --- a/docs/sitemap.txt +++ b/docs/sitemap.txt @@ -50,6 +50,7 @@ index.md Python-module.md Qt4-module.md Qt5-module.md + Qt6-module.md RPM-module.md Rust-module.md Simd-module.md diff --git a/mesonbuild/build.py b/mesonbuild/build.py index a685664..436a55d 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -43,6 +43,7 @@ if T.TYPE_CHECKING: from ._typing import ImmutableListProtocol, ImmutableSetProtocol from .interpreter.interpreter import Test, SourceOutputs, Interpreter from .mesonlib import FileMode, FileOrString + from .modules import ModuleState from .backend.backends import Backend from .interpreter.interpreterobjects import GeneratorHolder @@ -1498,113 +1499,71 @@ You probably should put it in link_with instead.''') return class Generator: - def __init__(self, args, kwargs): - if len(args) != 1: - raise InvalidArguments('Generator requires exactly one positional argument: the executable') - exe = unholder(args[0]) - if not isinstance(exe, (Executable, programs.ExternalProgram)): - raise InvalidArguments('First generator argument must be an executable.') + def __init__(self, exe: T.Union['Executable', programs.ExternalProgram], + arguments: T.List[str], + output: T.List[str], + *, + depfile: T.Optional[str] = None, + capture: bool = False, + depends: T.Optional[T.List[T.Union[BuildTarget, 'CustomTarget']]] = None, + name: str = 'Generator'): self.exe = exe - self.depfile = None - self.capture = False - self.depends = [] - self.process_kwargs(kwargs) + self.depfile = depfile + self.capture = capture + self.depends: T.List[T.Union[BuildTarget, 'CustomTarget']] = depends or [] + self.arglist = arguments + self.outputs = output + self.name = name - def __repr__(self): + def __repr__(self) -> str: repr_str = "<{0}: {1}>" return repr_str.format(self.__class__.__name__, self.exe) - def get_exe(self): + def get_exe(self) -> T.Union['Executable', programs.ExternalProgram]: return self.exe - def process_kwargs(self, kwargs): - if 'arguments' not in kwargs: - raise InvalidArguments('Generator must have "arguments" keyword argument.') - args = kwargs['arguments'] - if isinstance(args, str): - args = [args] - if not isinstance(args, list): - raise InvalidArguments('"Arguments" keyword argument must be a string or a list of strings.') - for a in args: - if not isinstance(a, str): - raise InvalidArguments('A non-string object in "arguments" keyword argument.') - self.arglist = args - if 'output' not in kwargs: - raise InvalidArguments('Generator must have "output" keyword argument.') - outputs = listify(kwargs['output']) - for rule in outputs: - if not isinstance(rule, str): - raise InvalidArguments('"output" may only contain strings.') - if '@BASENAME@' not in rule and '@PLAINNAME@' not in rule: - raise InvalidArguments('Every element of "output" must contain @BASENAME@ or @PLAINNAME@.') - if has_path_sep(rule): - raise InvalidArguments('"outputs" must not contain a directory separator.') - if len(outputs) > 1: - for o in outputs: - if '@OUTPUT@' in o: - raise InvalidArguments('Tried to use @OUTPUT@ in a rule with more than one output.') - self.outputs = outputs - if 'depfile' in kwargs: - depfile = kwargs['depfile'] - if not isinstance(depfile, str): - raise InvalidArguments('Depfile must be a string.') - if os.path.basename(depfile) != depfile: - raise InvalidArguments('Depfile must be a plain filename without a subdirectory.') - self.depfile = depfile - if 'capture' in kwargs: - capture = kwargs['capture'] - if not isinstance(capture, bool): - raise InvalidArguments('Capture must be boolean.') - self.capture = capture - if 'depends' in kwargs: - depends = unholder(listify(kwargs['depends'])) - for d in depends: - if not (isinstance(d, (BuildTarget, CustomTarget))): - raise InvalidArguments('Depends entries must be build targets.') - self.depends.append(d) - - def get_base_outnames(self, inname) -> T.List[str]: + def get_base_outnames(self, inname: str) -> T.List[str]: plainname = os.path.basename(inname) basename = os.path.splitext(plainname)[0] bases = [x.replace('@BASENAME@', basename).replace('@PLAINNAME@', plainname) for x in self.outputs] return bases - def get_dep_outname(self, inname): + def get_dep_outname(self, inname: str) -> T.List[str]: if self.depfile is None: raise InvalidArguments('Tried to get dep name for rule that does not have dependency file defined.') plainname = os.path.basename(inname) basename = os.path.splitext(plainname)[0] return self.depfile.replace('@BASENAME@', basename).replace('@PLAINNAME@', plainname) - def get_arglist(self, inname): + def get_arglist(self, inname: str) -> T.List[str]: plainname = os.path.basename(inname) basename = os.path.splitext(plainname)[0] return [x.replace('@BASENAME@', basename).replace('@PLAINNAME@', plainname) for x in self.arglist] - def is_parent_path(self, parent, trial): + @staticmethod + def is_parent_path(parent: str, trial: str) -> bool: relpath = pathlib.PurePath(trial).relative_to(parent) return relpath.parts[0] != '..' # For subdirs we can only go "down". - def process_files(self, name, files, state: 'Interpreter', preserve_path_from=None, extra_args=None): - new = False + def process_files(self, files: T.Iterable[T.Union[str, File, 'CustomTarget', 'CustomTargetIndex', 'GeneratedList']], + state: T.Union['Interpreter', 'ModuleState'], + preserve_path_from: T.Optional[str] = None, + extra_args: T.Optional[T.List[str]] = None) -> 'GeneratedList': output = GeneratedList(self, state.subdir, preserve_path_from, extra_args=extra_args if extra_args is not None else []) - #XXX - for e in unholder(files): - fs = [e] + + for e in files: if isinstance(e, CustomTarget): output.depends.add(e) if isinstance(e, CustomTargetIndex): output.depends.add(e.target) + if isinstance(e, (CustomTarget, CustomTargetIndex, GeneratedList)): self.depends.append(e) # BUG: this should go in the GeneratedList object, not this object. - fs = [] - for f in e.get_outputs(): - fs.append(File.from_built_file(state.subdir, f)) - new = True + fs = [File.from_built_file(state.subdir, f) for f in e.get_outputs()] elif isinstance(e, str): fs = [File.from_source_file(state.environment.source_dir, state.subdir, e)] - elif not isinstance(e, File): - raise InvalidArguments(f'{name} arguments must be strings, files or CustomTargets, not {e!r}.') + else: + fs = [e] for f in fs: if preserve_path_from: @@ -1612,26 +1571,28 @@ class Generator: if not self.is_parent_path(preserve_path_from, abs_f): raise InvalidArguments('generator.process: When using preserve_path_from, all input files must be in a subdirectory of the given dir.') output.add_file(f, state) - if new: - FeatureNew.single_use( - f'Calling "{name}" with CustomTaget or Index of CustomTarget.', - '0.57.0', state.subproject) return output class GeneratedList: - def __init__(self, generator: 'GeneratorHolder', subdir: str, preserve_path_from=None, extra_args=None): - self.generator = unholder(generator) - self.name = self.generator.exe - self.depends = set() # Things this target depends on (because e.g. a custom target was used as input) + + """The output of generator.process.""" + + def __init__(self, generator: Generator, subdir: str, + preserve_path_from: T.Optional[str], + extra_args: T.List[str]): + self.generator = generator + self.name = generator.exe + self.depends: T.Set['CustomTarget'] = set() # Things this target depends on (because e.g. a custom target was used as input) self.subdir = subdir self.infilelist: T.List['File'] = [] self.outfilelist: T.List[str] = [] - self.outmap: T.Dict['File', str] = {} - self.extra_depends = [] - self.depend_files = [] + self.outmap: T.Dict[File, T.List[str]] = {} + self.extra_depends = [] # XXX: Doesn't seem to be used? + self.depend_files: T.List[File] = [] self.preserve_path_from = preserve_path_from - self.extra_args = extra_args if extra_args is not None else [] + self.extra_args: T.List[str] = extra_args if extra_args is not None else [] + if isinstance(self.generator.exe, programs.ExternalProgram): if not self.generator.exe.found(): raise InvalidArguments('Tried to use not-found external program as generator') @@ -1641,7 +1602,7 @@ class GeneratedList: # know the absolute path of self.depend_files.append(File.from_absolute_file(path)) - def add_preserved_path_segment(self, infile: 'File', outfiles: T.List[str], state: 'Interpreter') -> T.List[str]: + def add_preserved_path_segment(self, infile: File, outfiles: T.List[str], state: T.Union['Interpreter', 'ModuleState']) -> T.List[str]: result: T.List[str] = [] in_abs = infile.absolute_path(state.environment.source_dir, state.environment.build_dir) assert os.path.isabs(self.preserve_path_from) @@ -1651,7 +1612,7 @@ class GeneratedList: result.append(os.path.join(path_segment, of)) return result - def add_file(self, newfile: 'File', state: 'Interpreter') -> None: + def add_file(self, newfile: File, state: T.Union['Interpreter', 'ModuleState']) -> None: self.infilelist.append(newfile) outfiles = self.generator.get_base_outnames(newfile.fname) if self.preserve_path_from: @@ -1671,7 +1632,7 @@ class GeneratedList: def get_generator(self) -> 'Generator': return self.generator - def get_extra_args(self): + def get_extra_args(self) -> T.List[str]: return self.extra_args class Executable(BuildTarget): diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 1a7d2fc..dce0391 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -267,7 +267,7 @@ class Interpreter(InterpreterBase): self.ast = ast self.sanity_check_ast() self.builtin.update({'meson': MesonMain(build, self)}) - self.generators = [] + self.generators: T.List['GeneratorHolder'] = [] self.processed_buildfiles = set() # type: T.Set[str] self.project_args_frozen = False self.global_args_frozen = False # implies self.project_args_frozen @@ -1954,10 +1954,34 @@ This will become a hard error in the future.''' % kwargs['input'], location=self @permittedKwargs({'arguments', 'output', 'depends', 'depfile', 'capture', 'preserve_path_from'}) - def func_generator(self, node, args, kwargs): - gen = GeneratorHolder(self, args, kwargs) - self.generators.append(gen) - return gen + @typed_pos_args('generator', (ExecutableHolder, ExternalProgramHolder)) + @typed_kwargs( + 'generator', + KwargInfo('arguments', ContainerTypeInfo(list, str, allow_empty=False), required=True, listify=True), + KwargInfo('output', ContainerTypeInfo(list, str, allow_empty=False), required=True, listify=True), + KwargInfo('depfile', str, validator=lambda x: 'Depfile must be a plain filename with a subdirectory' if has_path_sep(x) else None), + KwargInfo('capture', bool, default=False, since='0.43.0'), + KwargInfo('depends', ContainerTypeInfo(list, (BuildTargetHolder, CustomTargetHolder)), default=[], listify=True), + ) + def func_generator(self, node: mparser.FunctionNode, + args: T.Tuple[T.Union[ExecutableHolder, ExternalProgramHolder]], + kwargs: 'kwargs.FuncGenerator') -> GeneratorHolder: + for rule in kwargs['output']: + if '@BASENAME@' not in rule and '@PLAINNAME@' not in rule: + raise InvalidArguments('Every element of "output" must contain @BASENAME@ or @PLAINNAME@.') + if has_path_sep(rule): + raise InvalidArguments('"output" must not contain a directory separator.') + if len(kwargs['output']) > 1: + for o in kwargs['output']: + if '@OUTPUT@' in o: + raise InvalidArguments('Tried to use @OUTPUT@ in a rule with more than one output.') + + depends = [d.held_object for d in kwargs.pop('depends')] + + gen = build.Generator(args[0].held_object, depends=depends, **kwargs) + holder = GeneratorHolder(gen, self) + self.generators.append(holder) + return holder @typed_pos_args('benchmark', str, (ExecutableHolder, JarHolder, ExternalProgramHolder, mesonlib.File)) @typed_kwargs('benchmark', *TEST_KWARGS) diff --git a/mesonbuild/interpreter/interpreterobjects.py b/mesonbuild/interpreter/interpreterobjects.py index e87feb9..7b59a24 100644 --- a/mesonbuild/interpreter/interpreterobjects.py +++ b/mesonbuild/interpreter/interpreterobjects.py @@ -13,22 +13,33 @@ from .. import mlog from ..modules import ModuleReturnValue, ModuleObject, ModuleState, ExtensionModule from ..backend.backends import TestProtocol -from ..interpreterbase import (InterpreterObject, ObjectHolder, MutableInterpreterObject, +from ..interpreterbase import (ContainerTypeInfo, InterpreterObject, KwargInfo, + ObjectHolder, MutableInterpreterObject, FeatureNewKwargs, FeatureNew, FeatureDeprecated, - typed_pos_args, stringArgs, permittedKwargs, - noArgsFlattening, noPosargs, TYPE_var, TYPE_nkwargs, - flatten, InterpreterException, InvalidArguments, InvalidCode) + typed_kwargs, typed_pos_args, stringArgs, + permittedKwargs, noArgsFlattening, noPosargs, + TYPE_var, TYPE_nkwargs, flatten, + InterpreterException, InvalidArguments, + InvalidCode) +from ..interpreterbase.decorators import FeatureCheckBase from ..dependencies import Dependency, ExternalLibrary, InternalDependency from ..programs import ExternalProgram from ..mesonlib import FileMode, OptionKey, listify, Popen_safe import typing as T -def extract_required_kwarg(kwargs, subproject, feature_check=None, default=True): +if T.TYPE_CHECKING: + from . import kwargs + from .interpreter import Interpreter + + +def extract_required_kwarg(kwargs: 'kwargs.ExtractRequired', subproject: str, + feature_check: T.Optional['FeatureCheckBase'] = None, + default: bool = True) -> T.Tuple[bool, bool, T.Optional[str]]: val = kwargs.get('required', default) disabled = False required = False - feature = None + feature: T.Optional[str] = None if isinstance(val, FeatureOptionHolder): if not feature_check: feature_check = FeatureNew('User option "feature"', '0.47.0') @@ -46,6 +57,7 @@ def extract_required_kwarg(kwargs, subproject, feature_check=None, default=True) # Keep boolean value in kwargs to simplify other places where this kwarg is # checked. + # TODO: this should be removed, and those callers should learn about FeatureOptions kwargs['required'] = required return disabled, required, feature @@ -611,47 +623,16 @@ class ExternalLibraryHolder(InterpreterObject, ObjectHolder[ExternalLibrary]): pdep = self.held_object.get_partial_dependency(**kwargs) return DependencyHolder(pdep, self.subproject) -class GeneratorHolder(InterpreterObject, ObjectHolder[build.Generator]): - @FeatureNewKwargs('generator', '0.43.0', ['capture']) - def __init__(self, interp, args, kwargs): - self.interpreter = interp - InterpreterObject.__init__(self) - ObjectHolder.__init__(self, build.Generator(args, kwargs), interp.subproject) - self.methods.update({'process': self.process_method}) - - @FeatureNewKwargs('generator.process', '0.45.0', ['preserve_path_from']) - @permittedKwargs({'extra_args', 'preserve_path_from'}) - def process_method(self, args, kwargs): - extras = mesonlib.stringlistify(kwargs.get('extra_args', [])) - if 'preserve_path_from' in kwargs: - preserve_path_from = kwargs['preserve_path_from'] - if not isinstance(preserve_path_from, str): - raise InvalidArguments('Preserve_path_from must be a string.') - preserve_path_from = os.path.normpath(preserve_path_from) - if not os.path.isabs(preserve_path_from): - # This is a bit of a hack. Fix properly before merging. - raise InvalidArguments('Preserve_path_from must be an absolute path for now. Sorry.') - else: - preserve_path_from = None - gl = self.held_object.process_files('Generator', args, self.interpreter, - preserve_path_from, extra_args=extras) - return GeneratedListHolder(gl) - class GeneratedListHolder(InterpreterObject, ObjectHolder[build.GeneratedList]): - def __init__(self, arg1, extra_args=None): + def __init__(self, arg1: 'build.GeneratedList'): InterpreterObject.__init__(self) - if isinstance(arg1, GeneratorHolder): - ObjectHolder.__init__(self, build.GeneratedList(arg1.held_object, extra_args if extra_args is not None else [])) - else: - ObjectHolder.__init__(self, arg1) + ObjectHolder.__init__(self, arg1) - def __repr__(self): + def __repr__(self) -> str: r = '<{}: {!r}>' return r.format(self.__class__.__name__, self.held_object.get_outputs()) - def add_file(self, a): - self.held_object.add_file(a) # A machine that's statically known from the cross file class MachineHolder(InterpreterObject, ObjectHolder['MachineInfo']): @@ -1036,3 +1017,37 @@ class RunTargetHolder(TargetHolder): r = '<{} {}: {}>' h = self.held_object return r.format(self.__class__.__name__, h.get_id(), h.command) + + +class GeneratorHolder(InterpreterObject, ObjectHolder[build.Generator]): + + def __init__(self, gen: 'build.Generator', interpreter: 'Interpreter'): + InterpreterObject.__init__(self) + ObjectHolder.__init__(self, gen, interpreter.subproject) + self.interpreter = interpreter + self.methods.update({'process': self.process_method}) + + @typed_pos_args('generator.process', min_varargs=1, varargs=(str, mesonlib.File, CustomTargetHolder, CustomTargetIndexHolder, GeneratedListHolder)) + @typed_kwargs( + 'generator.process', + KwargInfo('preserve_path_from', str, since='0.45.0'), + KwargInfo('extra_args', ContainerTypeInfo(list, str), listify=True, default=[]), + ) + def process_method(self, args: T.Tuple[T.List[T.Union[str, mesonlib.File, CustomTargetHolder, CustomTargetIndexHolder, GeneratedListHolder]]], + kwargs: 'kwargs.GeneratorProcess') -> GeneratedListHolder: + preserve_path_from = kwargs['preserve_path_from'] + if preserve_path_from is not None: + preserve_path_from = os.path.normpath(preserve_path_from) + if not os.path.isabs(preserve_path_from): + # This is a bit of a hack. Fix properly before merging. + raise InvalidArguments('Preserve_path_from must be an absolute path for now. Sorry.') + + if any(isinstance(a, (CustomTargetHolder, CustomTargetIndexHolder, GeneratedListHolder)) for a in args[0]): + FeatureNew.single_use( + f'Calling generator.process with CustomTaget or Index of CustomTarget.', + '0.57.0', self.interpreter.subproject) + + gl = self.held_object.process_files(mesonlib.unholder(args[0]), self.interpreter, + preserve_path_from, extra_args=kwargs['extra_args']) + + return GeneratedListHolder(gl) diff --git a/mesonbuild/interpreter/kwargs.py b/mesonbuild/interpreter/kwargs.py index db6b02b..9734caa 100644 --- a/mesonbuild/interpreter/kwargs.py +++ b/mesonbuild/interpreter/kwargs.py @@ -9,7 +9,10 @@ import typing as T from typing_extensions import TypedDict, Literal from ..mesonlib import MachineChoice, File -from .interpreterobjects import BuildTargetHolder, CustomTargetHolder, EnvironmentVariablesHolder, TargetHolder +from .interpreterobjects import ( + BuildTargetHolder, CustomTargetHolder, EnvironmentVariablesHolder, + FeatureOptionHolder, TargetHolder +) class FuncAddProjectArgs(TypedDict): @@ -57,3 +60,33 @@ class FuncTest(FuncBenchmark): """ is_parallel: bool + + +class ExtractRequired(TypedDict): + + """Keyword Arguments consumed by the `extract_required_kwargs` function. + + Any function that uses the `required` keyword argument which accepts either + a boolean or a feature option should inherit it's arguments from this class. + """ + + required: T.Union[bool, 'FeatureOptionHolder'] + + +class FuncGenerator(TypedDict): + + """Keyword rguments for the generator function.""" + + arguments: T.List[str] + output: T.List[str] + depfile: bool + capture: bool + depends: T.List[T.Union['BuildTargetHolder', 'CustomTargetHolder']] + + +class GeneratorProcess(TypedDict): + + """Keyword Arguments for generator.process.""" + + preserve_path_from: T.Optional[str] + extra_args: T.List[str] diff --git a/mesonbuild/modules/__init__.py b/mesonbuild/modules/__init__.py index e95b9c6..69bb552 100644 --- a/mesonbuild/modules/__init__.py +++ b/mesonbuild/modules/__init__.py @@ -23,7 +23,9 @@ import typing as T if T.TYPE_CHECKING: from ..interpreter import Interpreter + from ..interpreter.interpreterobjects import IncludeDirsHolder, ExternalProgramHolder from ..interpreterbase import TYPE_var, TYPE_nvar, TYPE_nkwargs + from ..programs import ExternalProgram class ModuleState: """Object passed to all module methods. @@ -59,14 +61,14 @@ class ModuleState: self.target_machine = interpreter.builtin['target_machine'].held_object self.current_node = interpreter.current_node - def get_include_args(self, include_dirs, prefix='-I'): + def get_include_args(self, include_dirs: T.Iterable[T.Union[str, 'IncludeDirsHolder']], prefix: str = '-I') -> T.List[str]: if not include_dirs: return [] srcdir = self.environment.get_source_dir() builddir = self.environment.get_build_dir() - dirs_str = [] + dirs_str: T.List[str] = [] for dirs in unholder(include_dirs): if isinstance(dirs, str): dirs_str += [f'{prefix}{dirs}'] @@ -88,7 +90,7 @@ 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) -> 'ExternalProgramHolder': - return self._interpreter.find_program_impl(prog, required=required) + return self._interpreter.find_program_impl(prog, required=required, version_func=version_func, wanted=wanted) class ModuleObject: """Base class for all objects returned by modules diff --git a/mesonbuild/modules/qt.py b/mesonbuild/modules/qt.py index aecfe50..7d752db 100644 --- a/mesonbuild/modules/qt.py +++ b/mesonbuild/modules/qt.py @@ -1,4 +1,5 @@ # Copyright 2015 The Meson development team +# 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. @@ -12,34 +13,95 @@ # See the License for the specific language governing permissions and # limitations under the License. -from mesonbuild.dependencies import find_external_dependency import os import shutil import typing as T +import xml.etree.ElementTree as ET -from .. import mlog +from . import ModuleReturnValue, ExtensionModule from .. import build from .. import mesonlib -from ..mesonlib import MesonException, extract_as_list, File, unholder, version_compare -from ..dependencies import Dependency -import xml.etree.ElementTree as ET -from . import ModuleReturnValue, ExtensionModule -from ..interpreterbase import noPosargs, permittedKwargs, FeatureNew, FeatureNewKwargs +from .. import mlog +from ..dependencies import find_external_dependency from ..interpreter import extract_required_kwarg +from ..interpreter.interpreterobjects import DependencyHolder, ExternalLibraryHolder, IncludeDirsHolder, FeatureOptionHolder, GeneratedListHolder +from ..interpreterbase import ContainerTypeInfo, FeatureDeprecated, KwargInfo, noPosargs, FeatureNew, typed_kwargs +from ..mesonlib import MesonException, File from ..programs import NonExistingExternalProgram if T.TYPE_CHECKING: + from . import ModuleState + from ..dependencies.qt import QtPkgConfigDependency, QmakeQtDependency from ..interpreter import Interpreter - from ..dependencies.qt import QtBaseDependency - from ..environment import Environment + from ..interpreter import kwargs from ..programs import ExternalProgram + QtDependencyType = T.Union[QtPkgConfigDependency, QmakeQtDependency] + + from typing_extensions import TypedDict + + class ResourceCompilerKwArgs(TypedDict): + + """Keyword arguments for the Resource Compiler method.""" + + name: T.Optional[str] + sources: T.List[mesonlib.FileOrString] + extra_args: T.List[str] + method: str + + class UICompilerKwArgs(TypedDict): + + """Keyword arguments for the Ui Compiler method.""" + + sources: T.List[mesonlib.FileOrString] + extra_args: T.List[str] + method: str + + class MocCompilerKwArgs(TypedDict): + + """Keyword arguments for the Moc Compiler method.""" + + sources: T.List[mesonlib.FileOrString] + headers: T.List[mesonlib.FileOrString] + extra_args: T.List[str] + method: str + include_directories: T.List[IncludeDirsHolder] + dependencies: T.List[T.Union[DependencyHolder, ExternalLibraryHolder]] + + class PreprocessKwArgs(TypedDict): + + sources: T.List[mesonlib.FileOrString] + moc_sources: T.List[mesonlib.FileOrString] + moc_headers: T.List[mesonlib.FileOrString] + qresources: T.List[mesonlib.FileOrString] + ui_files: T.List[mesonlib.FileOrString] + moc_extra_arguments: T.List[str] + rcc_extra_arguments: T.List[str] + uic_extra_arguments: T.List[str] + include_directories: T.List[IncludeDirsHolder] + dependencies: T.List[T.Union[DependencyHolder, ExternalLibraryHolder]] + method: str + + class HasToolKwArgs(kwargs.ExtractRequired): + + method: str + + class CompileTranslationsKwArgs(TypedDict): + + build_by_default: bool + install: bool + install_dir: T.Optional[str] + method: str + qresource: T.Optional[str] + rcc_extra_arguments: T.List[str] + ts_files: T.List[str] + class QtBaseModule(ExtensionModule): - tools_detected = False - rcc_supports_depfiles = False + _tools_detected = False + _rcc_supports_depfiles = False - def __init__(self, interpreter: 'Interpreter', qt_version=5): + def __init__(self, interpreter: 'Interpreter', qt_version: int = 5): ExtensionModule.__init__(self, interpreter) self.qt_version = qt_version self.moc: 'ExternalProgram' = NonExistingExternalProgram('moc') @@ -50,9 +112,12 @@ class QtBaseModule(ExtensionModule): 'has_tools': self.has_tools, 'preprocess': self.preprocess, 'compile_translations': self.compile_translations, + 'compile_resources': self.compile_resources, + 'compile_ui': self.compile_ui, + 'compile_moc': self.compile_moc, }) - def compilers_detect(self, state, qt_dep: 'QtBaseDependency') -> None: + def compilers_detect(self, state: 'ModuleState', qt_dep: 'QtDependencyType') -> None: """Detect Qt (4 or 5) moc, uic, rcc in the specified bindir or in PATH""" # It is important that this list does not change order as the order of # the returned ExternalPrograms will change as well @@ -96,18 +161,19 @@ class QtBaseModule(ExtensionModule): if p.found(): setattr(self, name, p) - def _detect_tools(self, state, method, required=True): - if self.tools_detected: + def _detect_tools(self, state: 'ModuleState', method: str, required: bool = True) -> None: + if self._tools_detected: return - self.tools_detected = True + self._tools_detected = True mlog.log(f'Detecting Qt{self.qt_version} tools') kwargs = {'required': required, 'modules': 'Core', 'method': method} - qt = find_external_dependency(f'qt{self.qt_version}', state.environment, kwargs) + # Just pick one to make mypy happy + qt = T.cast('QtPkgConfigDependency', find_external_dependency(f'qt{self.qt_version}', state.environment, kwargs)) if qt.found(): # Get all tools and then make sure that they are the right version self.compilers_detect(state, qt) - if version_compare(qt.version, '>=5.14.0'): - self.rcc_supports_depfiles = True + if mesonlib.version_compare(qt.version, '>=5.14.0'): + self._rcc_supports_depfiles = True else: mlog.warning('rcc dependencies will not work properly until you move to Qt >= 5.14:', mlog.bold('https://bugreports.qt.io/browse/QTBUG-45460'), fatal=False) @@ -118,21 +184,24 @@ class QtBaseModule(ExtensionModule): self.rcc = NonExistingExternalProgram(name='rcc' + suffix) self.lrelease = NonExistingExternalProgram(name='lrelease' + suffix) - def qrc_nodes(self, state, rcc_file): - if type(rcc_file) is str: + @staticmethod + def _qrc_nodes(state: 'ModuleState', rcc_file: 'mesonlib.FileOrString') -> T.Tuple[str, T.List[str]]: + abspath: str + if isinstance(rcc_file, str): abspath = os.path.join(state.environment.source_dir, state.subdir, rcc_file) rcc_dirname = os.path.dirname(abspath) - elif type(rcc_file) is File: + else: abspath = rcc_file.absolute_path(state.environment.source_dir, state.environment.build_dir) rcc_dirname = os.path.dirname(abspath) + # FIXME: what error are we actually tring to check here? try: tree = ET.parse(abspath) root = tree.getroot() - result = [] + result: T.List[str] = [] for child in root[0]: if child.tag != 'file': - mlog.warning("malformed rcc file: ", os.path.join(state.subdir, rcc_file)) + mlog.warning("malformed rcc file: ", os.path.join(state.subdir, str(rcc_file))) break else: result.append(child.text) @@ -141,9 +210,9 @@ class QtBaseModule(ExtensionModule): except Exception: raise MesonException(f'Unable to parse resource file {abspath}') - def parse_qrc_deps(self, state, rcc_file): - rcc_dirname, nodes = self.qrc_nodes(state, rcc_file) - result = [] + def _parse_qrc_deps(self, state: 'ModuleState', rcc_file: 'mesonlib.FileOrString') -> T.List[File]: + rcc_dirname, nodes = self._qrc_nodes(state, rcc_file) + result: T.List[File] = [] for resource_path in nodes: # We need to guess if the pointed resource is: # a) in build directory -> implies a generated file @@ -170,11 +239,18 @@ class QtBaseModule(ExtensionModule): result.append(File(is_built=False, subdir=state.subdir, fname=path_from_rcc)) return result - @noPosargs - @permittedKwargs({'method', 'required'}) @FeatureNew('qt.has_tools', '0.54.0') - def has_tools(self, state, args, kwargs): + @noPosargs + @typed_kwargs( + 'qt.has_tools', + KwargInfo('required', (bool, FeatureOptionHolder), default=False), + KwargInfo('method', str, default='auto'), + ) + def has_tools(self, state: 'ModuleState', args: T.Tuple, kwargs: 'HasToolKwArgs') -> bool: method = kwargs.get('method', 'auto') + # We have to cast here because TypedDicts are invariant, even though + # ExtractRequiredKwArgs is a subset of HasToolKwArgs, type checkers + # will insist this is wrong disabled, required, feature = extract_required_kwarg(kwargs, state.subproject, default=False) if disabled: mlog.log('qt.has_tools skipped: feature', mlog.bold(feature), 'disabled') @@ -187,117 +263,227 @@ class QtBaseModule(ExtensionModule): return False return True - @FeatureNewKwargs('qt.preprocess', '0.49.0', ['uic_extra_arguments']) - @FeatureNewKwargs('qt.preprocess', '0.44.0', ['moc_extra_arguments']) - @FeatureNewKwargs('qt.preprocess', '0.49.0', ['rcc_extra_arguments']) - @permittedKwargs({'moc_headers', 'moc_sources', 'uic_extra_arguments', 'moc_extra_arguments', 'rcc_extra_arguments', 'include_directories', 'dependencies', 'ui_files', 'qresources', 'method'}) - def preprocess(self, state, args, kwargs): - rcc_files, ui_files, moc_headers, moc_sources, uic_extra_arguments, moc_extra_arguments, rcc_extra_arguments, sources, include_directories, dependencies \ - = [extract_as_list(kwargs, c, pop=True) for c in ['qresources', 'ui_files', 'moc_headers', 'moc_sources', 'uic_extra_arguments', 'moc_extra_arguments', 'rcc_extra_arguments', 'sources', 'include_directories', 'dependencies']] - sources += args[1:] - method = kwargs.get('method', 'auto') - self._detect_tools(state, method) - err_msg = "{0} sources specified and couldn't find {1}, " \ - "please check your qt{2} installation" - if (moc_headers or moc_sources) and not self.moc.found(): - raise MesonException(err_msg.format('MOC', f'moc-qt{self.qt_version}', self.qt_version)) - if rcc_files: - if not self.rcc.found(): - raise MesonException(err_msg.format('RCC', f'rcc-qt{self.qt_version}', self.qt_version)) + @FeatureNew('qt.compile_resources', '0.59.0') + @noPosargs + @typed_kwargs( + 'qt.compile_resources', + KwargInfo('name', str), + KwargInfo('sources', ContainerTypeInfo(list, (File, str), allow_empty=False), listify=True, required=True), + KwargInfo('extra_args', ContainerTypeInfo(list, str), listify=True, default=[]), + KwargInfo('method', str, default='auto') + ) + def compile_resources(self, state: 'ModuleState', args: T.Tuple, kwargs: 'ResourceCompilerKwArgs') -> ModuleReturnValue: + """Compile Qt resources files. + + Uses CustomTargets to generate .cpp files from .qrc files. + """ + self._detect_tools(state, kwargs['method']) + if not self.rcc.found(): + err_msg = ("{0} sources specified and couldn't find {1}, " + "please check your qt{2} installation") + raise MesonException(err_msg.format('RCC', f'rcc-qt{self.qt_version}', self.qt_version)) + + # List of generated CustomTargets + targets: T.List[build.CustomTarget] = [] + + # depfile arguments + DEPFILE_ARGS: T.List[str] = ['--depfile', '@DEPFILE@'] if self._rcc_supports_depfiles else [] + + name = kwargs['name'] + sources = kwargs['sources'] + extra_args = kwargs['extra_args'] + + # If a name was set generate a single .cpp file from all of the qrc + # files, otherwise generate one .cpp file per qrc file. + if name: + qrc_deps: T.List[File] = [] + for s in sources: + qrc_deps.extend(self._parse_qrc_deps(state, s)) + + rcc_kwargs: T.Dict[str, T.Any] = { # TODO: if CustomTarget had typing information we could use that here... + 'input': sources, + 'output': name + '.cpp', + 'command': self.rcc.get_command() + ['-name', name, '-o', '@OUTPUT@'] + extra_args + ['@INPUT@'] + DEPFILE_ARGS, + 'depend_files': qrc_deps, + 'depfile': f'{name}.d', + } + res_target = build.CustomTarget(name, state.subdir, state.subproject, rcc_kwargs) + targets.append(res_target) + else: + for rcc_file in sources: + qrc_deps = self._parse_qrc_deps(state, rcc_file) + if isinstance(rcc_file, str): + basename = os.path.basename(rcc_file) + else: + basename = os.path.basename(rcc_file.fname) + name = f'qt{self.qt_version}-{basename.replace(".", "_")}' + rcc_kwargs = { + 'input': rcc_file, + 'output': f'{name}.cpp', + 'command': self.rcc.get_command() + ['-name', '@BASENAME@', '-o', '@OUTPUT@'] + extra_args + ['@INPUT@'] + DEPFILE_ARGS, + 'depend_files': qrc_deps, + 'depfile': f'{name}.d', + } + res_target = build.CustomTarget(name, state.subdir, state.subproject, rcc_kwargs) + targets.append(res_target) + + return ModuleReturnValue(targets, [targets]) + + @FeatureNew('qt.compile_ui', '0.59.0') + @noPosargs + @typed_kwargs( + 'qt.compile_ui', + KwargInfo('sources', ContainerTypeInfo(list, (File, str), allow_empty=False), listify=True, required=True), + KwargInfo('extra_args', ContainerTypeInfo(list, str), listify=True, default=[]), + KwargInfo('method', str, default='auto') + ) + def compile_ui(self, state: 'ModuleState', args: T.Tuple, kwargs: 'ResourceCompilerKwArgs') -> ModuleReturnValue: + """Compile UI resources into cpp headers.""" + self._detect_tools(state, kwargs['method']) + if not self.uic.found(): + err_msg = ("{0} sources specified and couldn't find {1}, " + "please check your qt{2} installation") + raise MesonException(err_msg.format('UIC', f'uic-qt{self.qt_version}', self.qt_version)) + + # TODO: This generator isn't added to the generator list in the Interpreter + gen = build.Generator( + self.uic, + kwargs['extra_args'] + ['-o', '@OUTPUT@', '@INPUT@'], + ['ui_@BASENAME@.h'], + name=f'Qt{self.qt_version} ui') + out = GeneratedListHolder(gen.process_files(kwargs['sources'], state)) + return ModuleReturnValue(out, [out]) + + @FeatureNew('qt.compile_moc', '0.59.0') + @noPosargs + @typed_kwargs( + 'qt.compile_moc', + KwargInfo('sources', ContainerTypeInfo(list, (File, str)), listify=True, default=[]), + KwargInfo('headers', ContainerTypeInfo(list, (File, str)), listify=True, default=[]), + KwargInfo('extra_args', ContainerTypeInfo(list, str), listify=True, default=[]), + KwargInfo('method', str, default='auto'), + KwargInfo('include_directories', ContainerTypeInfo(list, IncludeDirsHolder), listify=True, default=[]), + KwargInfo('dependencies', ContainerTypeInfo(list, (DependencyHolder, ExternalLibraryHolder)), listify=True, default=[]), + ) + def compile_moc(self, state: 'ModuleState', args: T.Tuple, kwargs: 'MocCompilerKwArgs') -> ModuleReturnValue: + self._detect_tools(state, kwargs['method']) + if not self.moc.found(): + err_msg = ("{0} sources specified and couldn't find {1}, " + "please check your qt{2} installation") + raise MesonException(err_msg.format('MOC', f'uic-qt{self.qt_version}', self.qt_version)) + + if not (kwargs['headers'] or kwargs['sources']): + raise build.InvalidArguments('At least one of the "headers" or "sources" keyword arguments must be provied and not empty') + + inc = state.get_include_args(include_dirs=kwargs['include_directories']) + compile_args: T.List[str] = [] + for dep in kwargs['dependencies']: + compile_args.extend([a for a in dep.held_object.get_all_compile_args() if a.startswith(('-I', '-D'))]) + + output: T.List[build.GeneratedList] = [] + + arguments = kwargs['extra_args'] + inc + compile_args + ['@INPUT@', '-o', '@OUTPUT@'] + if kwargs['headers']: + moc_gen = build.Generator( + self.moc, arguments, ['moc_@BASENAME@.cpp'], + name=f'Qt{self.qt_version} moc header') + output.append(moc_gen.process_files(kwargs['headers'], state)) + if kwargs['sources']: + moc_gen = build.Generator( + self.moc, arguments, ['@BASENAME@.moc'], + name=f'Qt{self.qt_version} moc source') + output.append(moc_gen.process_files(kwargs['sources'], state)) + + return ModuleReturnValue(output, [output]) + + # We can't use typed_pos_args here, the signature is ambiguious + @typed_kwargs( + 'qt.preprocess', + KwargInfo('sources', ContainerTypeInfo(list, (File, str)), listify=True, default=[], deprecated='0.59.0'), + KwargInfo('qresources', ContainerTypeInfo(list, (File, str)), listify=True, default=[]), + KwargInfo('ui_files', ContainerTypeInfo(list, (File, str)), listify=True, default=[]), + KwargInfo('moc_sources', ContainerTypeInfo(list, (File, str)), listify=True, default=[]), + KwargInfo('moc_headers', ContainerTypeInfo(list, (File, str)), listify=True, default=[]), + KwargInfo('moc_extra_arguments', ContainerTypeInfo(list, str), listify=True, default=[], since='0.44.0'), + KwargInfo('rcc_extra_arguments', ContainerTypeInfo(list, str), listify=True, default=[], since='0.49.0'), + KwargInfo('uic_extra_arguments', ContainerTypeInfo(list, str), listify=True, default=[], since='0.49.0'), + KwargInfo('method', str, default='auto'), + KwargInfo('include_directories', ContainerTypeInfo(list, IncludeDirsHolder), listify=True, default=[]), + KwargInfo('dependencies', ContainerTypeInfo(list, (DependencyHolder, ExternalLibraryHolder)), listify=True, default=[]), + ) + def preprocess(self, state: 'ModuleState', args: T.List[T.Union[str, File]], kwargs: 'PreprocessKwArgs') -> ModuleReturnValue: + _sources = args[1:] + if _sources: + FeatureDeprecated.single_use('qt.preprocess positional sources', '0.59', state.subproject) + sources = _sources + kwargs['sources'] + for s in sources: + if not isinstance(s, (str, File)): + raise build.InvalidArguments('Variadic arguments to qt.preprocess must be Strings or Files') + method = kwargs['method'] + + if kwargs['qresources']: # custom output name set? -> one output file, multiple otherwise + rcc_kwargs: 'ResourceCompilerKwArgs' = {'name': '', 'sources': kwargs['qresources'], 'extra_args': kwargs['rcc_extra_arguments'], 'method': method} if args: - qrc_deps = [] - for i in rcc_files: - qrc_deps += self.parse_qrc_deps(state, i) - name = args[0] - rcc_kwargs = {'input': rcc_files, - 'output': name + '.cpp', - 'command': [self.rcc, '-name', name, '-o', '@OUTPUT@', rcc_extra_arguments, '@INPUT@'], - 'depend_files': qrc_deps} - res_target = build.CustomTarget(name, state.subdir, state.subproject, rcc_kwargs) - sources.append(res_target) - else: - for rcc_file in rcc_files: - qrc_deps = self.parse_qrc_deps(state, rcc_file) - if type(rcc_file) is str: - basename = os.path.basename(rcc_file) - elif type(rcc_file) is File: - basename = os.path.basename(rcc_file.fname) - name = 'qt' + str(self.qt_version) + '-' + basename.replace('.', '_') - rcc_kwargs = {'input': rcc_file, - 'output': name + '.cpp', - 'command': [self.rcc, '-name', '@BASENAME@', '-o', '@OUTPUT@', rcc_extra_arguments, '@INPUT@'], - 'depend_files': qrc_deps} - if self.rcc_supports_depfiles: - rcc_kwargs['depfile'] = name + '.d' - rcc_kwargs['command'] += ['--depfile', '@DEPFILE@'] - res_target = build.CustomTarget(name, state.subdir, state.subproject, rcc_kwargs) - sources.append(res_target) - if ui_files: - if not self.uic.found(): - raise MesonException(err_msg.format('UIC', f'uic-qt{self.qt_version}', self.qt_version)) - arguments = uic_extra_arguments + ['-o', '@OUTPUT@', '@INPUT@'] - ui_kwargs = {'output': 'ui_@BASENAME@.h', - 'arguments': arguments} - ui_gen = build.Generator([self.uic], ui_kwargs) - ui_output = ui_gen.process_files(f'Qt{self.qt_version} ui', ui_files, state) - sources.append(ui_output) - inc = state.get_include_args(include_dirs=include_directories) - compile_args = [] - for dep in unholder(dependencies): - if isinstance(dep, Dependency): - for arg in dep.get_all_compile_args(): - if arg.startswith('-I') or arg.startswith('-D'): - compile_args.append(arg) - else: - raise MesonException('Argument is of an unacceptable type {!r}.\nMust be ' - 'either an external dependency (returned by find_library() or ' - 'dependency()) or an internal dependency (returned by ' - 'declare_dependency()).'.format(type(dep).__name__)) - if moc_headers: - arguments = moc_extra_arguments + inc + compile_args + ['@INPUT@', '-o', '@OUTPUT@'] - moc_kwargs = {'output': 'moc_@BASENAME@.cpp', - 'arguments': arguments} - moc_gen = build.Generator([self.moc], moc_kwargs) - moc_output = moc_gen.process_files(f'Qt{self.qt_version} moc header', moc_headers, state) - sources.append(moc_output) - if moc_sources: - arguments = moc_extra_arguments + inc + compile_args + ['@INPUT@', '-o', '@OUTPUT@'] - moc_kwargs = {'output': '@BASENAME@.moc', - 'arguments': arguments} - moc_gen = build.Generator([self.moc], moc_kwargs) - moc_output = moc_gen.process_files(f'Qt{self.qt_version} moc source', moc_sources, state) - sources.append(moc_output) - return ModuleReturnValue(sources, sources) + if not isinstance(args[0], str): + raise build.InvalidArguments('First argument to qt.preprocess must be a string') + rcc_kwargs['name'] = args[0] + sources.extend(self.compile_resources(state, tuple(), rcc_kwargs).return_value) + + if kwargs['ui_files']: + ui_kwargs: 'UICompilerKwArgs' = {'sources': kwargs['ui_files'], 'extra_args': kwargs['uic_extra_arguments'], 'method': method} + sources.extend(self.compile_ui(state, tuple(), ui_kwargs).return_value) + + if kwargs['moc_headers'] or kwargs['moc_sources']: + moc_kwargs: 'MocCompilerKwArgs' = { + 'extra_args': kwargs['moc_extra_arguments'], + 'sources': kwargs['moc_sources'], + 'headers': kwargs['moc_headers'], + 'include_directories': kwargs['include_directories'], + 'dependencies': kwargs['dependencies'], + 'method': method, + } + sources.extend(self.compile_moc(state, tuple(), moc_kwargs).return_value) + + return ModuleReturnValue(sources, [sources]) @FeatureNew('qt.compile_translations', '0.44.0') - @FeatureNewKwargs('qt.compile_translations', '0.56.0', ['qresource']) - @FeatureNewKwargs('qt.compile_translations', '0.56.0', ['rcc_extra_arguments']) - @permittedKwargs({'ts_files', 'qresource', 'rcc_extra_arguments', 'install', 'install_dir', 'build_by_default', 'method'}) - def compile_translations(self, state, args, kwargs): - ts_files, install_dir = [extract_as_list(kwargs, c, pop=True) for c in ['ts_files', 'install_dir']] - qresource = kwargs.get('qresource') + @noPosargs + @typed_kwargs( + 'qt.compile_translations', + KwargInfo('build_by_default', bool, default=False), + KwargInfo('install', bool, default=False), + KwargInfo('install_dir', str), + KwargInfo('method', str, default='auto'), + KwargInfo('qresource', str, since='0.56.0'), + KwargInfo('rcc_extra_arguments', ContainerTypeInfo(list, str), listify=True, default=[], since='0.56.0'), + KwargInfo('ts_files', ContainerTypeInfo(list, (str, File)), listify=True, default=[]), + ) + def compile_translations(self, state: 'ModuleState', args: T.Tuple, kwargs: 'CompileTranslationsKwArgs') -> ModuleReturnValue: + ts_files = kwargs['ts_files'] + install_dir = kwargs['install_dir'] + qresource = kwargs['qresource'] if qresource: if ts_files: raise MesonException('qt.compile_translations: Cannot specify both ts_files and qresource') if os.path.dirname(qresource) != '': raise MesonException('qt.compile_translations: qresource file name must not contain a subdirectory.') - qresource = File.from_built_file(state.subdir, qresource) - infile_abs = os.path.join(state.environment.source_dir, qresource.relative_name()) - outfile_abs = os.path.join(state.environment.build_dir, qresource.relative_name()) + qresource_file = File.from_built_file(state.subdir, qresource) + infile_abs = os.path.join(state.environment.source_dir, qresource_file.relative_name()) + outfile_abs = os.path.join(state.environment.build_dir, qresource_file.relative_name()) os.makedirs(os.path.dirname(outfile_abs), exist_ok=True) shutil.copy2(infile_abs, outfile_abs) self.interpreter.add_build_def_file(infile_abs) - rcc_file, nodes = self.qrc_nodes(state, qresource) + _, nodes = self._qrc_nodes(state, qresource_file) for c in nodes: if c.endswith('.qm'): - ts_files.append(c.rstrip('.qm')+'.ts') + ts_files.append(c.rstrip('.qm') + '.ts') else: raise MesonException(f'qt.compile_translations: qresource can only contain qm files, found {c}') - results = self.preprocess(state, [], {'qresources': qresource, 'rcc_extra_arguments': kwargs.get('rcc_extra_arguments', [])}) - self._detect_tools(state, kwargs.get('method', 'auto')) - translations = [] + results = self.preprocess(state, [], {'qresources': qresource, 'rcc_extra_arguments': kwargs['rcc_extra_arguments']}) + self._detect_tools(state, kwargs['method']) + translations: T.List[build.CustomTarget] = [] for ts in ts_files: if not self.lrelease.found(): raise MesonException('qt.compile_translations: ' + @@ -320,4 +506,4 @@ class QtBaseModule(ExtensionModule): if qresource: return ModuleReturnValue(results.return_value[0], [results.new_objects, translations]) else: - return ModuleReturnValue(translations, translations) + return ModuleReturnValue(translations, [translations]) diff --git a/mesonbuild/modules/unstable_rust.py b/mesonbuild/modules/unstable_rust.py index d5678ea..f602e09 100644 --- a/mesonbuild/modules/unstable_rust.py +++ b/mesonbuild/modules/unstable_rust.py @@ -199,7 +199,7 @@ class RustModule(ExtensionModule): if self._bindgen_bin is None: # there's some bugs in the interpreter typeing. - self._bindgen_bin = T.cast('ExternalProgram', state.find_program('bindgen').held_object) + self._bindgen_bin = state.find_program('bindgen').held_object name: str if isinstance(header, File): diff --git a/run_mypy.py b/run_mypy.py index 00daac6..982a3ae 100755 --- a/run_mypy.py +++ b/run_mypy.py @@ -34,6 +34,7 @@ modules = [ 'mesonbuild/mlog.py', 'mesonbuild/modules/fs.py', 'mesonbuild/modules/unstable_rust.py', + 'mesonbuild/modules/qt.py', 'mesonbuild/mparser.py', 'mesonbuild/msetup.py', 'mesonbuild/mtest.py', diff --git a/run_unittests.py b/run_unittests.py index d17c243..92557b2 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -1846,7 +1846,7 @@ class DataTests(unittest.TestCase): markdownfiles = [f.name for f in Path("docs/markdown").iterdir() if f.is_file() and f.suffix == '.md'] exceptions = ['_Sidebar.md'] for f in markdownfiles: - if f not in exceptions: + if f not in exceptions and not f.startswith('_include'): self.assertIn(f, toc) def test_vim_syntax_highlighting(self): diff --git a/test cases/frameworks/4 qt/meson.build b/test cases/frameworks/4 qt/meson.build index fb7bd5a..2e85ddb 100644 --- a/test cases/frameworks/4 qt/meson.build +++ b/test cases/frameworks/4 qt/meson.build @@ -52,9 +52,10 @@ foreach qt : ['qt4', 'qt5', 'qt6'] prep = qtmodule.preprocess( moc_headers : ['mainWindow.h'], # These need to be fed through the moc tool before use. - ui_files : 'mainWindow.ui', # XML files that need to be compiled with the uic tol. method : get_option('method') ) + # XML files that need to be compiled with the uic tol. + prep += qtmodule.compile_ui(sources : 'mainWindow.ui', method: get_option('method')) # Resource file(s) for rcc compiler extra_cpp_args = [] @@ -66,7 +67,11 @@ foreach qt : ['qt4', 'qt5', 'qt6'] endif # Test that setting a unique name with a positional argument works - qtmodule.preprocess(qt + 'teststuff', qresources : files(['stuff.qrc', 'stuff2.qrc']), method : get_option('method')) + qtmodule.compile_resources( + name : qt + 'teststuff', + sources : files(['stuff.qrc', 'stuff2.qrc']), + method : get_option('method') + ) # Test that passing extra arguments to rcc works # qt4-rcc and qt5-rcc take different arguments, for example qt4: ['-compress', '3']; qt5: '--compress=3' @@ -95,10 +100,10 @@ foreach qt : ['qt4', 'qt5', 'qt6'] # The build system needs to include the cpp files from # headers but the user must manually include moc # files from sources. - manpreprocessed = qtmodule.preprocess( - moc_extra_arguments : ['-DMOC_EXTRA_FLAG'], # This is just a random macro to test `moc_extra_arguments` - moc_sources : 'manualinclude.cpp', - moc_headers : 'manualinclude.h', + manpreprocessed = qtmodule.compile_moc( + extra_args : ['-DMOC_EXTRA_FLAG'], # This is just a random macro to test `extra_arguments` + sources : 'manualinclude.cpp', + headers : 'manualinclude.h', method : get_option('method')) qtmaninclude = executable(qt + 'maninclude', |