--- libs.py.2.2_rc6 2008-08-01 15:41:14.000000000 -0500 +++ pym/portage/sets/libs.py 2008-08-11 13:38:01.000000000 -0500 @@ -2,10 +2,18 @@ # Distributed under the terms of the GNU General Public License v2 # $Id: libs.py 10759 2008-06-22 04:04:50Z zmedico $ +import os +import re +import time +from portage.dbapi.vartree import dblink +from portage.versions import catsplit from portage.sets.base import PackageSet from portage.sets import get_boolean from portage.versions import catpkgsplit +__all__ = ["LibraryConsumerSet", "PreservedLibraryConsumerSet", + "MissingLibraryConsumerSet"] + class LibraryConsumerSet(PackageSet): _operations = ["merge", "unmerge"] @@ -45,3 +53,311 @@ debug = get_boolean(options, "debug", False) return PreservedLibraryConsumerSet(trees["vartree"].dbapi, debug) singleBuilder = classmethod(singleBuilder) + + +class MissingLibraryConsumerSet(LibraryConsumerSet): + + """ + This class is the set of packages to emerge due to missing libraries. + + This class scans binaries for missing and broken shared library dependencies + and fixes them by emerging the packages containing the broken binaries. + + The user may also emerge packages containing consumers of specified + libraries by passing the name or a python regular expression through the + environment variable, LIBRARY. Due to a limitation in passing flags to + package sets through the portage cli, the user must set environment + variables to modify the behaviour of this package set. So if the + environment variable LIBRARY is set, the behaviour of this set changes. + + """ + + description = "The set of packages to emerge due to missing libraries." + _operations = ["merge"] + + def __init__(self, vardbapi, debug=False): + super(MissingLibraryConsumerSet, self).__init__(vardbapi, debug) + # FIXME Since we can't get command line arguments from the user, the + # soname can be passed through an environment variable for now. + self.libraryRegexp = os.getenv("LIBRARY") + self.root = self.dbapi.root + self.linkmap = self.dbapi.linkmap + + def load(self): + # brokenDependencies: object -> set-of-unsatisfied-dependencies, where + # object is an installed binary/library and + # set-of-unsatisfied-dependencies are sonames or libraries required by + # the object but have no corresponding libraries to fulfill the + # dependency. + brokenDependencies = {} + atoms = set() + + # If the LIBRARY environment variable is set, the resulting package set + # will be packages containing consumers of the libraries matched by the + # variable. + if self.libraryRegexp: + atoms = self.findAtomsOfLibraryConsumers(self.libraryRegexp) + self._setAtoms(atoms) + if self.debug: + print + print "atoms to be emerged:" + for x in sorted(atoms): + print x + return + + # Get the list of broken dependencies from LinkageMap. + if self.debug: + timeStart = time.time() + brokenDependencies = self.linkmap.listBrokenBinaries() + if self.debug: + timeListBrokenBinaries = time.time() - timeStart + + # Add broken libtool libraries into the brokenDependencies dict. + if self.debug: + timeStart = time.time() + brokenDependencies.update(self.listBrokenLibtoolLibraries()) + if self.debug: + timeLibtool = time.time() - timeStart + + # FIXME Too many atoms may be emerged because libraries in binary + # packages are not being handled properly eg openoffice, nvidia-drivers, + # sun-jdk. Certain binaries are run in an environment where additional + # library paths are added via LD_LIBRARY_PATH. Since these paths aren't + # registered in _obj_properties, they appear broken (and are if not run + # in the correct environment). I have to determine if libraries and lib + # paths should be masked using /etc/revdep-rebuild/* as done in + # revdep-rebuild or if there is a better way to identify and deal with + # these problematic packages (or if something entirely different should + # be done). For now directory and library masks are used. + + # Remove masked directories and libraries. + if self.debug: + timeStart = time.time() + if brokenDependencies: + brokenDependencies = self.removeMaskedDependencies(brokenDependencies) + if self.debug: + timeMask = time.time() - timeStart + + # Determine atoms to emerge based on broken objects in + # brokenDependencies. + if self.debug: + timeStart = time.time() + if brokenDependencies: + atoms = self.mapPathsToAtoms(set(brokenDependencies.keys())) + if self.debug: + timeAtoms = time.time() - timeStart + + # Debug output + if self.debug: + print + print len(brokenDependencies), "brokenDependencies:" + for x in sorted(brokenDependencies.keys()): + print + print x, "->" + print '\t', brokenDependencies[x] + print + print "atoms to be emerged:" + for x in sorted(atoms): + print x + print + print "Broken binaries time:", timeListBrokenBinaries + print "Broken libtool time:", timeLibtool + print "Remove mask time:", timeMask + print "mapPathsToAtoms time:", timeAtoms + print + + self._setAtoms(atoms) + + def removeMaskedDependencies(self, dependencies): + """ + Remove all masked dependencies and return the updated mapping. + + @param dependencies: dependencies from which to removed masked + dependencies + @type dependencies: dict (example: {'/usr/bin/foo': set(['libfoo.so'])}) + @rtype: dict + @return: shallow copy of dependencies with masked items removed + + """ + rValue = dependencies.copy() + dirMask, libMask = self.getDependencyMasks() + + # Remove entries that are masked. + if dirMask or libMask: + if self.debug: + print "The following are masked:" + for binary, libSet in rValue.items(): + for dir in dirMask: + # Check if the broken binary lies within the masked directory or + # its subdirectories. + # XXX Perhaps we should allow regexps as masks. + if binary.startswith(dir): + del rValue[binary] + if self.debug: + print "dirMask:",binary + break + # Check if all the required libraries are masked. + if binary in rValue and libSet.issubset(libMask): + del rValue[binary] + if self.debug: + print "libMask:", binary, libSet & libMask + + if self.debug: + print + print "Directory mask:", dirMask + print + print "Library mask:", libMask + + return rValue + + def getDependencyMasks(self): + """ + Return all dependency masks as a tuple. + + @rtype: 2-tuple of sets of strings + @return: 2-tuple in which the first component is a set of directory + masks and the second component is a set of library masks + + """ + dirMask = set() + libMask = set() + _dirMask_re = re.compile(r'SEARCH_DIRS_MASK\s*=\s*"([^"]*)"') + _libMask_re = re.compile(r'LD_LIBRARY_MASK\s*=\s*"([^"]*)"') + lines = [] + + # Reads the contents of /etc/revdep-rebuild/* + libMaskDir = os.path.join(self.root, "etc", "revdep-rebuild") + if os.path.exists(libMaskDir): + for file in os.listdir(libMaskDir): + try: + f = open(os.path.join(libMaskDir, file), "r") + try: + lines.extend(f.readlines()) + finally: + f.close() + except IOError: # OSError? + continue + # The following parses SEARCH_DIRS_MASK and LD_LIBRARY_MASK variables + # from /etc/revdep-rebuild/* + for line in lines: + matchDir = _dirMask_re.match(line) + matchLib = _libMask_re.match(line) + if matchDir: + dirMask.update(set(matchDir.group(1).split())) + if matchLib: + libMask.update(set(matchLib.group(1).split())) + + # These directories contain specially evaluated libraries. + # app-emulation/vmware-workstation-6.0.1.55017 + dirMask.add('/opt/vmware/workstation/lib') + # app-emulation/vmware-server-console-1.0.6.91891 + dirMask.add('/opt/vmware/server/console/lib') + # www-client/mozilla-firefox-2.0.0.15 + dirMask.add('/usr/lib/mozilla-firefox/plugins') + dirMask.add('/usr/lib64/mozilla-firefox/plugins') + # app-office/openoffice-2.4.1 + dirMask.add('/opt/OpenOffice') + dirMask.add('/usr/lib/openoffice') + # dev-libs/libmix-2.05 libmix.so is missing soname entry + libMask.add('libmix.so') + + return (dirMask, libMask) + + def findAtomsOfLibraryConsumers(self, searchString): + """ + Return atoms containing consumers of libraries matching the argument. + + @param searchString: a string used to search for libraries + @type searchString: string to be compiled as a regular expression + (example: 'libfoo.*') + @rtype: set of strings + @return: the returned set of atoms are valid to be used by package sets + + """ + atoms = set() + consumers = set() + matchedLibraries = set() + libraryObjects = [] + _librarySearch_re = re.compile(searchString) + + # Find libraries matching searchString. + libraryObjects = self.linkmap.listLibraryObjects() + for library in libraryObjects: + m = _librarySearch_re.search(library) + if m: + matchedLibraries.add(library) + consumers.update(self.linkmap.findConsumers(library)) + + if self.debug: + print + print "Consumers of the following libraries will be emerged:" + for x in matchedLibraries: + print x + + if consumers: + # The following prevents emerging the packages that own the matched + # libraries. Note that this will prevent updating the packages owning + # the libraries if there are newer versions available in the installed + # slot. See bug #30095 + atoms = self.mapPathsToAtoms(consumers) + libraryOwners = self.mapPathsToAtoms(matchedLibraries) + atoms.difference_update(libraryOwners) + + return atoms + + def listBrokenLibtoolLibraries(self): + """ + Find broken libtool libraries and their missing dependencies. + + @rtype: dict (example: {'/lib/libfoo.la': set(['/lib/libbar.la'])}) + @return: The return value is a library -> set-of-libraries mapping, where + library is a broken library and the set consists of dependencies + needed by library that do not exist on the filesystem. + + """ + rValue = {} + lines = [] + dependencies = [] + _la_re = re.compile(r".*\.la$") + _dependency_libs_re = re.compile(r"^dependency_libs\s*=\s*'(.*)'") + + # Loop over the contents of all packages. + for cpv in self.dbapi.cpv_all(): + mysplit = catsplit(cpv) + link = dblink(mysplit[0], mysplit[1], myroot=self.dbapi.root, \ + mysettings=self.dbapi.settings, treetype='vartree', \ + vartree=self.dbapi.vartree) + for file in link.getcontents(): + # Check if the file ends with '.la'. + matchLib = _la_re.match(file) + if matchLib: + # Read the lines from the library. + lines = [] + try: + f = open(file, "r") + try: + lines.extend(f.readlines()) + finally: + f.close() + except IOError: + continue + # Find the line listing the dependencies. + for line in lines: + matchLine = _dependency_libs_re.match(line) + if matchLine: + dependencies = matchLine.group(1).split() + # For each dependency that is a pathname (begins with + # os.sep), check that it exists on the filesystem. If it + # does not exist, then add the library and the missing + # dependency to rValue. + for dependency in dependencies: + if dependency[0] == os.sep and \ + not os.path.isfile(dependency): + rValue.setdefault(file, set()).add(dependency) + + return rValue + + def singleBuilder(self, options, settings, trees): + debug = get_boolean(options, "debug", False) + return MissingLibraryConsumerSet(trees["vartree"].dbapi, debug) + singleBuilder = classmethod(singleBuilder)