1import copy
2import fnmatch
3import re
4from collections import OrderedDict, defaultdict
5
6from conans.errors import ConanException
7from conans.model.ref import ConanFileReference
8from conans.util.log import logger
9
10
11def unquote(text):
12    text = text.strip()
13    if len(text) > 1 and (text[0] == text[-1]) and text[0] in "'\"":
14        return text[1:-1]
15    return text
16
17
18class EnvValues(object):
19    """ Object to represent the introduced env values entered by the user
20    with the -e or profiles etc.
21        self._data is a dictionary with: {package: {var: value}}
22            "package" can be None if the var is global.
23            "value" can be a list or a string. If it's a list the variable
24            is appendable like PATH or PYTHONPATH
25    """
26
27    def __init__(self):
28        self._data = defaultdict(dict)
29
30    def copy(self):
31        ret = EnvValues()
32        ret._data = copy.deepcopy(self._data)
33        return ret
34
35    @staticmethod
36    def load_value(the_value):
37        if the_value.startswith("[") and the_value.endswith("]"):
38            return [val.strip() for val in the_value[1:-1].split(",") if val]
39        else:
40            return the_value
41
42    @staticmethod
43    def loads(text):
44        ret = EnvValues()
45        if not text:
46            return ret
47        for env_def in text.splitlines():
48            try:
49                if env_def:
50                    if "=" not in env_def:
51                        raise ConanException("Invalid env line '%s'" % env_def)
52                    tmp = env_def.split("=", 1)
53                    name = tmp[0]
54                    value = unquote(tmp[1])
55                    package = None
56                    if ":" in name:
57                        tmp = name.split(":", 1)
58                        package = tmp[0].strip()
59                        name = tmp[1].strip()
60                    else:
61                        name = name.strip()
62                    # Lists values=> MYVAR=[1,2,three]
63                    value = EnvValues.load_value(value)
64                    ret.add(name, value, package)
65            except ConanException:
66                raise
67            except Exception as exc:
68                raise ConanException("Error parsing the env values: %s" % str(exc))
69
70        return ret
71
72    def dumps(self):
73
74        def append_vars(pairs, result):
75            for name, value in sorted(pairs.items()):
76                if isinstance(value, list):
77                    value = "[%s]" % ",".join(value)
78                if package:
79                    result.append("%s:%s=%s" % (package, name, value))
80                else:
81                    result.append("%s=%s" % (name, value))
82
83        result = []
84        # First the global vars
85        for package, pairs in self._sorted_data:
86            if package is None:
87                append_vars(pairs, result)
88
89        # Then the package scoped ones
90        for package, pairs in self._sorted_data:
91            if package is not None:
92                append_vars(pairs, result)
93
94        return "\n".join(result)
95
96    @property
97    def data(self):
98        return self._data
99
100    @property
101    def _sorted_data(self):
102        # Python 3 can't compare None with strings, so if None we order just with the var name
103        return [(key, self._data[key]) for key in sorted(self._data, key=lambda x: x if x else "a")]
104
105    def add(self, name, value, package=None):
106        # New data, not previous value
107        if name not in self._data[package]:
108            self._data[package][name] = value
109        # There is data already
110        else:
111            # Only append at the end if we had a list
112            if isinstance(self._data[package][name], list):
113                if isinstance(value, list):
114                    self._data[package][name].extend(value)
115                else:
116                    self._data[package][name].append(value)
117
118    def remove(self, name, package=None):
119        del self._data[package][name]
120
121    def update_replace(self, key, value):
122        """ method useful for command "conan profile update"
123        to execute real update instead of soft update
124        """
125        if ":" in key:
126            package_name, key = key.split(":", 1)
127        else:
128            package_name, key = None, key
129        self._data[package_name][key] = value
130
131    def update(self, env_obj):
132        """accepts other EnvValues object or DepsEnvInfo
133           it prioritize the values that are already at self._data
134        """
135        if env_obj:
136            if isinstance(env_obj, EnvValues):
137                for package_name, env_vars in env_obj.data.items():
138                    for name, value in env_vars.items():
139                        if isinstance(value, list):
140                            value = copy.copy(value)  # Aware of copying by reference the list
141                        self.add(name, value, package_name)
142            # DepsEnvInfo. the OLD values are always kept, never overwrite,
143            elif isinstance(env_obj, DepsEnvInfo):
144                for (name, value) in env_obj.vars.items():
145                    self.add(name, value)
146            else:
147                raise ConanException("unknown env type: %s" % env_obj)
148
149    def env_dicts(self, package_name, version=None, user=None, channel=None):
150        """Returns two dicts of env variables that applies to package 'name',
151         the first for simple values A=1, and the second for multiple A=1;2;3"""
152        ret = {}
153        ret_multi = {}
154        # First process the global variables
155
156        global_pairs = self._data.get(None)
157        own_pairs = None
158        str_ref = str(ConanFileReference(package_name, version, user, channel, validate=False))
159        for pattern, v in self._data.items():
160            if pattern is not None and (package_name == pattern or fnmatch.fnmatch(str_ref,
161                                                                                   pattern)):
162                own_pairs = v
163                break
164
165        if global_pairs:
166            for name, value in global_pairs.items():
167                if isinstance(value, list):
168                    ret_multi[name] = value
169                else:
170                    ret[name] = value
171
172        # Then the package scoped vars, that will override the globals
173        if own_pairs:
174            for name, value in own_pairs.items():
175                if isinstance(value, list):
176                    ret_multi[name] = value
177                    if name in ret:  # Already exists a global variable, remove it
178                        del ret[name]
179                else:
180                    ret[name] = value
181                    if name in ret_multi:  # Already exists a list global variable, remove it
182                        del ret_multi[name]
183
184        # FIXME: This dict is only used doing a ret.update(ret_multi). Unnecessary?
185        return ret, ret_multi
186
187    def __repr__(self):
188        return str(dict(self._data))
189
190
191class EnvInfo(object):
192    """ Object that stores all the environment variables required:
193
194    env = EnvInfo()
195    env.hola = True
196    env.Cosa.append("OTRO")
197    env.Cosa.append("MAS")
198    env.Cosa = "hello"
199    env.Cosa.append("HOLA")
200
201    """
202    def __init__(self):
203        self._values_ = {}
204
205    @staticmethod
206    def _adjust_casing(name):
207        """We don't want to mix "path" with "PATH", actually we don`t want to mix anything
208        with different casing. Furthermore in Windows all is uppercase, but managing all in
209        upper case will be breaking."""
210        return name.upper() if name.lower() == "path" else name
211
212    def __getattr__(self, name):
213        if name.startswith("_") and name.endswith("_"):
214            return super(EnvInfo, self).__getattr__(name)
215        name = self._adjust_casing(name)
216        attr = self._values_.get(name)
217        if not attr:
218            self._values_[name] = []
219        return self._values_[name]
220
221    def __setattr__(self, name, value):
222        if name.startswith("_") and name.endswith("_"):
223            return super(EnvInfo, self).__setattr__(name, value)
224        name = self._adjust_casing(name)
225        self._values_[name] = value
226
227    @property
228    def vars(self):
229        return self._values_
230
231
232class DepsEnvInfo(EnvInfo):
233    """ All the env info for a conanfile dependencies
234    """
235    def __init__(self):
236        super(DepsEnvInfo, self).__init__()
237        self._dependencies_ = OrderedDict()
238
239    @property
240    def dependencies(self):
241        return self._dependencies_.items()
242
243    @property
244    def deps(self):
245        return self._dependencies_.keys()
246
247    def __getitem__(self, item):
248        return self._dependencies_[item]
249
250    def update(self, dep_env_info, pkg_name):
251        self._dependencies_[pkg_name] = dep_env_info
252
253        def merge_lists(seq1, seq2):
254            return [s for s in seq1 if s not in seq2] + seq2
255
256        # With vars if its set the keep the set value
257        for varname, value in dep_env_info.vars.items():
258            if varname not in self.vars:
259                self.vars[varname] = value
260            elif isinstance(self.vars[varname], list):
261                if isinstance(value, list):
262                    self.vars[varname] = merge_lists(self.vars[varname], value)
263                else:
264                    self.vars[varname] = merge_lists(self.vars[varname], [value])
265            else:
266                logger.warning("DISCARDED variable %s=%s from %s" % (varname, value, pkg_name))
267
268    def update_deps_env_info(self, dep_env_info):
269        assert isinstance(dep_env_info, DepsEnvInfo)
270        for pkg_name, env_info in dep_env_info.dependencies:
271            self.update(env_info, pkg_name)
272
273    @staticmethod
274    def loads(text):
275        ret = DepsEnvInfo()
276        lib_name = None
277        env_info = None
278        for line in text.splitlines():
279            if not lib_name and not line.startswith("[ENV_"):
280                raise ConanException("Error, invalid file format reading env info variables")
281            elif line.startswith("[ENV_"):
282                if env_info:
283                    ret.update(env_info, lib_name)
284                lib_name = line[5:-1]
285                env_info = EnvInfo()
286            else:
287                var_name, value = line.split("=", 1)
288                if value and value[0] == "[" and value[-1] == "]":
289                    # Take all the items between quotes
290                    values = re.findall('"([^"]*)"', value[1:-1])
291                    for val in values:
292                        getattr(env_info, var_name).append(val)
293                else:
294                    setattr(env_info, var_name, value)  # peel quotes
295        if env_info:
296            ret.update(env_info, lib_name)
297
298        return ret
299
300    def dumps(self):
301        sections = []
302        for name, env_info in self._dependencies_.items():
303            sections.append("[ENV_%s]" % name)
304            for var, values in sorted(env_info.vars.items()):
305                tmp = "%s=" % var
306                if isinstance(values, list):
307                    tmp += "[%s]" % ",".join(['"%s"' % val for val in values])
308                else:
309                    tmp += '%s' % values
310                sections.append(tmp)
311        return "\n".join(sections)
312