1import re 2from collections import namedtuple 3 4from six import string_types 5 6from conans.errors import ConanException, InvalidNameException 7from conans.model.version import Version 8 9 10def _split_pair(pair, split_char): 11 if not pair or pair == split_char: 12 return None, None 13 if split_char not in pair: 14 return None 15 16 words = pair.split(split_char) 17 if len(words) != 2: 18 raise ConanException("The reference has too many '{}'".format(split_char)) 19 else: 20 return words 21 22 23def _noneize(text): 24 if not text or text == "_": 25 return None 26 return text 27 28 29def get_reference_fields(arg_reference, user_channel_input=False): 30 # FIXME: The partial references meaning user/channel should be disambiguated at 2.0 31 """ 32 :param arg_reference: String with a complete reference, or 33 only user/channel (if user_channel_input) 34 only name/version (if not pattern_is_user_channel) 35 :param user_channel_input: Two items means user/channel or not. 36 :return: name, version, user and channel, in a tuple 37 """ 38 39 if not arg_reference: 40 return None, None, None, None, None 41 42 revision = None 43 44 if "#" in arg_reference: 45 tmp = arg_reference.split("#", 1) 46 revision = tmp[1] 47 arg_reference = tmp[0] 48 49 if "@" in arg_reference: 50 name_version, user_channel = _split_pair(arg_reference, "@") 51 # FIXME: Conan 2.0 52 # In conan now "xxx@conan/stable" means that xxx is the version, I would say it should 53 # be the name 54 name, version = _split_pair(name_version, "/") or (None, name_version) 55 user, channel = _split_pair(user_channel, "/") or (user_channel, None) 56 57 return _noneize(name), _noneize(version), _noneize(user), _noneize(channel), \ 58 _noneize(revision) 59 else: 60 if user_channel_input: 61 # x/y is user and channel 62 el1, el2 = _split_pair(arg_reference, "/") or (arg_reference, None) 63 return None, None, _noneize(el1), _noneize(el2), _noneize(revision) 64 else: 65 # x/y is name and version 66 el1, el2 = _split_pair(arg_reference, "/") or (arg_reference, None) 67 return _noneize(el1), _noneize(el2), None, None, _noneize(revision) 68 69 70def check_valid_ref(reference, strict_mode=True): 71 """ 72 :param reference: string to be analyzed if it is a reference or not 73 :param strict_mode: Only if the reference contains the "@" is valid, used to disambiguate""" 74 try: 75 if not reference: 76 return False 77 if strict_mode: 78 if "@" not in reference: 79 return False 80 if "*" in reference: 81 ref = ConanFileReference.loads(reference, validate=True) 82 if "*" in ref.name or "*" in ref.user or "*" in ref.channel: 83 return False 84 if str(ref.version).startswith("["): # It is a version range 85 return True 86 return False 87 ConanFileReference.loads(reference, validate=True) 88 return True 89 except ConanException: 90 return False 91 92 93class ConanName(object): 94 _max_chars = 51 95 _min_chars = 2 96 _validation_pattern = re.compile("^[a-zA-Z0-9_][a-zA-Z0-9_\+\.-]{%s,%s}$" 97 % (_min_chars - 1, _max_chars - 1)) 98 99 _validation_revision_pattern = re.compile("^[a-zA-Z0-9]{1,%s}$" % _max_chars) 100 101 @staticmethod 102 def raise_invalid_name_error(value, reference_token=None): 103 if len(value) > ConanName._max_chars: 104 reason = "is too long. Valid names must contain at most %s characters."\ 105 % ConanName._max_chars 106 elif len(value) < ConanName._min_chars: 107 reason = "is too short. Valid names must contain at least %s characters."\ 108 % ConanName._min_chars 109 else: 110 reason = ("is an invalid name. Valid names MUST begin with a " 111 "letter, number or underscore, have between %s-%s chars, including " 112 "letters, numbers, underscore, dot and dash" 113 % (ConanName._min_chars, ConanName._max_chars)) 114 message = "Value provided{ref_token}, '{value}' (type {type}), {reason}".format( 115 ref_token=" for {}".format(reference_token) if reference_token else "", 116 value=value, type=type(value).__name__, reason=reason 117 ) 118 raise InvalidNameException(message) 119 120 @staticmethod 121 def raise_invalid_version_error(name, version): 122 message = ("Package {} has an invalid version number: '{}'. Valid names " 123 "MUST begin with a letter, number or underscore, have " 124 "between {}-{} chars, including letters, numbers, " 125 "underscore, dot and dash").format( 126 name, 127 version, 128 ConanName._min_chars, 129 ConanName._max_chars 130 ) 131 raise InvalidNameException(message) 132 133 @staticmethod 134 def validate_string(value, reference_token=None): 135 """Check for string""" 136 if not isinstance(value, string_types): 137 message = "Value provided{ref_token}, '{value}' (type {type}), {reason}".format( 138 ref_token=" for {}".format(reference_token) if reference_token else "", 139 value=value, type=type(value).__name__, 140 reason="is not a string" 141 ) 142 raise InvalidNameException(message) 143 144 @staticmethod 145 def validate_name(name, reference_token=None): 146 """Check for name compliance with pattern rules""" 147 ConanName.validate_string(name, reference_token=reference_token) 148 if name == "*": 149 return 150 if ConanName._validation_pattern.match(name) is None: 151 ConanName.raise_invalid_name_error(name, reference_token=reference_token) 152 153 @staticmethod 154 def validate_version(version, pkg_name): 155 ConanName.validate_string(version) 156 if version == "*": 157 return 158 if ConanName._validation_pattern.match(version) is None: 159 if ( 160 (version.startswith("[") and version.endswith("]")) 161 or (version.startswith("(") and version.endswith(")")) 162 ): 163 return 164 ConanName.raise_invalid_version_error(pkg_name, version) 165 166 @staticmethod 167 def validate_revision(revision): 168 if ConanName._validation_revision_pattern.match(revision) is None: 169 raise InvalidNameException("The revision field, must contain only letters " 170 "and numbers with a length between 1 and " 171 "%s" % ConanName._max_chars) 172 173 174class ConanFileReference(namedtuple("ConanFileReference", "name version user channel revision")): 175 """ Full reference of a package recipes, e.g.: 176 opencv/2.4.10@lasote/testing 177 """ 178 179 def __new__(cls, name, version, user, channel, revision=None, validate=True): 180 """Simple name creation. 181 @param name: string containing the desired name 182 @param version: string containing the desired version 183 @param user: string containing the user name 184 @param channel: string containing the user channel 185 @param revision: string containing the revision (optional) 186 """ 187 if (user and not channel) or (channel and not user): 188 raise InvalidNameException("Specify the 'user' and the 'channel' or neither of them") 189 190 version = Version(version) if version is not None else None 191 user = _noneize(user) 192 channel = _noneize(channel) 193 194 obj = super(cls, ConanFileReference).__new__(cls, name, version, user, channel, revision) 195 if validate: 196 obj._validate() 197 return obj 198 199 def _validate(self): 200 if self.name is not None: 201 ConanName.validate_name(self.name, reference_token="package name") 202 if self.version is not None: 203 ConanName.validate_version(self.version, self.name) 204 if self.user is not None: 205 ConanName.validate_name(self.user, reference_token="user name") 206 if self.channel is not None: 207 ConanName.validate_name(self.channel, reference_token="channel") 208 if self.revision is not None: 209 ConanName.validate_revision(self.revision) 210 211 if not self.name or not self.version: 212 raise InvalidNameException("Specify the 'name' and the 'version'") 213 214 if (self.user and not self.channel) or (self.channel and not self.user): 215 raise InvalidNameException("Specify the 'user' and the 'channel' or neither of them") 216 217 @staticmethod 218 def loads(text, validate=True): 219 """ Parses a text string to generate a ConanFileReference object 220 """ 221 name, version, user, channel, revision = get_reference_fields(text) 222 ref = ConanFileReference(name, version, user, channel, revision, validate=validate) 223 return ref 224 225 @staticmethod 226 def load_dir_repr(dir_repr): 227 name, version, user, channel = dir_repr.split("/") 228 if user == "_": 229 user = None 230 if channel == "_": 231 channel = None 232 return ConanFileReference(name, version, user, channel) 233 234 def __str__(self): 235 if self.name is None and self.version is None: 236 return "" 237 if self.user is None and self.channel is None: 238 return "%s/%s" % (self.name, self.version) 239 return "%s/%s@%s/%s" % (self.name, self.version, self.user, self.channel) 240 241 def __repr__(self): 242 str_rev = "#%s" % self.revision if self.revision else "" 243 user_channel = "@%s/%s" % (self.user, self.channel) if self.user or self.channel else "" 244 return "%s/%s%s%s" % (self.name, self.version, user_channel, str_rev) 245 246 def full_str(self): 247 str_rev = "#%s" % self.revision if self.revision else "" 248 return "%s%s" % (str(self), str_rev) 249 250 def dir_repr(self): 251 return "/".join([self.name, self.version, self.user or "_", self.channel or "_"]) 252 253 def copy_with_rev(self, revision): 254 return ConanFileReference(self.name, self.version, self.user, self.channel, revision, 255 validate=False) 256 257 def copy_clear_rev(self): 258 return ConanFileReference(self.name, self.version, self.user, self.channel, None, 259 validate=False) 260 261 def __lt__(self, other): 262 def de_noneize(ref): 263 return ref.name, ref.version, ref.user or "", ref.channel or "", ref.revision or "" 264 265 return de_noneize(self) < de_noneize(other) 266 267 def is_compatible_with(self, new_ref): 268 """Returns true if the new_ref is completing the RREV field of this object but 269 having the rest equal """ 270 if repr(self) == repr(new_ref): 271 return True 272 if self.copy_clear_rev() != new_ref.copy_clear_rev(): 273 return False 274 275 return self.revision is None 276 277 278class PackageReference(namedtuple("PackageReference", "ref id revision")): 279 """ Full package reference, e.g.: 280 opencv/2.4.10@lasote/testing, fe566a677f77734ae 281 """ 282 283 def __new__(cls, ref, package_id, revision=None, validate=True): 284 if "#" in package_id: 285 package_id, revision = package_id.rsplit("#", 1) 286 obj = super(cls, PackageReference).__new__(cls, ref, package_id, revision) 287 if validate: 288 obj.validate() 289 return obj 290 291 def validate(self): 292 if self.revision: 293 ConanName.validate_revision(self.revision) 294 295 @staticmethod 296 def loads(text, validate=True): 297 text = text.strip() 298 tmp = text.split(":") 299 try: 300 ref = ConanFileReference.loads(tmp[0].strip(), validate=validate) 301 package_id = tmp[1].strip() 302 except IndexError: 303 raise ConanException("Wrong package reference %s" % text) 304 return PackageReference(ref, package_id, validate=validate) 305 306 def __repr__(self): 307 str_rev = "#%s" % self.revision if self.revision else "" 308 tmp = "%s:%s%s" % (repr(self.ref), self.id, str_rev) 309 return tmp 310 311 def __str__(self): 312 return "%s:%s" % (self.ref, self.id) 313 314 def __lt__(self, other): 315 # We need this operator to sort prefs to compute the package_id 316 # package_id() -> ConanInfo.package_id() -> RequirementsInfo.sha() -> sorted(prefs) -> lt 317 me = self.ref, self.id, self.revision or "" 318 other = other.ref, other.id, other.revision or "" 319 return me < other 320 321 def full_str(self): 322 str_rev = "#%s" % self.revision if self.revision else "" 323 tmp = "%s:%s%s" % (self.ref.full_str(), self.id, str_rev) 324 return tmp 325 326 def copy_with_revs(self, revision, p_revision): 327 return PackageReference(self.ref.copy_with_rev(revision), self.id, p_revision) 328 329 def copy_clear_prev(self): 330 return self.copy_with_revs(self.ref.revision, None) 331 332 def copy_clear_revs(self): 333 return self.copy_with_revs(None, None) 334 335 def is_compatible_with(self, new_ref): 336 """Returns true if the new_ref is completing the PREV field of this object but 337 having the rest equal """ 338 if repr(self) == repr(new_ref): 339 return True 340 if not self.ref.is_compatible_with(new_ref.ref) or self.id != new_ref.id: 341 return False 342 343 return self.revision is None # Only the revision is different and we don't have one 344