diff options
| author | fuzzyray <fuzzyray@gentoo.org> | 2009-05-05 17:39:24 +0000 |
|---|---|---|
| committer | fuzzyray <fuzzyray@gentoo.org> | 2009-05-05 17:39:24 +0000 |
| commit | c819d146be6bce86d97019494173253e71b85d2f (patch) | |
| tree | 200d00c2b9a420540ff9c4e0d8b3080b762fb562 /bin | |
| parent | 61823fe68081c6f8edf24455bbb34123598c5bb4 (diff) | |
| download | gentoolkit-c819d146be6bce86d97019494173253e71b85d2f.tar.gz | |
Rearrange trunk to support gentoolkit version 0.3. Split into gentoolkit, gentoolkit-dev, and deprecated. Import djanderson's work on the gentoolkit library and equery
svn path=/trunk/gentoolkit/; revision=589
Diffstat (limited to 'bin')
| -rw-r--r-- | bin/eclean | 802 | ||||
| -rwxr-xr-x | bin/epkginfo | 207 | ||||
| -rw-r--r-- | bin/equery | 32 | ||||
| -rwxr-xr-x | bin/eread | 94 | ||||
| -rwxr-xr-x | bin/euse | 547 | ||||
| -rw-r--r-- | bin/glsa-check | 366 | ||||
| -rwxr-xr-x | bin/revdep-rebuild | 1094 |
7 files changed, 3142 insertions, 0 deletions
diff --git a/bin/eclean b/bin/eclean new file mode 100644 index 0000000..8b473f9 --- /dev/null +++ b/bin/eclean @@ -0,0 +1,802 @@ +#!/usr/bin/env python +# Copyright 2003-2005 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 +# $Header: $ + + +############################################################################### +# Meta: +__author__ = "Thomas de Grenier de Latour (tgl)" +__email__ = "degrenier@easyconnect.fr" +__version__ = open('/etc/gentoolkit-version').read().strip() +__productname__ = "eclean" +__description__ = "A cleaning tool for Gentoo distfiles and binaries." + + +############################################################################### +# Python imports: +import sys +import os, stat +import re +import time +import getopt +import fpformat +import signal + +import portage +from portage.output import * + +listdir = portage.listdir + +############################################################################### +# Misc. shortcuts to some portage stuff: +port_settings = portage.settings +distdir = port_settings["DISTDIR"] +pkgdir = port_settings["PKGDIR"] + + +############################################################################### +# printVersion: +def printVersion(): + print "%s (%s) - %s" \ + % (__productname__, __version__, __description__) + print + print "Author: %s <%s>" % (__author__,__email__) + print "Copyright 2003-2009 Gentoo Foundation" + print "Distributed under the terms of the GNU General Public License v2" + + +############################################################################### +# printUsage: print help message. May also print partial help to stderr if an +# error from {'options','actions'} is specified. +def printUsage(error=None,help=None): + out = sys.stdout + if error: out = sys.stderr + if not error in ('actions', 'global-options', \ + 'packages-options', 'distfiles-options', \ + 'merged-packages-options', 'merged-distfiles-options', \ + 'time', 'size'): + error = None + if not error and not help: help = 'all' + if error == 'time': + eerror("Wrong time specification") + print >>out, "Time specification should be an integer followed by a"+ \ + " single letter unit." + print >>out, "Available units are: y (years), m (months), w (weeks), "+ \ + "d (days) and h (hours)." + print >>out, "For instance: \"1y\" is \"one year\", \"2w\" is \"two"+ \ + " weeks\", etc. " + return + if error == 'size': + eerror("Wrong size specification") + print >>out, "Size specification should be an integer followed by a"+ \ + " single letter unit." + print >>out, "Available units are: G, M, K and B." + print >>out, "For instance: \"10M\" is \"ten megabytes\", \"200K\" "+ \ + "is \"two hundreds kilobytes\", etc." + return + if error in ('global-options', 'packages-options', 'distfiles-options', \ + 'merged-packages-options', 'merged-distfiles-options',): + eerror("Wrong option on command line.") + print >>out + elif error == 'actions': + eerror("Wrong or missing action name on command line.") + print >>out + print >>out, white("Usage:") + if error in ('actions','global-options', 'packages-options', \ + 'distfiles-options') or help == 'all': + print >>out, " "+turquoise(__productname__), \ + yellow("[global-option] ..."), \ + green("<action>"), \ + yellow("[action-option] ...") + if error == 'merged-distfiles-options' or help in ('all','distfiles'): + print >>out, " "+turquoise(__productname__+'-dist'), \ + yellow("[global-option, distfiles-option] ...") + if error == 'merged-packages-options' or help in ('all','packages'): + print >>out, " "+turquoise(__productname__+'-pkg'), \ + yellow("[global-option, packages-option] ...") + if error in ('global-options', 'actions'): + print >>out, " "+turquoise(__productname__), \ + yellow("[--help, --version]") + if help == 'all': + print >>out, " "+turquoise(__productname__+"(-dist,-pkg)"), \ + yellow("[--help, --version]") + if error == 'merged-packages-options' or help == 'packages': + print >>out, " "+turquoise(__productname__+'-pkg'), \ + yellow("[--help, --version]") + if error == 'merged-distfiles-options' or help == 'distfiles': + print >>out, " "+turquoise(__productname__+'-dist'), \ + yellow("[--help, --version]") + print >>out + if error in ('global-options', 'merged-packages-options', \ + 'merged-distfiles-options') or help: + print >>out, "Available global", yellow("options")+":" + print >>out, yellow(" -C, --nocolor")+ \ + " - turn off colors on output" + print >>out, yellow(" -d, --destructive")+ \ + " - only keep the minimum for a reinstallation" + print >>out, yellow(" -e, --exclude-file=<path>")+ \ + " - path to the exclusion file" + print >>out, yellow(" -i, --interactive")+ \ + " - ask confirmation before deletions" + print >>out, yellow(" -n, --package-names")+ \ + " - protect all versions (when --destructive)" + print >>out, yellow(" -p, --pretend")+ \ + " - only display what would be cleaned" + print >>out, yellow(" -q, --quiet")+ \ + " - be as quiet as possible" + print >>out, yellow(" -t, --time-limit=<time>")+ \ + " - don't delete files modified since "+yellow("<time>") + print >>out, " "+yellow("<time>"), "is a duration: \"1y\" is"+ \ + " \"one year\", \"2w\" is \"two weeks\", etc. " + print >>out, " "+"Units are: y (years), m (months), w (weeks), "+ \ + "d (days) and h (hours)." + print >>out, yellow(" -h, --help")+ \ + " - display the help screen" + print >>out, yellow(" -V, --version")+ \ + " - display version info" + print >>out + if error == 'actions' or help == 'all': + print >>out, "Available", green("actions")+":" + print >>out, green(" packages")+ \ + " - clean outdated binary packages from:" + print >>out, " ",teal(pkgdir) + print >>out, green(" distfiles")+ \ + " - clean outdated packages sources files from:" + print >>out, " ",teal(distdir) + print >>out + if error in ('packages-options','merged-packages-options') \ + or help in ('all','packages'): + print >>out, "Available", yellow("options"),"for the", \ + green("packages"),"action:" + print >>out, yellow(" NONE :)") + print >>out + if error in ('distfiles-options', 'merged-distfiles-options') \ + or help in ('all','distfiles'): + print >>out, "Available", yellow("options"),"for the", \ + green("distfiles"),"action:" + print >>out, yellow(" -f, --fetch-restricted")+ \ + " - protect fetch-restricted files (when --destructive)" + print >>out, yellow(" -s, --size-limit=<size>")+ \ + " - don't delete distfiles bigger than "+yellow("<size>") + print >>out, " "+yellow("<size>"), "is a size specification: "+ \ + "\"10M\" is \"ten megabytes\", \"200K\" is" + print >>out, " "+"\"two hundreds kilobytes\", etc. Units are: "+ \ + "G, M, K and B." + print >>out + print >>out, "More detailed instruction can be found in", \ + turquoise("`man %s`" % __productname__) + + +############################################################################### +# einfo: display an info message depending on a color mode +def einfo(message="", nocolor=False): + if not nocolor: prefix = " "+green('*') + else: prefix = ">>>" + print prefix,message + + +############################################################################### +# eerror: display an error depending on a color mode +def eerror(message="", nocolor=False): + if not nocolor: prefix = " "+red('*') + else: prefix = "!!!" + print >>sys.stderr,prefix,message + + +############################################################################### +# eprompt: display a user question depending on a color mode. +def eprompt(message, nocolor=False): + if not nocolor: prefix = " "+red('>')+" " + else: prefix = "??? " + sys.stdout.write(prefix+message) + sys.stdout.flush() + + +############################################################################### +# prettySize: integer -> byte/kilo/mega/giga converter. Optionnally justify the +# result. Output is a string. +def prettySize(size,justify=False): + units = [" G"," M"," K"," B"] + approx = 0 + while len(units) and size >= 1000: + approx = 1 + size = size / 1024. + units.pop() + sizestr = fpformat.fix(size,approx)+units[-1] + if justify: + sizestr = " " + blue("[ ") + " "*(7-len(sizestr)) \ + + green(sizestr) + blue(" ]") + return sizestr + + +############################################################################### +# yesNoAllPrompt: print a prompt until user answer in yes/no/all. Return a +# boolean for answer, and also may affect the 'accept_all' option. +# Note: i gave up with getch-like functions, to much bugs in case of escape +# sequences. Back to raw_input. +def yesNoAllPrompt(myoptions,message="Do you want to proceed?"): + user_string="xxx" + while not user_string.lower() in ["","y","n","a","yes","no","all"]: + eprompt(message+" [Y/n/a]: ", myoptions['nocolor']) + user_string = raw_input() + if user_string.lower() in ["a","all"]: + myoptions['accept_all'] = True + myanswer = user_string.lower() in ["","y","a","yes","all"] + return myanswer + + +############################################################################### +# ParseArgsException: for parseArgs() -> main() communication +class ParseArgsException(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + + +############################################################################### +# parseSize: convert a file size "Xu" ("X" is an integer, and "u" in [G,M,K,B]) +# into an integer (file size in Bytes). Raises ParseArgsException('size') in +# case of failure. +def parseSize(size): + myunits = { \ + 'G': (1024**3), \ + 'M': (1024**2), \ + 'K': 1024, \ + 'B': 1 \ + } + try: + mymatch = re.match(r"^(?P<value>\d+)(?P<unit>[GMKBgmkb])?$",size) + mysize = int(mymatch.group('value')) + if mymatch.group('unit'): + mysize *= myunits[mymatch.group('unit').capitalize()] + except: + raise ParseArgsException('size') + return mysize + + +############################################################################### +# parseTime: convert a duration "Xu" ("X" is an int, and "u" a time unit in +# [Y,M,W,D,H]) into an integer which is a past EPOCH date. +# Raises ParseArgsException('time') in case of failure. +# (yep, big approximations inside... who cares?) +def parseTime(timespec): + myunits = {'H' : (60 * 60)} + myunits['D'] = myunits['H'] * 24 + myunits['W'] = myunits['D'] * 7 + myunits['M'] = myunits['D'] * 30 + myunits['Y'] = myunits['D'] * 365 + try: + # parse the time specification + mymatch = re.match(r"^(?P<value>\d+)(?P<unit>[YMWDHymwdh])?$",timespec) + myvalue = int(mymatch.group('value')) + if not mymatch.group('unit'): myunit = 'D' + else: myunit = mymatch.group('unit').capitalize() + except: raise ParseArgsException('time') + # calculate the limit EPOCH date + mytime = time.time() - (myvalue * myunits[myunit]) + return mytime + + +############################################################################### +# parseCmdLine: parse the command line arguments. Raise exceptions on errors or +# non-action modes (help/version). Returns an action, and affect the options +# dict. +def parseArgs(myoptions={}): + + # local function for interpreting command line options + # and setting myoptions accordingly + def optionSwitch(myoption,opts,action=None): + return_code = True + for o, a in opts: + if o in ("-h", "--help"): + if action: raise ParseArgsException('help-'+action) + else: raise ParseArgsException('help') + elif o in ("-V", "--version"): + raise ParseArgsException('version') + elif o in ("-C", "--nocolor"): + myoptions['nocolor'] = True + nocolor() + elif o in ("-d", "--destructive"): + myoptions['destructive'] = True + elif o in ("-i", "--interactive") and not myoptions['pretend']: + myoptions['interactive'] = True + elif o in ("-p", "--pretend"): + myoptions['pretend'] = True + myoptions['interactive'] = False + elif o in ("-q", "--quiet"): + myoptions['quiet'] = True + elif o in ("-t", "--time-limit"): + myoptions['time-limit'] = parseTime(a) + elif o in ("-e", "--exclude-file"): + myoptions['exclude-file'] = a + elif o in ("-n", "--package-names"): + myoptions['package-names'] = True + elif o in ("-f", "--fetch-restricted"): + myoptions['fetch-restricted'] = True + elif o in ("-s", "--size-limit"): + myoptions['size-limit'] = parseSize(a) + else: return_code = False + # sanity check of --destructive only options: + for myopt in ('fetch-restricted', 'package-names'): + if (not myoptions['destructive']) and myoptions[myopt]: + if not myoptions['quiet']: + eerror("--%s only makes sense in --destructive mode." \ + % myopt, myoptions['nocolor']) + myoptions[myopt] = False + return return_code + + # here are the different allowed command line options (getopt args) + getopt_options = {'short':{}, 'long':{}} + getopt_options['short']['global'] = "Cdipqe:t:nhV" + getopt_options['long']['global'] = ["nocolor", "destructive", \ + "interactive", "pretend", "quiet", "exclude-file=", "time-limit=", \ + "package-names", "help", "version"] + getopt_options['short']['distfiles'] = "fs:" + getopt_options['long']['distfiles'] = ["fetch-restricted", "size-limit="] + getopt_options['short']['packages'] = "" + getopt_options['long']['packages'] = [""] + # set default options, except 'nocolor', which is set in main() + myoptions['interactive'] = False + myoptions['pretend'] = False + myoptions['quiet'] = False + myoptions['accept_all'] = False + myoptions['destructive'] = False + myoptions['time-limit'] = 0 + myoptions['package-names'] = False + myoptions['fetch-restricted'] = False + myoptions['size-limit'] = 0 + # if called by a well-named symlink, set the acction accordingly: + myaction = None + if os.path.basename(sys.argv[0]) in \ + (__productname__+'-pkg', __productname__+'-packages'): + myaction = 'packages' + elif os.path.basename(sys.argv[0]) in \ + (__productname__+'-dist', __productname__+'-distfiles'): + myaction = 'distfiles' + # prepare for the first getopt + if myaction: + short_opts = getopt_options['short']['global'] \ + + getopt_options['short'][myaction] + long_opts = getopt_options['long']['global'] \ + + getopt_options['long'][myaction] + opts_mode = 'merged-'+myaction + else: + short_opts = getopt_options['short']['global'] + long_opts = getopt_options['long']['global'] + opts_mode = 'global' + # apply getopts to command line, show partial help on failure + try: opts, args = getopt.getopt(sys.argv[1:], short_opts, long_opts) + except: raise ParseArgsException(opts_mode+'-options') + # set myoptions accordingly + optionSwitch(myoptions,opts,action=myaction) + # if action was already set, there should be no more args + if myaction and len(args): raise ParseArgsException(opts_mode+'-options') + # if action was set, there is nothing left to do + if myaction: return myaction + # So, we are in "eclean --foo action --bar" mode. Parse remaining args... + # Only two actions are allowed: 'packages' and 'distfiles'. + if not len(args) or not args[0] in ('packages','distfiles'): + raise ParseArgsException('actions') + myaction = args.pop(0) + # parse the action specific options + try: opts, args = getopt.getopt(args, \ + getopt_options['short'][myaction], \ + getopt_options['long'][myaction]) + except: raise ParseArgsException(myaction+'-options') + # set myoptions again, for action-specific options + optionSwitch(myoptions,opts,action=myaction) + # any remaning args? Then die! + if len(args): raise ParseArgsException(myaction+'-options') + # returns the action. Options dictionary is modified by side-effect. + return myaction + +############################################################################### +# isValidCP: check wether a string is a valid cat/pkg-name +# This is for 2.0.51 vs. CVS HEAD compatibility, i've not found any function +# for that which would exists in both. Weird... +def isValidCP(cp): + if not '/' in cp: return False + try: portage.cpv_getkey(cp+"-0") + except: return False + else: return True + + +############################################################################### +# ParseExcludeFileException: for parseExcludeFile() -> main() communication +class ParseExcludeFileException(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + + +############################################################################### +# parseExcludeFile: parses an exclusion file, returns an exclusion dictionnary +# Raises ParseExcludeFileException in case of fatal error. +def parseExcludeFile(filepath): + excl_dict = { \ + 'categories':{}, \ + 'packages':{}, \ + 'anti-packages':{}, \ + 'garbage':{} } + try: file = open(filepath,"r") + except IOError: + raise ParseExcludeFileException("Could not open exclusion file.") + filecontents = file.readlines() + file.close() + cat_re = re.compile('^(?P<cat>[a-zA-Z0-9]+-[a-zA-Z0-9]+)(/\*)?$') + cp_re = re.compile('^(?P<cp>[-a-zA-Z0-9_]+/[-a-zA-Z0-9_]+)$') + for line in filecontents: + line = line.strip() + if not len(line): continue + if line[0] == '#': continue + try: mycat = cat_re.match(line).group('cat') + except: pass + else: + if not mycat in portage.settings.categories: + raise ParseExcludeFileException("Invalid category: "+mycat) + excl_dict['categories'][mycat] = None + continue + dict_key = 'packages' + if line[0] == '!': + dict_key = 'anti-packages' + line = line[1:] + try: + mycp = cp_re.match(line).group('cp') + if isValidCP(mycp): + excl_dict[dict_key][mycp] = None + continue + else: raise ParseExcludeFileException("Invalid cat/pkg: "+mycp) + except: pass + #raise ParseExcludeFileException("Invalid line: "+line) + try: + excl_dict['garbage'][line] = re.compile(line) + except: + try: + excl_dict['garbage'][line] = re.compile(re.escape(line)) + except: + raise ParseExcludeFileException("Invalid file name/regular expression: "+line) + return excl_dict + + +############################################################################### +# exclDictExpand: returns a dictionary of all CP from porttree which match +# the exclusion dictionary +def exclDictExpand(excl_dict): + mydict = {} + if 'categories' in excl_dict: + # XXX: i smell an access to something which is really out of API... + for mytree in portage.portdb.porttrees: + for mycat in excl_dict['categories']: + for mypkg in listdir(os.path.join(mytree,mycat),ignorecvs=1): + mydict[mycat+'/'+mypkg] = None + if 'packages' in excl_dict: + for mycp in excl_dict['packages']: + mydict[mycp] = None + if 'anti-packages' in excl_dict: + for mycp in excl_dict['anti-packages']: + if mycp in mydict: + del mydict[mycp] + return mydict + + +############################################################################### +# exclDictMatch: checks whether a CP matches the exclusion rules +def exclDictMatch(excl_dict,pkg): + if 'anti-packages' in excl_dict \ + and pkg in excl_dict['anti-packages']: + return False + if 'packages' in excl_dict \ + and pkg in excl_dict['packages']: + return True + mycat = pkg.split('/')[0] + if 'categories' in excl_dict \ + and mycat in excl_dict['categories']: + return True + return False + + +############################################################################### +# findDistfiles: find all obsolete distfiles. +# XXX: what about cvs ebuilds? i should install some to see where it goes... +def findDistfiles( \ + exclude_dict={}, \ + destructive=False,\ + fetch_restricted=False, \ + package_names=False, \ + time_limit=0, \ + size_limit=0): + # this regexp extracts files names from SRC_URI. It is not very precise, + # but we don't care (may return empty strings, etc.), since it is fast. + file_regexp = re.compile('([a-zA-Z0-9_,\.\-\+\~]*)[\s\)]') + clean_dict = {} + keep = [] + pkg_dict = {} + + # create a big CPV->SRC_URI dict of packages whose distfiles should be kept + if (not destructive) or fetch_restricted: + # list all CPV from portree (yeah, that takes time...) + for package in portage.portdb.cp_all(): + for my_cpv in portage.portdb.cp_list(package): + # get SRC_URI and RESTRICT from aux_get + try: (src_uri,restrict) = \ + portage.portdb.aux_get(my_cpv,["SRC_URI","RESTRICT"]) + except KeyError: continue + # keep either all or fetch-restricted only + if (not destructive) or ('fetch' in restrict): + pkg_dict[my_cpv] = src_uri + if destructive: + if not package_names: + # list all CPV from vartree + pkg_list = portage.db[portage.root]["vartree"].dbapi.cpv_all() + else: + # list all CPV from portree for CP in vartree + pkg_list = [] + for package in portage.db[portage.root]["vartree"].dbapi.cp_all(): + pkg_list += portage.portdb.cp_list(package) + for my_cp in exclDictExpand(exclude_dict): + # add packages from the exclude file + pkg_list += portage.portdb.cp_list(my_cp) + for my_cpv in pkg_list: + # skip non-existing CPV (avoids ugly aux_get messages) + if not portage.portdb.cpv_exists(my_cpv): continue + # get SRC_URI from aux_get + try: pkg_dict[my_cpv] = \ + portage.portdb.aux_get(my_cpv,["SRC_URI"])[0] + except KeyError: continue + del pkg_list + + # create a dictionary of files which should be deleted + for file in os.listdir(distdir): + filepath = os.path.join(distdir, file) + try: file_stat = os.stat(filepath) + except: continue + if not stat.S_ISREG(file_stat[stat.ST_MODE]): continue + if size_limit and (file_stat[stat.ST_SIZE] >= size_limit): + continue + if time_limit and (file_stat[stat.ST_MTIME] >= time_limit): + continue + if 'garbage' in exclude_dict: + # Try to match file name directly + if file in exclude_dict['garbage']: + file_match = True + # See if file matches via regular expression matching + else: + file_match = False + for file_entry in exclude_dict['garbage']: + if exclude_dict['garbage'][file_entry].match(file): + file_match = True + break + + if file_match: + continue + # this is a candidate for cleaning + clean_dict[file]=[filepath] + # remove files owned by some protected packages + for my_cpv in pkg_dict: + for file in file_regexp.findall(pkg_dict[my_cpv]+"\n"): + if file in clean_dict: + del clean_dict[file] + # no need to waste IO time if there is nothing left to clean + if not len(clean_dict): return clean_dict + return clean_dict + + +############################################################################### +# findPackages: find all obsolete binary packages. +# XXX: packages are found only by symlinks. Maybe i should also return .tbz2 +# files from All/ that have no corresponding symlinks. +def findPackages( \ + exclude_dict={}, \ + destructive=False, \ + time_limit=0, \ + package_names=False): + clean_dict = {} + # create a full package dictionnary + for root, dirs, files in os.walk(pkgdir): + if root[-3:] == 'All': continue + for file in files: + if not file[-5:] == ".tbz2": + # ignore non-tbz2 files + continue + path = os.path.join(root, file) + category = os.path.split(root)[-1] + cpv = category+"/"+file[:-5] + mystat = os.lstat(path) + if time_limit and (mystat[stat.ST_MTIME] >= time_limit): + # time-limit exclusion + continue + # dict is cpv->[files] (2 files in general, because of symlink) + clean_dict[cpv] = [path] + #if os.path.islink(path): + if stat.S_ISLNK(mystat[stat.ST_MODE]): + clean_dict[cpv].append(os.path.realpath(path)) + # keep only obsolete ones + if destructive: + mydbapi = portage.db[portage.root]["vartree"].dbapi + if package_names: cp_all = dict.fromkeys(mydbapi.cp_all()) + else: cp_all = {} + else: + mydbapi = portage.db[portage.root]["porttree"].dbapi + cp_all = {} + for mycpv in clean_dict.keys(): + if exclDictMatch(exclude_dict,portage.cpv_getkey(mycpv)): + # exclusion because of the exclude file + del clean_dict[mycpv] + continue + if mydbapi.cpv_exists(mycpv): + # exclusion because pkg still exists (in porttree or vartree) + del clean_dict[mycpv] + continue + if portage.cpv_getkey(mycpv) in cp_all: + # exlusion because of --package-names + del clean_dict[mycpv] + + return clean_dict + + +############################################################################### +# doCleanup: takes a dictionnary {'display name':[list of files]}. Calculate +# size of each entry for display, prompt user if needed, delete files if needed +# and return the total size of files that [have been / would be] deleted. +def doCleanup(clean_dict,action,myoptions): + # define vocabulary of this action + if action == 'distfiles': file_type = 'file' + else: file_type = 'binary package' + # sorting helps reading + clean_keys = clean_dict.keys() + clean_keys.sort() + clean_size = 0 + # clean all entries one by one + for mykey in clean_keys: + key_size = 0 + for file in clean_dict[mykey]: + # get total size for an entry (may be several files, and + # symlinks count zero) + if os.path.islink(file): continue + try: key_size += os.path.getsize(file) + except: eerror("Could not read size of "+file, \ + myoptions['nocolor']) + if not myoptions['quiet']: + # pretty print mode + print prettySize(key_size,True),teal(mykey) + elif myoptions['pretend'] or myoptions['interactive']: + # file list mode + for file in clean_dict[mykey]: print file + #else: actually delete stuff, but don't print anything + if myoptions['pretend']: clean_size += key_size + elif not myoptions['interactive'] \ + or myoptions['accept_all'] \ + or yesNoAllPrompt(myoptions, \ + "Do you want to delete this " \ + + file_type+"?"): + # non-interactive mode or positive answer. + # For each file,... + for file in clean_dict[mykey]: + # ...get its size... + filesize = 0 + if not os.path.exists(file): continue + if not os.path.islink(file): + try: filesize = os.path.getsize(file) + except: eerror("Could not read size of "\ + +file, myoptions['nocolor']) + # ...and try to delete it. + try: os.unlink(file) + except: eerror("Could not delete "+file, \ + myoptions['nocolor']) + # only count size if successfully deleted + else: clean_size += filesize + # return total size of deleted or to delete files + return clean_size + + +############################################################################### +# doAction: execute one action, ie display a few message, call the right find* +# function, and then call doCleanup with its result. +def doAction(action,myoptions,exclude_dict={}): + # define vocabulary for the output + if action == 'packages': files_type = "binary packages" + else: files_type = "distfiles" + # find files to delete, depending on the action + if not myoptions['quiet']: + einfo("Building file list for "+action+" cleaning...", \ + myoptions['nocolor']) + if action == 'packages': + clean_dict = findPackages( \ + exclude_dict=exclude_dict, \ + destructive=myoptions['destructive'], \ + package_names=myoptions['package-names'], \ + time_limit=myoptions['time-limit']) + else: + clean_dict = findDistfiles( \ + exclude_dict=exclude_dict, \ + destructive=myoptions['destructive'], \ + fetch_restricted=myoptions['fetch-restricted'], \ + package_names=myoptions['package-names'], \ + time_limit=myoptions['time-limit'], \ + size_limit=myoptions['size-limit']) + # actually clean files if something was found + if len(clean_dict.keys()): + # verbose pretend message + if myoptions['pretend'] and not myoptions['quiet']: + einfo("Here are "+files_type+" that would be deleted:", \ + myoptions['nocolor']) + # verbose non-pretend message + elif not myoptions['quiet']: + einfo("Cleaning "+files_type+"...",myoptions['nocolor']) + # do the cleanup, and get size of deleted files + clean_size = doCleanup(clean_dict,action,myoptions) + # vocabulary for final message + if myoptions['pretend']: verb = "would be" + else: verb = "has been" + # display freed space + if not myoptions['quiet']: + einfo("Total space that "+verb+" freed in " \ + + action + " directory: " \ + + red(prettySize(clean_size)), \ + myoptions['nocolor']) + # nothing was found, return + elif not myoptions['quiet']: + einfo("Your "+action+" directory was already clean.", \ + myoptions['nocolor']) + + +############################################################################### +# main: parse command line and execute all actions +def main(): + # set default options + myoptions = {} + myoptions['nocolor'] = port_settings["NOCOLOR"] in ('yes','true') \ + and sys.stdout.isatty() + if myoptions['nocolor']: nocolor() + # parse command line options and actions + try: myaction = parseArgs(myoptions) + # filter exception to know what message to display + except ParseArgsException, e: + if e.value == 'help': + printUsage(help='all') + sys.exit(0) + elif e.value[:5] == 'help-': + printUsage(help=e.value[5:]) + sys.exit(0) + elif e.value == 'version': + printVersion() + sys.exit(0) + else: + printUsage(e.value) + sys.exit(2) + # parse the exclusion file + if not 'exclude-file' in myoptions: + my_exclude_file = "/etc/%s/%s.exclude" % (__productname__ , myaction) + if os.path.isfile(my_exclude_file): + myoptions['exclude-file'] = my_exclude_file + if 'exclude-file' in myoptions: + try: exclude_dict = parseExcludeFile(myoptions['exclude-file']) + except ParseExcludeFileException, e: + eerror(e, myoptions['nocolor']) + eerror("Invalid exclusion file: %s" % myoptions['exclude-file'], \ + myoptions['nocolor']) + eerror("See format of this file in `man %s`" % __productname__, \ + myoptions['nocolor']) + sys.exit(1) + else: exclude_dict={} + # security check for non-pretend mode + if not myoptions['pretend'] and portage.secpass == 0: + eerror("Permission denied: you must be root or belong to the portage group.", \ + myoptions['nocolor']) + sys.exit(1) + # execute action + doAction(myaction, myoptions, exclude_dict=exclude_dict) + + +############################################################################### +# actually call main() if launched as a script +if __name__ == "__main__": + try: main() + except KeyboardInterrupt: + print "Aborted." + sys.exit(130) + sys.exit(0) + diff --git a/bin/epkginfo b/bin/epkginfo new file mode 100755 index 0000000..313170a --- /dev/null +++ b/bin/epkginfo @@ -0,0 +1,207 @@ +#!/usr/bin/python +############################################################################## +# $Header: $ +############################################################################## +# Distributed under the terms of the GNU General Public License, v2 or later +# Author: Ned Ludd <solar@gentoo.org> (glue all the parts together) +# Author: Eldad Zack <eldad@gentoo.org> (earch) +# Author : Eric Olinger <EvvL AT RustedHalo DOT net> (metadata) + +# Gentoo metadata xml and arch keyword checking tool. + +import os +import sys +import re +from stat import * +from xml.sax import saxutils, make_parser, handler +from xml.sax.handler import feature_namespaces + +import portage +from portage.output import * + +version = open('/etc/gentoolkit-version').read().strip() + +def getvar(pkg, var): + file = open(pkg + ".ebuild") + for line in file.readlines(): + line = line.rstrip() + if re.match("^"+var+"=",line): + vars = re.split("\"",line)[1] + file.close + return re.split(" ",vars) + file.close() + +def earch(workdir): + """Prints arch keywords for a given dir""" + portdir = portage.settings["PORTDIR"] + #workdir = "." + os.chdir(workdir) + + archdict = {} + ebuildlist = [] + for file in os.listdir(workdir): + if re.search("\.ebuild$",file): + ebuildlist.append(re.split("\.ebuild$",file)[0]) + + ebuildlist.sort(lambda x,y: portage.pkgcmp(portage.pkgsplit(x),portage.pkgsplit(y))) + + for pkg in ebuildlist: + keywords = getvar(pkg, "KEYWORDS") + for arch in keywords: + if arch == "": + arch = None + archdict[arch] = pkg + + archlist = archdict.keys(); + archlist.sort() + + for pkg in ebuildlist: + print darkgreen("Keywords: ") + pkg + ":", + for value in archlist: + if (value and archdict[value] == pkg): + if value[0] == "-": + print red(value), + elif "~" == value[0]: + print blue(value), + else: + print green(value), + print "" + + +class Metadata_XML(handler.ContentHandler): + _inside_herd="No" + _inside_maintainer="No" + _inside_email="No" + _inside_longdescription="No" + + _herd = "" + _maintainers = [] + _longdescription = "" + + def startElement(self, tag, attr): + if tag == "herd": + self._inside_herd="Yes" + if tag == "longdescription": + self._inside_longdescription="Yes" + if tag == "maintainer": + self._inside_maintainer="Yes" + if tag == "email": + self._inside_email="Yes" + + def endElement(self, tag): + if tag == "herd": + self._inside_herd="No" + if tag == "longdescription": + self._inside_longdescription="No" + if tag == "maintainer": + self._inside_maintainer="No" + if tag == "email": + self._inside_email="No" + + def characters(self, contents): + if self._inside_herd == "Yes": + self._herd = contents + + if self._inside_longdescription == "Yes": + self._longdescription = contents + + if self._inside_maintainer=="Yes" and self._inside_email=="Yes": + self._maintainers.append(contents) + + +def check_metadata(full_package): + """Checks that the primary maintainer is still an active dev and list the hed the package belongs to""" + metadata_file=portage.settings["PORTDIR"] + "/" + portage.pkgsplit(full_package)[0] + "/metadata.xml" + if not os.path.exists(metadata_file): + print darkgreen("Maintainer: ") + red("Error (Missing metadata.xml)") + return 1 + + parser = make_parser() + handler = Metadata_XML() + handler._maintainers = [] + parser.setContentHandler(handler) + parser.parse( metadata_file ) + + if len(handler._herd) < 1: + print darkgreen("Herd: ") + red("Error (No Herd)") + return 1 + else: + print darkgreen("Herd: ") + handler._herd + + if len(handler._maintainers) < 1: + print darkgreen("Maintainer: ") + handler._herd + else: + print darkgreen("Maintainer: ") + ", ".join(handler._maintainers) + + if len(handler._longdescription) > 1: + print darkgreen("Description: ") + handler._longdescription + print darkgreen("Location: ") + os.path.normpath(portage.settings["PORTDIR"] + "/" + portage.pkgsplit(full_package)[0]) + + +def usage(code): + """Prints the uage information for this script""" + print green("epkginfo"), "(%s)" % version + print + print "Usage: epkginfo [package-cat/]package" + sys.exit(code) + + +# default color setup +if ( not sys.stdout.isatty() ) or ( portage.settings["NOCOLOR"] in ["yes","true"] ): + nocolor() + +def fc(x,y): + return cmp(y[0], x[0]) + + +def grab_changelog_devs(catpkg): + try: + os.chdir(portage.settings["PORTDIR"] + "/" + catpkg) + foo="" + r=re.compile("<[^@]+@gentoo.org>", re.I) + s="\n".join(portage.grabfile("ChangeLog")) + d={} + for x in r.findall(s): + if x not in d: + d[x] = 0 + d[x] += 1 + + l=[(d[x], x) for x in d.keys()] + #l.sort(lambda x,y: cmp(y[0], x[0])) + l.sort(fc) + for x in l: + p = str(x[0]) +" "+ x[1].lstrip("<").rstrip(">") + foo += p[:p.find("@")]+", " + return foo + except: + raise + +def main (): + if len( sys.argv ) < 2: + usage(1) + + for pkg in sys.argv[1:]: + + if sys.argv[1:][:1] == "-": + print "NOT WORKING?=="+sys.argv[1:] + continue + + try: + package_list = portage.portdb.xmatch("match-all", pkg) + if package_list: + + catpkg = portage.pkgsplit(package_list[0])[0] + + print darkgreen("Package: ") + catpkg + check_metadata(package_list[0]) + earch(portage.settings["PORTDIR"] + "/" + catpkg) + #print darkgreen("ChangeLog: ") + grab_changelog_devs(catpkg) + print "" + else: + print "!!! No package '%s'" % pkg + except: + print red("Error: "+pkg+"\n") + + +if __name__ == '__main__': + main() diff --git a/bin/equery b/bin/equery new file mode 100644 index 0000000..bac8a3a --- /dev/null +++ b/bin/equery @@ -0,0 +1,32 @@ +#!/usr/bin/python +# +# Copyright 2002-2009 Gentoo Technologies, Inc. +# Distributed under the terms of the GNU General Public License v2 or later + +"""equery is a flexible utility for Gentoo linux which can display various +information about packages, such as the files they own, their USE flags, +the MD5 sum of each file owned by a given package, and many other things. +""" + +import sys +# This block ensures that ^C interrupts are handled quietly. +try: + import signal + + def exithandler(signum,frame): + signal.signal(signal.SIGINT, signal.SIG_IGN) + signal.signal(signal.SIGTERM, signal.SIG_IGN) + print + sys.exit(1) + + signal.signal(signal.SIGINT, exithandler) + signal.signal(signal.SIGTERM, exithandler) + signal.signal(signal.SIGPIPE, signal.SIG_DFL) + +except KeyboardInterrupt: + print + sys.exit(1) + +from gentoolkit import equery + +equery.main() diff --git a/bin/eread b/bin/eread new file mode 100755 index 0000000..c6d4de1 --- /dev/null +++ b/bin/eread @@ -0,0 +1,94 @@ +#!/bin/bash + +# This is a script to read portage log items from einfo, ewarn etc, new in the +# portage-2.1 series. +# +# Author: Donnie Berkholz <spyderous@gentoo.org> +# Updated by: Uwe Klosa <uwe.klosa@gmail.com> + +# set decent PATH for bug 172969 + +PATH=/usr/bin:/bin:${PATH} + +# Set ELOGDIR +PORT_LOGDIR="$(portageq envvar PORT_LOGDIR)" +[ "$PORT_LOGDIR" = "" ] && PORT_LOGDIR="/var/log/portage" +ELOGDIR="$PORT_LOGDIR/elog" + +# Verify that ELOGDIR exists +if [ ! -d "$ELOGDIR" ]; then + echo "ELOG directory: $ELOGDIR does not exist!" + exit 1 +fi + +# Use the pager from the users environment +[ -z "$PAGER" ] && PAGER="less" + +# Set up select prompt +PS3="Choice? " + +select_loop() { + ANY_FILES=$(find . -type f) + ANY_FILES=$(echo ${ANY_FILES} | sed -e "s:\./::g") + + if [[ -z ${ANY_FILES} ]]; then + echo "No log items to read" + break + fi + + echo + echo "This is a list of portage log items. Choose a number to view that file or type q to quit." + echo + + # Pick which file to read + select FILE in ${ANY_FILES}; do + case ${REPLY} in + q) + echo "Quitting" + QUIT="yes" + break + ;; + *) + if [ -f "$FILE" ]; then + ${PAGER} ${FILE} + read -p "Delete file? [y/N] " DELETE + case ${DELETE} in + q) + echo "Quitting" + QUIT="yes" + break + ;; + y|Y) + rm -f ${FILE} + SUCCESS=$? + if [[ ${SUCCESS} = 0 ]]; then + echo "Deleted ${FILE}" + else + echo "Unable to delete ${FILE}" + fi + ;; + # Empty string defaults to N (save file) + n|N|"") + echo "Saving ${FILE}" + ;; + *) + echo "Invalid response. Saving ${FILE}" + ;; + esac + else + echo + echo "Invalid response." + fi + ;; + esac + break + done +} + +pushd ${ELOGDIR} > /dev/null + +until [[ -n ${QUIT} ]]; do + select_loop +done + +popd > /dev/null diff --git a/bin/euse b/bin/euse new file mode 100755 index 0000000..b49b120 --- /dev/null +++ b/bin/euse @@ -0,0 +1,547 @@ +#!/bin/bash + +# $Header$ + +# bash replacement for the original euse by Arun Bhanu +# Author: Marius Mauch <genone@gentoo.org> +# Licensed under the GPL v2 + +PROGRAM_NAME=euse +PROGRAM_VERSION=$(cat /etc/gentoolkit-version) + +MAKE_CONF_PATH=/etc/make.conf +MAKE_GLOBALS_PATH=/etc/make.globals +MAKE_PROFILE_PATH=/etc/make.profile +MAKE_CONF_BACKUP_PATH=/etc/make.conf.euse_backup + +[ -z "${MODE}" ] && MODE="showhelp" # available operation modes: showhelp, showversion, showdesc, showflags, modify + +parse_arguments() { + if [ -z "${1}" ]; then + return + fi + while [ -n "${1}" ]; do + case "${1}" in + -h | --help) MODE="showhelp";; + -V | -v | --version) MODE="showversion";; + -i | --info) MODE="showdesc";; + -I | --info-installed) MODE="showinstdesc";; + -l | --local) SCOPE="local";; + -g | --global) SCOPE="global";; + -a | --active) MODE="showflags";; + -E | --enable) MODE="modify"; ACTION="add";; + -D | --disable) MODE="modify"; ACTION="remove";; + -P | --prune) MODE="modify"; ACTION="prune";; + -*) + echo "ERROR: unknown option ${1} specified." + echo + MODE="showhelp" + ;; + "%active") + get_useflags + ARGUMENTS="${ARGUMENTS} ${ACTIVE_FLAGS[9]}" + ;; + *) + ARGUMENTS="${ARGUMENTS} ${1}" + ;; + esac + shift + done +} + +error() { + echo "ERROR: ${1}" + set +f + exit 1 +} + +get_real_path() { + set -P + cd "$1" + pwd + cd "$OLDPWD" + set +P +} + +check_sanity() { + # file permission tests + local descdir + local make_defaults + + descdir="$(get_portdir)/profiles" + + [ ! -r "${MAKE_CONF_PATH}" ] && error "${MAKE_CONF_PATH} is not readable" + [ ! -r "${MAKE_GLOBALS_PATH}" ] && error "${MAKE_GLOBALS_PATH} is not readable" + [ ! -h "${MAKE_PROFILE_PATH}" ] && error "${MAKE_PROFILE_PATH} is not a symlink" + [ -z "$(get_portdir)" ] && error "\$PORTDIR couldn't be determined" + [ ! -d "${descdir}" ] && error "${descdir} does not exist or is not a directory" + [ ! -r "${descdir}/use.desc" ] && error "${descdir}/use.desc is not readable" + [ ! -r "${descdir}/use.local.desc" ] && error "${descdir}/use.local.desc is not readable" + for make_defaults in $(get_all_make_defaults); do + [ ! -r "$make_defaults" ] && error "$_make_defaults is not readable" + done +# [ ! -r "$(get_make_defaults)" ] && error "$(get_make_defaults) is not readable" + [ "${MODE}" == "modify" -a ! -w "${MAKE_CONF_PATH}" ] && error ""${MAKE_CONF_PATH}" is not writable" +} + +showhelp() { +cat << HELP +${PROGRAM_NAME} (${PROGRAM_VERSION}) + +Syntax: ${PROGRAM_NAME} <option> [suboptions] [useflaglist] + +Options: -h, --help - show this message + -V, --version - show version information + -i, --info - show descriptions for the given useflags + -I, --info-installed - show descriptions for the given useflags and + their current impact on the installed system + -g, --global - show only global use flags (suboption) + -l, --local - show only local use flags (suboption) + -a, --active - show currently active useflags and their origin + -E, --enable - enable the given useflags + -D, --disable - disable the given useflags + -P, --prune - remove all references to the given flags from + make.conf to revert to default settings + +Notes: ${PROGRAM_NAME} currently only works for global flags defined + in make.globals, make.defaults or make.conf, it doesn't handle + use.defaults, use.mask or package.use yet (see portage(5) for details on + these files). It also might have issues with cascaded profiles. + If multiple options are specified only the last one will be used. +HELP +} + +showversion() { +cat << VER +${PROGRAM_NAME} (${PROGRAM_VERSION}) +Written by Marius Mauch + +Copyright (C) 2004-2009 Gentoo Foundation, Inc. +This is free software; see the source for copying conditions. +VER +} + +# remove duplicate flags from the given list in both positive and negative forms +# (but unlike portage always keep the last value even if it's negative) +# Otherwise the status flags could be incorrect if a flag appers multiple times in +# one location (like make.conf). +# Using python here as bash sucks for list handling. +# NOTE: bash isn't actually that bad at handling lists -- sh is. This may be +# worth another look to avoid calling python unnecessariy. Or we could +# just write the whole thing in python. ;) +reduce_incrementals() { + echo $@ | python -c "import sys +r=[] +for x in sys.stdin.read().split(): + if x[0] == '-' and x[1:] in r: + r.remove(x[1:]) + r.append(x) + elif x[0] != '-' and '-'+x in r: + r.remove('-'+x) + r.append(x) + elif x == '-*': + r = [] + r.append(x) + elif x not in r: + r.append(x) +print ' '.join(r)" +} + +# the following function creates a bash array ACTIVE_FLAGS that contains the +# global use flags, indexed by origin: 0: environment, 1: make.conf, +# 2: make.defaults, 3: make.globals +get_useflags() { + # only calculate once as calling emerge is painfully slow + [ -n "${USE_FLAGS_CALCULATED}" ] && return + + # backup portdir so get_portdir() doesn't give false results later + portdir_backup="${PORTDIR}" + + ACTIVE_FLAGS[0]="$(reduce_incrementals ${USE})" + USE="" + source "${MAKE_CONF_PATH}" + ACTIVE_FLAGS[1]="$(reduce_incrementals ${USE})" + USE="" + for x in $(get_all_make_defaults); do + source "${x}" + ACTIVE_FLAGS[2]="$(reduce_incrementals ${ACTIVE_FLAGS[2]} ${USE})" + done + USE="" + source "${MAKE_GLOBALS_PATH}" + ACTIVE_FLAGS[3]="$(reduce_incrementals ${USE})" + + # restore saved env variables + USE="${ACTIVE_FLAGS[0]}" + PORTDIR="${portdir_backup}" + + # get the currently active USE flags as seen by portage, this has to be after + # restoring USE or portage won't see the original environment + ACTIVE_FLAGS[9]="$(emerge --info | grep 'USE=' | cut -b 5- | sed -e 's:"::g')" #' + USE_FLAGS_CALCULATED=1 +} + +# get the list of all known USE flags by reading use.desc and/or use.local.desc +# (depending on the value of $SCOPE) +get_useflaglist() { + local descdir + + descdir="$(get_portdir)/profiles" + + if [ -z "${SCOPE}" -o "${SCOPE}" == "global" ]; then + egrep "^[^# ]+ +-" "${descdir}/use.desc" | cut -d\ -f 1 + fi + if [ -z "${SCOPE}" -o "${SCOPE}" == "local" ]; then + egrep "^[^# :]+:[^ ]+ +-" "${descdir}/use.local.desc" | cut -d: -f 2 | cut -d\ -f 1 + fi +} + +# get all make.defaults by traversing the cascaded profile directories +get_all_make_defaults() { + local curdir + local parent + local rvalue + + curdir="${1:-$(get_real_path ${MAKE_PROFILE_PATH})}" + + [ -f "${curdir}/make.defaults" ] && rvalue="${curdir}/make.defaults ${rvalue}" + if [ -f "${curdir}/parent" ]; then + for parent in $(egrep -v '(^#|^ *$)' ${curdir}/parent); do + pdir="$(get_real_path ${curdir}/${parent})" + rvalue="$(get_all_make_defaults ${pdir}) ${rvalue}" + done + fi + + echo "${rvalue}" +} + +# get the path to make.defaults by traversing the cascaded profile directories +get_make_defaults() { + local curdir + local parent + + curdir="${1:-$(get_real_path ${MAKE_PROFILE_PATH})}" + + if [ ! -f "${curdir}/make.defaults" -a -f "${curdir}/parent" ]; then + for parent in $(egrep -v '(^#|^ *$)' ${curdir}/parent); do + if [ -f "$(get_make_defaults ${curdir}/${parent})" ]; then + curdir="${curdir}/${parent}" + break + fi + done + fi + + echo "${curdir}/make.defaults" +} + +# little helper function to get the status of a given flag in one of the +# ACTIVE_FLAGS elements. Arguments are 1: flag to test, 2: index of ACTIVE_FLAGS, +# 3: echo value for positive (and as lowercase for negative) test result, +# 4 (optional): echo value for "missing" test result, defaults to blank +get_flagstatus_helper() { + if echo " ${ACTIVE_FLAGS[${2}]} " | grep " ${1} " > /dev/null; then + echo -n "${3}" + elif echo " ${ACTIVE_FLAGS[${2}]} " | grep " -${1} " > /dev/null; then + echo -n "$(echo ${3} | tr [[:upper:]] [[:lower:]])" + else + echo -n "${4:- }" + fi +} + +# prints a status string for the given flag, each column indicating the presence +# for portage, in the environment, in make.conf, in make.defaults and in make.globals. +# full positive value would be "[+ECDG]", full negative value would be [-ecdg], +# full missing value would be "[- ]" (portage only sees present or not present) +get_flagstatus() { + get_useflags + + echo -n '[' + get_flagstatus_helper "${1}" 9 "+" "-" + get_flagstatus_helper "${1}" 0 "E" + get_flagstatus_helper "${1}" 1 "C" + get_flagstatus_helper "${1}" 2 "D" + get_flagstatus_helper "${1}" 3 "G" + echo -n '] ' +} + +# faster replacement to `portageq portdir` +get_portdir() { + if [ -z "${PORTDIR}" ]; then + use_backup="${USE}" + source "${MAKE_GLOBALS_PATH}" + for x in $(get_all_make_defaults); do + source "${x}" + done + source "${MAKE_CONF_PATH}" + USE="${use_backup}" + fi + echo "${PORTDIR}" +} + +# This function takes a list of use flags and shows the status and +# the description for each one, honoring $SCOPE +showdesc() { + local descdir + local current_desc + local found_one + local args + + args="${*:-*}" + + if [ -z "${SCOPE}" ]; then + SCOPE="global" showdesc ${args} + echo + SCOPE="local" showdesc ${args} + return + fi + + descdir="$(get_portdir)/profiles" + + [ "${SCOPE}" == "global" ] && echo "global use flags (searching: ${args})" + [ "${SCOPE}" == "local" ] && echo "local use flags (searching: ${args})" + echo "************************************************************" + + if [ "${args}" == "*" ]; then + args="$(get_useflaglist | sort -u)" + fi + + set ${args} + + foundone=0 + while [ -n "${1}" ]; do + if [ "${SCOPE}" == "global" ]; then + if grep "^${1} *-" "${descdir}/use.desc" > /dev/null; then + get_flagstatus "${1}" + foundone=1 + fi + grep "^${1} *-" "${descdir}/use.desc" + fi + # local flags are a bit more complicated as there can be multiple + # entries per flag and we can't pipe into printf + if [ "${SCOPE}" == "local" ]; then + if grep ":${1} *-" "${descdir}/use.local.desc" > /dev/null; then + foundone=1 + fi + grep ":${1} *-" "${descdir}/use.local.desc" \ + | sed -e "s/^\([^:]\+\):\(${1}\) *- *\(.\+\)/\1|\2|\3/g" \ + | while read line; do + pkg="$(echo $line | cut -d\| -f 1)" + flag="$(echo $line | cut -d\| -f 2)" + desc="$(echo $line | cut -d\| -f 3)" + get_flagstatus "${flag}" + printf "%s (%s):\n%s\n\n" "${flag}" "${pkg}" "${desc}" + done + fi + shift + done + + if [ ${foundone} == 0 ]; then + echo "no matching entries found" + fi +} + +# Works like showdesc() but displays only descriptions of which the appropriate +# ebuild is installed and prints the name of those packages. +showinstdesc() { + local descdir + local current_desc + local args + local -i foundone=0 + local IFS + + args=("${@:-*}") + + case "${SCOPE}" in + "global") echo "global use flags (searching: ${args})";; + "local") echo "local use flags (searching: ${args})";; + *) SCOPE="global" showinstdesc "${args[@]}" + echo + SCOPE="local" showinstdesc "${args[@]}" + return;; + esac + + descdir="$(get_portdir)/profiles" + echo "************************************************************" + + if [ "${args}" = "*" ]; then + args="$(get_useflaglist | sort -u)" + fi + + set "${args[@]}" + + while [ -n "${1}" ]; do + case "${SCOPE}" in + "global") + if desc=$(grep "^${1} *-" "${descdir}/use.desc"); then + get_flagstatus "${1}" + echo "$desc" + # get list of installed packages matching this USE flag. + IFS=$'\n' + packages=($(equery -q -C hasuse -i "${1}" | awk '{ print $(NF-1) }')) + foundone+=${#packages[@]} + printf "\nInstalled packages matching this USE flag: " + if [ ${foundone} -gt 0 ]; then + echo $'\n'"${packages[*]}" + else + echo "none" + fi + fi + ;; + "local") + # local flags are a bit more complicated as there can be multiple + # entries per flag and we can't pipe into printf + IFS=': ' # Use a space instead of a dash because dashes occur in cat/pkg + while read pkg flag desc; do + # print name only if package is installed + # NOTE: If we implement bug #114086 's enhancement we can just use the + # exit status of equery instead of a subshell and pipe to wc -l + if [ $(equery -q -C list -i -e "${pkg}" | wc -l) -gt 0 ]; then + foundone=1 + get_flagstatus "${flag}" + printf "%s (%s):\n%s\n\n" "${flag}" "${pkg}" "${desc#- }" + fi + done < <(grep ":${1} *-" "${descdir}/use.local.desc") + ;; + esac + shift + done + + if [ ${foundone} -lt 1 ]; then + echo "no matching entries found" + fi +} + +# show a list of all currently active flags and where they are activated +showflags() { + local args + + get_useflags + + args="${*:-*}" + + if [ "${args}" == "*" ]; then + args="$(get_useflaglist | sort -u)" + fi + + set ${args} + + while [ -n "${1}" ]; do + if echo " ${ACTIVE_FLAGS[9]} " | grep " ${1} " > /dev/null; then + printf "%-20s" ${1} + get_flagstatus ${1} + echo + fi + shift + done +} + +# two small helpers to add or remove a flag from a USE string +add_flag() { + NEW_MAKE_CONF_USE="${NEW_MAKE_CONF_USE} ${1}" +} + +remove_flag() { + NEW_MAKE_CONF_USE="${NEW_MAKE_CONF_USE// ${1} / }" +} + +# USE flag modification function. Mainly a loop with calls to add_flag and +# remove_flag to create a new USE string which is then inserted into make.conf. +modify() { + if [ -z "${*}" ]; then + if [ "${ACTION}" != "prune" ]; then + echo "WARNING: no USE flags listed for modification, do you really" + echo " want to ${ACTION} *all* known USE flags?" + echo " If you don't please press Ctrl-C NOW!!!" + sleep 5 + set $(get_useflaglist | sort -u) + fi + fi + + get_useflags + + NEW_MAKE_CONF_USE=" ${ACTIVE_FLAGS[1]} " + + while [ -n "${1}" ]; do + if [ "${ACTION}" == "add" ]; then + if echo " ${NEW_MAKE_CONF_USE} " | grep " ${1} " > /dev/null; then + shift + elif echo " ${NEW_MAKE_CONF_USE} " | grep " -${1} " > /dev/null; then + remove_flag "-${1}" + else + add_flag "${1}" + shift + fi + elif [ "${ACTION}" == "remove" ]; then + if echo " ${NEW_MAKE_CONF_USE} " | grep " -${1} " > /dev/null; then + shift + elif echo " ${NEW_MAKE_CONF_USE} " | grep " ${1} " > /dev/null; then + remove_flag "${1}" + else + add_flag "-${1}" + shift + fi + elif [ "${ACTION}" == "prune" ]; then + if echo " ${NEW_MAKE_CONF_USE} " | grep " ${1} " > /dev/null; then + remove_flag "${1}" + elif echo " ${NEW_MAKE_CONF_USE} " | grep " -${1} " > /dev/null; then + remove_flag "-${1}" + fi + shift + fi + done + + #echo "old flags:" + #echo ${ACTIVE_FLAGS[1]} + #echo + #echo "new flags:" + #echo ${NEW_MAKE_CONF_USE} + + # a little loop to add linebreaks so we don't end with one ultra-long line + NEW_MAKE_CONF_USE_2="" + for x in ${NEW_MAKE_CONF_USE}; do + if [ $(((${#NEW_MAKE_CONF_USE_2}%70)+${#x}+2)) -gt 70 ]; then + NEW_MAKE_CONF_USE_2="${NEW_MAKE_CONF_USE_2}\\ \\n $x " + else + NEW_MAKE_CONF_USE_2="${NEW_MAKE_CONF_USE_2}${x} " + fi + done + + # make a backup just in case the user doesn't like the new make.conf + cp -p "${MAKE_CONF_PATH}" "${MAKE_CONF_BACKUP_PATH}" + + # as sed doesn't really work with multi-line patterns we have to replace USE + # on our own here. Basically just skip everything between USE=" and the + # closing ", printing our new USE line there instead. + inuse=0 + had_use=0 + x=0 + (while [ "$x" -eq "0" ]; do + read -r line + x="$?" + [ "${line:0:4}" == "USE=" ] && inuse=1 + [ "${inuse}" == "0" ] && echo -E "${line}" + if [ "${inuse}" == "1" ] && echo "${line}" | egrep '" *(#.*)?$' > /dev/null; then + echo -n 'USE="' + echo -ne "${NEW_MAKE_CONF_USE_2%% }" + echo '"' + inuse=0 + had_use=1 + fi + done + if [ ${had_use} -eq 0 ]; then + echo -n 'USE="' + echo -ne "${NEW_MAKE_CONF_USE_2%% }" + echo '"' + fi ) < "${MAKE_CONF_BACKUP_PATH}" | sed -e 's:\\ $:\\:' > "${MAKE_CONF_PATH}" + + echo "${MAKE_CONF_PATH} was modified, a backup copy has been placed at ${MAKE_CONF_BACKUP_PATH}" +} + +##### main program comes now ##### + +# disable globbing as it fucks up with args=* +set -f +parse_arguments "$@" +check_sanity + +eval ${MODE} ${ARGUMENTS} +set +f diff --git a/bin/glsa-check b/bin/glsa-check new file mode 100644 index 0000000..1fb577d --- /dev/null +++ b/bin/glsa-check @@ -0,0 +1,366 @@ +#!/usr/bin/python + +# $Header: $ +# This program is licensed under the GPL, version 2 + +import os +import sys +try: + import portage +except ImportError: + sys.path.insert(0, "/usr/lib/portage/pym") + import portage + +try: + from portage.output import * +except ImportError: + from output import * + +from getopt import getopt,GetoptError + +__program__ = "glsa-check" +__author__ = "Marius Mauch <genone@gentoo.org>" +__version__ = open("/etc/gentoolkit-version").read().strip() + +optionmap = [ +["-l", "--list", "list all unapplied GLSA"], +["-d", "--dump", "--print", "show all information about the given GLSA"], +["-t", "--test", "test if this system is affected by the given GLSA"], +["-p", "--pretend", "show the necessary commands to apply this GLSA"], +["-f", "--fix", "try to auto-apply this GLSA (experimental)"], +["-i", "--inject", "inject the given GLSA into the checkfile"], +["-n", "--nocolor", "disable colors (option)"], +["-e", "--emergelike", "do not use a least-change algorithm (option)"], +["-h", "--help", "show this help message"], +["-V", "--version", "some information about this tool"], +["-v", "--verbose", "print more information (option)"], +["-c", "--cve", "show CAN ids in listing mode (option)"], +["-m", "--mail", "send a mail with the given GLSAs to the administrator"] +] + +# print a warning as this is beta code (but proven by now, so no more warning) +#sys.stderr.write("WARNING: This tool is completely new and not very tested, so it should not be\n") +#sys.stderr.write("used on production systems. It's mainly a test tool for the new GLSA release\n") +#sys.stderr.write("and distribution system, it's functionality will later be merged into emerge\n") +#sys.stderr.write("and equery.\n") +#sys.stderr.write("Please read http://www.gentoo.org/proj/en/portage/glsa-integration.xml\n") +#sys.stderr.write("before using this tool AND before reporting a bug.\n\n") + +# option parsing +args = [] +params = [] +try: + args, params = getopt(sys.argv[1:], "".join([o[0][1] for o in optionmap]), \ + [x[2:] for x in reduce(lambda x,y: x+y, [z[1:-1] for z in optionmap])]) +# ["dump", "print", "list", "pretend", "fix", "inject", "help", "verbose", "version", "test", "nocolor", "cve", "mail"]) + args = [a for a,b in args] + + for option in ["--nocolor", "-n"]: + if option in args: + nocolor() + args.remove(option) + + verbose = False + for option in ["--verbose", "-v"]: + if option in args: + verbose = True + args.remove(option) + + list_cve = False + for option in ["--cve", "-c"]: + if option in args: + list_cve = True + args.remove(option) + + least_change = True + for option in ["--emergelike", "-e"]: + if option in args: + least_change = False + args.remove(option) + + # sanity checking + if len(args) <= 0: + sys.stderr.write("no option given: what should I do ?\n") + mode="help" + elif len(args) > 1: + sys.stderr.write("please use only one command per call\n") + mode = "help" + else: + # in what mode are we ? + args = args[0] + for m in optionmap: + if args in [o for o in m[:-1]]: + mode = m[1][2:] + +except GetoptError, e: + sys.stderr.write("unknown option given: ") + sys.stderr.write(str(e)+"\n") + mode = "help" + +# we need a set of glsa for most operation modes +if len(params) <= 0 and mode in ["fix", "test", "pretend", "dump", "inject", "mail"]: + sys.stderr.write("\nno GLSA given, so we'll do nothing for now. \n") + sys.stderr.write("If you want to run on all GLSA please tell me so \n") + sys.stderr.write("(specify \"all\" as parameter)\n\n") + mode = "help" +elif len(params) <= 0 and mode == "list": + params.append("new") + +# show help message +if mode == "help": + sys.stderr.write("\nSyntax: glsa-check <option> [glsa-list]\n\n") + for m in optionmap: + sys.stderr.write(m[0] + "\t" + m[1] + " \t: " + m[-1] + "\n") + for o in m[2:-1]: + sys.stderr.write("\t" + o + "\n") + sys.stderr.write("\nglsa-list can contain an arbitrary number of GLSA ids, \n") + sys.stderr.write("filenames containing GLSAs or the special identifiers \n") + sys.stderr.write("'all', 'new' and 'affected'\n") + sys.exit(1) + +# we need root priviledges for write access +if mode in ["fix", "inject"] and os.geteuid() != 0: + sys.stderr.write("\nThis tool needs root access to "+mode+" this GLSA\n\n") + sys.exit(2) + +# show version and copyright information +if mode == "version": + sys.stderr.write("%(program)s (%(version)s)\n" % { + "program": __program__, + "version": __version__ + }) + sys.stderr.write("Author: %s\n" % __author__) + sys.stderr.write("This program is licensed under the GPL, version 2\n") + sys.exit(0) + +# delay this for speed increase +from gentoolkit.glsa import * + +glsaconfig = checkconfig(portage.config(clone=portage.settings)) + +vardb = portage.db["/"]["vartree"].dbapi +portdb = portage.db["/"]["porttree"].dbapi + +# Check that we really have a glsa dir to work on +if not (os.path.exists(glsaconfig["GLSA_DIR"]) and os.path.isdir(glsaconfig["GLSA_DIR"])): + sys.stderr.write(red("ERROR")+": GLSA_DIR %s doesn't exist. Please fix this.\n" % glsaconfig["GLSA_DIR"]) + sys.exit(1) + +# build glsa lists +completelist = get_glsa_list(glsaconfig["GLSA_DIR"], glsaconfig) + +if os.access(glsaconfig["CHECKFILE"], os.R_OK): + checklist = [line.strip() for line in open(glsaconfig["CHECKFILE"], "r").readlines()] +else: + checklist = [] +todolist = [e for e in completelist if e not in checklist] + +glsalist = [] +if "new" in params: + glsalist = todolist + params.remove("new") + +if "all" in params: + glsalist = completelist + params.remove("all") +if "affected" in params: + # replaced completelist with todolist on request of wschlich + for x in todolist: + try: + myglsa = Glsa(x, glsaconfig) + except (GlsaTypeException, GlsaFormatException), e: + if verbose: + sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (x, e))) + continue + if myglsa.isVulnerable(): + glsalist.append(x) + params.remove("affected") + +# remove invalid parameters +for p in params[:]: + if not (p in completelist or os.path.exists(p)): + sys.stderr.write(("(removing %s from parameter list as it isn't a valid GLSA specification)\n" % p)) + params.remove(p) + +glsalist.extend([g for g in params if g not in glsalist]) + +def summarylist(myglsalist, fd1=sys.stdout, fd2=sys.stderr): + fd2.write(white("[A]")+" means this GLSA was already applied,\n") + fd2.write(green("[U]")+" means the system is not affected and\n") + fd2.write(red("[N]")+" indicates that the system might be affected.\n\n") + + for myid in myglsalist: + try: + myglsa = Glsa(myid, glsaconfig) + except (GlsaTypeException, GlsaFormatException), e: + if verbose: + fd2.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e))) + continue + if myglsa.isApplied(): + status = "[A]" + color = white + elif myglsa.isVulnerable(): + status = "[N]" + color = red + else: + status = "[U]" + color = green + + if verbose: + access = ("[%-8s] " % myglsa.access) + else: + access="" + + fd1.write(color(myglsa.nr) + " " + color(status) + " " + color(access) + myglsa.title + " (") + if not verbose: + for pkg in myglsa.packages.keys()[:3]: + fd1.write(" " + pkg + " ") + if len(myglsa.packages) > 3: + fd1.write("... ") + else: + for pkg in myglsa.packages.keys(): + mylist = vardb.match(portage.dep_getkey(pkg)) + if len(mylist) > 0: + pkg = color(" ".join(mylist)) + fd1.write(" " + pkg + " ") + + fd1.write(")") + if list_cve: + fd1.write(" "+(",".join([r[:13] for r in myglsa.references if r[:4] in ["CAN-", "CVE-"]]))) + fd1.write("\n") + return 0 + +if mode == "list": + sys.exit(summarylist(glsalist)) + +# dump, fix, inject and fix are nearly the same code, only the glsa method call differs +if mode in ["dump", "fix", "inject", "pretend"]: + for myid in glsalist: + try: + myglsa = Glsa(myid, glsaconfig) + except (GlsaTypeException, GlsaFormatException), e: + if verbose: + sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e))) + continue + if mode == "dump": + myglsa.dump() + elif mode == "fix": + sys.stdout.write("fixing "+myid+"\n") + mergelist = myglsa.getMergeList(least_change=least_change) + for pkg in mergelist: + sys.stdout.write(">>> merging "+pkg+"\n") + # using emerge for the actual merging as it contains the dependency + # code and we want to be consistent in behaviour. Also this functionality + # will be integrated in emerge later, so it shouldn't hurt much. + emergecmd = "emerge --oneshot " + glsaconfig["EMERGE_OPTS"] + " =" + pkg + if verbose: + sys.stderr.write(emergecmd+"\n") + exitcode = os.system(emergecmd) + # system() returns the exitcode in the high byte of a 16bit integer + if exitcode >= 1<<8: + exitcode >>= 8 + if exitcode: + sys.exit(exitcode) + myglsa.inject() + elif mode == "pretend": + sys.stdout.write("Checking GLSA "+myid+"\n") + mergelist = myglsa.getMergeList(least_change=least_change) + if mergelist: + sys.stdout.write("The following updates will be performed for this GLSA:\n") + for pkg in mergelist: + oldver = None + for x in vardb.match(portage.dep_getkey(pkg)): + if vardb.aux_get(x, ["SLOT"]) == portdb.aux_get(pkg, ["SLOT"]): + oldver = x + if oldver == None: + raise ValueError("could not find old version for package %s" % pkg) + oldver = oldver[len(portage.dep_getkey(oldver))+1:] + sys.stdout.write(" " + pkg + " (" + oldver + ")\n") + else: + sys.stdout.write("Nothing to do for this GLSA\n") + elif mode == "inject": + sys.stdout.write("injecting " + myid + "\n") + myglsa.inject() + sys.stdout.write("\n") + sys.exit(0) + +# test is a bit different as Glsa.test() produces no output +if mode == "test": + outputlist = [] + for myid in glsalist: + try: + myglsa = Glsa(myid, glsaconfig) + except (GlsaTypeException, GlsaFormatException), e: + if verbose: + sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e))) + continue + if myglsa.isVulnerable(): + if verbose: + outputlist.append(str(myglsa.nr)+" ( "+myglsa.title+" ) ") + else: + outputlist.append(str(myglsa.nr)) + if len(outputlist) > 0: + sys.stderr.write("This system is affected by the following GLSAs:\n") + sys.stdout.write("\n".join(outputlist)+"\n") + else: + sys.stderr.write("This system is not affected by any of the listed GLSAs\n") + sys.exit(0) + +# mail mode as requested by solar +if mode == "mail": + try: + import portage.mail as portage_mail + except ImportError: + import portage_mail + + import socket + from StringIO import StringIO + try: + from email.mime.text import MIMEText + except ImportError: + from email.MIMEText import MIMEText + + # color doesn't make any sense for mail + nocolor() + + if glsaconfig.has_key("PORTAGE_ELOG_MAILURI"): + myrecipient = glsaconfig["PORTAGE_ELOG_MAILURI"].split()[0] + else: + myrecipient = "root@localhost" + + if glsaconfig.has_key("PORTAGE_ELOG_MAILFROM"): + myfrom = glsaconfig["PORTAGE_ELOG_MAILFROM"] + else: + myfrom = "glsa-check" + + mysubject = "[glsa-check] Summary for %s" % socket.getfqdn() + + # need a file object for summarylist() + myfd = StringIO() + myfd.write("GLSA Summary report for host %s\n" % socket.getfqdn()) + myfd.write("(Command was: %s)\n\n" % " ".join(sys.argv)) + summarylist(glsalist, fd1=myfd, fd2=myfd) + summary = str(myfd.getvalue()) + myfd.close() + + myattachments = [] + for myid in glsalist: + try: + myglsa = Glsa(myid, glsaconfig) + except (GlsaTypeException, GlsaFormatException), e: + if verbose: + sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e))) + continue + myfd = StringIO() + myglsa.dump(outstream=myfd) + myattachments.append(MIMEText(str(myfd.getvalue()), _charset="utf8")) + myfd.close() + + mymessage = portage_mail.create_message(myfrom, myrecipient, mysubject, summary, myattachments) + portage_mail.send_mail(glsaconfig, mymessage) + + sys.exit(0) + +# something wrong here, all valid paths are covered with sys.exit() +sys.stderr.write("nothing more to do\n") +sys.exit(2) diff --git a/bin/revdep-rebuild b/bin/revdep-rebuild new file mode 100755 index 0000000..fac03d8 --- /dev/null +++ b/bin/revdep-rebuild @@ -0,0 +1,1094 @@ +#!/bin/bash +# Copyright 1999-2008 Gentoo Foundation + +# revdep-rebuild: Reverse dependency rebuilder. +# Original Author: Stanislav Brabec +# Rewrite Author: Michael A. Smith +# Current Maintainer: Paul Varner <fuzzyray@gentoo.org> + +# TODO: +# - Use more /etc/init.d/functions.sh +# - Try to reduce the number of global vars + +## +# Global Variables: + +# Must-be-blank: +unset GREP_OPTIONS + +# Readonly variables: +declare -r APP_NAME="${0##*/}" # The name of this application +declare -r OIFS="$IFS" # Save the IFS +declare -r ENV_FILE=0_env.rr # Contains environment variables +declare -r FILES_FILE=1_files.rr # Contains a list of files to search +declare -r LDPATH_FILE=2_ldpath.rr # Contains the LDPATH +declare -r BROKEN_FILE=3_broken.rr # Contains the list of broken files +declare -r ERRORS_FILE=3_errors.rr # Contains the ldd error output +declare -r RAW_FILE=4_raw.rr # Contains the raw list of packages +declare -r OWNERS_FILE=4_owners.rr # Contains the file owners +declare -r PKGS_FILE=4_pkgs.rr # Contains the unsorted bare package names +declare -r EBUILDS_FILE=4_ebuilds.rr # Contains the unsorted atoms + # (Appropriately slotted or versioned) +declare -r ORDER_FILE=5_order.rr # Contains the sorted atoms +declare -r STATUS_FILE=6_status.rr # Contains the ldd error output +declare -ra FILES=( + "$ENV_FILE" + "$FILES_FILE" + "$LDPATH_FILE" + "$BROKEN_FILE" + "$ERRORS_FILE" + "$RAW_FILE" + "$OWNERS_FILE" + "$PKGS_FILE" + "$EBUILDS_FILE" + "$ORDER_FILE" + "$STATUS_FILE" +) + +# "Boolean" variables: Considered "true" if it has any value at all +# "True" indicates we should... +declare FULL_LD_PATH # ...search across the COMPLETE_LD_LIBRARY_PATH +declare KEEP_TEMP # ...not delete tempfiles from the current run +declare ORDER_PKGS # ...sort the atoms in deep dependency order +declare PACKAGE_NAMES # ...emerge by slot, not by versionated atom +declare RM_OLD_TEMPFILES # ...remove tempfiles from prior runs +declare SEARCH_BROKEN # ...search for broken libraries and binaries +declare VERBOSE # ...give verbose output + +# Globals that impact portage directly: +declare EMERGE_DEFAULT_OPTS # String of options portage assumes to be set +declare EMERGE_OPTIONS # Array of options to pass to portage +declare PORTAGE_NICENESS # Renice to this value +declare PORTAGE_ROOT # The root path for portage + +# Customizable incremental variables: +# These variables can be prepended to either by setting the variable in +# your environment prior to execution, or by placing an entry in +# /etc/make.conf. +# +# An entry of "-*" means to clear the variable from that point forward. +# Example: env SEARCH_DIRS="/usr/bin -*" revdep-rebuild will set SEARCH_DIRS +# to contain only /usr/bin +declare LD_LIBRARY_MASK # Mask of specially evaluated libraries +declare SEARCH_DIRS # List of dirs to search for executables and libraries +declare SEARCH_DIRS_MASK # List of dirs not to search + +# Other globals: +declare OLDPROG # Previous pass through the progress meter +declare EXACT_PKG # Versionated atom to emerge +declare HEAD_TEXT # Feedback string about the search +declare NOCOLOR # Set to "true" not to output term colors +declare OK_TEXT # Feedback about a search which found no errors +declare RC_NOCOLOR # Hack to insure we respect NOCOLOR +declare REBUILD_LIST # Array of atoms to emerge +declare SKIP_LIST # Array of atoms that cannot be emerged (masked?) +declare SONAME # Soname/soname path pattern given on commandline +declare SONAME_SEARCH # Value of SONAME modified to match ldd's output +declare WORKING_TEXT # Feedback about the search +declare WORKING_DIR # Working directory where cache files are kept + +main() { + # preliminary setup + get_opts "$@" + setup_portage + setup_search_paths_and_masks + get_search_env + echo + + # Search for broken binaries + get_files + get_ldpath + main_checks + + # Associate broken binaries with packages to rebuild + if [[ $PACKAGE_NAMES ]]; then + get_packages + clean_packages + assign_packages_to_ebuilds + else + get_exact_ebuilds + fi + + # Rebuild packages owning broken binaries + get_build_order + rebuild + + # All done + cleanup +} +## +# Refuse to delete anything before we cd to our tmpdir +# (See mkdir_and_cd_to_tmpdir() +rm() { + eerror "I was instructed to rm '$@'" + die 1 "Refusing to delete anything before changing to temporary directory." +} +## +# GNU find has -executable, but if our users' finds do not have that flag +# we emulate it with this function. Also emulates -writable and -readable. +# Usage: find PATH ARGS -- use find like normal, except use -executable instead +# of various versions of -perm /+ blah blah and hacks +find() { + hash find || { die 1 'find not found!'; } + # We can be pretty sure find itself should be executable. + local testsubject="$(type -P find)" + if [[ $(command find "$testsubject" -executable 2> /dev/null) ]]; then + unset -f find # We can just use the command find + elif [[ $(command find "$testsubject" -perm /u+x 2> /dev/null) ]]; then + find() { + a=(${@//-executable/-perm \/u+x}) + a=(${a[@]//-writable/-perm \/u+w}) + a=(${a[@]//-readable/-perm \/r+w}) + command find "${a[@]}" + } + elif [[ $(command find "$testsubject" -perm +u+x 2> /dev/null) ]]; then + find() { + a=(${@//-executable/-perm +u+x}) + a=(${a[@]//-writable/-perm +u+w}) + a=(${a[@]//-readable/-perm +r+w}) + command find "${a[@]}" + } + else # Last resort + find() { + a=(${@//-executable/-exec test -x '{}' \; -print}) + a=(${a[@]//-writable/-exec test -w '{}' \; -print}) + a=(${a[@]//-readable/-exec test -r '{}' \; -print}) + command find "${a[@]}" + } + fi + find "$@" +} + +print_usage() { +cat << EOF +Usage: $APP_NAME [OPTIONS] [--] [EMERGE_OPTIONS] + +Broken reverse dependency rebuilder. + + -C, --nocolor Turn off colored output + -d, --debug Print way too much information (uses bash's set -xv) + -e, --exact Emerge based on exact package version + -h, --help Print this usage + -i, --ignore Ignore temporary files from previous runs + -k, --keep-temp Do not delete temporary files on exit + -L, --library NAME Emerge existing packages that use the library with NAME + --library=NAME NAME can be a full path to the library or a basic + regular expression (man grep) + -l, --no-ld-path Do not set LD_LIBRARY_PATH + -o, --no-order Do not check the build order + (Saves time, but may cause breakage.) + -p, --pretend Do a trial run without actually emerging anything + (also passed to emerge command) + -P, --no-progress Turn off the progress meter + -q, --quiet Be less verbose (also passed to emerge command) + -v, --verbose Be more verbose (also passed to emerge command) + +Calls emerge, options after -- are ignored by $APP_NAME +and passed directly to emerge. + +Report bugs to <http://bugs.gentoo.org> +EOF +} +## +# Usage: progress i n +# i: current item +# n: total number of items to process +progress() { + if [[ -t 1 ]]; then + progress() { + local curProg=$(( $1 * 100 / $2 )) + (( curProg == OLDPROG )) && return # no change, output nothing + OLDPROG="$curProg" # must be a global variable + (( $1 == $2 )) && local lb=$'\n' + echo -ne '\r \r'"[ $curProg% ] $lb" + } + progress $@ + else # STDOUT is not a tty. Disable progress meter. + progress() { :; } + fi +} +## +# Usage: countdown n +# n: number of seconds to count +countdown() { + local i + for ((i=1; i<$1; i++)); do + echo -ne '\a.' + ((i<$1)) && sleep 1 + done + echo -e '\a.' +} +## +# Replace whitespace with linebreaks, normalize repeated '/' chars, and sort -u +# (If any libs have whitespace in their filenames, someone needs punishment.) +clean_var() { + awk 'BEGIN {RS="[[:space:]]"} + /-\*/ {exit} + /[^[:space:]]/ {gsub(/\/\/+/, "/"); print}' | sort -u +} +## +# Exit and optionally output to sterr +die() { + local status=$1 + shift + eerror "$@" + exit $status +} +## +# What to do when dynamic linking is consistent +clean_exit() { + if [[ ! $KEEP_TEMP ]]; then + rm -f "${FILES[@]}" + if [[ "$WORKING_DIR" != "/var/cache/${APP_NAME}" ]]; then + # Remove the working directory + builtin cd; rmdir "$WORKING_DIR" + fi + fi + echo + einfo "$OK_TEXT... All done. " + exit 0 +} +## +# Get the name of the package that owns a file or list of files given as args. +get_file_owner() { + local IFS=$'\n' + # ${*/%/ } adds a space to the end of each object name to prevent false + # matches, for example /usr/bin/dia matching /usr/bin/dialog (bug #196460). + find -L /var/db/pkg -name CONTENTS -print0 | + xargs -0 grep -Fl "${*/%/ }" | + sed 's:/var/db/pkg/\(.*\)/CONTENTS:\1:' +} +## +# Normalize some EMERGE_OPTIONS +normalize_emerge_opts() { + # Normalize some EMERGE_OPTIONS + EMERGE_OPTIONS=(${EMERGE_OPTIONS[@]/%-p/--pretend}) + EMERGE_OPTIONS=(${EMERGE_OPTIONS[@]/%-f/--fetchonly}) + EMERGE_OPTIONS=(${EMERGE_OPTIONS[@]/%-v/--verbose}) +} +## +# Use the color preference from portage +setup_color() { + # This should still work if NOCOLOR is set by the -C flag or in the user's + # environment. + export NOCOLOR=$(portageq envvar NOCOLOR) + [[ $NOCOLOR = yes || $NOCOLOR = true ]] && export RC_NOCOLOR=yes # HACK! (grr) + . /etc/init.d/functions.sh +} +## +# Die if an argument is missing. +die_if_missing_arg() { + [[ ! $2 || $2 = -* ]] && die 1 "Missing expected argument to $1" +} +## +# Die because an option is not recognized. +die_invalid_option() { + # Can't use eerror and einfo because this gets called before function.sh + # is sourced + echo + echo "Encountered unrecognized option $1." >&2 + echo + echo "$APP_NAME no longer automatically passes unrecognized options to portage." + echo "Separate emerge-only options from revdep-rebuild options with the -- flag." + echo + echo "For example, $APP_NAME -v -- --ask" + echo + echo "See the man page or $APP_NAME -h for more detail." + echo + exit 1 +} +## +# Warn about deprecated options. +warn_deprecated_opt() { + # Can't use eerror and einfo because this gets called before function.sh + # is sourced + echo + echo "Encountered deprecated option $1." >&2 + [[ $2 ]] && echo "Please use $2 instead." >&2 +} +## +# Get whole-word commandline options preceded by two dashes. +get_longopts() { + case $1 in + --nocolor) export NOCOLOR="yes";; + --no-color) warn_deprecated_opt "$1" "--nocolor" + export NOCOLOR="yes";; + --debug) set -xv;; + --exact) unset PACKAGE_NAMES;; + --help) print_usage + exit 0;; + --ignore) RM_OLD_TEMPFILES=1;; + --keep-temp) KEEP_TEMP=1;; + --library=*) # TODO: check for invalid values + SONAME="${1#*=}" + unset SEARCH_BROKEN;; + --soname=*|--soname-regexp=*) # TODO: check for invalid values + warn_deprecated_opt "${1%=*}" "--library" + SONAME="${1#*=}" + unset SEARCH_BROKEN;; + --library) # TODO: check for invalid values + die_if_missing_arg $1 $2 + shift + SONAME="$1" + unset SEARCH_BROKEN;; + --soname|--soname-regexp) # TODO: check for invalid values + warn_deprecated_opt "$1" "--library" + die_if_missing_arg $1 $2 + shift + SONAME="$1" + unset SEARCH_BROKEN;; + --no-ld-path) unset FULL_LD_PATH;; + --no-order) unset ORDER_PKGS;; + --no-progress) progress() { :; };; + --pretend) EMERGE_OPTIONS+=("--pretend");; + --quiet) echo_v() { :; } + progress() { :; } + quiet=1 + EMERGE_OPTIONS+=($1);; + --verbose) VERBOSE=1 + EMERGE_OPTIONS+=("--verbose");; + --extra-verbose) warn_deprecated_opt "$1" "--verbose" + VERBOSE=1 + EMERGE_OPTIONS+=("--verbose");; + --package-names) # No longer used, since it is the + # default. We accept it for + # backwards compatibility. + warn_deprecated_opt "$1" + PACKAGE_NAMES=1;; + *) die_invalid_option $1;; + esac +} + +## +# Get single-letter commandline options preceded by a single dash. +get_shortopts() { + local OPT OPTSTRING OPTARG OPTIND + while getopts ":CdehikL:loPpqu:vX" OPT; do + case "$OPT" in + C) # TODO: Match syntax with the rest of gentoolkit + export NOCOLOR="yes";; + d) set -xv;; + e) unset PACKAGE_NAMES;; + h) print_usage + exit 0;; + i) RM_OLD_TEMPFILES=1;; + k) KEEP_TEMP=1;; + L) # TODO: Check for invalid values + SONAME="${OPTARG#*=}" + unset SEARCH_BROKEN;; + l) unset FULL_LD_PATH;; + o) unset ORDER_PKGS;; + P) progress() { :; };; + p) EMERGE_OPTIONS+=("--pretend");; + q) echo_v() { :; } + progress() { :; } + quiet=1 + EMERGE_OPTIONS+=("--quiet");; + v) VERBOSE=1 + EMERGE_OPTIONS+=("--verbose");; + X) # No longer used, since it is the default. + # We accept it for backwards compatibility. + warn_deprecated_opt "-X" + PACKAGE_NAMES=1;; + *) die_invalid_option "-$OPTARG";; + esac + done +} +## +# Get command-line options. +get_opts() { + local avoid_utils + local -a args + echo_v() { ewarn "$@"; } + unset VERBOSE KEEP_TEMP EMERGE_OPTIONS RM_OLD_TEMPFILES + ORDER_PKGS=1 + PACKAGE_NAMES=1 + SONAME="not found" + SEARCH_BROKEN=1 + FULL_LD_PATH=1 + while [[ $1 ]]; do + case $1 in + --) shift + EMERGE_OPTIONS+=("$@") + break;; + -*) while true; do + args+=("$1") + shift + [[ ${1:--} = -* ]] && break + done + if [[ ${args[0]} = --* ]]; then + get_longopts "${args[@]}" + else + get_shortopts "${args[@]}" + fi;; + *) die_invalid_option "$1";; + esac + unset args + done + + setup_color + normalize_emerge_opts + + # If the user is not super, add --pretend to EMERGE_OPTIONS + if [[ ${EMERGE_OPTIONS[@]} != *--pretend* && $UID -ne 0 ]]; then + ewarn "You are not superuser. Adding --pretend to emerge options." + EMERGE_OPTIONS+=(--pretend) + fi +} +## +# Is there a --pretend or --fetchonly flag in the EMERGE_OPTIONS array? +is_real_merge() { + [[ ${EMERGE_OPTIONS[@]} != *--pretend* && + ${EMERGE_OPTIONS[@]} != *--fetchonly* ]] +} +## +# Clean up temporary files and exit +cleanup_and_die() { + rm -f "$@" + die 1 " ...terminated. Removing incomplete $@." +} +## +# Clean trap +clean_trap() { + trap "cleanup_and_die $*" SIGHUP SIGINT SIGQUIT SIGABRT SIGTERM + rm -f "$@" +} +## +# Returns 0 if the first arg is found in the remaining args, 1 otherwise +# (Returns 2 if given fewer than 2 arguments) +has() { + (( $# > 1 )) || return 2 + local IFS=$'\a' target="$1" + shift + [[ $'\a'"$*"$'\a' = *$'\a'$target$'\a'* ]] +} +## +# Dies when it can't change directories +cd() { + if builtin cd -P "$@"; then + if [[ $1 != $PWD ]]; then + # Some symlink malfeasance is going on + die 1 "Working directory expected to be $1, but it is $PWD" + fi + else + die 1 "Unable to change working directory to '$@'" + fi +} +## +# Tries not to delete any files or directories it shouldn't +setup_rm() { + ## + # Anything in the FILES array in tmpdir is fair game for removal + rm() { + local i IFS=$'\a' + [[ $APP_NAME ]] || die 1 '$APP_NAME is not defined! (This is a bug.)' + case $@ in + */*|*-r*|*-R*) die 1 "Oops, I'm not allowed to delete that. ($@)";; + esac + for i; do + # Don't delete files that are not listed in the array + # Allow no slashes or recursive deletes at all. + case $i in + */*|-*r*|-*R*) :;; # Not OK + -*) continue;; # OK + esac + has "$i" "${FILES[@]}" && continue + die 1 "Oops, I'm not allowed to delete that. ($@)" + done + command rm "$@" + } + # delete this setup function so it's harmless to re-run + setup_rm() { :; } +} +## +# Make our temporary files directory +# $1 - directory name +# $2 - user name +verify_tmpdir() { + umask 007 || die $? "Unable to set umask 007" + if [[ ! $1 ]]; then + die 1 'Temporary file path is unset! (This is a bug.)' + elif [[ -d $1 ]]; then + # HACK: I hate using find this way + if [[ $(find "$1" -type d ! \( -user $2 -perm -0700 \) ) ]]; then + eerror "Incorrect permissions on $1" + eerror "or at least one file in $1." + die 1 "Please make sure it's not a symlink and then remove it." + fi + cd "$1" + else + die 1 "Unable to find a satisfactory location for temporary files ($1)" + fi + [[ $VERBOSE ]] && einfo "Temporary cache files are located in $PWD" + setup_rm +} +get_search_env() { + local new_env + local old_env + local uid=$(python -c 'import os; import pwd; print pwd.getpwuid(os.getuid())[0]') + # Find a place to put temporary files + if [[ "$uid" == "root" ]]; then + local tmp_target="/var/cache/${APP_NAME}" + else + local tmp_target="$(mktemp -d -t revdep-rebuild.XXXXXXXXXX)" + fi + + # From here on all work is done inside the temporary directory + verify_tmpdir "$tmp_target" "$uid" + WORKING_DIR="$tmp_target" + + if [[ $SEARCH_BROKEN ]]; then + SONAME_SEARCH="$SONAME" + HEAD_TEXT="broken by a package update" + OK_TEXT="Dynamic linking on your system is consistent" + WORKING_TEXT="consistency" + else + # first case is needed to test against /path/to/foo.so + if [[ $SONAME = /* ]]; then + # Set to "<space>$SONAME<space>" + SONAME_SEARCH=" $SONAME " + # Escape the "/" characters + SONAME_SEARCH="${SONAME_SEARCH//\//\\/}" + else + # Set to "<tab>$SONAME<space>" + SONAME_SEARCH=$'\t'"$SONAME " + fi + HEAD_TEXT="using $SONAME" + OK_TEXT="There are no dynamic links to $SONAME" + unset WORKING_TEXT + fi + + # If any of our temporary files are older than 1 day, remove them all + if [[ ! $KEEP_TEMP ]]; then + while read; do + RM_OLD_TEMPFILES=1 + break + done < <(find -L . -maxdepth 1 -type f -name '*.rr' -mmin +1440 -print 2>/dev/null) + fi + + # Compare old and new environments + # Don't use our previous files if environment doesn't match + new_env=$( + # We do not care if these emerge options change + EMERGE_OPTIONS=(${EMERGE_OPTIONS[@]//--pretend/}) + EMERGE_OPTIONS=(${EMERGE_OPTIONS[@]//--fetchonly/}) + EMERGE_OPTIONS=(${EMERGE_OPTIONS[@]//--verbose/}) + cat <<- EOF + SEARCH_DIRS="$SEARCH_DIRS" + SEARCH_DIRS_MASK="$SEARCH_DIRS_MASK" + LD_LIBRARY_MASK="$LD_LIBRARY_MASK" + PORTAGE_ROOT="$PORTAGE_ROOT" + EMERGE_OPTIONS="${EMERGE_OPTIONS[@]}" + ORDER_PKGS="$ORDER_PKGS" + FULL_LD_PATH="$FULL_LD_PATH" + EOF + ) + if [[ -r "$ENV_FILE" && -s "$ENV_FILE" ]]; then + old_env=$(<"$ENV_FILE") + if [[ $old_env != $new_env ]]; then + ewarn 'Environment mismatch from previous run, deleting temporary files...' + RM_OLD_TEMPFILES=1 + fi + else + # No env file found, silently delete any other tempfiles that may exist + RM_OLD_TEMPFILES=1 + fi + + # If we should remove old tempfiles, do so + if [[ $RM_OLD_TEMPFILES ]]; then + rm -f "${FILES[@]}" + else + for file in "${FILES[@]}"; do + if [ -e "$file" ]; then + chown ${uid}:portage "$file" + chmod 700 "$file" + fi + done + fi + + # Save the environment in a file for next time + echo "$new_env" > "$ENV_FILE" + + [[ $VERBOSE ]] && echo $'\n'"$APP_NAME environment:"$'\n'"$new_env" + + echo + einfo "Checking reverse dependencies" + einfo "Packages containing binaries and libraries $HEAD_TEXT" + einfo "will be emerged." +} + +get_files() { + einfo "Collecting system binaries and libraries" + if [[ -r "$FILES_FILE" && -s "$FILES_FILE" ]]; then + einfo "Found existing $FILES_FILE" + else + # Be safe and remove any extraneous temporary files + # Don't remove 0_env.rr - The first file in the array + rm -f "${FILES[@]:1}" + + clean_trap "$FILES_FILE" + + if [[ $SEARCH_DIRS_MASK ]]; then + findMask=($SEARCH_DIRS_MASK) + findMask="${findMask[@]/#/-o -path }" + findMask="( ${findMask#-o } ) -prune -o" + fi + # TODO: Check this + find ${SEARCH_DIRS[@]} $findMask -type f \( -executable -o \ + -name '*.so' -o -name '*.so.*' -o -name '*.la' \) -print 2> /dev/null | + sort -u > "$FILES_FILE" || + die $? "find failed to list binary files (This is a bug.)" + einfo "Generated new $FILES_FILE" + fi +} +get_ldpath() { + local COMPLETE_LD_LIBRARY_PATH + [[ $SEARCH_BROKEN && $FULL_LD_PATH ]] || return + einfo 'Collecting complete LD_LIBRARY_PATH' + if [[ -r "$LDPATH_FILE" && -s "$LDPATH_FILE" ]]; then + einfo "Found existing $LDPATH_FILE." + else + clean_trap "$LDPATH_FILE" + # Ensure that the "trusted" lib directories are at the start of the path + COMPLETE_LD_LIBRARY_PATH=( + /lib* + /usr/lib* + $(sed '/^#/d;s/#.*$//' < /etc/ld.so.conf) + $(sed 's:/[^/]*$::' < "$FILES_FILE" | sort -ru) + ) + IFS=':' + COMPLETE_LD_LIBRARY_PATH="${COMPLETE_LD_LIBRARY_PATH[*]}" + IFS="$OIFS" + echo "$COMPLETE_LD_LIBRARY_PATH" > "$LDPATH_FILE" + einfo "Generated new $LDPATH_FILE" + fi +} +main_checks() { + local target_file + local -a files + local i=0 + local ldd_output + local ldd_status + local numFiles + local COMPLETE_LD_LIBRARY_PATH + if [[ $SEARCH_BROKEN && $FULL_LD_PATH ]]; then + [[ -r "$LDPATH_FILE" && -s "$LDPATH_FILE" ]] || + die 1 "Unable to find $LDPATH_FILE" + COMPLETE_LD_LIBRARY_PATH=$(<"$LDPATH_FILE") + fi + einfo "Checking dynamic linking $WORKING_TEXT" + if [[ -r "$BROKEN_FILE" && -s "$BROKEN_FILE" ]]; then + einfo "Found existing $BROKEN_FILE." + else + clean_trap "$BROKEN_FILE" "$ERRORS_FILE" + files=($(<"$FILES_FILE")) + numFiles="${#files[@]}" + for target_file in "${files[@]}"; do + if [[ $target_file != *.la ]]; then + # Note: double checking seems to be faster than single with complete path + # (special add ons are rare). + ldd_output=$(ldd "$target_file" 2>> "$ERRORS_FILE" | sort -u) + ldd_status=$? # TODO: Check this for problems with sort + # HACK: if LD_LIBRARY_MASK is null or undefined grep -vF doesn't work + if grep -vF "${LD_LIBRARY_MASK:=$'\a'}" <<< "$ldd_output" | + grep -q "$SONAME_SEARCH"; then + if [[ $SEARCH_BROKEN && $FULL_LD_PATH ]]; then + if LD_LIBRARY_PATH="$COMPLETE_LD_LIBRARY_PATH" ldd "$target_file" 2>/dev/null | + grep -vF "$LD_LIBRARY_MASK" | grep -q "$SONAME_SEARCH"; then + # FIXME: I hate duplicating code + # Only build missing direct dependencies + MISSING_LIBS=$( + expr='s/[[:space:]]*\([^[:space:]]*\) => not found/\1/p' + sed -n "$expr" <<< "$ldd_output" + ) + REQUIRED_LIBS=$( + expr='s/^[[:space:]]*NEEDED[[:space:]]*\([^[:space:]]*\).*/\1/p'; + objdump -x "$target_file" | grep NEEDED | sed "$expr" | sort -u + ) + MISSING_LIBS=$(grep -F "$REQUIRED_LIBS" <<< "$MISSING_LIBS") + if [[ $MISSING_LIBS ]]; then + echo "obj $target_file" >> "$BROKEN_FILE" + echo_v " broken $target_file (requires $MISSING_LIBS)" + fi + fi + else + # FIXME: I hate duplicating code + # Only rebuild for direct dependencies + MISSING_LIBS=$( + expr="/$SONAME_SEARCH/s/^[[:space:]]*\([^[:space:]]*\).*$/\1/p" + sort -u <<< "$ldd_output" | sed -n "$expr" + ) + REQUIRED_LIBS=$( + expr='s/^[[:space:]]*NEEDED[[:space:]]*\([^[:space:]]*\).*/\1/p'; + objdump -x "$target_file" | grep NEEDED | sed "$expr" | sort -u + ) + MISSING_LIBS=$(grep -F "$REQUIRED_LIBS" <<< "$MISSING_LIBS") + if [[ $MISSING_LIBS ]]; then + echo "obj $target_file" >> "$BROKEN_FILE" + if [[ $SEARCH_BROKEN ]]; then + echo_v " broken $target_file (requires $MISSING_LIBS)" + else + echo_v " found $target_file" + fi + fi + fi + fi + elif [[ $SEARCH_BROKEN ]]; then + # Look for broken .la files + for depend in $( + awk -F"[=']" '/^dependency_libs/{ + gsub("^-[^[:space:]]*", "", $3); + gsub("[[:space:]]-[^[:space:]]*", "", $3); + print $3 + }' "$target_file" + ); do + if [[ $depend = /* && ! -e $depend ]]; then + echo "obj $target_file" >> "$BROKEN_FILE" + echo_v " broken $target_file (requires $depend)" + fi + done + fi + [[ $VERBOSE ]] && + progress $((++i)) $numFiles $target_file || + progress $((++i)) $numFiles + done + if [[ $SEARCH_BROKEN ]]; then + # Look for missing version + while read target_file; do + echo "obj $target_file" >> "$BROKEN_FILE" + echo_v " broken $target_file (no version information available)" + done < <( + # Regexify LD_LIBRARY_MASK. Exclude it from the search. + LD_LIBRARY_MASK="${LD_LIBRARY_MASK//$'\n'/|}" + awk -v ldmask="(${LD_LIBRARY_MASK//./\\\.})" ' + /no version information available/ && $0 !~ ldmask { + gsub(/[()]/, "", $NF) + if (seen[$NF]++) next + print $NF + }' "$ERRORS_FILE" + ) + fi + [[ -r "$BROKEN_FILE" && -s "$BROKEN_FILE" ]] || clean_exit + sort -u "$BROKEN_FILE" -o "$BROKEN_FILE" + einfo "Generated new $BROKEN_FILE" + fi +} +get_packages() { + local target_file + local EXACT_PKG + local PKG + local obj + einfo 'Assigning files to packages' + if [[ -r "$RAW_FILE" && -s "$RAW_FILE" ]]; then + einfo "Found existing $RAW_FILE" + else + clean_trap "$RAW_FILE" "$OWNERS_FILE" + while read obj target_file; do + EXACT_PKG=$(get_file_owner $target_file) + if [[ $EXACT_PKG ]]; then + # Strip version information + PKG="${EXACT_PKG%%-r[[:digit:]]*}" + PKG="${PKG%-*}" + echo "$EXACT_PKG" >> "$RAW_FILE" + echo "$target_file -> $EXACT_PKG" >> "$OWNERS_FILE" + echo_v " $target_file -> $PKG" + else + ewarn " !!! $target_file not owned by any package is broken !!!" + echo "$target_file -> (none)" >> "$OWNERS_FILE" + echo_v " $target_file -> (none)" + fi + done < "$BROKEN_FILE" + einfo "Generated new $RAW_FILE and $OWNERS_FILE" + fi + # if we find '(none)' on every line, exit out + if ! grep -qvF '(none)' "$OWNERS_FILE"; then + ewarn "Found some broken files, but none of them were associated with known packages" + ewarn "Unable to proceed with automatic repairs." + ewarn "The broken files are listed in $OWNERS_FILE" + if [[ $VERBOSE ]]; then + ewarn "The broken files are:" + while read filename junk; do + ewarn " $filename" + done < "$OWNERS_FILE" + fi + exit 0 # FIXME: Should we exit 1 here? + fi +} +clean_packages() { + einfo 'Cleaning list of packages to rebuild' + if [[ -r "$PKGS_FILE" && -s "$PKGS_FILE" ]]; then + einfo "Found existing $PKGS_FILE" + else + sort -u "$RAW_FILE" > "$PKGS_FILE" + einfo "Generated new $PKGS_FILE" + fi +} +assign_packages_to_ebuilds() { + local EXACT_PKG + local PKG + local SLOT + einfo 'Assigning packages to ebuilds' + if [[ -r "$EBUILDS_FILE" && -s "$EBUILDS_FILE" ]]; then + einfo "Found existing $EBUILDS_FILE" + elif [[ -r "$PKGS_FILE" && -s "$PKGS_FILE" ]]; then + clean_trap "$EBUILDS_FILE" + while read EXACT_PKG; do + # Get the slot + PKG="${EXACT_PKG%%-r[[:digit:]]*}" + PKG="${PKG%-*}" + SLOT=$(</var/db/pkg/$EXACT_PKG/SLOT) + echo "$PKG:$SLOT" + done < "$PKGS_FILE" > "$EBUILDS_FILE" + einfo "Generated new $EBUILDS_FILE" + else + einfo 'Nothing to rebuild.' + die 1 '(The program should have already quit, so this is a minor bug.)' + fi +} +get_exact_ebuilds() { + einfo 'Assigning files to ebuilds' + if [[ -r $EBUILDS_FILE && -s $EBUILDS_FILE ]]; then + einfo "Found existing $EBUILDS_FILE" + elif [[ -r $BROKEN_FILE && -s $BROKEN_FILE ]]; then + rebuildList=" $(<"$BROKEN_FILE") " + rebuildList=(${rebuildList//[[:space:]]obj[[:space:]]/ }) + get_file_owner "${rebuildList[@]}" | sed 's/^/=/' > "$EBUILDS_FILE" + einfo "Generated new $EBUILDS_FILE" + else + einfo 'Nothing to rebuild.' + die 1 '(The program should have already quit, so this is a minor bug.)' + fi +} +list_skipped_packages() { + ewarn + ewarn 'Portage could not find any version of the following packages it could build:' + ewarn "${SKIP_LIST[@]}" + ewarn + ewarn '(Perhaps they are masked, blocked, or removed from portage.)' + ewarn 'Try to emerge them manually.' + ewarn +} +get_build_order() { + local -r OLD_EMERGE_DEFAULT_OPTS="$EMERGE_DEFAULT_OPTS" + local RAW_REBUILD_LIST + local REBUILD_GREP + local i + if [[ ! $ORDER_PKGS ]]; then + einfo 'Skipping package ordering' + return + fi + einfo 'Evaluating package order' + if [[ -r "$ORDER_FILE" && -s "$ORDER_FILE" ]]; then + einfo "Found existing $ORDER_FILE" + else + clean_trap "$ORDER_FILE" + RAW_REBUILD_LIST=$(<"$EBUILDS_FILE") + if [[ $RAW_REBUILD_LIST ]]; then + export EMERGE_DEFAULT_OPTS="--nospinner --pretend --oneshot --quiet" + RAW_REBUILD_LIST=($RAW_REBUILD_LIST) # convert into array + # If PACKAGE_NAMES is defined we're using slots, not versions + if [[ $PACKAGE_NAMES ]]; then + # Eliminate atoms that can't be built + for i in "${!RAW_REBUILD_LIST[@]}"; do + if [[ "${RAW_REBUILD_LIST[i]}" = *[A-Za-z]* ]]; then + portageq best_visible "$PORTAGE_ROOT" "${RAW_REBUILD_LIST[i]}" >/dev/null && continue + SKIP_LIST+=("${RAW_REBUILD_LIST[i]}") + fi + unset RAW_REBUILD_LIST[i] + done + # If RAW_REBUILD_LIST is empty, then we have nothing to build. + if (( ${#RAW_REBUILD_LIST[@]} == 0 )); then + if (( ${#SKIP_LIST[@]} == 0 )); then + ewarn "The list of packages to skip is empty, but there are no" + ewarn "packages listed to rebuild either. (This is a bug.)" + else + list_skipped_packages + fi + die 1 'Warning: Portage cannot rebuild any of the necessary packages.' + fi + fi + RAW_REBUILD_LIST="${RAW_REBUILD_LIST[@]}" + REBUILD_GREP=$(emerge --nodeps $RAW_REBUILD_LIST | sed 's/\[[^]]*\]//g') + if (( ${PIPESTATUS[0]} == 0 )); then + emerge --deep $RAW_REBUILD_LIST | + sed 's/\[[^]]*\]//g' | + grep -F "$REBUILD_GREP" > "$ORDER_FILE" + fi + + # Here we use the PIPESTATUS from the second emerge, the --deep one. + if (( ${PIPESTATUS[0]} != 0 )); then + eerror + eerror 'Warning: Failed to resolve package order.' + eerror 'Will merge in arbitrary order' + eerror + cat <<- EOF + Possible reasons: + - An ebuild is no longer in the portage tree. + - An ebuild is masked, use /etc/portage/packages.keyword + and/or /etc/portage/package.unmask to unmask it + EOF + countdown 5 + rm -f "$ORDER_FILE" + fi + export EMERGE_DEFAULT_OPTS="$OLD_EMERGE_DEFAULT_OPTS" + else + einfo 'Nothing to rebuild.' + die 1 '(The program should have already quit, so this is a minor bug.)' + fi + fi + [[ -r "$ORDER_FILE" && -s "$ORDER_FILE" ]] && einfo "Generated new $ORDER_FILE" +} + +show_unowned_files() { + if grep -qF '(none)' "$OWNERS_FILE"; then + ewarn "Found some broken files that weren't associated with known packages" + ewarn "The broken files are:" + while read filename junk; do + [[ $junk = *none* ]] && ewarn " $filename" + done < "$OWNERS_FILE" | awk '!s[$0]++' # (omit dupes) + fi +} +## +# Setup portage and the search paths +setup_portage() { + local PORTAGE_NICENESS=$(portageq envvar PORTAGE_NICENESS) + PORTAGE_ROOT=$(portageq envvar ROOT) + + # Obey PORTAGE_NICENESS + if [[ $PORTAGE_NICENESS ]]; then + renice $PORTAGE_NICENESS $$ > /dev/null + # Since we have already set our nice value for our processes, + # reset PORTAGE_NICENESS to zero to avoid having emerge renice again. + export PORTAGE_NICENESS="0" + fi + + PORTAGE_ROOT="${PORTAGE_ROOT:-/}" +} + +## +# Setup the paths to search (and filter the ones to avoid) +setup_search_paths_and_masks() { + local configfile sdir mdir skip_me filter_SEARCH_DIRS + + einfo "Configuring search environment for $APP_NAME" + + # Update the incremental variables using /etc/profile.env, /etc/ld.so.conf, + # portage, and the environment + + # Read the incremental variables from environment and portage + # Until such time as portage supports these variables as incrementals + # The value will be what is in /etc/make.conf + SEARCH_DIRS+=" "$(unset SEARCH_DIRS; portageq envvar SEARCH_DIRS) + SEARCH_DIRS_MASK+=" "$(unset SEARCH_DIRS_MASK; portageq envvar SEARCH_DIRS_MASK) + LD_LIBRARY_MASK+=" "$(unset LD_LIBRARY_MASK; portageq envvar LD_LIBRARY_MASK) + + # Add the defaults + if [[ -d /etc/revdep-rebuild ]]; then + for configfile in /etc/revdep-rebuild/*; do + SEARCH_DIRS+=" "$(. $configfile; echo $SEARCH_DIRS) + SEARCH_DIRS_MASK+=" "$(. $configfile; echo $SEARCH_DIRS_MASK) + LD_LIBRARY_MASK+=" "$(. $configfile; echo $LD_LIBRARY_MASK) + done + else + SEARCH_DIRS+=" /bin /sbin /usr/bin /usr/sbin /lib* /usr/lib*" + SEARCH_DIRS_MASK+=" /opt/OpenOffice /usr/lib/openoffice" + LD_LIBRARY_MASK+=" libodbcinst.so libodbc.so libjava.so libjvm.so" + fi + + # Get the ROOTPATH and PATH from /etc/profile.env + if [[ -r "/etc/profile.env" && -s "/etc/profile.env" ]]; then + SEARCH_DIRS+=" "$(. /etc/profile.env; /usr/bin/tr ':' ' ' <<< "$ROOTPATH $PATH") + fi + + # Get the directories from /etc/ld.so.conf + if [[ -r /etc/ld.so.conf && -s /etc/ld.so.conf ]]; then + SEARCH_DIRS+=" "$(sed '/^#/d;s/#.*$//' /etc/ld.so.conf) + fi + + # Set the final variables + SEARCH_DIRS=$(clean_var <<< "$SEARCH_DIRS") + SEARCH_DIRS_MASK=$(clean_var <<< "$SEARCH_DIRS_MASK") + LD_LIBRARY_MASK=$(clean_var <<< "$LD_LIBRARY_MASK") + # Filter masked paths from SEARCH_DIRS + for sdir in ${SEARCH_DIRS} ; do + skip_me= + for mdir in ${SEARCH_DIRS_MASK}; do + [[ ${sdir} == ${mdir}/* ]] && skip_me=1 && break + done + [[ -n ${skip_me} ]] || filter_SEARCH_DIRS+=" ${sdir}" + done + SEARCH_DIRS=$(clean_var <<< "${filter_SEARCH_DIRS}") + [[ $SEARCH_DIRS ]] || die 1 "No search defined -- this is a bug." +} +## +# Rebuild packages owning broken binaries +rebuild() { + if [[ -r $LIST.5_order && -s $LIST.5_order ]]; then + REBUILD_LIST=( $(<"$LIST.5_order") ) + REBUILD_LIST="${REBUILD_LIST[@]/#/=}" + else + REBUILD_LIST=$(sort -u "$EBUILDS_FILE") + fi + + trap - SIGHUP SIGINT SIGQUIT SIGABRT SIGTERM + + einfo 'All prepared. Starting rebuild' + echo "emerge --oneshot ${EMERGE_OPTIONS[@]} $REBUILD_LIST" + + is_real_merge && countdown 10 + + # Link file descriptor #6 with stdin so --ask will work + exec 6<&0 + + # Run in background to correctly handle Ctrl-C + { + EMERGE_DEFAULT_OPTS="--oneshot ${EMERGE_OPTIONS[@]}" emerge $REBUILD_LIST <&6 + echo $? > "$STATUS_FILE" + } & + wait + + # Now restore stdin from fd #6, where it had been saved, and close fd #6 ( 6<&- ) to free it for other processes to use. + exec 0<&6 6<&- +} +## +# Finish up +cleanup() { + if (( $(<"$STATUS_FILE") != 0 )); then + ewarn + ewarn "$APP_NAME failed to emerge all packages." + ewarn 'you have the following choices:' + einfo "- If emerge failed during the build, fix the problems and re-run $APP_NAME." + einfo '- Use /etc/portage/package.keywords to unmask a newer version of the package.' + einfo " (and remove $ORDER_FILE to be evaluated again)" + einfo '- Modify the above emerge command and run it manually.' + einfo '- Compile or unmerge unsatisfied packages manually,' + einfo ' remove temporary files, and try again.' + einfo ' (you can edit package/ebuild list first)' + einfo + einfo 'To remove temporary files, please run:' + einfo "rm ${WORKING_DIR}/*.rr" + show_unowned_files + exit $EMERGE_STATUS + elif is_real_merge; then + trap_cmd() { + eerror "terminated. Please remove the temporary files manually:" + eerror "rm ${WORKING_DIR}/*.rr" + exit 1 + } + [[ "${SKIP_LIST[@]}" != "" ]] && list_skipped_packages + trap trap_cmd SIGHUP SIGINT SIGQUIT SIGABRT SIGTERM + einfo 'Build finished correctly. Removing temporary files...' + einfo + einfo 'You can re-run revdep-rebuild to verify that all libraries and binaries' + einfo 'are fixed. If some inconsistency remains, it can be orphaned file, deep' + einfo 'dependency, binary package or specially evaluated library.' + if [[ -r "$OWNERS_FILE" && -s "$OWNERS_FILE" ]]; then + show_unowned_files + fi + [[ $KEEP_TEMP ]] || rm "${FILES[@]}" + else + einfo 'Now you can remove -p (or --pretend) from arguments and re-run revdep-rebuild.' + fi +} + +main "$@" |
