diff --git a/mesonbuild/dependencies/pkgconfig.py b/mesonbuild/dependencies/pkgconfig.py index 1d9863a1c..9729baa84 100644 --- a/mesonbuild/dependencies/pkgconfig.py +++ b/mesonbuild/dependencies/pkgconfig.py @@ -16,7 +16,7 @@ from __future__ import annotations from pathlib import Path from .base import ExternalDependency, DependencyException, sort_libpaths, DependencyTypeName -from ..mesonlib import EnvironmentVariables, OptionKey, OrderedSet, PerMachine, Popen_safe, Popen_safe_logged +from ..mesonlib import EnvironmentVariables, OptionKey, OrderedSet, PerMachine, Popen_safe, Popen_safe_logged, MachineChoice from ..programs import find_external_program, ExternalProgram from .. import mlog from pathlib import PurePath @@ -29,20 +29,31 @@ if T.TYPE_CHECKING: from typing_extensions import Literal from ..environment import Environment - from ..mesonlib import MachineChoice from ..utils.core import EnvironOrDict from ..interpreter.type_checking import PkgConfigDefineType class PkgConfigInterface: '''Base class wrapping a pkg-config implementation''' + class_impl: PerMachine[T.Union[Literal[False], T.Optional[PkgConfigInterface]]] = PerMachine(False, False) + @staticmethod def instance(env: Environment, for_machine: MachineChoice, silent: bool) -> T.Optional[PkgConfigInterface]: - impl = PkgConfigCLI(env, for_machine, silent) - if not impl.found(): - return None + for_machine = for_machine if env.is_cross_build() else MachineChoice.HOST + impl = PkgConfigInterface.class_impl[for_machine] + if impl is False: + impl = PkgConfigCLI(env, for_machine, silent) + if not impl.found(): + impl = None + if not impl and not silent: + mlog.log('Found pkg-config:', mlog.red('NO')) + PkgConfigInterface.class_impl[for_machine] = impl return impl + def __init__(self, env: Environment, for_machine: MachineChoice) -> None: + self.env = env + self.for_machine = for_machine + def found(self) -> bool: '''Return whether pkg-config is supported''' raise NotImplementedError @@ -78,9 +89,6 @@ class PkgConfigInterface: class PkgConfigCLI(PkgConfigInterface): '''pkg-config CLI implementation''' - # The class's copy of the pkg-config path. Avoids having to search for it - # multiple times in the same Meson invocation. - class_pkgbin: PerMachine[T.Union[None, Literal[False], ExternalProgram]] = PerMachine(None, None) # We cache all pkg-config subprocess invocations to avoid redundant calls pkgbin_cache: T.Dict[ T.Tuple[ExternalProgram, T.Tuple[str, ...], T.FrozenSet[T.Tuple[str, str]]], @@ -88,11 +96,12 @@ class PkgConfigCLI(PkgConfigInterface): ] = {} def __init__(self, env: Environment, for_machine: MachineChoice, silent: bool) -> None: - self.env = env - self.for_machine = for_machine + super().__init__(env, for_machine) # Store a copy of the pkg-config path on the object itself so it is # stored in the pickled coredata and recovered. - self.pkgbin = self._detect_pkgbin(env, for_machine, silent) + self.pkgbin = self._detect_pkgbin(env, for_machine) + if self.pkgbin and not silent: + mlog.log('Found pkg-config:', mlog.green('YES'), mlog.blue(self.pkgbin.get_path())) def found(self) -> bool: return bool(self.pkgbin) @@ -162,41 +171,21 @@ class PkgConfigCLI(PkgConfigInterface): raise DependencyException(f'could not list modules:\n{err}\n') return [i.split(' ', 1)[0] for i in out.splitlines()] - def _split_args(self, cmd: str) -> T.List[str]: + @staticmethod + def _split_args(cmd: str) -> T.List[str]: # pkg-config paths follow Unix conventions, even on Windows; split the # output using shlex.split rather than mesonlib.split_args return shlex.split(cmd) - @classmethod - def _detect_pkgbin(cls, env: Environment, for_machine: MachineChoice, silent: bool) -> T.Optional[ExternalProgram]: - # Only search for pkg-config for each machine the first time and store - # the result in the class definition - if cls.class_pkgbin[for_machine] is False: - mlog.debug(f'Pkg-config binary for {for_machine} is cached as not found.') - elif cls.class_pkgbin[for_machine] is not None: - mlog.debug(f'Pkg-config binary for {for_machine} is cached.') - else: - assert cls.class_pkgbin[for_machine] is None, 'for mypy' - mlog.debug(f'Pkg-config binary for {for_machine} is not cached.') - for potential_pkgbin in find_external_program( - env, for_machine, 'pkgconfig', 'Pkg-config', - env.default_pkgconfig, allow_default_for_cross=False): - version_if_ok = cls.check_pkgconfig(env, potential_pkgbin) - if not version_if_ok: - continue - if not silent: - mlog.log('Found pkg-config:', mlog.bold(potential_pkgbin.get_path()), - f'({version_if_ok})') - cls.class_pkgbin[for_machine] = potential_pkgbin - break - else: - if not silent: - mlog.log('Found pkg-config:', mlog.red('NO')) - # Set to False instead of None to signify that we've already - # searched for it and not found it - cls.class_pkgbin[for_machine] = False - - return cls.class_pkgbin[for_machine] or None + @staticmethod + def _detect_pkgbin(env: Environment, for_machine: MachineChoice) -> T.Optional[ExternalProgram]: + for potential_pkgbin in find_external_program( + env, for_machine, 'pkgconfig', 'Pkg-config', + env.default_pkgconfig, allow_default_for_cross=False): + version_if_ok = PkgConfigCLI.check_pkgconfig(env, potential_pkgbin) + if version_if_ok: + return potential_pkgbin + return None def _call_pkgbin_real(self, args: T.List[str], env: T.Dict[str, str]) -> T.Tuple[int, str, str]: assert isinstance(self.pkgbin, ExternalProgram) diff --git a/mesonbuild/utils/universal.py b/mesonbuild/utils/universal.py index 1d0e382a8..8216e7804 100644 --- a/mesonbuild/utils/universal.py +++ b/mesonbuild/utils/universal.py @@ -524,6 +524,10 @@ class PerMachine(T.Generic[_T]): unfreeze.host = None return unfreeze + def assign(self, build: _T, host: _T) -> None: + self.build = build + self.host = host + def __repr__(self) -> str: return f'PerMachine({self.build!r}, {self.host!r})' diff --git a/run_tests.py b/run_tests.py index 699e29356..a959d6ab5 100755 --- a/run_tests.py +++ b/run_tests.py @@ -36,7 +36,7 @@ import typing as T from mesonbuild.compilers.c import CCompiler from mesonbuild.compilers.detect import detect_c_compiler -from mesonbuild.dependencies.pkgconfig import PkgConfigCLI +from mesonbuild.dependencies.pkgconfig import PkgConfigInterface from mesonbuild import mesonlib from mesonbuild import mesonmain from mesonbuild import mtest @@ -161,6 +161,8 @@ def get_fake_env(sdir='', bdir=None, prefix='', opts=None): env = Environment(sdir, bdir, opts) env.coredata.options[OptionKey('args', lang='c')] = FakeCompilerOptions() env.machines.host.cpu_family = 'x86_64' # Used on macOS inside find_library + # Invalidate cache when using a different Environment object. + clear_meson_configure_class_caches() return env def get_convincing_fake_env_and_cc(bdir, prefix): @@ -309,11 +311,10 @@ def run_mtest_inprocess(commandlist: T.List[str]) -> T.Tuple[int, str, str]: return returncode, out.getvalue() def clear_meson_configure_class_caches() -> None: - CCompiler.find_library_cache = {} - CCompiler.find_framework_cache = {} - PkgConfigCLI.pkgbin_cache = {} - PkgConfigCLI.class_pkgbin = mesonlib.PerMachine(None, None) - mesonlib.project_meson_versions = collections.defaultdict(str) + CCompiler.find_library_cache.clear() + CCompiler.find_framework_cache.clear() + PkgConfigInterface.class_impl.assign(False, False) + mesonlib.project_meson_versions.clear() def run_configure_inprocess(commandlist: T.List[str], env: T.Optional[T.Dict[str, str]] = None, catch_exception: bool = False) -> T.Tuple[int, str, str]: stderr = StringIO()