1# Copyright 2015-2016 The Meson development team
2
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6
7#     http://www.apache.org/licenses/LICENSE-2.0
8
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15'''This module provides helper functions for Gnome/GLib related
16functionality such as gobject-introspection, gresources and gtk-doc'''
17
18import os
19import copy
20import subprocess
21import functools
22import typing as T
23
24from .. import build
25from .. import mlog
26from .. import mesonlib
27from .. import interpreter
28from . import GResourceTarget, GResourceHeaderTarget, GirTarget, TypelibTarget, VapiTarget
29from . import ExtensionModule
30from . import ModuleReturnValue
31from ..mesonlib import (
32    MachineChoice, MesonException, OrderedSet, Popen_safe, extract_as_list,
33    join_args, HoldableObject
34)
35from ..dependencies import Dependency, PkgConfigDependency, InternalDependency
36from ..interpreterbase import noPosargs, noKwargs, permittedKwargs, FeatureNew, FeatureNewKwargs, FeatureDeprecatedKwargs, FeatureDeprecated
37from ..interpreterbase import typed_kwargs, KwargInfo, ContainerTypeInfo
38from ..programs import ExternalProgram, OverrideProgram
39from ..build import CustomTarget, CustomTargetIndex, GeneratedList
40
41if T.TYPE_CHECKING:
42    from ..compilers import Compiler
43    from ..interpreter import Interpreter
44
45# gresource compilation is broken due to the way
46# the resource compiler and Ninja clash about it
47#
48# https://github.com/ninja-build/ninja/issues/1184
49# https://bugzilla.gnome.org/show_bug.cgi?id=774368
50gresource_dep_needed_version = '>= 2.51.1'
51
52native_glib_version = None
53
54class GnomeModule(ExtensionModule):
55    def __init__(self, interpreter: 'Interpreter') -> None:
56        super().__init__(interpreter)
57        self.gir_dep = None
58        self.install_glib_compile_schemas = False
59        self.install_gio_querymodules = []
60        self.install_gtk_update_icon_cache = False
61        self.install_update_desktop_database = False
62        self.devenv = None
63        self.methods.update({
64            'post_install': self.post_install,
65            'compile_resources': self.compile_resources,
66            'generate_gir': self.generate_gir,
67            'compile_schemas': self.compile_schemas,
68            'yelp': self.yelp,
69            'gtkdoc': self.gtkdoc,
70            'gtkdoc_html_dir': self.gtkdoc_html_dir,
71            'gdbus_codegen': self.gdbus_codegen,
72            'mkenums': self.mkenums,
73            'mkenums_simple': self.mkenums_simple,
74            'genmarshal': self.genmarshal,
75            'generate_vapi': self.generate_vapi,
76        })
77
78    @staticmethod
79    def _get_native_glib_version(state):
80        global native_glib_version
81        if native_glib_version is None:
82            glib_dep = PkgConfigDependency('glib-2.0', state.environment,
83                                           {'native': True, 'required': False})
84            if glib_dep.found():
85                native_glib_version = glib_dep.get_version()
86            else:
87                mlog.warning('Could not detect glib version, assuming 2.54. '
88                             'You may get build errors if your glib is older.')
89                native_glib_version = '2.54'
90        return native_glib_version
91
92    @mesonlib.run_once
93    def __print_gresources_warning(self, state):
94        if not mesonlib.version_compare(self._get_native_glib_version(state),
95                                        gresource_dep_needed_version):
96            mlog.warning('GLib compiled dependencies do not work reliably with \n'
97                         'the current version of GLib. See the following upstream issue:',
98                         mlog.bold('https://bugzilla.gnome.org/show_bug.cgi?id=774368'))
99
100    @staticmethod
101    def _print_gdbus_warning():
102        mlog.warning('Code generated with gdbus_codegen() requires the root directory be added to\n'
103                     '  include_directories of targets with GLib < 2.51.3:',
104                     mlog.bold('https://github.com/mesonbuild/meson/issues/1387'),
105                     once=True)
106
107    def _get_dep(self, state, depname, native=False, required=True):
108        kwargs = {'native': native, 'required': required}
109        return self.interpreter.func_dependency(state.current_node, [depname], kwargs)
110
111    def _get_native_binary(self, state, name, depname, varname, required=True):
112        # Look in overrides in case glib/gtk/etc are built as subproject
113        prog = self.interpreter.program_from_overrides([name], [])
114        if prog is not None:
115            return prog
116
117        # Look in machine file
118        prog = state.environment.lookup_binary_entry(MachineChoice.HOST, name)
119        if prog is not None:
120            return ExternalProgram.from_entry(name, prog)
121
122        # Check if pkgconfig has a variable
123        dep = self._get_dep(state, depname, native=True, required=False)
124        if dep.found() and dep.type_name == 'pkgconfig':
125            value = dep.get_pkgconfig_variable(varname, {})
126            if value:
127                return ExternalProgram(name, value)
128
129        # Normal program lookup
130        return state.find_program(name, required=required)
131
132    @typed_kwargs('gnome.post_install',
133        KwargInfo('glib_compile_schemas', bool, default=False),
134        KwargInfo('gio_querymodules', ContainerTypeInfo(list, str), default=[], listify=True),
135        KwargInfo('gtk_update_icon_cache', bool, default=False),
136        KwargInfo('update_desktop_database', bool, default=False, since='0.59.0'),
137    )
138    @noPosargs
139    @FeatureNew('gnome.post_install', '0.57.0')
140    def post_install(self, state, args, kwargs):
141        rv = []
142        datadir_abs = os.path.join(state.environment.get_prefix(), state.environment.get_datadir())
143        if kwargs['glib_compile_schemas'] and not self.install_glib_compile_schemas:
144            self.install_glib_compile_schemas = True
145            prog = self._get_native_binary(state, 'glib-compile-schemas', 'gio-2.0', 'glib_compile_schemas')
146            schemasdir = os.path.join(datadir_abs, 'glib-2.0', 'schemas')
147            script = state.backend.get_executable_serialisation([prog, schemasdir])
148            script.skip_if_destdir = True
149            rv.append(script)
150        for d in kwargs['gio_querymodules']:
151            if d not in self.install_gio_querymodules:
152                self.install_gio_querymodules.append(d)
153                prog = self._get_native_binary(state, 'gio-querymodules', 'gio-2.0', 'gio_querymodules')
154                moduledir = os.path.join(state.environment.get_prefix(), d)
155                script = state.backend.get_executable_serialisation([prog, moduledir])
156                script.skip_if_destdir = True
157                rv.append(script)
158        if kwargs['gtk_update_icon_cache'] and not self.install_gtk_update_icon_cache:
159            self.install_gtk_update_icon_cache = True
160            prog = self._get_native_binary(state, 'gtk4-update-icon-cache', 'gtk-4.0', 'gtk4_update_icon_cache', required=False)
161            found = isinstance(prog, build.Executable) or prog.found()
162            if not found:
163                prog = self._get_native_binary(state, 'gtk-update-icon-cache', 'gtk+-3.0', 'gtk_update_icon_cache')
164            icondir = os.path.join(datadir_abs, 'icons', 'hicolor')
165            script = state.backend.get_executable_serialisation([prog, '-q', '-t', '-f', icondir])
166            script.skip_if_destdir = True
167            rv.append(script)
168        if kwargs['update_desktop_database'] and not self.install_update_desktop_database:
169            self.install_update_desktop_database = True
170            prog = self._get_native_binary(state, 'update-desktop-database', 'desktop-file-utils', 'update_desktop_database')
171            appdir = os.path.join(datadir_abs, 'applications')
172            script = state.backend.get_executable_serialisation([prog, '-q', appdir])
173            script.skip_if_destdir = True
174            rv.append(script)
175        return ModuleReturnValue(None, rv)
176
177    @FeatureNewKwargs('gnome.compile_resources', '0.37.0', ['gresource_bundle', 'export', 'install_header'])
178    @permittedKwargs({'source_dir', 'c_name', 'dependencies', 'export', 'gresource_bundle', 'install_header',
179                      'install', 'install_dir', 'extra_args', 'build_by_default'})
180    def compile_resources(self, state, args, kwargs):
181        self.__print_gresources_warning(state)
182        glib_version = self._get_native_glib_version(state)
183
184        glib_compile_resources = state.find_program('glib-compile-resources')
185        cmd = [glib_compile_resources, '@INPUT@']
186
187        source_dirs, dependencies = (mesonlib.extract_as_list(kwargs, c, pop=True) for c in ['source_dir', 'dependencies'])
188
189        if len(args) < 2:
190            raise MesonException('Not enough arguments; the name of the resource '
191                                 'and the path to the XML file are required')
192
193        # Validate dependencies
194        subdirs = []
195        depends = []
196        for (ii, dep) in enumerate(dependencies):
197            if isinstance(dep, mesonlib.File):
198                subdirs.append(dep.subdir)
199            elif isinstance(dep, (build.CustomTarget, build.CustomTargetIndex)):
200                depends.append(dep)
201                subdirs.append(dep.get_subdir())
202                if not mesonlib.version_compare(glib_version, gresource_dep_needed_version):
203                    m = 'The "dependencies" argument of gnome.compile_resources() can not\n' \
204                        'be used with the current version of glib-compile-resources due to\n' \
205                        '<https://bugzilla.gnome.org/show_bug.cgi?id=774368>'
206                    raise MesonException(m)
207            else:
208                m = f'Unexpected dependency type {dep!r} for gnome.compile_resources() ' \
209                    '"dependencies" argument.\nPlease pass the return value of ' \
210                    'custom_target() or configure_file()'
211                raise MesonException(m)
212
213        if not mesonlib.version_compare(glib_version, gresource_dep_needed_version):
214            ifile = args[1]
215            if isinstance(ifile, mesonlib.File):
216                # glib-compile-resources will be run inside the source dir,
217                # so we need either 'src_to_build' or the absolute path.
218                # Absolute path is the easiest choice.
219                if ifile.is_built:
220                    ifile = os.path.join(state.environment.get_build_dir(), ifile.subdir, ifile.fname)
221                else:
222                    ifile = os.path.join(ifile.subdir, ifile.fname)
223            elif isinstance(ifile, str):
224                ifile = os.path.join(state.subdir, ifile)
225            elif isinstance(ifile, (build.CustomTarget,
226                                    build.CustomTargetIndex,
227                                    build.GeneratedList)):
228                m = 'Resource xml files generated at build-time cannot be used ' \
229                    'with gnome.compile_resources() because we need to scan ' \
230                    'the xml for dependencies. Use configure_file() instead ' \
231                    'to generate it at configure-time.'
232                raise MesonException(m)
233            else:
234                raise MesonException(f'Invalid file argument: {ifile!r}')
235            depend_files, depends, subdirs = self._get_gresource_dependencies(
236                state, ifile, source_dirs, dependencies)
237
238        # Make source dirs relative to build dir now
239        source_dirs = [os.path.join(state.build_to_src, state.subdir, d) for d in source_dirs]
240        # Ensure build directories of generated deps are included
241        source_dirs += subdirs
242        # Always include current directory, but after paths set by user
243        source_dirs.append(os.path.join(state.build_to_src, state.subdir))
244
245        for source_dir in OrderedSet(source_dirs):
246            cmd += ['--sourcedir', source_dir]
247
248        if 'c_name' in kwargs:
249            cmd += ['--c-name', kwargs.pop('c_name')]
250        export = kwargs.pop('export', False)
251        if not export:
252            cmd += ['--internal']
253
254        cmd += ['--generate', '--target', '@OUTPUT@']
255
256        cmd += mesonlib.stringlistify(kwargs.pop('extra_args', []))
257
258        gresource = kwargs.pop('gresource_bundle', False)
259        if gresource:
260            output = args[0] + '.gresource'
261            name = args[0] + '_gresource'
262        else:
263            if 'c' in state.environment.coredata.compilers.host.keys():
264                output = args[0] + '.c'
265                name = args[0] + '_c'
266            elif 'cpp' in state.environment.coredata.compilers.host.keys():
267                output = args[0] + '.cpp'
268                name = args[0] + '_cpp'
269            else:
270                raise MesonException('Compiling GResources into code is only supported in C and C++ projects')
271
272        if kwargs.get('install', False) and not gresource:
273            raise MesonException('The install kwarg only applies to gresource bundles, see install_header')
274
275        install_header = kwargs.pop('install_header', False)
276        if install_header and gresource:
277            raise MesonException('The install_header kwarg does not apply to gresource bundles')
278        if install_header and not export:
279            raise MesonException('GResource header is installed yet export is not enabled')
280
281        c_kwargs = kwargs.copy()
282        c_kwargs['input'] = args[1]
283        c_kwargs['output'] = output
284        c_kwargs['depends'] = depends
285        c_kwargs.setdefault('install_dir', [])
286        if not mesonlib.version_compare(glib_version, gresource_dep_needed_version):
287            # This will eventually go out of sync if dependencies are added
288            c_kwargs['depend_files'] = depend_files
289            c_kwargs['command'] = cmd
290        else:
291            depfile = f'{output}.d'
292            c_kwargs['depfile'] = depfile
293            c_kwargs['command'] = copy.copy(cmd) + ['--dependency-file', '@DEPFILE@']
294        target_c = GResourceTarget(name, state.subdir, state.subproject, c_kwargs)
295
296        if gresource: # Only one target for .gresource files
297            return ModuleReturnValue(target_c, [target_c])
298
299        h_kwargs = {
300            'command': cmd,
301            'input': args[1],
302            'output': args[0] + '.h',
303            # The header doesn't actually care about the files yet it errors if missing
304            'depends': depends
305        }
306        if 'build_by_default' in kwargs:
307            h_kwargs['build_by_default'] = kwargs['build_by_default']
308        if install_header:
309            h_kwargs['install'] = install_header
310            h_kwargs['install_dir'] = kwargs.get('install_dir',
311                                                 state.environment.coredata.get_option(mesonlib.OptionKey('includedir')))
312        target_h = GResourceHeaderTarget(args[0] + '_h', state.subdir, state.subproject, h_kwargs)
313        rv = [target_c, target_h]
314        return ModuleReturnValue(rv, rv)
315
316    def _get_gresource_dependencies(self, state, input_file, source_dirs, dependencies):
317
318        cmd = ['glib-compile-resources',
319               input_file,
320               '--generate-dependencies']
321
322        # Prefer generated files over source files
323        cmd += ['--sourcedir', state.subdir] # Current build dir
324        for source_dir in source_dirs:
325            cmd += ['--sourcedir', os.path.join(state.subdir, source_dir)]
326
327        try:
328            pc, stdout, stderr = Popen_safe(cmd, cwd=state.environment.get_source_dir())
329        except (FileNotFoundError, PermissionError):
330            raise MesonException('Could not execute glib-compile-resources.')
331        if pc.returncode != 0:
332            m = f'glib-compile-resources failed to get dependencies for {cmd[1]}:\n{stderr}'
333            mlog.warning(m)
334            raise subprocess.CalledProcessError(pc.returncode, cmd)
335
336        dep_files = stdout.split('\n')[:-1]
337
338        depends = []
339        subdirs = []
340        for resfile in dep_files[:]:
341            resbasename = os.path.basename(resfile)
342            for dep in dependencies:
343                if isinstance(dep, mesonlib.File):
344                    if dep.fname != resbasename:
345                        continue
346                    dep_files.remove(resfile)
347                    dep_files.append(dep)
348                    subdirs.append(dep.subdir)
349                    break
350                elif isinstance(dep, (build.CustomTarget, build.CustomTargetIndex)):
351                    fname = None
352                    outputs = {(o, os.path.basename(o)) for o in dep.get_outputs()}
353                    for o, baseo in outputs:
354                        if baseo == resbasename:
355                            fname = o
356                            break
357                    if fname is not None:
358                        dep_files.remove(resfile)
359                        depends.append(dep)
360                        subdirs.append(dep.get_subdir())
361                        break
362            else:
363                # In generate-dependencies mode, glib-compile-resources doesn't raise
364                # an error for missing resources but instead prints whatever filename
365                # was listed in the input file.  That's good because it means we can
366                # handle resource files that get generated as part of the build, as
367                # follows.
368                #
369                # If there are multiple generated resource files with the same basename
370                # then this code will get confused.
371                try:
372                    f = mesonlib.File.from_source_file(state.environment.get_source_dir(),
373                                                       ".", resfile)
374                except MesonException:
375                    raise MesonException(
376                        'Resource "%s" listed in "%s" was not found. If this is a '
377                        'generated file, pass the target that generates it to '
378                        'gnome.compile_resources() using the "dependencies" '
379                        'keyword argument.' % (resfile, input_file))
380                dep_files.remove(resfile)
381                dep_files.append(f)
382        return dep_files, depends, subdirs
383
384    def _get_link_args(self, state, lib, depends, include_rpath=False,
385                       use_gir_args=False):
386        link_command = []
387        # Construct link args
388        if isinstance(lib, build.SharedLibrary):
389            libdir = os.path.join(state.environment.get_build_dir(), state.backend.get_target_dir(lib))
390            link_command.append('-L' + libdir)
391            if include_rpath:
392                link_command.append('-Wl,-rpath,' + libdir)
393            depends.append(lib)
394            # Needed for the following binutils bug:
395            # https://github.com/mesonbuild/meson/issues/1911
396            # However, g-ir-scanner does not understand -Wl,-rpath
397            # so we need to use -L instead
398            for d in state.backend.determine_rpath_dirs(lib):
399                d = os.path.join(state.environment.get_build_dir(), d)
400                link_command.append('-L' + d)
401                if include_rpath:
402                    link_command.append('-Wl,-rpath,' + d)
403        if use_gir_args and self._gir_has_option('--extra-library'):
404            link_command.append('--extra-library=' + lib.name)
405        else:
406            link_command.append('-l' + lib.name)
407        return link_command
408
409    def _get_dependencies_flags(self, deps, state, depends, include_rpath=False,
410                                use_gir_args=False, separate_nodedup=False):
411        cflags = OrderedSet()
412        internal_ldflags = OrderedSet()
413        external_ldflags = OrderedSet()
414        # External linker flags that can't be de-duped reliably because they
415        # require two args in order, such as -framework AVFoundation
416        external_ldflags_nodedup = []
417        gi_includes = OrderedSet()
418        deps = mesonlib.listify(deps)
419
420        for dep in deps:
421            if isinstance(dep, Dependency):
422                girdir = dep.get_variable(pkgconfig='girdir', internal='girdir', default_value='')
423                if girdir:
424                    gi_includes.update([girdir])
425            if isinstance(dep, InternalDependency):
426                cflags.update(dep.get_compile_args())
427                cflags.update(state.get_include_args(dep.include_directories))
428                for lib in dep.libraries:
429                    if isinstance(lib, build.SharedLibrary):
430                        internal_ldflags.update(self._get_link_args(state, lib, depends, include_rpath))
431                        libdepflags = self._get_dependencies_flags(lib.get_external_deps(), state, depends, include_rpath,
432                                                                   use_gir_args, True)
433                        cflags.update(libdepflags[0])
434                        internal_ldflags.update(libdepflags[1])
435                        external_ldflags.update(libdepflags[2])
436                        external_ldflags_nodedup += libdepflags[3]
437                        gi_includes.update(libdepflags[4])
438                extdepflags = self._get_dependencies_flags(dep.ext_deps, state, depends, include_rpath,
439                                                           use_gir_args, True)
440                cflags.update(extdepflags[0])
441                internal_ldflags.update(extdepflags[1])
442                external_ldflags.update(extdepflags[2])
443                external_ldflags_nodedup += extdepflags[3]
444                gi_includes.update(extdepflags[4])
445                for source in dep.sources:
446                    if isinstance(source, GirTarget):
447                        gi_includes.update([os.path.join(state.environment.get_build_dir(),
448                                            source.get_subdir())])
449            # This should be any dependency other than an internal one.
450            elif isinstance(dep, Dependency):
451                cflags.update(dep.get_compile_args())
452                ldflags = iter(dep.get_link_args(raw=True))
453                for lib in ldflags:
454                    if (os.path.isabs(lib) and
455                            # For PkgConfigDependency only:
456                            getattr(dep, 'is_libtool', False)):
457                        lib_dir = os.path.dirname(lib)
458                        external_ldflags.update(["-L%s" % lib_dir])
459                        if include_rpath:
460                            external_ldflags.update([f'-Wl,-rpath {lib_dir}'])
461                        libname = os.path.basename(lib)
462                        if libname.startswith("lib"):
463                            libname = libname[3:]
464                        libname = libname.split(".so")[0]
465                        lib = "-l%s" % libname
466                    # FIXME: Hack to avoid passing some compiler options in
467                    if lib.startswith("-W"):
468                        continue
469                    # If it's a framework arg, slurp the framework name too
470                    # to preserve the order of arguments
471                    if lib == '-framework':
472                        external_ldflags_nodedup += [lib, next(ldflags)]
473                    else:
474                        external_ldflags.update([lib])
475            elif isinstance(dep, (build.StaticLibrary, build.SharedLibrary)):
476                cflags.update(state.get_include_args(dep.get_include_dirs()))
477                depends.append(dep)
478            else:
479                mlog.log(f'dependency {dep!r} not handled to build gir files')
480                continue
481
482        if use_gir_args and self._gir_has_option('--extra-library'):
483            def fix_ldflags(ldflags):
484                fixed_ldflags = OrderedSet()
485                for ldflag in ldflags:
486                    if ldflag.startswith("-l"):
487                        ldflag = ldflag.replace('-l', '--extra-library=', 1)
488                    fixed_ldflags.add(ldflag)
489                return fixed_ldflags
490            internal_ldflags = fix_ldflags(internal_ldflags)
491            external_ldflags = fix_ldflags(external_ldflags)
492        if not separate_nodedup:
493            external_ldflags.update(external_ldflags_nodedup)
494            return cflags, internal_ldflags, external_ldflags, gi_includes
495        else:
496            return cflags, internal_ldflags, external_ldflags, external_ldflags_nodedup, gi_includes
497
498    def _unwrap_gir_target(self, girtarget, state):
499        if not isinstance(girtarget, (build.Executable, build.SharedLibrary,
500                                      build.StaticLibrary)):
501            raise MesonException(f'Gir target must be an executable or library but is "{girtarget}" of type {type(girtarget).__name__}')
502
503        STATIC_BUILD_REQUIRED_VERSION = ">=1.58.1"
504        if isinstance(girtarget, (build.StaticLibrary)) and \
505           not mesonlib.version_compare(
506               self._get_gir_dep(state)[0].get_version(),
507               STATIC_BUILD_REQUIRED_VERSION):
508            raise MesonException('Static libraries can only be introspected with GObject-Introspection ' + STATIC_BUILD_REQUIRED_VERSION)
509
510        return girtarget
511
512    def _devenv_prepend(self, varname: str, value: str) -> None:
513        if self.devenv is None:
514            self.devenv = build.EnvironmentVariables()
515            self.interpreter.build.devenv.append(self.devenv)
516        self.devenv.prepend(varname, [value])
517
518    def _get_gir_dep(self, state):
519        if not self.gir_dep:
520            self.gir_dep = self._get_dep(state, 'gobject-introspection-1.0')
521            self.giscanner = self._get_native_binary(state, 'g-ir-scanner', 'gobject-introspection-1.0', 'g_ir_scanner')
522            self.gicompiler = self._get_native_binary(state, 'g-ir-compiler', 'gobject-introspection-1.0', 'g_ir_compiler')
523        return self.gir_dep, self.giscanner, self.gicompiler
524
525    @functools.lru_cache(maxsize=None)
526    def _gir_has_option(self, option) -> bool:
527        exe = self.giscanner
528        if isinstance(exe, OverrideProgram):
529            # Handle overridden g-ir-scanner
530            assert option in ['--extra-library', '--sources-top-dirs']
531            return True
532        p, o, e = Popen_safe(exe.get_command() + ['--help'], stderr=subprocess.STDOUT)
533        return p.returncode == 0 and option in o
534
535    def _scan_header(self, kwargs):
536        ret = []
537        header = kwargs.pop('header', None)
538        if header:
539            if not isinstance(header, str):
540                raise MesonException('header must be a string')
541            ret = ['--c-include=' + header]
542        return ret
543
544    def _scan_extra_args(self, kwargs):
545        return mesonlib.stringlistify(kwargs.pop('extra_args', []))
546
547    def _scan_link_withs(self, state, depends, kwargs):
548        ret = []
549        if 'link_with' in kwargs:
550            link_with = mesonlib.extract_as_list(kwargs, 'link_with', pop = True)
551
552            for link in link_with:
553                ret += self._get_link_args(state, link, depends,
554                                           use_gir_args=True)
555        return ret
556
557    # May mutate depends and gir_inc_dirs
558    def _scan_include(self, state, depends, gir_inc_dirs, kwargs):
559        ret = []
560
561        if 'includes' in kwargs:
562            includes = mesonlib.extract_as_list(kwargs, 'includes', pop = True)
563            for inc in includes:
564                if isinstance(inc, str):
565                    ret += [f'--include={inc}']
566                elif isinstance(inc, GirTarget):
567                    gir_inc_dirs += [
568                        os.path.join(state.environment.get_build_dir(),
569                                     inc.get_subdir()),
570                    ]
571                    ret += [
572                        "--include-uninstalled={}".format(os.path.join(inc.get_subdir(), inc.get_basename()))
573                    ]
574                    depends += [inc]
575                else:
576                    raise MesonException(
577                        'Gir includes must be str, GirTarget, or list of them. '
578                        'Got %s.' % type(inc).__name__)
579
580        return ret
581
582    def _scan_symbol_prefix(self, kwargs):
583        ret = []
584
585        if 'symbol_prefix' in kwargs:
586            sym_prefixes = mesonlib.stringlistify(kwargs.pop('symbol_prefix', []))
587            ret += ['--symbol-prefix=%s' % sym_prefix for sym_prefix in sym_prefixes]
588
589        return ret
590
591    def _scan_identifier_prefix(self, kwargs):
592        ret = []
593
594        if 'identifier_prefix' in kwargs:
595            identifier_prefix = kwargs.pop('identifier_prefix')
596            if not isinstance(identifier_prefix, str):
597                raise MesonException('Gir identifier prefix must be str')
598            ret += ['--identifier-prefix=%s' % identifier_prefix]
599
600        return ret
601
602    def _scan_export_packages(self, kwargs):
603        ret = []
604
605        if 'export_packages' in kwargs:
606            pkgs = kwargs.pop('export_packages')
607            if isinstance(pkgs, str):
608                ret += ['--pkg-export=%s' % pkgs]
609            elif isinstance(pkgs, list):
610                ret += ['--pkg-export=%s' % pkg for pkg in pkgs]
611            else:
612                raise MesonException('Gir export packages must be str or list')
613
614        return ret
615
616    def _scan_inc_dirs(self, kwargs):
617        ret = mesonlib.extract_as_list(kwargs, 'include_directories', pop = True)
618        for incd in ret:
619            if not isinstance(incd, (str, build.IncludeDirs)):
620                raise MesonException(
621                    'Gir include dirs should be include_directories().')
622        return ret
623
624    def _scan_langs(self, state, langs):
625        ret = []
626
627        for lang in langs:
628            link_args = state.environment.coredata.get_external_link_args(MachineChoice.HOST, lang)
629            for link_arg in link_args:
630                if link_arg.startswith('-L'):
631                    ret.append(link_arg)
632
633        return ret
634
635    def _scan_gir_targets(self, state, girtargets):
636        ret = []
637
638        for girtarget in girtargets:
639            if isinstance(girtarget, build.Executable):
640                ret += ['--program', girtarget]
641            else:
642                # Because of https://gitlab.gnome.org/GNOME/gobject-introspection/merge_requests/72
643                # we can't use the full path until this is merged.
644                libpath = os.path.join(girtarget.get_subdir(), girtarget.get_filename())
645                # Must use absolute paths here because g-ir-scanner will not
646                # add them to the runtime path list if they're relative. This
647                # means we cannot use @BUILD_ROOT@
648                build_root = state.environment.get_build_dir()
649                if isinstance(girtarget, build.SharedLibrary):
650                    # need to put our output directory first as we need to use the
651                    # generated libraries instead of any possibly installed system/prefix
652                    # ones.
653                    ret += ["-L{}/{}".format(build_root, os.path.dirname(libpath))]
654                    libname = girtarget.get_basename()
655                else:
656                    libname = os.path.join(f"{build_root}/{libpath}")
657                ret += ['--library', libname]
658                # Needed for the following binutils bug:
659                # https://github.com/mesonbuild/meson/issues/1911
660                # However, g-ir-scanner does not understand -Wl,-rpath
661                # so we need to use -L instead
662                for d in state.backend.determine_rpath_dirs(girtarget):
663                    d = os.path.join(state.environment.get_build_dir(), d)
664                    ret.append('-L' + d)
665
666        return ret
667
668    def _get_girtargets_langs_compilers(self, girtargets: T.List[GirTarget]) -> T.List[T.Tuple[str, 'Compiler']]:
669        ret: T.List[T.Tuple[str, 'Compiler']] = []
670        for girtarget in girtargets:
671            for lang, compiler in girtarget.compilers.items():
672                # XXX: Can you use g-i with any other language?
673                if lang in ('c', 'cpp', 'objc', 'objcpp', 'd'):
674                    ret.append((lang, compiler))
675                    break
676
677        return ret
678
679    def _get_gir_targets_deps(self, girtargets):
680        ret = []
681        for girtarget in girtargets:
682            ret += girtarget.get_all_link_deps()
683            ret += girtarget.get_external_deps()
684        return ret
685
686    def _get_gir_targets_inc_dirs(self, girtargets):
687        ret = []
688        for girtarget in girtargets:
689            ret += girtarget.get_include_dirs()
690        return ret
691
692    def _get_langs_compilers_flags(self, state, langs_compilers: T.List[T.Tuple[str, 'Compiler']]):
693        cflags = []
694        internal_ldflags = []
695        external_ldflags = []
696
697        for lang, compiler in langs_compilers:
698            if state.global_args.get(lang):
699                cflags += state.global_args[lang]
700            if state.project_args.get(lang):
701                cflags += state.project_args[lang]
702            if mesonlib.OptionKey('b_sanitize') in compiler.base_options:
703                sanitize = state.environment.coredata.options[mesonlib.OptionKey('b_sanitize')].value
704                cflags += compiler.sanitizer_compile_args(sanitize)
705                sanitize = sanitize.split(',')
706                # These must be first in ldflags
707                if 'address' in sanitize:
708                    internal_ldflags += ['-lasan']
709                if 'thread' in sanitize:
710                    internal_ldflags += ['-ltsan']
711                if 'undefined' in sanitize:
712                    internal_ldflags += ['-lubsan']
713                # FIXME: Linking directly to lib*san is not recommended but g-ir-scanner
714                # does not understand -f LDFLAGS. https://bugzilla.gnome.org/show_bug.cgi?id=783892
715                # ldflags += compiler.sanitizer_link_args(sanitize)
716
717        return cflags, internal_ldflags, external_ldflags
718
719    def _make_gir_filelist(self, state, srcdir, ns, nsversion, girtargets, libsources):
720        gir_filelist_dir = state.backend.get_target_private_dir_abs(girtargets[0])
721        if not os.path.isdir(gir_filelist_dir):
722            os.mkdir(gir_filelist_dir)
723        gir_filelist_filename = os.path.join(gir_filelist_dir, f'{ns}_{nsversion}_gir_filelist')
724
725        with open(gir_filelist_filename, 'w', encoding='utf-8') as gir_filelist:
726            for s in libsources:
727                if isinstance(s, (build.CustomTarget, build.CustomTargetIndex)):
728                    for custom_output in s.get_outputs():
729                        gir_filelist.write(os.path.join(state.environment.get_build_dir(),
730                                                        state.backend.get_target_dir(s),
731                                                        custom_output) + '\n')
732                elif isinstance(s, mesonlib.File):
733                    gir_filelist.write(s.rel_to_builddir(state.build_to_src) + '\n')
734                elif isinstance(s, build.GeneratedList):
735                    for gen_src in s.get_outputs():
736                        gir_filelist.write(os.path.join(srcdir, gen_src) + '\n')
737                else:
738                    gir_filelist.write(os.path.join(srcdir, s) + '\n')
739
740        return gir_filelist_filename
741
742    def _make_gir_target(self, state, girfile, scan_command, generated_files, depends, kwargs):
743        scankwargs = {'input': generated_files,
744                      'output': girfile,
745                      'command': scan_command,
746                      'depends': depends}
747
748        if 'install' in kwargs:
749            install_dir = kwargs.get('install_dir_gir',
750                                     os.path.join(state.environment.get_datadir(), 'gir-1.0'))
751            if install_dir is not False:
752                scankwargs['install_dir'] = install_dir
753                scankwargs['install'] = kwargs['install']
754                scankwargs['install_tag'] = 'devel'
755
756        if 'build_by_default' in kwargs:
757            scankwargs['build_by_default'] = kwargs['build_by_default']
758
759        return GirTarget(girfile, state.subdir, state.subproject, scankwargs)
760
761    def _make_typelib_target(self, state, typelib_output, typelib_cmd, generated_files, kwargs):
762        typelib_kwargs = {
763            'input': generated_files,
764            'output': typelib_output,
765            'command': typelib_cmd,
766        }
767
768        if 'install' in kwargs:
769            install_dir = kwargs.get('install_dir_typelib',
770                                     os.path.join(state.environment.get_libdir(), 'girepository-1.0'))
771            if install_dir is not False:
772                typelib_kwargs['install'] = kwargs['install']
773                typelib_kwargs['install_dir'] = install_dir
774                typelib_kwargs['install_tag'] = 'typelib'
775
776        if 'build_by_default' in kwargs:
777            typelib_kwargs['build_by_default'] = kwargs['build_by_default']
778
779        return TypelibTarget(typelib_output, state.subdir, state.subproject, typelib_kwargs)
780
781    # May mutate depends
782    def _gather_typelib_includes_and_update_depends(self, state, deps, depends):
783        # Need to recursively add deps on GirTarget sources from our
784        # dependencies and also find the include directories needed for the
785        # typelib generation custom target below.
786        typelib_includes = []
787        for dep in deps:
788            # Add a dependency on each GirTarget listed in dependencies and add
789            # the directory where it will be generated to the typelib includes
790            if isinstance(dep, InternalDependency):
791                for source in dep.sources:
792                    if isinstance(source, GirTarget) and source not in depends:
793                        depends.append(source)
794                        subdir = os.path.join(state.environment.get_build_dir(),
795                                              source.get_subdir())
796                        if subdir not in typelib_includes:
797                            typelib_includes.append(subdir)
798            # Do the same, but for dependencies of dependencies. These are
799            # stored in the list of generated sources for each link dep (from
800            # girtarget.get_all_link_deps() above).
801            # FIXME: Store this in the original form from declare_dependency()
802            # so it can be used here directly.
803            elif isinstance(dep, build.SharedLibrary):
804                for source in dep.generated:
805                    if isinstance(source, GirTarget):
806                        subdir = os.path.join(state.environment.get_build_dir(),
807                                              source.get_subdir())
808                        if subdir not in typelib_includes:
809                            typelib_includes.append(subdir)
810            if isinstance(dep, Dependency):
811                girdir = dep.get_variable(pkgconfig='girdir', internal='girdir', default_value='')
812                if girdir and girdir not in typelib_includes:
813                    typelib_includes.append(girdir)
814        return typelib_includes
815
816    def _get_external_args_for_langs(self, state, langs):
817        ret = []
818        for lang in langs:
819            ret += state.environment.coredata.get_external_args(MachineChoice.HOST, lang)
820        return ret
821
822    @staticmethod
823    def _get_scanner_cflags(cflags):
824        'g-ir-scanner only accepts -I/-D/-U; must ignore all other flags'
825        for f in cflags:
826            # _FORTIFY_SOURCE depends on / works together with -O, on the other hand this
827            # just invokes the preprocessor anyway
828            if f.startswith(('-D', '-U', '-I')) and not f.startswith('-D_FORTIFY_SOURCE'):
829                yield f
830
831    @staticmethod
832    def _get_scanner_ldflags(ldflags):
833        'g-ir-scanner only accepts -L/-l; must ignore -F and other linker flags'
834        for f in ldflags:
835            if f.startswith(('-L', '-l', '--extra-library')):
836                yield f
837
838    @FeatureNewKwargs('generate_gir', '0.55.0', ['fatal_warnings'])
839    @FeatureNewKwargs('generate_gir', '0.40.0', ['build_by_default'])
840    @permittedKwargs({'sources', 'nsversion', 'namespace', 'symbol_prefix', 'identifier_prefix',
841                      'export_packages', 'includes', 'dependencies', 'link_with', 'include_directories',
842                      'install', 'install_dir_gir', 'install_dir_typelib', 'extra_args',
843                      'packages', 'header', 'build_by_default', 'fatal_warnings'})
844    def generate_gir(self, state, args, kwargs: T.Dict[str, T.Any]):
845        if not args:
846            raise MesonException('generate_gir takes at least one argument')
847        if kwargs.get('install_dir'):
848            raise MesonException('install_dir is not supported with generate_gir(), see "install_dir_gir" and "install_dir_typelib"')
849
850        girtargets = [self._unwrap_gir_target(arg, state) for arg in args]
851
852        if len(girtargets) > 1 and any([isinstance(el, build.Executable) for el in girtargets]):
853            raise MesonException('generate_gir only accepts a single argument when one of the arguments is an executable')
854
855        gir_dep, giscanner, gicompiler = self._get_gir_dep(state)
856
857        ns = kwargs.get('namespace')
858        if not ns:
859            raise MesonException('Missing "namespace" keyword argument')
860        nsversion = kwargs.get('nsversion')
861        if not nsversion:
862            raise MesonException('Missing "nsversion" keyword argument')
863        libsources = mesonlib.extract_as_list(kwargs, 'sources', pop=True)
864        girfile = f'{ns}-{nsversion}.gir'
865        srcdir = os.path.join(state.environment.get_source_dir(), state.subdir)
866        builddir = os.path.join(state.environment.get_build_dir(), state.subdir)
867        depends = gir_dep.sources + girtargets
868        gir_inc_dirs = []
869        langs_compilers = self._get_girtargets_langs_compilers(girtargets)
870        cflags, internal_ldflags, external_ldflags = self._get_langs_compilers_flags(state, langs_compilers)
871        deps = self._get_gir_targets_deps(girtargets)
872        deps += extract_as_list(kwargs, 'dependencies', pop=True)
873        deps += [gir_dep]
874        typelib_includes = self._gather_typelib_includes_and_update_depends(state, deps, depends)
875        # ldflags will be misinterpreted by gir scanner (showing
876        # spurious dependencies) but building GStreamer fails if they
877        # are not used here.
878        dep_cflags, dep_internal_ldflags, dep_external_ldflags, gi_includes = \
879            self._get_dependencies_flags(deps, state, depends, use_gir_args=True)
880        scan_cflags = []
881        scan_cflags += list(self._get_scanner_cflags(cflags))
882        scan_cflags += list(self._get_scanner_cflags(dep_cflags))
883        scan_cflags += list(self._get_scanner_cflags(self._get_external_args_for_langs(state, [lc[0] for lc in langs_compilers])))
884        scan_internal_ldflags = []
885        scan_internal_ldflags += list(self._get_scanner_ldflags(internal_ldflags))
886        scan_internal_ldflags += list(self._get_scanner_ldflags(dep_internal_ldflags))
887        scan_external_ldflags = []
888        scan_external_ldflags += list(self._get_scanner_ldflags(external_ldflags))
889        scan_external_ldflags += list(self._get_scanner_ldflags(dep_external_ldflags))
890        girtargets_inc_dirs = self._get_gir_targets_inc_dirs(girtargets)
891        inc_dirs = self._scan_inc_dirs(kwargs)
892
893        scan_command = [giscanner]
894        scan_command += ['--no-libtool']
895        scan_command += ['--namespace=' + ns, '--nsversion=' + nsversion]
896        scan_command += ['--warn-all']
897        scan_command += ['--output', '@OUTPUT@']
898        scan_command += self._scan_header(kwargs)
899        scan_command += self._scan_extra_args(kwargs)
900        scan_command += ['-I' + srcdir, '-I' + builddir]
901        scan_command += state.get_include_args(girtargets_inc_dirs)
902        scan_command += ['--filelist=' + self._make_gir_filelist(state, srcdir, ns, nsversion, girtargets, libsources)]
903        scan_command += self._scan_link_withs(state, depends, kwargs)
904        scan_command += self._scan_include(state, depends, gir_inc_dirs, kwargs)
905        scan_command += self._scan_symbol_prefix(kwargs)
906        scan_command += self._scan_identifier_prefix(kwargs)
907        scan_command += self._scan_export_packages(kwargs)
908        scan_command += ['--cflags-begin']
909        scan_command += scan_cflags
910        scan_command += ['--cflags-end']
911        scan_command += state.get_include_args(inc_dirs)
912        scan_command += state.get_include_args(list(gi_includes) + gir_inc_dirs + inc_dirs, prefix='--add-include-path=')
913        scan_command += list(scan_internal_ldflags)
914        scan_command += self._scan_gir_targets(state, girtargets)
915        scan_command += self._scan_langs(state, [lc[0] for lc in langs_compilers])
916        scan_command += list(scan_external_ldflags)
917
918        if self._gir_has_option('--sources-top-dirs'):
919            scan_command += ['--sources-top-dirs', os.path.join(state.environment.get_source_dir(), self.interpreter.subproject_dir, state.subproject)]
920            scan_command += ['--sources-top-dirs', os.path.join(state.environment.get_build_dir(), self.interpreter.subproject_dir, state.subproject)]
921
922        if '--warn-error' in scan_command:
923            mlog.deprecation('Passing --warn-error is deprecated in favor of "fatal_warnings" keyword argument since v0.55')
924        fatal_warnings = kwargs.get('fatal_warnings', False)
925        if not isinstance(fatal_warnings, bool):
926            raise MesonException('fatal_warnings keyword argument must be a boolean')
927        if fatal_warnings:
928            scan_command.append('--warn-error')
929
930        generated_files = [f for f in libsources if isinstance(f, (GeneratedList, CustomTarget, CustomTargetIndex))]
931
932        scan_target = self._make_gir_target(state, girfile, scan_command, generated_files, depends, kwargs)
933
934        typelib_output = f'{ns}-{nsversion}.typelib'
935        typelib_cmd = [gicompiler, scan_target, '--output', '@OUTPUT@']
936        typelib_cmd += state.get_include_args(gir_inc_dirs, prefix='--includedir=')
937
938        for incdir in typelib_includes:
939            typelib_cmd += ["--includedir=" + incdir]
940
941        typelib_target = self._make_typelib_target(state, typelib_output, typelib_cmd, generated_files, kwargs)
942
943        self._devenv_prepend('GI_TYPELIB_PATH', os.path.join(state.environment.get_build_dir(), state.subdir))
944
945        rv = [scan_target, typelib_target]
946
947        return ModuleReturnValue(rv, rv)
948
949    @FeatureNewKwargs('build target', '0.40.0', ['build_by_default'])
950    @permittedKwargs({'build_by_default', 'depend_files'})
951    def compile_schemas(self, state, args, kwargs):
952        if args:
953            raise MesonException('Compile_schemas does not take positional arguments.')
954        srcdir = os.path.join(state.build_to_src, state.subdir)
955        outdir = state.subdir
956
957        cmd = [state.find_program('glib-compile-schemas')]
958        cmd += ['--targetdir', outdir, srcdir]
959        kwargs['command'] = cmd
960        kwargs['input'] = []
961        kwargs['output'] = 'gschemas.compiled'
962        if state.subdir == '':
963            targetname = 'gsettings-compile'
964        else:
965            targetname = 'gsettings-compile-' + state.subdir.replace('/', '_')
966        target_g = build.CustomTarget(targetname, state.subdir, state.subproject, kwargs)
967        self._devenv_prepend('GSETTINGS_SCHEMA_DIR', os.path.join(state.environment.get_build_dir(), state.subdir))
968        return ModuleReturnValue(target_g, [target_g])
969
970    @permittedKwargs({'sources', 'media', 'symlink_media', 'languages'})
971    @FeatureDeprecatedKwargs('gnome.yelp', '0.43.0', ['languages'],
972                             'Use a LINGUAS file in the source directory instead')
973    def yelp(self, state, args, kwargs):
974        if len(args) < 1:
975            raise MesonException('Yelp requires a project id')
976
977        project_id = args[0]
978        if len(args) > 1:
979            FeatureDeprecated.single_use('gnome.yelp more than one positional argument', '0.60.0', 'use the "sources" keyword argument instead.')
980
981        sources = mesonlib.stringlistify(kwargs.pop('sources', []))
982        if not sources:
983            if len(args) > 1:
984                sources = mesonlib.stringlistify(args[1:])
985            if not sources:
986                raise MesonException('Yelp requires a list of sources')
987        else:
988            if len(args) > 1:
989                mlog.warning('"gnome.yelp" ignores positional sources arguments when the "sources" keyword argument is set')
990        source_str = '@@'.join(sources)
991
992        langs = mesonlib.stringlistify(kwargs.pop('languages', []))
993        media = mesonlib.stringlistify(kwargs.pop('media', []))
994        symlinks = kwargs.pop('symlink_media', True)
995
996        if not isinstance(symlinks, bool):
997            raise MesonException('symlink_media must be a boolean')
998
999        if kwargs:
1000            raise MesonException('Unknown arguments passed: {}'.format(', '.join(kwargs.keys())))
1001
1002        script = state.environment.get_build_command()
1003        args = ['--internal',
1004                'yelphelper',
1005                'install',
1006                '--subdir=' + state.subdir,
1007                '--id=' + project_id,
1008                '--installdir=' + os.path.join(state.environment.get_datadir(), 'help'),
1009                '--sources=' + source_str]
1010        if symlinks:
1011            args.append('--symlinks=true')
1012        if media:
1013            args.append('--media=' + '@@'.join(media))
1014        if langs:
1015            args.append('--langs=' + '@@'.join(langs))
1016        inscript = state.backend.get_executable_serialisation(script + args)
1017
1018        potargs = state.environment.get_build_command() + [
1019            '--internal', 'yelphelper', 'pot',
1020            '--subdir=' + state.subdir,
1021            '--id=' + project_id,
1022            '--sources=' + source_str,
1023        ]
1024        pottarget = build.RunTarget('help-' + project_id + '-pot', potargs,
1025                                    [], state.subdir, state.subproject)
1026
1027        poargs = state.environment.get_build_command() + [
1028            '--internal', 'yelphelper', 'update-po',
1029            '--subdir=' + state.subdir,
1030            '--id=' + project_id,
1031            '--sources=' + source_str,
1032            '--langs=' + '@@'.join(langs),
1033        ]
1034        potarget = build.RunTarget('help-' + project_id + '-update-po', poargs,
1035                                   [], state.subdir, state.subproject)
1036
1037        rv = [inscript, pottarget, potarget]
1038        return ModuleReturnValue(None, rv)
1039
1040    @FeatureNewKwargs('gnome.gtkdoc', '0.52.0', ['check'])
1041    @FeatureNewKwargs('gnome.gtkdoc', '0.48.0', ['c_args'])
1042    @FeatureNewKwargs('gnome.gtkdoc', '0.48.0', ['module_version'])
1043    @FeatureNewKwargs('gnome.gtkdoc', '0.37.0', ['namespace', 'mode'])
1044    @permittedKwargs({'main_xml', 'main_sgml', 'src_dir', 'dependencies', 'install',
1045                      'install_dir', 'scan_args', 'scanobjs_args', 'gobject_typesfile',
1046                      'fixxref_args', 'html_args', 'html_assets', 'content_files',
1047                      'mkdb_args', 'ignore_headers', 'include_directories',
1048                      'namespace', 'mode', 'expand_content_files', 'module_version',
1049                      'c_args', 'check'})
1050    def gtkdoc(self, state, args, kwargs):
1051        if len(args) != 1:
1052            raise MesonException('Gtkdoc must have one positional argument.')
1053        modulename = args[0]
1054        if not isinstance(modulename, str):
1055            raise MesonException('Gtkdoc arg must be string.')
1056        if 'src_dir' not in kwargs:
1057            raise MesonException('Keyword argument src_dir missing.')
1058        main_file = kwargs.get('main_sgml', '')
1059        if not isinstance(main_file, str):
1060            raise MesonException('Main sgml keyword argument must be a string.')
1061        main_xml = kwargs.get('main_xml', '')
1062        if not isinstance(main_xml, str):
1063            raise MesonException('Main xml keyword argument must be a string.')
1064        moduleversion = kwargs.get('module_version', '')
1065        if not isinstance(moduleversion, str):
1066            raise MesonException('Module version keyword argument must be a string.')
1067        if main_xml != '':
1068            if main_file != '':
1069                raise MesonException('You can only specify main_xml or main_sgml, not both.')
1070            main_file = main_xml
1071        targetname = modulename + ('-' + moduleversion if moduleversion else '') + '-doc'
1072        command = state.environment.get_build_command()
1073
1074        namespace = kwargs.get('namespace', '')
1075        mode = kwargs.get('mode', 'auto')
1076        VALID_MODES = ('xml', 'sgml', 'none', 'auto')
1077        if mode not in VALID_MODES:
1078            raise MesonException(f'gtkdoc: Mode {mode} is not a valid mode: {VALID_MODES}')
1079
1080        src_dirs = mesonlib.extract_as_list(kwargs, 'src_dir')
1081        header_dirs = []
1082        for src_dir in src_dirs:
1083            if isinstance(src_dir, HoldableObject):
1084                if not isinstance(src_dir, build.IncludeDirs):
1085                    raise MesonException('Invalid keyword argument for src_dir.')
1086                for inc_dir in src_dir.get_incdirs():
1087                    header_dirs.append(os.path.join(state.environment.get_source_dir(),
1088                                                    src_dir.get_curdir(), inc_dir))
1089                    header_dirs.append(os.path.join(state.environment.get_build_dir(),
1090                                                    src_dir.get_curdir(), inc_dir))
1091            else:
1092                header_dirs.append(src_dir)
1093
1094        args = ['--internal', 'gtkdoc',
1095                '--sourcedir=' + state.environment.get_source_dir(),
1096                '--builddir=' + state.environment.get_build_dir(),
1097                '--subdir=' + state.subdir,
1098                '--headerdirs=' + '@@'.join(header_dirs),
1099                '--mainfile=' + main_file,
1100                '--modulename=' + modulename,
1101                '--moduleversion=' + moduleversion,
1102                '--mode=' + mode]
1103        for tool in ['scan', 'scangobj', 'mkdb', 'mkhtml', 'fixxref']:
1104            program_name = 'gtkdoc-' + tool
1105            program = state.find_program(program_name)
1106            path = program.get_path()
1107            args.append(f'--{program_name}={path}')
1108        if namespace:
1109            args.append('--namespace=' + namespace)
1110        args += self._unpack_args('--htmlargs=', 'html_args', kwargs)
1111        args += self._unpack_args('--scanargs=', 'scan_args', kwargs)
1112        args += self._unpack_args('--scanobjsargs=', 'scanobjs_args', kwargs)
1113        args += self._unpack_args('--gobjects-types-file=', 'gobject_typesfile', kwargs, state)
1114        args += self._unpack_args('--fixxrefargs=', 'fixxref_args', kwargs)
1115        args += self._unpack_args('--mkdbargs=', 'mkdb_args', kwargs)
1116        args += self._unpack_args('--html-assets=', 'html_assets', kwargs, state)
1117
1118        depends = []
1119        content_files = []
1120        for s in mesonlib.extract_as_list(kwargs, 'content_files'):
1121            if isinstance(s, (build.CustomTarget, build.CustomTargetIndex)):
1122                depends.append(s)
1123                for o in s.get_outputs():
1124                    content_files.append(os.path.join(state.environment.get_build_dir(),
1125                                                      state.backend.get_target_dir(s),
1126                                                      o))
1127            elif isinstance(s, mesonlib.File):
1128                content_files.append(s.absolute_path(state.environment.get_source_dir(),
1129                                                     state.environment.get_build_dir()))
1130            elif isinstance(s, build.GeneratedList):
1131                depends.append(s)
1132                for gen_src in s.get_outputs():
1133                    content_files.append(os.path.join(state.environment.get_source_dir(),
1134                                                      state.subdir,
1135                                                      gen_src))
1136            elif isinstance(s, str):
1137                content_files.append(os.path.join(state.environment.get_source_dir(),
1138                                                  state.subdir,
1139                                                  s))
1140            else:
1141                raise MesonException(
1142                    f'Invalid object type: {s.__class__.__name__!r}')
1143        args += ['--content-files=' + '@@'.join(content_files)]
1144
1145        args += self._unpack_args('--expand-content-files=', 'expand_content_files', kwargs, state)
1146        args += self._unpack_args('--ignore-headers=', 'ignore_headers', kwargs)
1147        args += self._unpack_args('--installdir=', 'install_dir', kwargs)
1148        args += self._get_build_args(kwargs, state, depends)
1149        custom_kwargs = {'output': modulename + '-decl.txt',
1150                         'command': command + args,
1151                         'depends': depends,
1152                         'build_always_stale': True,
1153                         }
1154        custom_target = build.CustomTarget(targetname, state.subdir, state.subproject, custom_kwargs)
1155        alias_target = build.AliasTarget(targetname, [custom_target], state.subdir, state.subproject)
1156        if kwargs.get('check', False):
1157            check_cmd = state.find_program('gtkdoc-check')
1158            check_env = ['DOC_MODULE=' + modulename,
1159                         'DOC_MAIN_SGML_FILE=' + main_file]
1160            check_args = [targetname + '-check', check_cmd]
1161            check_workdir = os.path.join(state.environment.get_build_dir(), state.subdir)
1162            state.test(check_args, env=check_env, workdir=check_workdir, depends=custom_target)
1163        res = [custom_target, alias_target]
1164        if kwargs.get('install', True):
1165            res.append(state.backend.get_executable_serialisation(command + args, tag='doc'))
1166        return ModuleReturnValue(custom_target, res)
1167
1168    def _get_build_args(self, kwargs, state, depends):
1169        args = []
1170        deps = extract_as_list(kwargs, 'dependencies')
1171        cflags = []
1172        cflags.extend(mesonlib.stringlistify(kwargs.pop('c_args', [])))
1173        deps_cflags, internal_ldflags, external_ldflags, gi_includes = \
1174            self._get_dependencies_flags(deps, state, depends, include_rpath=True)
1175        inc_dirs = mesonlib.extract_as_list(kwargs, 'include_directories')
1176        for incd in inc_dirs:
1177            if not isinstance(incd, (str, build.IncludeDirs)):
1178                raise MesonException(
1179                    'Gir include dirs should be include_directories().')
1180
1181        cflags.extend(deps_cflags)
1182        cflags.extend(state.get_include_args(inc_dirs))
1183        ldflags = []
1184        ldflags.extend(internal_ldflags)
1185        ldflags.extend(external_ldflags)
1186
1187        cflags.extend(state.environment.coredata.get_external_args(MachineChoice.HOST, 'c'))
1188        ldflags.extend(state.environment.coredata.get_external_link_args(MachineChoice.HOST, 'c'))
1189        compiler = state.environment.coredata.compilers[MachineChoice.HOST]['c']
1190
1191        compiler_flags = self._get_langs_compilers_flags(state, [('c', compiler)])
1192        cflags.extend(compiler_flags[0])
1193        ldflags.extend(compiler_flags[1])
1194        ldflags.extend(compiler_flags[2])
1195        if compiler:
1196            args += ['--cc=%s' % join_args(compiler.get_exelist())]
1197            args += ['--ld=%s' % join_args(compiler.get_linker_exelist())]
1198        if cflags:
1199            args += ['--cflags=%s' % join_args(cflags)]
1200        if ldflags:
1201            args += ['--ldflags=%s' % join_args(ldflags)]
1202
1203        return args
1204
1205    @noKwargs
1206    def gtkdoc_html_dir(self, state, args, kwargs):
1207        if len(args) != 1:
1208            raise MesonException('Must have exactly one argument.')
1209        modulename = args[0]
1210        if not isinstance(modulename, str):
1211            raise MesonException('Argument must be a string')
1212        return os.path.join('share/gtk-doc/html', modulename)
1213
1214    @staticmethod
1215    def _unpack_args(arg, kwarg_name, kwargs, expend_file_state=None):
1216        if kwarg_name not in kwargs:
1217            return []
1218
1219        new_args = mesonlib.extract_as_list(kwargs, kwarg_name)
1220        args = []
1221        for i in new_args:
1222            if expend_file_state and isinstance(i, mesonlib.File):
1223                i = i.absolute_path(expend_file_state.environment.get_source_dir(), expend_file_state.environment.get_build_dir())
1224            elif expend_file_state and isinstance(i, str):
1225                i = os.path.join(expend_file_state.environment.get_source_dir(), expend_file_state.subdir, i)
1226            elif not isinstance(i, str):
1227                raise MesonException(kwarg_name + ' values must be strings.')
1228            args.append(i)
1229
1230        if args:
1231            return [arg + '@@'.join(args)]
1232
1233        return []
1234
1235    def _get_autocleanup_args(self, kwargs, glib_version):
1236        if not mesonlib.version_compare(glib_version, '>= 2.49.1'):
1237            # Warn if requested, silently disable if not
1238            if 'autocleanup' in kwargs:
1239                mlog.warning(f'Glib version ({glib_version}) is too old to support the \'autocleanup\' '
1240                             'kwarg, need 2.49.1 or newer')
1241            return []
1242        autocleanup = kwargs.pop('autocleanup', 'all')
1243        values = ('none', 'objects', 'all')
1244        if autocleanup not in values:
1245            raise MesonException('gdbus_codegen does not support {!r} as an autocleanup value, '
1246                                 'must be one of: {!r}'.format(autocleanup, ', '.join(values)))
1247        return ['--c-generate-autocleanup', autocleanup]
1248
1249    @FeatureNewKwargs('build target', '0.46.0', ['install_header', 'install_dir', 'sources'])
1250    @FeatureNewKwargs('build target', '0.40.0', ['build_by_default'])
1251    @FeatureNewKwargs('build target', '0.47.0', ['extra_args', 'autocleanup'])
1252    @permittedKwargs({'interface_prefix', 'namespace', 'extra_args', 'autocleanup', 'object_manager', 'build_by_default',
1253                      'annotations', 'docbook', 'install_header', 'install_dir', 'sources'})
1254    def gdbus_codegen(self, state, args, kwargs):
1255        if len(args) not in (1, 2):
1256            raise MesonException('gdbus_codegen takes at most two arguments, name and xml file.')
1257        namebase = args[0]
1258        xml_files = args[1:]
1259        cmd = [state.find_program('gdbus-codegen')]
1260        extra_args = mesonlib.stringlistify(kwargs.pop('extra_args', []))
1261        cmd += extra_args
1262        # Autocleanup supported?
1263        glib_version = self._get_native_glib_version(state)
1264        cmd += self._get_autocleanup_args(kwargs, glib_version)
1265        if 'interface_prefix' in kwargs:
1266            cmd += ['--interface-prefix', kwargs.pop('interface_prefix')]
1267        if 'namespace' in kwargs:
1268            cmd += ['--c-namespace', kwargs.pop('namespace')]
1269        if kwargs.get('object_manager', False):
1270            cmd += ['--c-generate-object-manager']
1271        if 'sources' in kwargs:
1272            xml_files += mesonlib.listify(kwargs.pop('sources'))
1273        build_by_default = kwargs.get('build_by_default', False)
1274
1275        # Annotations are a bit ugly in that they are a list of lists of strings...
1276        annotations = kwargs.pop('annotations', [])
1277        if not isinstance(annotations, list):
1278            raise MesonException('annotations takes a list')
1279        if annotations and isinstance(annotations, list) and not isinstance(annotations[0], list):
1280            annotations = [annotations]
1281
1282        for annotation in annotations:
1283            if len(annotation) != 3 or not all(isinstance(i, str) for i in annotation):
1284                raise MesonException('Annotations must be made up of 3 strings for ELEMENT, KEY, and VALUE')
1285            cmd += ['--annotate'] + annotation
1286
1287        targets = []
1288        install_header = kwargs.get('install_header', False)
1289        install_dir = kwargs.get('install_dir', state.environment.coredata.get_option(mesonlib.OptionKey('includedir')))
1290
1291        output = namebase + '.c'
1292        # Added in https://gitlab.gnome.org/GNOME/glib/commit/e4d68c7b3e8b01ab1a4231bf6da21d045cb5a816 (2.55.2)
1293        # Fixed in https://gitlab.gnome.org/GNOME/glib/commit/cd1f82d8fc741a2203582c12cc21b4dacf7e1872 (2.56.2)
1294        if mesonlib.version_compare(glib_version, '>= 2.56.2'):
1295            custom_kwargs = {'input': xml_files,
1296                             'output': output,
1297                             'command': cmd + ['--body', '--output', '@OUTPUT@', '@INPUT@'],
1298                             'build_by_default': build_by_default
1299                             }
1300        else:
1301            if 'docbook' in kwargs:
1302                docbook = kwargs['docbook']
1303                if not isinstance(docbook, str):
1304                    raise MesonException('docbook value must be a string.')
1305
1306                cmd += ['--generate-docbook', docbook]
1307
1308            # https://git.gnome.org/browse/glib/commit/?id=ee09bb704fe9ccb24d92dd86696a0e6bb8f0dc1a
1309            if mesonlib.version_compare(glib_version, '>= 2.51.3'):
1310                cmd += ['--output-directory', '@OUTDIR@', '--generate-c-code', namebase, '@INPUT@']
1311            else:
1312                self._print_gdbus_warning()
1313                cmd += ['--generate-c-code', '@OUTDIR@/' + namebase, '@INPUT@']
1314
1315            custom_kwargs = {'input': xml_files,
1316                             'output': output,
1317                             'command': cmd,
1318                             'build_by_default': build_by_default
1319                             }
1320
1321        cfile_custom_target = build.CustomTarget(output, state.subdir, state.subproject, custom_kwargs)
1322        targets.append(cfile_custom_target)
1323
1324        output = namebase + '.h'
1325        if mesonlib.version_compare(glib_version, '>= 2.56.2'):
1326            custom_kwargs = {'input': xml_files,
1327                             'output': output,
1328                             'command': cmd + ['--header', '--output', '@OUTPUT@', '@INPUT@'],
1329                             'build_by_default': build_by_default,
1330                             'install': install_header,
1331                             'install_dir': install_dir
1332                             }
1333        else:
1334            custom_kwargs = {'input': xml_files,
1335                             'output': output,
1336                             'command': cmd,
1337                             'build_by_default': build_by_default,
1338                             'install': install_header,
1339                             'install_dir': install_dir,
1340                             'depends': cfile_custom_target
1341                             }
1342
1343        hfile_custom_target = build.CustomTarget(output, state.subdir, state.subproject, custom_kwargs)
1344        targets.append(hfile_custom_target)
1345
1346        if 'docbook' in kwargs:
1347            docbook = kwargs['docbook']
1348            if not isinstance(docbook, str):
1349                raise MesonException('docbook value must be a string.')
1350
1351            docbook_cmd = cmd + ['--output-directory', '@OUTDIR@', '--generate-docbook', docbook, '@INPUT@']
1352
1353            # The docbook output is always ${docbook}-${name_of_xml_file}
1354            output = namebase + '-docbook'
1355            outputs = []
1356            for f in xml_files:
1357                outputs.append('{}-{}'.format(docbook, os.path.basename(str(f))))
1358
1359            if mesonlib.version_compare(glib_version, '>= 2.56.2'):
1360                custom_kwargs = {'input': xml_files,
1361                                 'output': outputs,
1362                                 'command': docbook_cmd,
1363                                 'build_by_default': build_by_default
1364                                 }
1365            else:
1366                custom_kwargs = {'input': xml_files,
1367                                 'output': outputs,
1368                                 'command': cmd,
1369                                 'build_by_default': build_by_default,
1370                                 'depends': cfile_custom_target
1371                                 }
1372
1373            docbook_custom_target = build.CustomTarget(output, state.subdir, state.subproject, custom_kwargs)
1374            targets.append(docbook_custom_target)
1375
1376        return ModuleReturnValue(targets, targets)
1377
1378    @permittedKwargs({'sources', 'c_template', 'h_template', 'install_header', 'install_dir',
1379                      'comments', 'identifier_prefix', 'symbol_prefix', 'eprod', 'vprod',
1380                      'fhead', 'fprod', 'ftail', 'vhead', 'vtail', 'depends'})
1381    def mkenums(self, state, args, kwargs):
1382        if len(args) != 1:
1383            raise MesonException('Mkenums requires one positional argument.')
1384        basename = args[0]
1385
1386        if 'sources' not in kwargs:
1387            raise MesonException('Missing keyword argument "sources".')
1388        sources = kwargs.pop('sources')
1389        if isinstance(sources, str):
1390            sources = [sources]
1391        elif not isinstance(sources, list):
1392            raise MesonException(
1393                'Sources keyword argument must be a string or array.')
1394
1395        cmd = []
1396        known_kwargs = ['comments', 'eprod', 'fhead', 'fprod', 'ftail',
1397                        'identifier_prefix', 'symbol_prefix', 'template',
1398                        'vhead', 'vprod', 'vtail']
1399        known_custom_target_kwargs = ['install_dir', 'build_always',
1400                                      'depends', 'depend_files']
1401        c_template = h_template = None
1402        install_header = False
1403        for arg, value in kwargs.items():
1404            if arg == 'sources':
1405                raise AssertionError("sources should've already been handled")
1406            elif arg == 'c_template':
1407                c_template = value
1408                if isinstance(c_template, mesonlib.File):
1409                    c_template = c_template.absolute_path(state.environment.source_dir, state.environment.build_dir)
1410                if 'template' in kwargs:
1411                    raise MesonException('Mkenums does not accept both '
1412                                         'c_template and template keyword '
1413                                         'arguments at the same time.')
1414            elif arg == 'h_template':
1415                h_template = value
1416                if isinstance(h_template, mesonlib.File):
1417                    h_template = h_template.absolute_path(state.environment.source_dir, state.environment.build_dir)
1418                if 'template' in kwargs:
1419                    raise MesonException('Mkenums does not accept both '
1420                                         'h_template and template keyword '
1421                                         'arguments at the same time.')
1422            elif arg == 'install_header':
1423                install_header = value
1424            elif arg in known_kwargs:
1425                cmd += ['--' + arg.replace('_', '-'), value]
1426            elif arg not in known_custom_target_kwargs:
1427                raise MesonException(
1428                    f'Mkenums does not take a {arg} keyword argument.')
1429        cmd = [state.find_program(['glib-mkenums', 'mkenums'])] + cmd
1430        custom_kwargs = {}
1431        for arg in known_custom_target_kwargs:
1432            if arg in kwargs:
1433                custom_kwargs[arg] = kwargs[arg]
1434
1435        targets = []
1436
1437        if h_template is not None:
1438            h_output = os.path.basename(os.path.splitext(h_template)[0])
1439            # We always set template as the first element in the source array
1440            # so --template consumes it.
1441            h_cmd = cmd + ['--template', '@INPUT@']
1442            h_sources = [h_template] + sources
1443
1444            # Copy so we don't mutate the arguments for the c_template
1445            h_kwargs = custom_kwargs.copy()
1446            h_kwargs['install'] = install_header
1447            if 'install_dir' not in h_kwargs:
1448                h_kwargs['install_dir'] = \
1449                    state.environment.coredata.get_option(mesonlib.OptionKey('includedir'))
1450            h_target = self._make_mkenum_custom_target(state, h_sources,
1451                                                       h_output, h_cmd,
1452                                                       h_kwargs)
1453            targets.append(h_target)
1454
1455        if c_template is not None:
1456            c_output = os.path.basename(os.path.splitext(c_template)[0])
1457            # We always set template as the first element in the source array
1458            # so --template consumes it.
1459            c_cmd = cmd + ['--template', '@INPUT@']
1460            c_sources = [c_template] + sources
1461
1462            c_kwargs = custom_kwargs.copy()
1463            # Never install the C file. Complain on bug tracker if you need it.
1464            c_kwargs['install'] = False
1465            c_kwargs['install_dir'] = []
1466            if h_template is not None:
1467                if 'depends' in custom_kwargs:
1468                    c_kwargs['depends'] += [h_target]
1469                else:
1470                    c_kwargs['depends'] = h_target
1471            c_target = self._make_mkenum_custom_target(state, c_sources,
1472                                                       c_output, c_cmd,
1473                                                       c_kwargs)
1474            targets.insert(0, c_target)
1475
1476        if c_template is None and h_template is None:
1477            generic_cmd = cmd + ['@INPUT@']
1478            custom_kwargs['install'] = install_header
1479            if 'install_dir' not in custom_kwargs:
1480                custom_kwargs['install_dir'] = \
1481                    state.environment.coredata.get_option(mesonlib.OptionKey('includedir'))
1482            target = self._make_mkenum_custom_target(state, sources, basename,
1483                                                     generic_cmd, custom_kwargs)
1484            return ModuleReturnValue(target, [target])
1485        elif len(targets) == 1:
1486            return ModuleReturnValue(targets[0], [targets[0]])
1487        else:
1488            return ModuleReturnValue(targets, targets)
1489
1490    @FeatureNew('gnome.mkenums_simple', '0.42.0')
1491    def mkenums_simple(self, state, args, kwargs):
1492        hdr_filename = args[0] + '.h'
1493        body_filename = args[0] + '.c'
1494
1495        # not really needed, just for sanity checking
1496        forbidden_kwargs = ['c_template', 'h_template', 'eprod', 'fhead',
1497                            'fprod', 'ftail', 'vhead', 'vtail', 'comments']
1498        for arg in forbidden_kwargs:
1499            if arg in kwargs:
1500                raise MesonException(f'mkenums_simple() does not take a {arg} keyword argument')
1501
1502        # kwargs to pass as-is from mkenums_simple() to mkenums()
1503        shared_kwargs = ['sources', 'install_header', 'install_dir',
1504                         'identifier_prefix', 'symbol_prefix']
1505        mkenums_kwargs = {}
1506        for arg in shared_kwargs:
1507            if arg in kwargs:
1508                mkenums_kwargs[arg] = kwargs[arg]
1509
1510        # .c file generation
1511        c_file_kwargs = copy.deepcopy(mkenums_kwargs)
1512        if 'sources' not in kwargs:
1513            raise MesonException('Missing keyword argument "sources".')
1514        sources = kwargs['sources']
1515        if isinstance(sources, str):
1516            sources = [sources]
1517        elif not isinstance(sources, list):
1518            raise MesonException(
1519                'Sources keyword argument must be a string or array.')
1520
1521        # The `install_header` argument will be used by mkenums() when
1522        # not using template files, so we need to forcibly unset it
1523        # when generating the C source file, otherwise we will end up
1524        # installing it
1525        c_file_kwargs['install_header'] = False
1526
1527        header_prefix = kwargs.get('header_prefix', '')
1528        decl_decorator = kwargs.get('decorator', '')
1529        func_prefix = kwargs.get('function_prefix', '')
1530        body_prefix = kwargs.get('body_prefix', '')
1531
1532        # Maybe we should write our own template files into the build dir
1533        # instead, but that seems like much more work, nice as it would be.
1534        fhead = ''
1535        if body_prefix != '':
1536            fhead += '%s\n' % body_prefix
1537        fhead += '#include "%s"\n' % hdr_filename
1538        for hdr in sources:
1539            fhead += '#include "{}"\n'.format(os.path.basename(str(hdr)))
1540        fhead += '''
1541#define C_ENUM(v) ((gint) v)
1542#define C_FLAGS(v) ((guint) v)
1543'''
1544        c_file_kwargs['fhead'] = fhead
1545
1546        c_file_kwargs['fprod'] = '''
1547/* enumerations from "@basename@" */
1548'''
1549
1550        c_file_kwargs['vhead'] = f'''
1551GType
1552{func_prefix}@enum_name@_get_type (void)
1553{{
1554  static gsize gtype_id = 0;
1555  static const G@Type@Value values[] = {{'''
1556
1557        c_file_kwargs['vprod'] = '    { C_@TYPE@(@VALUENAME@), "@VALUENAME@", "@valuenick@" },'
1558
1559        c_file_kwargs['vtail'] = '''    { 0, NULL, NULL }
1560  };
1561  if (g_once_init_enter (&gtype_id)) {
1562    GType new_type = g_@type@_register_static (g_intern_static_string ("@EnumName@"), values);
1563    g_once_init_leave (&gtype_id, new_type);
1564  }
1565  return (GType) gtype_id;
1566}'''
1567
1568        rv = self.mkenums(state, [body_filename], c_file_kwargs)
1569        c_file = rv.return_value
1570
1571        # .h file generation
1572        h_file_kwargs = copy.deepcopy(mkenums_kwargs)
1573
1574        h_file_kwargs['fhead'] = f'''#pragma once
1575
1576#include <glib-object.h>
1577{header_prefix}
1578
1579G_BEGIN_DECLS
1580'''
1581
1582        h_file_kwargs['fprod'] = '''
1583/* enumerations from "@basename@" */
1584'''
1585
1586        h_file_kwargs['vhead'] = f'''
1587{decl_decorator}
1588GType {func_prefix}@enum_name@_get_type (void);
1589#define @ENUMPREFIX@_TYPE_@ENUMSHORT@ ({func_prefix}@enum_name@_get_type())'''
1590
1591        h_file_kwargs['ftail'] = '''
1592G_END_DECLS'''
1593
1594        rv = self.mkenums(state, [hdr_filename], h_file_kwargs)
1595        h_file = rv.return_value
1596
1597        return ModuleReturnValue([c_file, h_file], [c_file, h_file])
1598
1599    @staticmethod
1600    def _make_mkenum_custom_target(state, sources, output, cmd, kwargs):
1601        custom_kwargs = {
1602            'input': sources,
1603            'output': output,
1604            'capture': True,
1605            'command': cmd
1606        }
1607        custom_kwargs.update(kwargs)
1608        return build.CustomTarget(output, state.subdir, state.subproject, custom_kwargs,
1609                                  # https://github.com/mesonbuild/meson/issues/973
1610                                  absolute_paths=True)
1611
1612    @permittedKwargs({'sources', 'prefix', 'install_header', 'install_dir', 'stdinc',
1613                      'nostdinc', 'internal', 'skip_source', 'valist_marshallers',
1614                      'extra_args'})
1615    def genmarshal(self, state, args, kwargs):
1616        if len(args) != 1:
1617            raise MesonException(
1618                'Genmarshal requires one positional argument.')
1619        output = args[0]
1620
1621        if 'sources' not in kwargs:
1622            raise MesonException('Missing keyword argument "sources".')
1623        sources = kwargs.pop('sources')
1624        if isinstance(sources, str):
1625            sources = [sources]
1626        elif not isinstance(sources, list):
1627            raise MesonException(
1628                'Sources keyword argument must be a string or array.')
1629
1630        new_genmarshal = mesonlib.version_compare(self._get_native_glib_version(state), '>= 2.53.3')
1631
1632        cmd = [state.find_program('glib-genmarshal')]
1633        known_kwargs = ['internal', 'nostdinc', 'skip_source', 'stdinc',
1634                        'valist_marshallers', 'extra_args']
1635        known_custom_target_kwargs = ['build_always', 'depends',
1636                                      'depend_files', 'install_dir',
1637                                      'install_header']
1638        for arg, value in kwargs.items():
1639            if arg == 'prefix':
1640                cmd += ['--prefix', value]
1641            elif arg == 'extra_args':
1642                if new_genmarshal:
1643                    cmd += mesonlib.stringlistify(value)
1644                else:
1645                    mlog.warning('The current version of GLib does not support extra arguments \n'
1646                                 'for glib-genmarshal. You need at least GLib 2.53.3. See ',
1647                                 mlog.bold('https://github.com/mesonbuild/meson/pull/2049'))
1648            elif arg in known_kwargs and value:
1649                cmd += ['--' + arg.replace('_', '-')]
1650            elif arg not in known_custom_target_kwargs:
1651                raise MesonException(f'Genmarshal does not take a {arg} keyword argument.')
1652
1653        install_header = kwargs.pop('install_header', False)
1654        install_dir = kwargs.pop('install_dir', [])
1655
1656        custom_kwargs = {
1657            'input': sources,
1658        }
1659
1660        # https://github.com/GNOME/glib/commit/0fbc98097fac4d3e647684f344e508abae109fdf
1661        if mesonlib.version_compare(self._get_native_glib_version(state), '>= 2.51.0'):
1662            cmd += ['--output', '@OUTPUT@']
1663        else:
1664            custom_kwargs['capture'] = True
1665
1666        for arg in known_custom_target_kwargs:
1667            if arg in kwargs:
1668                custom_kwargs[arg] = kwargs[arg]
1669
1670        header_file = output + '.h'
1671        custom_kwargs['command'] = cmd + ['--body', '@INPUT@']
1672        if mesonlib.version_compare(self._get_native_glib_version(state), '>= 2.53.4'):
1673            # Silence any warnings about missing prototypes
1674            custom_kwargs['command'] += ['--include-header', header_file]
1675        custom_kwargs['output'] = output + '.c'
1676        body = build.CustomTarget(output + '_c', state.subdir, state.subproject, custom_kwargs)
1677
1678        custom_kwargs['install'] = install_header
1679        custom_kwargs['install_dir'] = install_dir
1680        if new_genmarshal:
1681            cmd += ['--pragma-once']
1682        custom_kwargs['command'] = cmd + ['--header', '@INPUT@']
1683        custom_kwargs['output'] = header_file
1684        header = build.CustomTarget(output + '_h', state.subdir, state.subproject, custom_kwargs)
1685
1686        rv = [body, header]
1687        return ModuleReturnValue(rv, rv)
1688
1689    @staticmethod
1690    def _vapi_args_to_command(prefix, variable, kwargs, accept_vapi=False):
1691        arg_list = mesonlib.extract_as_list(kwargs, variable)
1692        ret = []
1693        for arg in arg_list:
1694            if not isinstance(arg, str):
1695                types = 'strings' + ' or InternalDependencys' if accept_vapi else ''
1696                raise MesonException(f'All {variable} must be {types}')
1697            ret.append(prefix + arg)
1698        return ret
1699
1700    def _extract_vapi_packages(self, state, kwargs):
1701        '''
1702        Packages are special because we need to:
1703        - Get a list of packages for the .deps file
1704        - Get a list of depends for any VapiTargets
1705        - Get package name from VapiTargets
1706        - Add include dirs for any VapiTargets
1707        '''
1708        arg_list = kwargs.get('packages')
1709        if not arg_list:
1710            return [], [], [], []
1711        arg_list = mesonlib.listify(arg_list)
1712        vapi_depends = []
1713        vapi_packages = []
1714        vapi_includes = []
1715        ret = []
1716        remaining_args = []
1717        for arg in arg_list:
1718            if isinstance(arg, InternalDependency):
1719                targets = [t for t in arg.sources if isinstance(t, VapiTarget)]
1720                for target in targets:
1721                    srcdir = os.path.join(state.environment.get_source_dir(),
1722                                          target.get_subdir())
1723                    outdir = os.path.join(state.environment.get_build_dir(),
1724                                          target.get_subdir())
1725                    outfile = target.get_outputs()[0][:-5] # Strip .vapi
1726                    ret.append('--vapidir=' + outdir)
1727                    ret.append('--girdir=' + outdir)
1728                    ret.append('--pkg=' + outfile)
1729                    vapi_depends.append(target)
1730                    vapi_packages.append(outfile)
1731                    vapi_includes.append(srcdir)
1732            else:
1733                vapi_packages.append(arg)
1734                remaining_args.append(arg)
1735
1736        kwargs['packages'] = remaining_args
1737        vapi_args = ret + self._vapi_args_to_command('--pkg=', 'packages', kwargs, accept_vapi=True)
1738        return vapi_args, vapi_depends, vapi_packages, vapi_includes
1739
1740    def _generate_deps(self, state, library, packages, install_dir):
1741        outdir = state.environment.scratch_dir
1742        fname = os.path.join(outdir, library + '.deps')
1743        with open(fname, 'w', encoding='utf-8') as ofile:
1744            for package in packages:
1745                ofile.write(package + '\n')
1746        return build.Data([mesonlib.File(True, outdir, fname)], install_dir, install_dir, None, state.subproject)
1747
1748    def _get_vapi_link_with(self, target):
1749        link_with = []
1750        for dep in target.get_target_dependencies():
1751            if isinstance(dep, build.SharedLibrary):
1752                link_with.append(dep)
1753            elif isinstance(dep, GirTarget):
1754                link_with += self._get_vapi_link_with(dep)
1755        return link_with
1756
1757    @permittedKwargs({'sources', 'packages', 'metadata_dirs', 'gir_dirs',
1758                      'vapi_dirs', 'install', 'install_dir'})
1759    def generate_vapi(self, state, args, kwargs):
1760        if len(args) != 1:
1761            raise MesonException('The library name is required')
1762
1763        if not isinstance(args[0], str):
1764            raise MesonException('The first argument must be the name of the library')
1765        created_values = []
1766
1767        library = args[0]
1768        build_dir = os.path.join(state.environment.get_build_dir(), state.subdir)
1769        source_dir = os.path.join(state.environment.get_source_dir(), state.subdir)
1770        pkg_cmd, vapi_depends, vapi_packages, vapi_includes = self._extract_vapi_packages(state, kwargs)
1771        if 'VAPIGEN' in os.environ:
1772            cmd = [state.find_program(os.environ['VAPIGEN'])]
1773        else:
1774            cmd = [state.find_program('vapigen')]
1775        cmd += ['--quiet', '--library=' + library, '--directory=' + build_dir]
1776        cmd += self._vapi_args_to_command('--vapidir=', 'vapi_dirs', kwargs)
1777        cmd += self._vapi_args_to_command('--metadatadir=', 'metadata_dirs', kwargs)
1778        cmd += self._vapi_args_to_command('--girdir=', 'gir_dirs', kwargs)
1779        cmd += pkg_cmd
1780        cmd += ['--metadatadir=' + source_dir]
1781
1782        if 'sources' not in kwargs:
1783            raise MesonException('sources are required to generate the vapi file')
1784
1785        inputs = mesonlib.extract_as_list(kwargs, 'sources')
1786
1787        link_with = []
1788        for i in inputs:
1789            if isinstance(i, str):
1790                cmd.append(os.path.join(source_dir, i))
1791            elif isinstance(i, GirTarget):
1792                link_with += self._get_vapi_link_with(i)
1793                subdir = os.path.join(state.environment.get_build_dir(),
1794                                      i.get_subdir())
1795                gir_file = os.path.join(subdir, i.get_outputs()[0])
1796                cmd.append(gir_file)
1797            else:
1798                raise MesonException('Input must be a str or GirTarget')
1799
1800        vapi_output = library + '.vapi'
1801        custom_kwargs = {
1802            'command': cmd,
1803            'input': inputs,
1804            'output': vapi_output,
1805            'depends': vapi_depends,
1806        }
1807        install_dir = kwargs.get('install_dir',
1808                                 os.path.join(state.environment.coredata.get_option(mesonlib.OptionKey('datadir')),
1809                                              'vala', 'vapi'))
1810        if kwargs.get('install'):
1811            custom_kwargs['install'] = kwargs['install']
1812            custom_kwargs['install_dir'] = install_dir
1813
1814            # We shouldn't need this locally but we install it
1815            deps_target = self._generate_deps(state, library, vapi_packages, install_dir)
1816            created_values.append(deps_target)
1817        vapi_target = VapiTarget(vapi_output, state.subdir, state.subproject, custom_kwargs)
1818
1819        # So to try our best to get this to just work we need:
1820        # - link with with the correct library
1821        # - include the vapi and dependent vapi files in sources
1822        # - add relevant directories to include dirs
1823        incs = [build.IncludeDirs(state.subdir, ['.'] + vapi_includes, False)]
1824        sources = [vapi_target] + vapi_depends
1825        rv = InternalDependency(None, incs, [], [], link_with, [], sources, [], {})
1826        created_values.append(rv)
1827        return ModuleReturnValue(rv, created_values)
1828
1829def initialize(*args, **kwargs):
1830    mod = GnomeModule(*args, **kwargs)
1831    mod.interpreter.append_holder_map(GResourceTarget, interpreter.CustomTargetHolder)
1832    mod.interpreter.append_holder_map(GResourceHeaderTarget, interpreter.CustomTargetHolder)
1833    mod.interpreter.append_holder_map(GirTarget, interpreter.CustomTargetHolder)
1834    mod.interpreter.append_holder_map(TypelibTarget, interpreter.CustomTargetHolder)
1835    mod.interpreter.append_holder_map(VapiTarget, interpreter.CustomTargetHolder)
1836    return mod
1837