# 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 .base import ExternalDependency, DependencyException, DependencyMethods from ..mesonlib import listify, Popen_safe, split_args, version_compare, version_compare_many from ..programs import find_external_program from .. import mlog import re import typing as T 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 :returncode_value int: The value of the correct returncode Because some tools are stupid and don't return 0 """ tools = None tool_name = None version_arg = '--version' __strip_version = re.compile(r'^[0-9][0-9.]+') def __init__(self, name, environment, kwargs, language: T.Optional[str] = None): super().__init__('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 = kwargs.get('version', None) 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): """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.Optional[T.List[str]] = None, returncode: int = 0) \ -> T.Tuple[T.Optional[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. """ if not isinstance(versions, list) and versions is not None: versions = listify(versions) best_match = (None, None) # type: T.Tuple[T.Optional[str], T.Optional[str]] for potential_bin in find_external_program( self.env, self.for_machine, self.tool_name, self.tool_name, self.tools, allow_default_for_cross=False): 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: 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, req_version): """Helper method to print messages about the tool.""" found_msg = [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 is not None: 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(self.config + args) if p.returncode != 0: if self.required: raise DependencyException( 'Could not generate {} for {}.\n{}'.format( stage, self.name, err)) return [] return split_args(out) @staticmethod def get_methods(): return [DependencyMethods.AUTO, DependencyMethods.CONFIG_TOOL] def get_configtool_variable(self, variable_name): 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 def log_tried(self): return self.type_name 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) -> T.Union[str, T.List[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}')