blob: 679c69f5d9be9bb77d50e4a09b4584c64cd28f7d [file] [log] [blame]
# SPDX-License-Identifier: Apache-2.0
# Copyright 2013-2021 The Meson development team
from __future__ import annotations
from .base import ExternalDependency, DependencyException, DependencyTypeName
from ..mesonlib import listify, Popen_safe, Popen_safe_logged, split_args, version_compare, version_compare_many
from ..programs import find_external_program
from .. import mlog
import re
import typing as T
from mesonbuild import mesonlib
if T.TYPE_CHECKING:
from ..environment import Environment
from ..interpreter.type_checking import PkgConfigDefineType
class ConfigToolDependency(ExternalDependency):
"""Class representing dependencies found using a config tool.
Takes the following extra keys in kwargs that it uses internally:
:tools List[str]: A list of tool names to use
:version_arg str: The argument to pass to the tool to get it's version
:skip_version str: The argument to pass to the tool to ignore its version
(if ``version_arg`` fails, but it may start accepting it in the future)
Because some tools are stupid and don't accept --version
:returncode_value int: The value of the correct returncode
Because some tools are stupid and don't return 0
"""
tools: T.Optional[T.List[str]] = None
tool_name: T.Optional[str] = None
version_arg = '--version'
skip_version: T.Optional[str] = None
allow_default_for_cross = False
__strip_version = re.compile(r'^[0-9][0-9.]+')
def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None):
super().__init__(DependencyTypeName('config-tool'), environment, kwargs, language=language)
self.name = name
# You may want to overwrite the class version in some cases
self.tools = listify(kwargs.get('tools', self.tools))
if not self.tool_name:
self.tool_name = self.tools[0]
if 'version_arg' in kwargs:
self.version_arg = kwargs['version_arg']
req_version_raw = kwargs.get('version', None)
if req_version_raw is not None:
req_version = mesonlib.stringlistify(req_version_raw)
else:
req_version = []
tool, version = self.find_config(req_version, kwargs.get('returncode_value', 0))
self.config = tool
self.is_found = self.report_config(version, req_version)
if not self.is_found:
self.config = None
return
self.version = version
def _sanitize_version(self, version: str) -> str:
"""Remove any non-numeric, non-point version suffixes."""
m = self.__strip_version.match(version)
if m:
# Ensure that there isn't a trailing '.', such as an input like
# `1.2.3.git-1234`
return m.group(0).rstrip('.')
return version
def find_config(self, versions: T.List[str], returncode: int = 0) \
-> T.Tuple[T.Optional[T.List[str]], T.Optional[str]]:
"""Helper method that searches for config tool binaries in PATH and
returns the one that best matches the given version requirements.
"""
best_match: T.Tuple[T.Optional[T.List[str]], T.Optional[str]] = (None, None)
for potential_bin in find_external_program(
self.env, self.for_machine, self.tool_name,
self.tool_name, self.tools, allow_default_for_cross=self.allow_default_for_cross):
if not potential_bin.found():
continue
tool = potential_bin.get_command()
try:
p, out = Popen_safe(tool + [self.version_arg])[:2]
except (FileNotFoundError, PermissionError):
continue
if p.returncode != returncode:
if self.skip_version:
# maybe the executable is valid even if it doesn't support --version
p = Popen_safe(tool + [self.skip_version])[0]
if p.returncode != returncode:
continue
else:
continue
out = self._sanitize_version(out.strip())
# Some tools, like pcap-config don't supply a version, but also
# don't fail with --version, in that case just assume that there is
# only one version and return it.
if not out:
return (tool, None)
if versions:
is_found = version_compare_many(out, versions)[0]
# This allows returning a found version without a config tool,
# which is useful to inform the user that you found version x,
# but y was required.
if not is_found:
tool = None
if best_match[1]:
if version_compare(out, '> {}'.format(best_match[1])):
best_match = (tool, out)
else:
best_match = (tool, out)
return best_match
def report_config(self, version: T.Optional[str], req_version: T.List[str]) -> bool:
"""Helper method to print messages about the tool."""
found_msg: T.List[T.Union[str, mlog.AnsiDecorator]] = [mlog.bold(self.tool_name), 'found:']
if self.config is None:
found_msg.append(mlog.red('NO'))
if version is not None and req_version:
found_msg.append(f'found {version!r} but need {req_version!r}')
elif req_version:
found_msg.append(f'need {req_version!r}')
else:
found_msg += [mlog.green('YES'), '({})'.format(' '.join(self.config)), version]
mlog.log(*found_msg)
return self.config is not None
def get_config_value(self, args: T.List[str], stage: str) -> T.List[str]:
p, out, err = Popen_safe_logged(self.config + args)
if p.returncode != 0:
if self.required:
raise DependencyException(f'Could not generate {stage} for {self.name}.\n{err}')
return []
return split_args(out)
def get_variable_args(self, variable_name: str) -> T.List[str]:
return [f'--{variable_name}']
@staticmethod
def log_tried() -> str:
return 'config-tool'
def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None,
configtool: T.Optional[str] = None, internal: T.Optional[str] = None,
default_value: T.Optional[str] = None,
pkgconfig_define: PkgConfigDefineType = None) -> str:
if configtool:
p, out, _ = Popen_safe(self.config + self.get_variable_args(configtool))
if p.returncode == 0:
variable = out.strip()
mlog.debug(f'Got config-tool variable {configtool} : {variable}')
return variable
if default_value is not None:
return default_value
raise DependencyException(f'Could not get config-tool variable and no default provided for {self!r}')