1143 lines
33 KiB
Python
1143 lines
33 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Entropy miscellaneous tools module
|
|
"""
|
|
|
|
@author: Fabio Erculiani <lxnay@sabayon.org>
|
|
@contact: lxnay@sabayon.org
|
|
@copyright: Fabio Erculiani
|
|
@license: GPL-2
|
|
|
|
B{Entropy dependency functions module}.
|
|
This module contains Entropy package dependency manipulation functions.
|
|
|
|
"""
|
|
import re
|
|
from entropy.exceptions import InvalidAtom, EntropyException
|
|
from entropy.const import etpConst, const_cmp
|
|
|
|
# Imported from Gentoo portage_dep.py
|
|
# Copyright 1999-2010 Gentoo Foundation
|
|
|
|
# 2.1.1 A category name may contain any of the characters [A-Za-z0-9+_.-].
|
|
# It must not begin with a hyphen or a dot.
|
|
_cat = r'[\w+][\w+.-]*'
|
|
|
|
# 2.1.2 A package name may contain any of the characters [A-Za-z0-9+_-].
|
|
# It must not begin with a hyphen,
|
|
# and must not end in a hyphen followed by one or more digits.
|
|
_pkg = r'[\w+][\w+-]*?'
|
|
|
|
_v = r'(cvs\.)?(\d+)((\.\d+)*)([a-z]?)((_(pre|p|beta|alpha|rc)\d*)*)'
|
|
_rev = r'\d+'
|
|
_vr = _v + '(-r(' + _rev + '))?'
|
|
|
|
_cp = '(' + _cat + '/' + _pkg + '(-' + _vr + ')?)'
|
|
_cpv = '(' + _cp + '-' + _vr + ')'
|
|
_pv = '(?P<pn>' + _pkg + '(?P<pn_inval>-' + _vr + ')?)' + '-(?P<ver>' + _v + ')(-r(?P<rev>' + _rev + '))?'
|
|
|
|
ver_regexp = re.compile("^" + _vr + "$")
|
|
suffix_regexp = re.compile("^(alpha|beta|rc|pre|p)(\\d*)$")
|
|
suffix_value = {"pre": -2, "p": 0, "alpha": -4, "beta": -3, "rc": -1}
|
|
endversion_keys = ["pre", "p", "alpha", "beta", "rc"]
|
|
|
|
valid_category = re.compile("^\w[\w-]*")
|
|
invalid_atom_chars_regexp = re.compile("[()|@]")
|
|
|
|
def _ververify(myver):
|
|
if myver.endswith("*"):
|
|
m = ver_regexp.match(myver[:-1])
|
|
else:
|
|
m = ver_regexp.match(myver)
|
|
if m:
|
|
return True
|
|
return False
|
|
|
|
_pv_re = re.compile('^' + _pv + '$', re.VERBOSE)
|
|
def _pkgsplit(mypkg):
|
|
"""
|
|
@param mypkg: pv
|
|
@return:
|
|
1. None if input is invalid.
|
|
2. (pn, ver, rev) if input is pv
|
|
"""
|
|
m = _pv_re.match(mypkg)
|
|
if m is None:
|
|
return None
|
|
|
|
if m.group('pn_inval') is not None:
|
|
# package name appears to have a version-like suffix
|
|
return None
|
|
|
|
rev = m.group('rev')
|
|
if rev is None:
|
|
rev = '0'
|
|
rev = 'r' + rev
|
|
|
|
return (m.group('pn'), m.group('ver'), rev)
|
|
|
|
def _generic_sorter(inputlist, cmp_func):
|
|
|
|
inputs = inputlist[:]
|
|
if len(inputs) < 2:
|
|
return inputs
|
|
max_idx = len(inputs)
|
|
|
|
while True:
|
|
changed = False
|
|
for idx in range(max_idx):
|
|
second_idx = idx+1
|
|
if second_idx == max_idx:
|
|
continue
|
|
str_a = inputs[idx]
|
|
str_b = inputs[second_idx]
|
|
if cmp_func(str_a, str_b) < 0:
|
|
inputs[idx] = str_b
|
|
inputs[second_idx] = str_a
|
|
changed = True
|
|
if not changed:
|
|
break
|
|
|
|
return inputs
|
|
|
|
def isjustname(mypkg):
|
|
"""
|
|
Checks to see if the depstring is only the package name (no version parts)
|
|
|
|
Example usage:
|
|
>>> isjustname('media-libs/test-3.0')
|
|
False
|
|
>>> isjustname('test')
|
|
True
|
|
>>> isjustname('media-libs/test')
|
|
True
|
|
|
|
@param mypkg: the package atom to check
|
|
@param mypkg: string
|
|
@rtype: int
|
|
@return: if the package string is not just the package name
|
|
"""
|
|
# must match, in case of "1.2.3-r1".
|
|
rev = dep_get_spm_revision(mypkg)
|
|
if rev == "r0":
|
|
mypkg += "-r0"
|
|
ver_rev = '-'.join(mypkg.split('-')[-2:])
|
|
return not _ververify(ver_rev)
|
|
|
|
def catpkgsplit(mydata):
|
|
"""
|
|
Takes a Category/Package-Version-Rev and returns a list of each.
|
|
|
|
@param mydata: data to split
|
|
@type mydata: string
|
|
@rype: tuple
|
|
@return:
|
|
1. If each exists, it returns (cat, pkgname, version, rev)
|
|
2. If cat is not specificed in mydata, cat will be "null"
|
|
3. if rev does not exist it will be '-r0'
|
|
"""
|
|
|
|
# Categories may contain a-zA-z0-9+_- but cannot start with -
|
|
mysplit = mydata.split("/")
|
|
p_split = None
|
|
if len(mysplit) == 1:
|
|
retval = ("null",)
|
|
p_split = _pkgsplit(mydata)
|
|
elif len(mysplit) == 2:
|
|
retval = (mysplit[0],)
|
|
p_split = _pkgsplit(mysplit[1])
|
|
if not p_split:
|
|
return None
|
|
retval += p_split
|
|
return retval
|
|
|
|
def dep_getkey(mydep):
|
|
"""
|
|
Return the category/package-name of a depstring.
|
|
|
|
Example usage:
|
|
>>> dep_getkey('media-libs/test-3.0')
|
|
'media-libs/test'
|
|
|
|
@param mydep: the depstring to retrieve the category/package-name of
|
|
@type mydep: string
|
|
@rtype: string
|
|
@return: the package category/package-version
|
|
"""
|
|
if not mydep:
|
|
return mydep
|
|
mydep = remove_tag(mydep)
|
|
mydep = remove_usedeps(mydep)
|
|
|
|
mydep = dep_getcpv(mydep)
|
|
if mydep and (not isjustname(mydep)):
|
|
mysplit = catpkgsplit(mydep)
|
|
if not mysplit:
|
|
return mydep
|
|
return mysplit[0] + "/" + mysplit[1]
|
|
|
|
return mydep
|
|
|
|
def dep_getcat(mydep):
|
|
"""
|
|
Extract package category from dependency.
|
|
"""
|
|
return dep_getkey(mydep).split("/")[0]
|
|
|
|
def remove_cat(mydep):
|
|
"""
|
|
Drop category part from dependency, if any.
|
|
"""
|
|
if "/" in mydep:
|
|
return mydep.split("/", 1)[1]
|
|
return mydep
|
|
|
|
def dep_getcpv(mydep):
|
|
"""
|
|
Return the category-package-version with any operators/slot specifications stripped off
|
|
|
|
Example usage:
|
|
>>> dep_getcpv('>=media-libs/test-3.0')
|
|
'media-libs/test-3.0'
|
|
|
|
@param mydep: the depstring
|
|
@type mydep: string
|
|
@rtype: string
|
|
@return: the depstring with the operator removed
|
|
"""
|
|
if mydep and mydep[0] == "*":
|
|
mydep = mydep[1:]
|
|
if mydep and mydep[-1] == "*":
|
|
mydep = mydep[:-1]
|
|
if mydep and mydep[0] == "!":
|
|
mydep = mydep[1:]
|
|
if mydep[:2] in [">=", "<="]:
|
|
mydep = mydep[2:]
|
|
elif mydep[:1] in "=<>~":
|
|
mydep = mydep[1:]
|
|
colon = mydep.rfind(":")
|
|
if colon != -1:
|
|
mydep = mydep[:colon]
|
|
|
|
return mydep
|
|
|
|
def dep_getslot(mydep):
|
|
"""
|
|
# Imported from portage.dep
|
|
# $Id: dep.py 11281 2008-07-30 06:12:19Z zmedico $
|
|
|
|
Retrieve the slot on a depend.
|
|
|
|
Example usage:
|
|
>>> dep_getslot('app-misc/test:3')
|
|
'3'
|
|
|
|
@param mydep: the depstring to retrieve the slot of
|
|
@type mydep: string
|
|
@rtype: string
|
|
@return: the slot
|
|
"""
|
|
colon = mydep.find(":")
|
|
if colon != -1:
|
|
bracket = mydep.find("[", colon)
|
|
if bracket == -1:
|
|
return mydep[colon+1:]
|
|
else:
|
|
return mydep[colon+1:bracket]
|
|
return None
|
|
|
|
def dep_getusedeps(depend):
|
|
"""
|
|
# Imported from portage.dep
|
|
# $Id: dep.py 11281 2008-07-30 06:12:19Z zmedico $
|
|
|
|
Pull a listing of USE Dependencies out of a dep atom.
|
|
|
|
Example usage:
|
|
>>> dep_getusedeps('app-misc/test:3[foo,-bar]')
|
|
('foo', '-bar')
|
|
|
|
@param depend: The depstring to process
|
|
@type depend: String
|
|
@rtype: List
|
|
@return: List of use flags ( or [] if no flags exist )
|
|
"""
|
|
use_list = []
|
|
open_bracket = depend.find('[')
|
|
# -1 = failure (think c++ string::npos)
|
|
comma_separated = False
|
|
bracket_count = 0
|
|
while( open_bracket != -1 ):
|
|
bracket_count += 1
|
|
if bracket_count > 1:
|
|
InvalidAtom("USE Dependency with more " + \
|
|
"than one set of brackets: %s" % (depend,))
|
|
close_bracket = depend.find(']', open_bracket )
|
|
if close_bracket == -1:
|
|
InvalidAtom("USE Dependency with no closing bracket: %s" % depend )
|
|
use = depend[open_bracket + 1: close_bracket]
|
|
# foo[1:1] may return '' instead of None, we don't want '' in the result
|
|
if not use:
|
|
InvalidAtom("USE Dependency with " + \
|
|
"no use flag ([]): %s" % depend )
|
|
if not comma_separated:
|
|
comma_separated = "," in use
|
|
|
|
if comma_separated and bracket_count > 1:
|
|
InvalidAtom("USE Dependency contains a mixture of " + \
|
|
"comma and bracket separators: %s" % depend )
|
|
|
|
if comma_separated:
|
|
for x in use.split(","):
|
|
if x:
|
|
use_list.append(x)
|
|
else:
|
|
InvalidAtom("USE Dependency with no use " + \
|
|
"flag next to comma: %s" % depend )
|
|
else:
|
|
use_list.append(use)
|
|
|
|
# Find next use flag
|
|
open_bracket = depend.find( '[', open_bracket+1 )
|
|
|
|
return tuple(use_list)
|
|
|
|
def remove_usedeps(depend):
|
|
"""
|
|
docstring_title
|
|
|
|
@param depend:
|
|
@type depend:
|
|
@return:
|
|
@rtype:
|
|
"""
|
|
new_depend = ""
|
|
skip = 0
|
|
for char in depend:
|
|
if char == "[":
|
|
skip += 1
|
|
elif char == "]":
|
|
skip -= 1
|
|
continue
|
|
if skip == 0:
|
|
new_depend += char
|
|
|
|
return new_depend
|
|
|
|
def remove_slot(mydep):
|
|
"""
|
|
# Imported from portage.dep
|
|
# $Id: dep.py 11281 2008-07-30 06:12:19Z zmedico $
|
|
|
|
Removes dep components from the right side of an atom:
|
|
* slot
|
|
* use
|
|
* repo
|
|
"""
|
|
colon = mydep.find(":")
|
|
if colon != -1:
|
|
mydep = mydep[:colon]
|
|
else:
|
|
bracket = mydep.find("[")
|
|
if bracket != -1:
|
|
mydep = mydep[:bracket]
|
|
return mydep
|
|
|
|
def remove_tag_from_slot(slot):
|
|
"""
|
|
Remove, if present, the tag part from SLOT string.
|
|
Packages append the kernel tag to the slot, by comma separating it.
|
|
"""
|
|
return slot[::-1].split(",", 1)[-1][::-1]
|
|
|
|
# input must be a valid package version or a full atom
|
|
def remove_revision(ver):
|
|
"""
|
|
docstring_title
|
|
|
|
@param ver:
|
|
@type ver:
|
|
@return:
|
|
@rtype:
|
|
"""
|
|
myver = ver.split("-")
|
|
if myver[-1][0] == "r":
|
|
return '-'.join(myver[:-1])
|
|
return ver
|
|
|
|
def remove_tag(mydep):
|
|
"""
|
|
docstring_title
|
|
|
|
@param mydep:
|
|
@type mydep:
|
|
@return:
|
|
@rtype:
|
|
"""
|
|
colon = mydep.rfind(etpConst['entropytagprefix'])
|
|
if colon == -1:
|
|
return mydep
|
|
return mydep[:colon]
|
|
|
|
def remove_entropy_revision(mydep):
|
|
"""
|
|
docstring_title
|
|
|
|
@param mydep:
|
|
@type mydep:
|
|
@return:
|
|
@rtype:
|
|
"""
|
|
dep = remove_package_operators(mydep)
|
|
operators = mydep[:-len(dep)]
|
|
colon = dep.rfind("~")
|
|
if colon == -1:
|
|
return mydep
|
|
return operators+dep[:colon]
|
|
|
|
def dep_get_entropy_revision(mydep):
|
|
"""
|
|
docstring_title
|
|
|
|
@param mydep:
|
|
@type mydep:
|
|
@return:
|
|
@rtype:
|
|
"""
|
|
#dep = remove_package_operators(mydep)
|
|
colon = mydep.rfind("~")
|
|
if colon != -1:
|
|
myrev = mydep[colon+1:]
|
|
try:
|
|
myrev = int(myrev)
|
|
except ValueError:
|
|
return None
|
|
return myrev
|
|
return None
|
|
|
|
def dep_split_or_deps(mydep):
|
|
"""
|
|
docstring_title
|
|
|
|
@param mydep:
|
|
@type mydep:
|
|
@return:
|
|
@rtype:
|
|
"""
|
|
dep = mydep.rstrip(etpConst['entropyordepquestion'])
|
|
return dep.split(etpConst['entropyordepsep'])
|
|
|
|
dep_revmatch = re.compile('^r[0-9]')
|
|
def dep_get_spm_revision(mydep):
|
|
"""
|
|
docstring_title
|
|
|
|
@param mydep:
|
|
@type mydep:
|
|
@return:
|
|
@rtype:
|
|
"""
|
|
myver = mydep.split("-")
|
|
myrev = myver[-1]
|
|
if dep_revmatch.match(myrev):
|
|
return myrev
|
|
else:
|
|
return "r0"
|
|
|
|
def dep_get_match_in_repos(mydep):
|
|
"""
|
|
docstring_title
|
|
|
|
@param mydep:
|
|
@type mydep:
|
|
@return:
|
|
@rtype:
|
|
"""
|
|
colon = mydep.rfind("@")
|
|
if colon != -1:
|
|
mydata = mydep[colon+1:]
|
|
mydata = mydata.split(",")
|
|
if not mydata:
|
|
mydata = None
|
|
return mydep[:colon], mydata
|
|
else:
|
|
return mydep, None
|
|
|
|
def dep_gettag(mydep):
|
|
|
|
"""
|
|
Retrieve the slot on a depend.
|
|
|
|
Example usage:
|
|
>>> dep_gettag('app-misc/test#2.6.23-sabayon-r1')
|
|
'2.6.23-sabayon-r1'
|
|
|
|
"""
|
|
dep = mydep[:]
|
|
dep = remove_entropy_revision(dep)
|
|
colon = dep.rfind(etpConst['entropytagprefix'])
|
|
if colon != -1:
|
|
mydep = dep[colon+1:]
|
|
rslt = remove_slot(mydep)
|
|
return rslt
|
|
return None
|
|
|
|
def remove_package_operators(atom):
|
|
"""
|
|
docstring_title
|
|
|
|
@param atom:
|
|
@type atom:
|
|
@return:
|
|
@rtype:
|
|
"""
|
|
return atom.lstrip("><=~")
|
|
|
|
def compare_versions(ver1, ver2):
|
|
"""
|
|
docstring_title
|
|
|
|
@param ver1:
|
|
@type ver1:
|
|
@param ver2:
|
|
@type ver2:
|
|
@return:
|
|
@rtype:
|
|
"""
|
|
if ver1 == ver2:
|
|
return 0
|
|
match1 = None
|
|
match2 = None
|
|
if ver1:
|
|
match1 = ver_regexp.match(ver1)
|
|
if ver2:
|
|
match2 = ver_regexp.match(ver2)
|
|
|
|
# checking that the versions are valid
|
|
invalid = False
|
|
invalid_rc = 0
|
|
if not match1:
|
|
invalid = True
|
|
elif not match1.groups():
|
|
invalid = True
|
|
elif not match2:
|
|
invalid_rc = 1
|
|
invalid = True
|
|
elif not match2.groups():
|
|
invalid_rc = 1
|
|
invalid = True
|
|
if invalid:
|
|
return invalid_rc
|
|
|
|
# building lists of the version parts before the suffix
|
|
# first part is simple
|
|
list1 = [int(match1.group(2))]
|
|
list2 = [int(match2.group(2))]
|
|
|
|
# this part would greatly benefit from a fixed-length version pattern
|
|
if len(match1.group(3)) or len(match2.group(3)):
|
|
vlist1 = match1.group(3)[1:].split(".")
|
|
vlist2 = match2.group(3)[1:].split(".")
|
|
for i in range(0, max(len(vlist1), len(vlist2))):
|
|
# Implcit .0 is given a value of -1, so that 1.0.0 > 1.0, since it
|
|
# would be ambiguous if two versions that aren't literally equal
|
|
# are given the same value (in sorting, for example).
|
|
if len(vlist1) <= i or len(vlist1[i]) == 0:
|
|
list1.append(-1)
|
|
list2.append(int(vlist2[i]))
|
|
elif len(vlist2) <= i or len(vlist2[i]) == 0:
|
|
list1.append(int(vlist1[i]))
|
|
list2.append(-1)
|
|
# Let's make life easy and use integers unless we're forced to use floats
|
|
elif (vlist1[i][0] != "0" and vlist2[i][0] != "0"):
|
|
list1.append(int(vlist1[i]))
|
|
list2.append(int(vlist2[i]))
|
|
# now we have to use floats so 1.02 compares correctly against 1.1
|
|
else:
|
|
list1.append(float("0."+vlist1[i]))
|
|
list2.append(float("0."+vlist2[i]))
|
|
|
|
# and now the final letter
|
|
if len(match1.group(5)):
|
|
list1.append(ord(match1.group(5)))
|
|
if len(match2.group(5)):
|
|
list2.append(ord(match2.group(5)))
|
|
|
|
for i in range(0, max(len(list1), len(list2))):
|
|
if len(list1) <= i:
|
|
return -1
|
|
elif len(list2) <= i:
|
|
return 1
|
|
elif list1[i] != list2[i]:
|
|
return list1[i] - list2[i]
|
|
|
|
# main version is equal, so now compare the _suffix part
|
|
list1 = match1.group(6).split("_")[1:]
|
|
list2 = match2.group(6).split("_")[1:]
|
|
|
|
for i in range(0, max(len(list1), len(list2))):
|
|
if len(list1) <= i:
|
|
s1 = ("p", "0")
|
|
else:
|
|
s1 = suffix_regexp.match(list1[i]).groups()
|
|
if len(list2) <= i:
|
|
s2 = ("p", "0")
|
|
else:
|
|
s2 = suffix_regexp.match(list2[i]).groups()
|
|
if s1[0] != s2[0]:
|
|
return suffix_value[s1[0]] - suffix_value[s2[0]]
|
|
if s1[1] != s2[1]:
|
|
# it's possible that the s(1|2)[1] == ''
|
|
# in such a case, fudge it.
|
|
try:
|
|
r1 = int(s1[1])
|
|
except ValueError:
|
|
r1 = 0
|
|
try:
|
|
r2 = int(s2[1])
|
|
except ValueError:
|
|
r2 = 0
|
|
return r1 - r2
|
|
|
|
# the suffix part is equal to, so finally check the revision
|
|
if match1.group(10):
|
|
r1 = int(match1.group(10))
|
|
else:
|
|
r1 = 0
|
|
if match2.group(10):
|
|
r2 = int(match2.group(10))
|
|
else:
|
|
r2 = 0
|
|
return r1 - r2
|
|
|
|
tag_regexp = re.compile("^([A-Za-z0-9+_.-]+)?$")
|
|
def is_valid_package_tag(tag):
|
|
"""
|
|
Return whether string is a valid package tag.
|
|
|
|
@param tag: package tag to test
|
|
@type tag: string
|
|
@return: True, if valid
|
|
@rtype: bool
|
|
"""
|
|
match = tag_regexp.match(tag)
|
|
if not match:
|
|
return False
|
|
if not match.groups():
|
|
return False
|
|
return True
|
|
|
|
def entropy_compare_package_tags(tag_a, tag_b):
|
|
"""
|
|
Compare two Entropy package tags using builtin cmp().
|
|
|
|
@param tag_a: Entropy package tag
|
|
@type tag_a: string
|
|
@param tag_b: Entropy package tag
|
|
@type tag_b: string
|
|
return: negative number if tag_a < tag_b, positive number if tag_a > tag_b.
|
|
zero if tag_a == tag_b.
|
|
rtype: int
|
|
"""
|
|
return const_cmp(tag_a, tag_b)
|
|
|
|
def sort_entropy_package_tags(tags):
|
|
"""
|
|
Return a sorted list of Entropy package tags.
|
|
|
|
@param tags: list of Entropy package tags
|
|
@type tags: list
|
|
@return: sorted list of Entropy package tags
|
|
@rtype: list
|
|
"""
|
|
return sorted(tags)
|
|
|
|
def entropy_compare_versions(ver_data, ver_data2):
|
|
"""
|
|
@description: compare two lists composed by
|
|
[version,tag,revision] and [version,tag,revision]
|
|
if ver_data > ver_data2 --> positive number
|
|
if ver_data == ver_data2 --> 0
|
|
if ver_data < ver_data2 --> negative number
|
|
@input package: ver_data[version,tag,rev] and ver_data2[version,tag,rev]
|
|
@output: integer number
|
|
"""
|
|
a_ver, a_tag, a_rev = ver_data
|
|
b_ver, b_tag, b_rev = ver_data2
|
|
|
|
# if both are tagged, check tag first
|
|
rc = 0
|
|
if a_tag and b_tag:
|
|
rc = const_cmp(a_tag, b_tag)
|
|
if rc == 0:
|
|
rc = compare_versions(a_ver, b_ver)
|
|
|
|
if rc == 0:
|
|
# check tag
|
|
tag_cmp = entropy_compare_package_tags(a_tag, b_tag)
|
|
if tag_cmp < 0:
|
|
return -1
|
|
elif tag_cmp > 0:
|
|
return 1
|
|
else:
|
|
# check rev
|
|
if a_rev > b_rev:
|
|
return 1
|
|
elif a_rev < b_rev:
|
|
return -1
|
|
return 0
|
|
|
|
return rc
|
|
|
|
def get_newer_version(versions):
|
|
"""
|
|
Return a sorted list of versions
|
|
|
|
@param versions: input version list
|
|
@type versions: list
|
|
@return: sorted version list
|
|
@rtype: list
|
|
"""
|
|
return _generic_sorter(versions, compare_versions)
|
|
|
|
def get_entropy_newer_version(versions):
|
|
"""
|
|
Sort a list of entropy package versions.
|
|
|
|
@param versions: list of package versions
|
|
@type versions: list
|
|
@return: sorted list
|
|
@rtype: list
|
|
"""
|
|
return _generic_sorter(versions, entropy_compare_versions)
|
|
|
|
def create_package_filename(category, name, version, package_tag, ext = None):
|
|
"""
|
|
Create package filename string.
|
|
|
|
@param category: package category
|
|
@type category: string
|
|
@param name: package name
|
|
@type name: string
|
|
@param version: package version
|
|
@type version: string
|
|
@param package_tag: package tag, if any, or None
|
|
@type package_tag: string or None
|
|
@keyword ext: alternative package file extension
|
|
@type ext: string
|
|
@return: package file name string
|
|
@rtype: string
|
|
"""
|
|
if package_tag:
|
|
package_tag = "%s%s" % (etpConst['entropytagprefix'], package_tag,)
|
|
else:
|
|
package_tag = ''
|
|
|
|
package_name = "%s:%s-%s" % (category, name, version,)
|
|
package_name += package_tag
|
|
if ext is None:
|
|
ext = etpConst['packagesext']
|
|
package_name += ext
|
|
return package_name
|
|
|
|
def strip_entropy_package_extension(pkg_path):
|
|
"""
|
|
Strip entropy package file extension from package path pkg_path.
|
|
|
|
@param pkg_path: package path
|
|
@type pkg_path: string
|
|
@return: stripped package path
|
|
@rtype: string
|
|
"""
|
|
return pkg_path.rstrip(etpConst['packagesext'])
|
|
|
|
def exploit_package_filename(package_name):
|
|
"""
|
|
This is the inverse function of create_package_filename, and returns
|
|
a tuple composed by category, name, version, package_tag (None if not set)
|
|
and additional revision (as int).
|
|
package_name should be a string like this:
|
|
<category>:<name>-<version>[~<revision>[#<tag>]][.tbz2]
|
|
|
|
@param package_name: package file name
|
|
@type package_name: string
|
|
@return: tuple of strings/int composed by (category, name, version,
|
|
package_tag, revision)
|
|
@rtype: tuple
|
|
@raise AttributeError: if package_name string passed is improperly formatted
|
|
"""
|
|
pkg_str = strip_entropy_package_extension(package_name)
|
|
pkg_str = pkg_str.replace(":", "/")
|
|
pkg_str = strip_entropy_package_extension(pkg_str)
|
|
etp_tag = dep_gettag(pkg_str)
|
|
pkg_str = remove_tag(pkg_str)
|
|
etp_rev = dep_get_entropy_revision(pkg_str)
|
|
pkg_str = remove_entropy_revision(pkg_str)
|
|
split_data = catpkgsplit(pkg_str)
|
|
if split_data is None:
|
|
raise AttributeError("invalid package name passed: %s" % (package_name,))
|
|
etp_cat, etp_name, ver, rev = split_data
|
|
if rev != "r0":
|
|
ver += "-" + rev
|
|
return etp_cat, etp_name, ver, etp_tag, etp_rev
|
|
|
|
def create_package_atom_string(category, name, version, package_tag):
|
|
"""
|
|
Create Entropy package atom string.
|
|
|
|
@param category: package category
|
|
@type category: string
|
|
@param name: package name
|
|
@type name: string
|
|
@param version: package version
|
|
@type version: string
|
|
@param package_tag: package tag, if any, or None
|
|
@type package_tag: string or None
|
|
@return: package atom string
|
|
@rtype: string
|
|
"""
|
|
if package_tag:
|
|
package_tag = "%s%s" % (etpConst['entropytagprefix'], package_tag,)
|
|
else:
|
|
package_tag = ''
|
|
package_name = "%s/%s-%s" % (category, name, version,)
|
|
package_name += package_tag
|
|
return package_name
|
|
|
|
class Dependency(object):
|
|
|
|
"""
|
|
Helper class used to evaluate dependency string containing boolean
|
|
expressions such as: (dep1 & dep2) | dep 3
|
|
"""
|
|
|
|
def __init__(self, entropy_dep, entropy_repository_list):
|
|
"""
|
|
Dependency constructor.
|
|
|
|
@param entropy_dep: entropy package dependency
|
|
@type entropy_dep: string
|
|
@param entropy_repository_list: ordered list of EntropyRepositoryBase
|
|
instances
|
|
@type entropy_repository_list: list
|
|
"""
|
|
self.__entropy_repository_list = entropy_repository_list
|
|
self.__dep = entropy_dep
|
|
|
|
def get(self):
|
|
"""
|
|
Return encapsulated depdenency string
|
|
|
|
@rtype: string
|
|
"""
|
|
return self.__dep
|
|
|
|
def __bool__(self):
|
|
"""
|
|
Same as __nonzero__ but meant for Python 3.x support
|
|
"""
|
|
for entropy_repository in self.__entropy_repository_list:
|
|
pkg_id, res = entropy_repository.atomMatch(self.__dep)
|
|
if res == 0:
|
|
return True
|
|
return False
|
|
|
|
def __nonzero__(self):
|
|
"""
|
|
Tries to match entropy_dep and returns True or False if dependency
|
|
is matched.
|
|
"""
|
|
for entropy_repository in self.__entropy_repository_list:
|
|
pkg_id, res = entropy_repository.atomMatch(self.__dep)
|
|
if res == 0:
|
|
return True
|
|
return False
|
|
|
|
def evaluate(self):
|
|
"""
|
|
Evaluate dependency trying to match dentropy_dep across all the
|
|
available repositories and return package matches.
|
|
"""
|
|
eval_data = set()
|
|
for entropy_repository in self.__entropy_repository_list:
|
|
repo_id = entropy_repository.repository_id()
|
|
pkg_deps, res = entropy_repository.atomMatch(self.__dep,
|
|
multiMatch = True)
|
|
if res == 0:
|
|
eval_data.update((x, repo_id) for x in pkg_deps)
|
|
return eval_data
|
|
|
|
class DependencyStringParser(object):
|
|
|
|
"""
|
|
Conditional dependency string parser. It is used by Entropy dependency
|
|
matching logic to evaluate dependency conditions containing boolean
|
|
operators. Example: "( app-foo/foo & foo-misc/foo ) | foo-misc/new-foo"
|
|
|
|
Example usage (self is an EntropyRepositoryBase instance):
|
|
>>> parser = DependencyStringParser("app-foo/foo & foo-misc/foo", self)
|
|
>>> matched, outcome = parser.parse()
|
|
>>> matched
|
|
True
|
|
>>> outcome
|
|
["app-foo/foo", "foo-misc/foo"]
|
|
|
|
"""
|
|
LOGIC_AND = "&"
|
|
LOGIC_OR = "|"
|
|
|
|
class MalformedDependency(EntropyException):
|
|
"""
|
|
Raised when dependency string is malformed.
|
|
"""
|
|
|
|
def __init__(self, entropy_dep, entropy_repository_list,
|
|
selected_matches = None):
|
|
"""
|
|
DependencyStringParser constructor.
|
|
|
|
@param entropy_dep: the dependency string to parse
|
|
@type entropy_dep: string
|
|
@param entropy_repository_list: ordered list of EntropyRepositoryBase
|
|
based instances
|
|
@type entropy_repository_list: list
|
|
@keyword selected_matches: if given, it will be used in the decisional
|
|
process of selecting conditional dependencies. Generally, a list
|
|
of selected matches comes directly from user packages selection.
|
|
@type selected_matches: set
|
|
"""
|
|
self.__dep = entropy_dep
|
|
self.__entropy_repository_list = entropy_repository_list
|
|
self.__selected_matches = None
|
|
if selected_matches:
|
|
self.__selected_matches = frozenset(selected_matches)
|
|
self.__dep_cache = {}
|
|
self.__eval_cache = {}
|
|
|
|
def __clear_cache(self):
|
|
self.__dep_cache.clear()
|
|
self.__eval_cache.clear()
|
|
|
|
def __dependency(self, dep):
|
|
"""
|
|
Helper function to make instantianting Dependency classes less annoying.
|
|
"""
|
|
cached = self.__dep_cache.get(dep)
|
|
if cached is not None:
|
|
return cached
|
|
obj = Dependency(dep, self.__entropy_repository_list)
|
|
self.__dep_cache[dep] = obj
|
|
return obj
|
|
|
|
def __evaluate(self, dep):
|
|
"""
|
|
Helper function to make instantianting Dependency classes and retrieving
|
|
match results less annoying.
|
|
"""
|
|
cached = self.__eval_cache.get(dep)
|
|
if cached is not None:
|
|
return cached
|
|
obj = Dependency(dep, self.__entropy_repository_list).evaluate()
|
|
self.__eval_cache[dep] = obj
|
|
return obj
|
|
|
|
def __split_subs(self, substring):
|
|
deep_count = 0
|
|
cur_str = ""
|
|
subs = []
|
|
for char in substring:
|
|
if char == " ":
|
|
continue
|
|
elif char == "(" and deep_count == 0:
|
|
if cur_str.strip():
|
|
subs.append(cur_str.strip())
|
|
cur_str = char
|
|
deep_count += 1
|
|
elif char == "(":
|
|
cur_str += char
|
|
deep_count += 1
|
|
elif char == self.LOGIC_OR and deep_count == 0:
|
|
if cur_str.strip():
|
|
subs.append(cur_str.strip())
|
|
subs.append(char)
|
|
cur_str = ""
|
|
elif char == self.LOGIC_AND and deep_count == 0:
|
|
if cur_str.strip():
|
|
subs.append(cur_str.strip())
|
|
subs.append(char)
|
|
cur_str = ""
|
|
elif char == ")":
|
|
cur_str += char
|
|
deep_count -= 1
|
|
if deep_count == 0:
|
|
cur_str = cur_str.strip()
|
|
deps = self.__encode_sub(cur_str)
|
|
if len(deps) == 1:
|
|
subs.append(deps[0])
|
|
elif deps:
|
|
subs.append(deps)
|
|
else:
|
|
raise DependencyStringParser.MalformedDependency()
|
|
cur_str = ""
|
|
else:
|
|
cur_str += char
|
|
|
|
if cur_str:
|
|
subs.append(cur_str.strip())
|
|
|
|
return subs
|
|
|
|
def __evaluate_subs(self, iterable):
|
|
|
|
if self.LOGIC_AND in iterable and self.LOGIC_OR in iterable:
|
|
raise DependencyStringParser.MalformedDependency(
|
|
"more than one operator in domain, not yet supported")
|
|
|
|
if self.LOGIC_AND in iterable:
|
|
iterable = [x for x in iterable if x != self.LOGIC_AND]
|
|
|
|
outcomes = []
|
|
for and_el in iterable:
|
|
if isinstance(and_el, list):
|
|
outcome = self.__evaluate_subs(and_el)
|
|
if outcome:
|
|
outcomes.extend(outcome)
|
|
else:
|
|
return []
|
|
elif self.__dependency(and_el):
|
|
outcomes.append(and_el)
|
|
else:
|
|
return []
|
|
return outcomes
|
|
|
|
elif self.LOGIC_OR in iterable:
|
|
iterable = [x for x in iterable if x != self.LOGIC_OR]
|
|
|
|
if self.__selected_matches:
|
|
# if there is something to prioritize
|
|
for or_el in iterable:
|
|
if isinstance(or_el, list):
|
|
outcome = self.__evaluate_subs(or_el)
|
|
if outcome:
|
|
difference = set(outcome) - self.__selected_matches
|
|
if not difference:
|
|
# everything matched, so this should be taken
|
|
return outcome
|
|
else: # simple string
|
|
outcome = self.__evaluate(or_el)
|
|
if outcome:
|
|
difference = set(outcome) - self.__selected_matches
|
|
if len(outcome) != len(difference):
|
|
# ok cool, got it!
|
|
return [or_el]
|
|
# no match using selected_matches priority list, fallback to
|
|
# first available.
|
|
for or_el in iterable:
|
|
if isinstance(or_el, list):
|
|
outcome = self.__evaluate_subs(or_el)
|
|
if outcome:
|
|
return outcome
|
|
elif self.__dependency(or_el):
|
|
return [or_el]
|
|
return []
|
|
|
|
# don't know what to do at the moment with this malformation
|
|
return []
|
|
|
|
def __encode_sub(self, dep):
|
|
"""
|
|
Generate a list of lists and strings from a plain dependency match
|
|
condition.
|
|
"""
|
|
open_bracket = dep.find("(")
|
|
closed_bracket = dep.rfind(")")
|
|
|
|
try:
|
|
substring = dep[open_bracket + 1:closed_bracket]
|
|
except IndexError:
|
|
raise DependencyStringParser.MalformedDependency()
|
|
if not substring:
|
|
raise DependencyStringParser.MalformedDependency()
|
|
|
|
|
|
subs = self.__split_subs(substring)
|
|
if not subs:
|
|
raise DependencyStringParser.MalformedDependency()
|
|
|
|
return subs
|
|
|
|
def parse(self):
|
|
"""
|
|
Execute the actual parsing and return the result.
|
|
|
|
@return: tuple composed by boolean (matched? not matched?) and list
|
|
of evaluated/matched dependencies.
|
|
@rtype: tuple
|
|
@raise MalformedDependency: if dependency string is malformed
|
|
"""
|
|
self.__clear_cache()
|
|
matched = False
|
|
try:
|
|
matched_deps = self.__evaluate_subs(
|
|
self.__encode_sub("(" + self.__dep + ")"))
|
|
if matched_deps:
|
|
matched = True
|
|
except DependencyStringParser.MalformedDependency:
|
|
matched_deps = []
|
|
return matched, matched_deps
|
|
|
|
|
|
def expand_dependencies(dependencies, entropy_repository_list,
|
|
selected_matches = None):
|
|
"""
|
|
Expand a list of dependencies resolving conditional ones.
|
|
NOTE: it automatically handles dependencies metadata extended format:
|
|
[(dep, type), ...]
|
|
|
|
@param dependencies: list of raw package dependencies, as
|
|
returned by EntropyRepositoryBase.retrieveDependencies{,List}()
|
|
@type dependencies: iterable
|
|
@param entropy_repository_list: ordered list of EntropyRepositoryBase instances
|
|
used to execute the actual resolution
|
|
@type entropy_repository_list: list
|
|
@keyword selected_matches: list of preferred package matches used to
|
|
evaluate or-dependencies.
|
|
@type selected_matches: set
|
|
@return: list (keeping the iterable order when possible) of expanded
|
|
dependencies
|
|
@rtype: list
|
|
@raise AttributeError: if dependencies structure is unsupported (this
|
|
function supports list of strings, or list of tuples of length 2)
|
|
"""
|
|
pkg_deps = []
|
|
for dep in dependencies:
|
|
dep_type = None
|
|
if isinstance(dep, tuple):
|
|
if len(dep) == 2:
|
|
dep, dep_type = dep
|
|
elif len(dep) == 1:
|
|
dep = dep[0]
|
|
else:
|
|
raise AttributeError("malformed input dependencies")
|
|
if dep.startswith("("):
|
|
try:
|
|
deps = DependencyStringParser(dep,
|
|
entropy_repository_list,
|
|
selected_matches = selected_matches).parse()
|
|
except DependencyStringParser.MalformedDependency:
|
|
# wtf! add as-is
|
|
if dep_type is None:
|
|
pkg_deps.append(dep)
|
|
else:
|
|
pkg_deps.append((dep, dep_type))
|
|
continue
|
|
if dep_type is None:
|
|
pkg_deps.extend(deps)
|
|
else:
|
|
pkg_deps.extend([(x, dep_type) for x in deps])
|
|
elif dep_type is None:
|
|
pkg_deps.append(dep)
|
|
else:
|
|
pkg_deps.append((dep, dep_type))
|
|
|
|
return pkg_deps
|