diff options
Diffstat (limited to '')
-rw-r--r-- | mesonbuild/modules/pkgconfig.py | 742 |
1 files changed, 742 insertions, 0 deletions
diff --git a/mesonbuild/modules/pkgconfig.py b/mesonbuild/modules/pkgconfig.py new file mode 100644 index 0000000..af8467c --- /dev/null +++ b/mesonbuild/modules/pkgconfig.py @@ -0,0 +1,742 @@ +# Copyright 2015-2022 The Meson development team + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +from collections import defaultdict +from dataclasses import dataclass +from pathlib import PurePath +import os +import typing as T + +from . import NewExtensionModule, ModuleInfo +from . import ModuleReturnValue +from .. import build +from .. import dependencies +from .. import mesonlib +from .. import mlog +from ..coredata import BUILTIN_DIR_OPTIONS +from ..dependencies import ThreadDependency +from ..interpreter.type_checking import D_MODULE_VERSIONS_KW, INSTALL_DIR_KW, VARIABLES_KW, NoneType +from ..interpreterbase import FeatureNew, FeatureDeprecated +from ..interpreterbase.decorators import ContainerTypeInfo, KwargInfo, typed_kwargs, typed_pos_args + +if T.TYPE_CHECKING: + from typing_extensions import TypedDict + + from . import ModuleState + from .. import mparser + from ..interpreter import Interpreter + + ANY_DEP = T.Union[dependencies.Dependency, build.BuildTargetTypes, str] + LIBS = T.Union[build.LibTypes, str] + + class GenerateKw(TypedDict): + + version: T.Optional[str] + name: T.Optional[str] + filebase: T.Optional[str] + description: T.Optional[str] + url: str + subdirs: T.List[str] + conflicts: T.List[str] + dataonly: bool + libraries: T.List[ANY_DEP] + libraries_private: T.List[ANY_DEP] + requires: T.List[T.Union[str, build.StaticLibrary, build.SharedLibrary, dependencies.Dependency]] + requires_private: T.List[T.Union[str, build.StaticLibrary, build.SharedLibrary, dependencies.Dependency]] + install_dir: T.Optional[str] + d_module_versions: T.List[T.Union[str, int]] + extra_cflags: T.List[str] + variables: T.Dict[str, str] + uninstalled_variables: T.Dict[str, str] + unescaped_variables: T.Dict[str, str] + unescaped_uninstalled_variables: T.Dict[str, str] + + +_PKG_LIBRARIES: KwargInfo[T.List[T.Union[str, dependencies.Dependency, build.SharedLibrary, build.StaticLibrary, build.CustomTarget, build.CustomTargetIndex]]] = KwargInfo( + 'libraries', + ContainerTypeInfo(list, (str, dependencies.Dependency, + build.SharedLibrary, build.StaticLibrary, + build.CustomTarget, build.CustomTargetIndex)), + default=[], + listify=True, +) + +_PKG_REQUIRES: KwargInfo[T.List[T.Union[str, build.SharedLibrary, build.StaticLibrary, dependencies.Dependency]]] = KwargInfo( + 'requires', + ContainerTypeInfo(list, (str, build.SharedLibrary, build.StaticLibrary, dependencies.Dependency)), + default=[], + listify=True, +) + + +def _as_str(obj: object) -> str: + assert isinstance(obj, str) + return obj + + +@dataclass +class MetaData: + + filebase: str + display_name: str + location: mparser.BaseNode + warned: bool = False + + +class DependenciesHelper: + def __init__(self, state: ModuleState, name: str, metadata: T.Dict[str, MetaData]) -> None: + self.state = state + self.name = name + self.pub_libs: T.List[LIBS] = [] + self.pub_reqs: T.List[str] = [] + self.priv_libs: T.List[LIBS] = [] + self.priv_reqs: T.List[str] = [] + self.cflags: T.List[str] = [] + self.version_reqs: T.DefaultDict[str, T.Set[str]] = defaultdict(set) + self.link_whole_targets: T.List[T.Union[build.CustomTarget, build.CustomTargetIndex, build.StaticLibrary]] = [] + self.metadata = metadata + + def add_pub_libs(self, libs: T.List[ANY_DEP]) -> None: + p_libs, reqs, cflags = self._process_libs(libs, True) + self.pub_libs = p_libs + self.pub_libs # prepend to preserve dependencies + self.pub_reqs += reqs + self.cflags += cflags + + def add_priv_libs(self, libs: T.List[ANY_DEP]) -> None: + p_libs, reqs, _ = self._process_libs(libs, False) + self.priv_libs = p_libs + self.priv_libs + self.priv_reqs += reqs + + def add_pub_reqs(self, reqs: T.List[T.Union[str, build.StaticLibrary, build.SharedLibrary, dependencies.Dependency]]) -> None: + self.pub_reqs += self._process_reqs(reqs) + + def add_priv_reqs(self, reqs: T.List[T.Union[str, build.StaticLibrary, build.SharedLibrary, dependencies.Dependency]]) -> None: + self.priv_reqs += self._process_reqs(reqs) + + def _check_generated_pc_deprecation(self, obj: T.Union[build.CustomTarget, build.CustomTargetIndex, build.StaticLibrary, build.SharedLibrary]) -> None: + if obj.get_id() in self.metadata: + return + data = self.metadata[obj.get_id()] + if data.warned: + return + mlog.deprecation('Library', mlog.bold(obj.name), 'was passed to the ' + '"libraries" keyword argument of a previous call ' + 'to generate() method instead of first positional ' + 'argument.', 'Adding', mlog.bold(data.display_name), + 'to "Requires" field, but this is a deprecated ' + 'behaviour that will change in a future version ' + 'of Meson. Please report the issue if this ' + 'warning cannot be avoided in your case.', + location=data.location) + data.warned = True + + def _process_reqs(self, reqs: T.Sequence[T.Union[str, build.StaticLibrary, build.SharedLibrary, dependencies.Dependency]]) -> T.List[str]: + '''Returns string names of requirements''' + processed_reqs: T.List[str] = [] + for obj in mesonlib.listify(reqs): + if not isinstance(obj, str): + FeatureNew.single_use('pkgconfig.generate requirement from non-string object', '0.46.0', self.state.subproject) + if (isinstance(obj, (build.CustomTarget, build.CustomTargetIndex, build.SharedLibrary, build.StaticLibrary)) + and obj.get_id() in self.metadata): + self._check_generated_pc_deprecation(obj) + processed_reqs.append(self.metadata[obj.get_id()].filebase) + elif isinstance(obj, dependencies.PkgConfigDependency): + if obj.found(): + processed_reqs.append(obj.name) + self.add_version_reqs(obj.name, obj.version_reqs) + elif isinstance(obj, str): + name, version_req = self.split_version_req(obj) + processed_reqs.append(name) + self.add_version_reqs(name, [version_req] if version_req is not None else None) + elif isinstance(obj, dependencies.Dependency) and not obj.found(): + pass + elif isinstance(obj, ThreadDependency): + pass + else: + raise mesonlib.MesonException('requires argument not a string, ' + 'library with pkgconfig-generated file ' + f'or pkgconfig-dependency object, got {obj!r}') + return processed_reqs + + def add_cflags(self, cflags: T.List[str]) -> None: + self.cflags += mesonlib.stringlistify(cflags) + + def _process_libs( + self, libs: T.List[ANY_DEP], public: bool + ) -> T.Tuple[T.List[T.Union[str, build.SharedLibrary, build.StaticLibrary, build.CustomTarget, build.CustomTargetIndex]], T.List[str], T.List[str]]: + libs = mesonlib.listify(libs) + processed_libs: T.List[T.Union[str, build.SharedLibrary, build.StaticLibrary, build.CustomTarget, build.CustomTargetIndex]] = [] + processed_reqs: T.List[str] = [] + processed_cflags: T.List[str] = [] + for obj in libs: + if (isinstance(obj, (build.CustomTarget, build.CustomTargetIndex, build.SharedLibrary, build.StaticLibrary)) + and obj.get_id() in self.metadata): + self._check_generated_pc_deprecation(obj) + processed_reqs.append(self.metadata[obj.get_id()].filebase) + elif isinstance(obj, dependencies.ValgrindDependency): + pass + elif isinstance(obj, dependencies.PkgConfigDependency): + if obj.found(): + processed_reqs.append(obj.name) + self.add_version_reqs(obj.name, obj.version_reqs) + elif isinstance(obj, dependencies.InternalDependency): + if obj.found(): + processed_libs += obj.get_link_args() + processed_cflags += obj.get_compile_args() + self._add_lib_dependencies(obj.libraries, obj.whole_libraries, obj.ext_deps, public, private_external_deps=True) + elif isinstance(obj, dependencies.Dependency): + if obj.found(): + processed_libs += obj.get_link_args() + processed_cflags += obj.get_compile_args() + elif isinstance(obj, build.SharedLibrary) and obj.shared_library_only: + # Do not pull dependencies for shared libraries because they are + # only required for static linking. Adding private requires has + # the side effect of exposing their cflags, which is the + # intended behaviour of pkg-config but force Debian to add more + # than needed build deps. + # See https://bugs.freedesktop.org/show_bug.cgi?id=105572 + processed_libs.append(obj) + elif isinstance(obj, (build.SharedLibrary, build.StaticLibrary)): + processed_libs.append(obj) + # If there is a static library in `Libs:` all its deps must be + # public too, otherwise the generated pc file will never be + # usable without --static. + self._add_lib_dependencies(obj.link_targets, + obj.link_whole_targets, + obj.external_deps, + isinstance(obj, build.StaticLibrary) and public) + elif isinstance(obj, (build.CustomTarget, build.CustomTargetIndex)): + if not obj.is_linkable_target(): + raise mesonlib.MesonException('library argument contains a not linkable custom_target.') + FeatureNew.single_use('custom_target in pkgconfig.generate libraries', '0.58.0', self.state.subproject) + processed_libs.append(obj) + elif isinstance(obj, str): + processed_libs.append(obj) + else: + raise mesonlib.MesonException(f'library argument of type {type(obj).__name__} not a string, library or dependency object.') + + return processed_libs, processed_reqs, processed_cflags + + def _add_lib_dependencies( + self, link_targets: T.Sequence[build.BuildTargetTypes], + link_whole_targets: T.Sequence[T.Union[build.StaticLibrary, build.CustomTarget, build.CustomTargetIndex]], + external_deps: T.List[dependencies.Dependency], + public: bool, + private_external_deps: bool = False) -> None: + add_libs = self.add_pub_libs if public else self.add_priv_libs + # Recursively add all linked libraries + for t in link_targets: + # Internal libraries (uninstalled static library) will be promoted + # to link_whole, treat them as such here. + if t.is_internal(): + # `is_internal` shouldn't return True for anything but a + # StaticLibrary, or a CustomTarget that is a StaticLibrary + assert isinstance(t, (build.StaticLibrary, build.CustomTarget, build.CustomTargetIndex)), 'for mypy' + self._add_link_whole(t, public) + else: + add_libs([t]) + for t in link_whole_targets: + self._add_link_whole(t, public) + # And finally its external dependencies + if private_external_deps: + self.add_priv_libs(T.cast('T.List[ANY_DEP]', external_deps)) + else: + add_libs(T.cast('T.List[ANY_DEP]', external_deps)) + + def _add_link_whole(self, t: T.Union[build.CustomTarget, build.CustomTargetIndex, build.StaticLibrary], public: bool) -> None: + # Don't include static libraries that we link_whole. But we still need to + # include their dependencies: a static library we link_whole + # could itself link to a shared library or an installed static library. + # Keep track of link_whole_targets so we can remove them from our + # lists in case a library is link_with and link_whole at the same time. + # See remove_dups() below. + self.link_whole_targets.append(t) + if isinstance(t, build.BuildTarget): + self._add_lib_dependencies(t.link_targets, t.link_whole_targets, t.external_deps, public) + + def add_version_reqs(self, name: str, version_reqs: T.Optional[T.List[str]]) -> None: + if version_reqs: + # Note that pkg-config is picky about whitespace. + # 'foo > 1.2' is ok but 'foo>1.2' is not. + # foo, bar' is ok, but 'foo,bar' is not. + self.version_reqs[name].update(version_reqs) + + def split_version_req(self, s: str) -> T.Tuple[str, T.Optional[str]]: + for op in ['>=', '<=', '!=', '==', '=', '>', '<']: + pos = s.find(op) + if pos > 0: + return s[0:pos].strip(), s[pos:].strip() + return s, None + + def format_vreq(self, vreq: str) -> str: + # vreq are '>=1.0' and pkgconfig wants '>= 1.0' + for op in ['>=', '<=', '!=', '==', '=', '>', '<']: + if vreq.startswith(op): + return op + ' ' + vreq[len(op):] + return vreq + + def format_reqs(self, reqs: T.List[str]) -> str: + result: T.List[str] = [] + for name in reqs: + vreqs = self.version_reqs.get(name, None) + if vreqs: + result += [name + ' ' + self.format_vreq(vreq) for vreq in vreqs] + else: + result += [name] + return ', '.join(result) + + def remove_dups(self) -> None: + # Set of ids that have already been handled and should not be added any more + exclude: T.Set[str] = set() + + # We can't just check if 'x' is excluded because we could have copies of + # the same SharedLibrary object for example. + def _ids(x: T.Union[str, build.CustomTarget, build.CustomTargetIndex, build.StaticLibrary, build.SharedLibrary]) -> T.Iterable[str]: + if isinstance(x, str): + yield x + else: + if x.get_id() in self.metadata: + yield self.metadata[x.get_id()].display_name + yield x.get_id() + + # Exclude 'x' in all its forms and return if it was already excluded + def _add_exclude(x: T.Union[str, build.CustomTarget, build.CustomTargetIndex, build.StaticLibrary, build.SharedLibrary]) -> bool: + was_excluded = False + for i in _ids(x): + if i in exclude: + was_excluded = True + else: + exclude.add(i) + return was_excluded + + # link_whole targets are already part of other targets, exclude them all. + for t in self.link_whole_targets: + _add_exclude(t) + + # Mypy thinks these overlap, but since List is invariant they don't, + # `List[str]`` is not a valid input to `List[str | BuildTarget]`. + # pylance/pyright gets this right, but for mypy we have to ignore the + # error + @T.overload + def _fn(xs: T.List[str], libs: bool = False) -> T.List[str]: ... # type: ignore + + @T.overload + def _fn(xs: T.List[LIBS], libs: bool = False) -> T.List[LIBS]: ... + + def _fn(xs: T.Union[T.List[str], T.List[LIBS]], libs: bool = False) -> T.Union[T.List[str], T.List[LIBS]]: + # Remove duplicates whilst preserving original order + result = [] + for x in xs: + # Don't de-dup unknown strings to avoid messing up arguments like: + # ['-framework', 'CoreAudio', '-framework', 'CoreMedia'] + known_flags = ['-pthread'] + cannot_dedup = libs and isinstance(x, str) and \ + not x.startswith(('-l', '-L')) and \ + x not in known_flags + if not cannot_dedup and _add_exclude(x): + continue + result.append(x) + return result + + # Handle lists in priority order: public items can be excluded from + # private and Requires can excluded from Libs. + self.pub_reqs = _fn(self.pub_reqs) + self.pub_libs = _fn(self.pub_libs, True) + self.priv_reqs = _fn(self.priv_reqs) + self.priv_libs = _fn(self.priv_libs, True) + # Reset exclude list just in case some values can be both cflags and libs. + exclude = set() + self.cflags = _fn(self.cflags) + +class PkgConfigModule(NewExtensionModule): + + INFO = ModuleInfo('pkgconfig') + + # Track already generated pkg-config files This is stored as a class + # variable so that multiple `import()`s share metadata + _metadata: T.ClassVar[T.Dict[str, MetaData]] = {} + + def __init__(self) -> None: + super().__init__() + self.methods.update({ + 'generate': self.generate, + }) + + def _get_lname(self, l: T.Union[build.SharedLibrary, build.StaticLibrary, build.CustomTarget, build.CustomTargetIndex], + msg: str, pcfile: str) -> str: + if isinstance(l, (build.CustomTargetIndex, build.CustomTarget)): + basename = os.path.basename(l.get_filename()) + name = os.path.splitext(basename)[0] + if name.startswith('lib'): + name = name[3:] + return name + # Nothing special + if not l.name_prefix_set: + return l.name + # Sometimes people want the library to start with 'lib' everywhere, + # which is achieved by setting name_prefix to '' and the target name to + # 'libfoo'. In that case, try to get the pkg-config '-lfoo' arg correct. + if l.prefix == '' and l.name.startswith('lib'): + return l.name[3:] + # If the library is imported via an import library which is always + # named after the target name, '-lfoo' is correct. + if isinstance(l, build.SharedLibrary) and l.import_filename: + return l.name + # In other cases, we can't guarantee that the compiler will be able to + # find the library via '-lfoo', so tell the user that. + mlog.warning(msg.format(l.name, 'name_prefix', l.name, pcfile)) + return l.name + + def _escape(self, value: T.Union[str, PurePath]) -> str: + ''' + We cannot use quote_arg because it quotes with ' and " which does not + work with pkg-config and pkgconf at all. + ''' + # We should always write out paths with / because pkg-config requires + # spaces to be quoted with \ and that messes up on Windows: + # https://bugs.freedesktop.org/show_bug.cgi?id=103203 + if isinstance(value, PurePath): + value = value.as_posix() + return value.replace(' ', r'\ ') + + def _make_relative(self, prefix: T.Union[PurePath, str], subdir: T.Union[PurePath, str]) -> str: + prefix = PurePath(prefix) + subdir = PurePath(subdir) + try: + libdir = subdir.relative_to(prefix) + except ValueError: + libdir = subdir + # pathlib joining makes sure absolute libdir is not appended to '${prefix}' + return ('${prefix}' / libdir).as_posix() + + def _generate_pkgconfig_file(self, state: ModuleState, deps: DependenciesHelper, + subdirs: T.List[str], name: T.Optional[str], + description: T.Optional[str], url: str, version: str, + pcfile: str, conflicts: T.List[str], + variables: T.List[T.Tuple[str, str]], + unescaped_variables: T.List[T.Tuple[str, str]], + uninstalled: bool = False, dataonly: bool = False, + pkgroot: T.Optional[str] = None) -> None: + coredata = state.environment.get_coredata() + referenced_vars = set() + optnames = [x.name for x in BUILTIN_DIR_OPTIONS.keys()] + + if not dataonly: + # includedir is always implied, although libdir may not be + # needed for header-only libraries + referenced_vars |= {'prefix', 'includedir'} + if deps.pub_libs or deps.priv_libs: + referenced_vars |= {'libdir'} + # also automatically infer variables referenced in other variables + implicit_vars_warning = False + redundant_vars_warning = False + varnames = set() + varstrings = set() + for k, v in variables + unescaped_variables: + varnames |= {k} + varstrings |= {v} + for optname in optnames: + optvar = f'${{{optname}}}' + if any(x.startswith(optvar) for x in varstrings): + if optname in varnames: + redundant_vars_warning = True + else: + # these 3 vars were always "implicit" + if dataonly or optname not in {'prefix', 'includedir', 'libdir'}: + implicit_vars_warning = True + referenced_vars |= {'prefix', optname} + if redundant_vars_warning: + FeatureDeprecated.single_use('pkgconfig.generate variable for builtin directories', '0.62.0', + state.subproject, 'They will be automatically included when referenced', + state.current_node) + if implicit_vars_warning: + FeatureNew.single_use('pkgconfig.generate implicit variable for builtin directories', '0.62.0', + state.subproject, location=state.current_node) + + if uninstalled: + outdir = os.path.join(state.environment.build_dir, 'meson-uninstalled') + if not os.path.exists(outdir): + os.mkdir(outdir) + prefix = PurePath(state.environment.get_build_dir()) + srcdir = PurePath(state.environment.get_source_dir()) + else: + outdir = state.environment.scratch_dir + prefix = PurePath(_as_str(coredata.get_option(mesonlib.OptionKey('prefix')))) + if pkgroot: + pkgroot_ = PurePath(pkgroot) + if not pkgroot_.is_absolute(): + pkgroot_ = prefix / pkgroot + elif prefix not in pkgroot_.parents: + raise mesonlib.MesonException('Pkgconfig prefix cannot be outside of the prefix ' + 'when pkgconfig.relocatable=true. ' + f'Pkgconfig prefix is {pkgroot_.as_posix()}.') + prefix = PurePath('${pcfiledir}', os.path.relpath(prefix, pkgroot_)) + fname = os.path.join(outdir, pcfile) + with open(fname, 'w', encoding='utf-8') as ofile: + for optname in optnames: + if optname in referenced_vars - varnames: + if optname == 'prefix': + ofile.write('prefix={}\n'.format(self._escape(prefix))) + else: + dirpath = PurePath(_as_str(coredata.get_option(mesonlib.OptionKey(optname)))) + ofile.write('{}={}\n'.format(optname, self._escape('${prefix}' / dirpath))) + if uninstalled and not dataonly: + ofile.write('srcdir={}\n'.format(self._escape(srcdir))) + if variables or unescaped_variables: + ofile.write('\n') + for k, v in variables: + ofile.write('{}={}\n'.format(k, self._escape(v))) + for k, v in unescaped_variables: + ofile.write(f'{k}={v}\n') + ofile.write('\n') + ofile.write(f'Name: {name}\n') + if len(description) > 0: + ofile.write(f'Description: {description}\n') + if len(url) > 0: + ofile.write(f'URL: {url}\n') + ofile.write(f'Version: {version}\n') + reqs_str = deps.format_reqs(deps.pub_reqs) + if len(reqs_str) > 0: + ofile.write(f'Requires: {reqs_str}\n') + reqs_str = deps.format_reqs(deps.priv_reqs) + if len(reqs_str) > 0: + ofile.write(f'Requires.private: {reqs_str}\n') + if len(conflicts) > 0: + ofile.write('Conflicts: {}\n'.format(' '.join(conflicts))) + + def generate_libs_flags(libs: T.List[LIBS]) -> T.Iterable[str]: + msg = 'Library target {0!r} has {1!r} set. Compilers ' \ + 'may not find it from its \'-l{2}\' linker flag in the ' \ + '{3!r} pkg-config file.' + Lflags = [] + for l in libs: + if isinstance(l, str): + yield l + else: + install_dir: T.Union[str, bool] + if uninstalled: + install_dir = os.path.dirname(state.backend.get_target_filename_abs(l)) + else: + _i = l.get_custom_install_dir() + install_dir = _i[0] if _i else None + if install_dir is False: + continue + if isinstance(l, build.BuildTarget) and 'cs' in l.compilers: + if isinstance(install_dir, str): + Lflag = '-r{}/{}'.format(self._escape(self._make_relative(prefix, install_dir)), l.filename) + else: # install_dir is True + Lflag = '-r${libdir}/%s' % l.filename + else: + if isinstance(install_dir, str): + Lflag = '-L{}'.format(self._escape(self._make_relative(prefix, install_dir))) + else: # install_dir is True + Lflag = '-L${libdir}' + if Lflag not in Lflags: + Lflags.append(Lflag) + yield Lflag + lname = self._get_lname(l, msg, pcfile) + # If using a custom suffix, the compiler may not be able to + # find the library + if isinstance(l, build.BuildTarget) and l.name_suffix_set: + mlog.warning(msg.format(l.name, 'name_suffix', lname, pcfile)) + if isinstance(l, (build.CustomTarget, build.CustomTargetIndex)) or 'cs' not in l.compilers: + yield f'-l{lname}' + + def get_uninstalled_include_dirs(libs: T.List[LIBS]) -> T.List[str]: + result: T.List[str] = [] + for l in libs: + if isinstance(l, (str, build.CustomTarget, build.CustomTargetIndex)): + continue + if l.get_subdir() not in result: + result.append(l.get_subdir()) + for i in l.get_include_dirs(): + curdir = i.get_curdir() + for d in i.get_incdirs(): + path = os.path.join(curdir, d) + if path not in result: + result.append(path) + return result + + def generate_uninstalled_cflags(libs: T.List[LIBS]) -> T.Iterable[str]: + for d in get_uninstalled_include_dirs(libs): + for basedir in ['${prefix}', '${srcdir}']: + path = PurePath(basedir, d) + yield '-I%s' % self._escape(path.as_posix()) + + if len(deps.pub_libs) > 0: + ofile.write('Libs: {}\n'.format(' '.join(generate_libs_flags(deps.pub_libs)))) + if len(deps.priv_libs) > 0: + ofile.write('Libs.private: {}\n'.format(' '.join(generate_libs_flags(deps.priv_libs)))) + + cflags: T.List[str] = [] + if uninstalled: + cflags += generate_uninstalled_cflags(deps.pub_libs + deps.priv_libs) + else: + for d in subdirs: + if d == '.': + cflags.append('-I${includedir}') + else: + cflags.append(self._escape(PurePath('-I${includedir}') / d)) + cflags += [self._escape(f) for f in deps.cflags] + if cflags and not dataonly: + ofile.write('Cflags: {}\n'.format(' '.join(cflags))) + + @typed_pos_args('pkgconfig.generate', optargs=[(build.SharedLibrary, build.StaticLibrary)]) + @typed_kwargs( + 'pkgconfig.generate', + D_MODULE_VERSIONS_KW.evolve(since='0.43.0'), + INSTALL_DIR_KW, + KwargInfo('conflicts', ContainerTypeInfo(list, str), default=[], listify=True), + KwargInfo('dataonly', bool, default=False, since='0.54.0'), + KwargInfo('description', (str, NoneType)), + KwargInfo('extra_cflags', ContainerTypeInfo(list, str), default=[], listify=True, since='0.42.0'), + KwargInfo('filebase', (str, NoneType), validator=lambda x: 'must not be an empty string' if x == '' else None), + KwargInfo('name', (str, NoneType), validator=lambda x: 'must not be an empty string' if x == '' else None), + KwargInfo('subdirs', ContainerTypeInfo(list, str), default=[], listify=True), + KwargInfo('url', str, default=''), + KwargInfo('version', (str, NoneType)), + VARIABLES_KW.evolve(name="unescaped_uninstalled_variables", since='0.59.0'), + VARIABLES_KW.evolve(name="unescaped_variables", since='0.59.0'), + VARIABLES_KW.evolve(name="uninstalled_variables", since='0.54.0', since_values={dict: '0.56.0'}), + VARIABLES_KW.evolve(since='0.41.0', since_values={dict: '0.56.0'}), + _PKG_LIBRARIES, + _PKG_LIBRARIES.evolve(name='libraries_private'), + _PKG_REQUIRES, + _PKG_REQUIRES.evolve(name='requires_private'), + ) + def generate(self, state: ModuleState, + args: T.Tuple[T.Optional[T.Union[build.SharedLibrary, build.StaticLibrary]]], + kwargs: GenerateKw) -> ModuleReturnValue: + default_version = state.project_version + default_install_dir: T.Optional[str] = None + default_description: T.Optional[str] = None + default_name: T.Optional[str] = None + mainlib: T.Optional[T.Union[build.SharedLibrary, build.StaticLibrary]] = None + default_subdirs = ['.'] + if args[0]: + FeatureNew.single_use('pkgconfig.generate optional positional argument', '0.46.0', state.subproject) + mainlib = args[0] + default_name = mainlib.name + default_description = state.project_name + ': ' + mainlib.name + install_dir = mainlib.get_custom_install_dir() + if install_dir and isinstance(install_dir[0], str): + default_install_dir = os.path.join(install_dir[0], 'pkgconfig') + else: + if kwargs['version'] is None: + FeatureNew.single_use('pkgconfig.generate implicit version keyword', '0.46.0', state.subproject) + if kwargs['name'] is None: + raise build.InvalidArguments( + 'pkgconfig.generate: if a library is not passed as a ' + 'positional argument, the name keyword argument is ' + 'required.') + + dataonly = kwargs['dataonly'] + if dataonly: + default_subdirs = [] + blocked_vars = ['libraries', 'libraries_private', 'requires_private', 'extra_cflags', 'subdirs'] + if any(kwargs[k] for k in blocked_vars): # type: ignore + raise mesonlib.MesonException(f'Cannot combine dataonly with any of {blocked_vars}') + default_install_dir = os.path.join(state.environment.get_datadir(), 'pkgconfig') + + subdirs = kwargs['subdirs'] or default_subdirs + version = kwargs['version'] if kwargs['version'] is not None else default_version + name = kwargs['name'] if kwargs['name'] is not None else default_name + assert isinstance(name, str), 'for mypy' + filebase = kwargs['filebase'] if kwargs['filebase'] is not None else name + description = kwargs['description'] if kwargs['description'] is not None else default_description + url = kwargs['url'] + conflicts = kwargs['conflicts'] + + # Prepend the main library to public libraries list. This is required + # so dep.add_pub_libs() can handle dependency ordering correctly and put + # extra libraries after the main library. + libraries = kwargs['libraries'].copy() + if mainlib: + libraries.insert(0, mainlib) + + deps = DependenciesHelper(state, filebase, self._metadata) + deps.add_pub_libs(libraries) + deps.add_priv_libs(kwargs['libraries_private']) + deps.add_pub_reqs(kwargs['requires']) + deps.add_priv_reqs(kwargs['requires_private']) + deps.add_cflags(kwargs['extra_cflags']) + + dversions = kwargs['d_module_versions'] + if dversions: + compiler = state.environment.coredata.compilers.host.get('d') + if compiler: + deps.add_cflags(compiler.get_feature_args({'versions': dversions}, None)) + + deps.remove_dups() + + def parse_variable_list(vardict: T.Dict[str, str]) -> T.List[T.Tuple[str, str]]: + reserved = ['prefix', 'libdir', 'includedir'] + variables = [] + for name, value in vardict.items(): + if not dataonly and name in reserved: + raise mesonlib.MesonException(f'Variable "{name}" is reserved') + variables.append((name, value)) + return variables + + variables = parse_variable_list(kwargs['variables']) + unescaped_variables = parse_variable_list(kwargs['unescaped_variables']) + + pcfile = filebase + '.pc' + pkgroot = pkgroot_name = kwargs['install_dir'] or default_install_dir + if pkgroot is None: + if mesonlib.is_freebsd(): + pkgroot = os.path.join(_as_str(state.environment.coredata.get_option(mesonlib.OptionKey('prefix'))), 'libdata', 'pkgconfig') + pkgroot_name = os.path.join('{prefix}', 'libdata', 'pkgconfig') + elif mesonlib.is_haiku(): + pkgroot = os.path.join(_as_str(state.environment.coredata.get_option(mesonlib.OptionKey('prefix'))), 'develop', 'lib', 'pkgconfig') + pkgroot_name = os.path.join('{prefix}', 'develop', 'lib', 'pkgconfig') + else: + pkgroot = os.path.join(_as_str(state.environment.coredata.get_option(mesonlib.OptionKey('libdir'))), 'pkgconfig') + pkgroot_name = os.path.join('{libdir}', 'pkgconfig') + relocatable = state.get_option('relocatable', module='pkgconfig') + self._generate_pkgconfig_file(state, deps, subdirs, name, description, url, + version, pcfile, conflicts, variables, + unescaped_variables, False, dataonly, + pkgroot=pkgroot if relocatable else None) + res = build.Data([mesonlib.File(True, state.environment.get_scratch_dir(), pcfile)], pkgroot, pkgroot_name, None, state.subproject, install_tag='devel') + variables = parse_variable_list(kwargs['uninstalled_variables']) + unescaped_variables = parse_variable_list(kwargs['unescaped_uninstalled_variables']) + + pcfile = filebase + '-uninstalled.pc' + self._generate_pkgconfig_file(state, deps, subdirs, name, description, url, + version, pcfile, conflicts, variables, + unescaped_variables, uninstalled=True, dataonly=dataonly) + # Associate the main library with this generated pc file. If the library + # is used in any subsequent call to the generated, it will generate a + # 'Requires:' or 'Requires.private:'. + # Backward compatibility: We used to set 'generated_pc' on all public + # libraries instead of just the main one. Keep doing that but warn if + # anyone is relying on that deprecated behaviour. + if mainlib: + if mainlib.get_id() not in self._metadata: + self._metadata[mainlib.get_id()] = MetaData( + filebase, name, state.current_node) + else: + mlog.warning('Already generated a pkg-config file for', mlog.bold(mainlib.name)) + else: + for lib in deps.pub_libs: + if not isinstance(lib, str) and lib.get_id() not in self._metadata: + self._metadata[lib.get_id()] = MetaData( + filebase, name, state.current_node) + return ModuleReturnValue(res, [res]) + + +def initialize(interp: Interpreter) -> PkgConfigModule: + return PkgConfigModule() |