The Meson Build System
http://mesonbuild.com/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
187 lines
8.0 KiB
187 lines
8.0 KiB
# 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, 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(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}')
|
|
|