# Copyright 2013-2021 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 .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 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_configtool_variable(self, variable_name: str) -> str: p, out, _ = Popen_safe(self.config + [f'--{variable_name}']) if p.returncode != 0: if self.required: raise DependencyException( 'Could not get variable "{}" for dependency {}'.format( variable_name, self.name)) variable = out.strip() mlog.debug(f'Got config-tool variable {variable_name} : {variable}') return variable @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: T.Optional[T.List[str]] = None) -> str: if configtool: # In the not required case '' (empty string) will be returned if the # variable is not found. Since '' is a valid value to return we # set required to True here to force and error, and use the # finally clause to ensure it's restored. restore = self.required self.required = True try: return self.get_configtool_variable(configtool) except DependencyException: pass finally: self.required = restore 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}')