aboutsummaryrefslogtreecommitdiff
path: root/mesonbuild/dependencies/configtool.py
blob: 7dccee4b66080aa280795113ca7a71d47e8b9244 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# 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, 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
    :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'
    __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=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: 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

    def log_tried(self) -> str:
        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}')