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