#! /usr/bin/env python3 import io, os, copy, math, codecs, tarfile, optparse, subprocess from math import cos, sin, atan2, hypot from random import random, randint from itertools import * from collections import deque, defaultdict tau = math.pi * 2 pj = os.path.join # depends contains %CONFLICTS%, %DEPENDS%, %OPTDEPENDS%, %PROVIDES% # desc contains %URL%, %REPLACES%, %LICENSE%, # %NAME%, %GROUPS%, %BUILDDATE%, %REASON%, %DESC%, # %SIZE%, %PACKAGER%, %ARCH%, %INSTALLDATE%, %VERSION% class Node(object): def __init__(self, **kwargs): self.name = '' # matches dict key self.links = set() # of name strings self.inverse = set() # of name strings self.size = 0 # bytes, usually self.font_pt = 0 # scaled from size self.explicit = False # True if explicitly installed package self.center = (0,0) # (x,y) self.color = '#000000' self.__dict__.update(kwargs) def __repr__(self): return repr({'links' : self.links, 'size' : self.size}) # cache the slowest of these? @property def all(self): return self.links | self.inverse @property def dim(self): return pt2dim(self.name, self.font_pt) @property def box(self): return bbox(self.center, self.dim) @property def cd(self): # phase this out return [self.center, self.dim] class MockOptions(object): def __init__(self): self.opt_deps = False def unrip(path): "path -> dict of Nodes" tree = eval(open(path).read()) return bilink_tree(dict((p,Node(**n)) for p,n in list(tree.items()))) class Arch(object): def clean(self, n): n = n.strip() for c in '><:=': n = n.partition(c)[0] return n def load_info(self, arch_file): info = defaultdict(list) mode = None for line in (self.clean(l) for l in arch_file): if not line: continue if line.startswith('%'): mode = line continue info[mode].append(line) arch_file.close() return info def strip_info(self, info): keep = set(['DEPENDS', 'OPTDEPENDS', 'PROVIDES', 'SIZE', 'ISIZE', 'REASON']) info = dict((k.strip('%'),v) for k,v in info.items()) name = info['NAME'][0] info = dict((k,v) for k,v in info.items() if k in keep) if 'ISIZE' in info: info['SIZE'] = info['ISIZE'] if 'SIZE' in info: info['SIZE'] = int(info['SIZE'][0], 10) else: info['SIZE'] = 0 if 'REASON' in info: info['REASON'] = info['REASON'] == '1' else: info['REASON'] = False return name, info def load_tarball(self, tgz): db = tarfile.open(tgz) packages = defaultdict(dict) for member in db.getmembers(): if not member.isfile(): continue path = member.name #raw = codecs.getreader('utf8')(db.extractfile(member)) raw = io.StringIO(db.extractfile(member).read().decode('utf8')) name,sub = os.path.split(path) packages[name][sub] = raw tree = {} for p in packages.values(): info = {} [info.update(self.load_info(v)) for v in list(p.values())] name,info = self.strip_info(info) tree[name] = info return tree def load_tree(self, dirs): dirs = set(filter(os.path.isdir, dirs)) packages = set(p for r in dirs for p,d,f in os.walk(r) if f) tree = {} for p in packages - dirs: try: info = {} if os.path.isfile(pj(p, 'depends')): arch_file = open(pj(p,'depends'), 'r') info.update(self.load_info(arch_file)) arch_file = open(pj(p,'desc'), 'r') info.update(self.load_info(arch_file)) name, info = self.strip_info(info) tree[name] = info except KeyboardInterrupt: raise except: print('Error reading package', p) return tree def actually_installed_fn(self, tree): "use only on load_tree data, returns function" p_set = set(tree) errors = {} p_tree = dict((p,tree[p]['PROVIDES']) for p in tree if 'PROVIDES' in tree[p]) def actually(packages): installed = set(packages) & p_set maybe = set(packages) - installed for pack in maybe: if pack in errors: installed.add(errors[pack]) continue provides = sorted(p for p in p_tree if pack in p_tree[p]) if len(provides) > 1: print('warning: %s found in %s, assuming %s' % (pack, provides, provides[0])) errors[pack] = provides[0] if provides: installed.add(provides[0]) # len 0 means not installed optdep return list(installed) return actually def merge_tree(self, tree): "merge provides, depends, optdepends" options, args = parse() tree2 = {} actually_installed = self.actually_installed_fn(tree) # merge for p,info in tree.items(): tp = defaultdict(list, info) deps = tp['DEPENDS'] if options.opt_deps: deps += tp['OPTDEPENDS'] # remove unused optdeps deps = actually_installed(deps) tree2[p] = Node(name=p, size=info['SIZE'], links=set(deps), explicit=info['REASON']) return tree2 def dbpath(self): "super hacky" if not os.path.isfile('/etc/pacman.conf'): return '/var/lib/pacman/' for line in open('/etc/pacman.conf'): line = line.strip() if not line.startswith('DBPath'): continue return line.partition('=')[2].strip() return '/var/lib/pacman/' def local_load(self): dbpath = self.dbpath() dirs = ['local'] dirs = [pj(dbpath, d) for d in dirs] tree = compress_chains(bilink_tree(self.merge_tree(self.load_tree(dirs)))) return legal_links(tree) def repo_load(self): options, args = parse() packages = args dbpath = self.dbpath() dirs = ['community', 'core', 'extra', 'multilib'] dirs = [pj(dbpath, 'sync', d) for d in dirs] tars = ['community', 'core', 'extra', 'multilib'] tars = [pj(dbpath, 'sync', '%s.db' % t) for t in tars] #tars = [pj(dbpath, '%s.db.tar.gz' % t) for t in tars] tars = [t for t in tars if os.path.isfile(t)] if tars: tree = {} [tree.update(self.load_tarball(t)) for t in tars] else: tree = self.load_tree(dirs) tree = self.merge_tree(tree) if not packages: return legal_links(compress_chains(bilink_tree(tree))) reqs = set() if options.show_req: reqs = set(k for k,v in tree.items() if set(packages) & v.links) deps = set() for p in set(packages) | reqs: deps.update(full_deps(p, tree)) tree2 = dict((k,v) for k,v in tree.items() if k in deps | reqs) return legal_links(bilink_tree(tree2)) class Frugal(Arch): def dbpath(self): "super hacky" if not os.path.isfile('/etc/pacman-g2.conf'): return '/var/lib/pacman-g2/' for line in open('/etc/pacman-g2.conf'): line = line.strip() if not line.startswith('DBPath'): continue return line.partition('=')[2].strip() return '/var/lib/pacman-g2/' class Debian(object): "Expect lots of 'error: unknown' messages. May be due to pseudo-dependencies being filled by 'Provides:' entries." def load_tree(self, status_path = None): "New implementation not extensively tested, your mileage may vary" tree = {} installed = False deps = set() name = '' size = 0 if status_path is None: status_path = '/var/lib/dpkg/status' data = open(status_path, 'r') for line in data: if line.startswith('Package:'): name = line.split(':')[1].strip() elif line.startswith('Status'): installed = ('install ok installed' in line) or ('install user installed' in line) elif line.startswith('Installed-Size:') or line.startswith('Size:'): size = int(line.split(':')[1].strip(), 10) * 1024 elif line.startswith('Depends:'): deps = line.partition(':')[2] deps = [d for e in deps.split(',') for d in e.split('|')] deps = [d.partition('(')[0].strip() for d in deps] deps = set(deps) - set('') elif line == '\n' and installed and name and size: tree[name] = Node(name = name, links = deps, size = size) installed = False deps = set() name = '' size = 0 data.close() return tree def local_load(self, status_path=None): tree = legal_links(self.load_tree(status_path)) tree = compress_chains(bilink_tree(tree)) return legal_links(tree) def repo_load(self): print('not implemented') raise class Redhat(object): "RPM uses a capability/provider model. load_tree() has room for improvement" def load_tree(self): stdout = call_stdout('rpm --query --all --queryformat "%{NAME}<(^_^)>%{SIZE}<(^_^)>[%{REQUIRENAME}, ]<(^_^)>[%{PROVIDENAME}, ]\n"') tree = {} requirer_lookup = {} provider_lookup = {} for line in stdout: if line == '': break name,nullo,line = line.partition('<(^_^)>') size,nullo,line = line.partition('<(^_^)>') size = int(size, 10) requires,nullo,provides = line.partition('<(^_^)>') requires = set(capability.strip() for capability in requires.split(',')) - set(['']) for capability in requires: if capability not in requirer_lookup: requirer_lookup[capability] = set() requirer_lookup[capability] |= set([name]) provides = set(capability.strip() for capability in provides.split(',')) - set(['']) for capability in provides: if capability not in provider_lookup: provider_lookup[capability] = set() provider_lookup[capability].add(name) if name in tree: tree[name].size += size else: tree[name] = Node(name = name, links = set(), size = size) for capability in set(requirer_lookup) & set(provider_lookup): for package in requirer_lookup[capability]: tree[package].links |= provider_lookup[capability] return tree def local_load(self): tree = legal_links(self.load_tree()) tree = compress_chains(bilink_tree(tree)) return legal_links(tree) def repo_load(self): print('not implemented') raise class Crux(object): "based entirely on docs, needs to be tested" def find_all(self, package): raw = call_stdout('pkginfo -i') for line in raw.split('\n'): yield line.split(' ')[0] def find_size(self, package): return call_stdout('pkgsize %s' % package) def find_deps(self, package): return call_stdout('finddeps %s' % package) def local_load(self): "unfinished" tree = {} all_packages = [] for name in all_packages: size = self.find_size(name) deps = self.find_deps(name) tree[name] = Node(name=name, size=size, links=set(deps)) return tree class Gentoo(object): def get_name(self, pkg): name = pkg.strip() name = pkg.partition('/')[2] ver = name.rfind('-') if name[ver+1] == 'r': name = name[:name.rfind('-', 0, ver)] else: name = name[:ver] return name def load_tree(self): print("Getting packages size") tree = {} out = call_stdout('qsize --bytes --nocolor --all') for line in out: if line == '': continue words = line.split() name = self.get_name(words[0]) size = words[5] tree[name] = Node(name=name, size=int(size), link=set()) print("Getting dependencies") out = call_stdout('emerge --pretend --verbose --depclean') for line in out: if line[:4] == ' ': if line[4] == '@': if line[4:] == '@selected': tree[name].explicit = True else: name = self.get_name(line) tree[name].links.add(dep) elif line[:2] == ' ': dep = self.get_name(line) elif line[:3] == '>>>': break return tree def local_load(self): tree = legal_links(self.load_tree()) tree = compress_chains(bilink_tree(tree)) return legal_links(tree) def repo_load(self): print('not implemented') raise class Textfile(object): """ VERY ALPHA, frequent to change! loads a space delineated text file file should be named "data", each line looks like node_name size link1 link2 link3 ...""" def load_tree(self): tree = {} filename = 'data' for line in open(filename): words = line.split() node = words[0] size = int(words[1]) links = words[2:] tree[node] = Node(name=node, size=size, links=set(links)) return tree def local_load(self): tree = legal_links(self.load_tree()) tree = compress_chains(bilink_tree(tree)) return legal_links(tree) class Rtree(object): "Why? Pyrtree is insert-then-query (but mine has no delete), Rtree has crazy C deps." def __init__(self, parent=None, box=None, name=None): "Only root has no parent, only leaves have names." # Half an R-tree. Not height balanced! Missing delete(). bigm = 5 self.parent = parent self.name = name self.bigm = bigm self.lilm = bigm//2 self.children = [] self.box = (0,0,0,0) self.last_hit = None if box: self.box = tuple(box) def __lt__(self, other): return True # haaaack def show(self, depth=0): head = ' ' * depth + str(self.box) + ' ' if self.name: return head + self.name + '\n' head += str(len(self.children)) return head + '\n' + ''.join(c.show(depth+1) for c in self.children) def root(self): if self.parent is None: return self return self.parent.root() def best_node(self, to_add): "smallest place for box to_add, does not change anything" return smallest_merge(self.children, to_add) def search(self, box=None): "yields matching leaf objects, no box for all leaves" todo = [self] while todo: node = todo.pop() if box and not in_box(node.box, box): continue if node.name: self.last_hit = node yield node todo.extend(node.children) def search_cache(self, box): "assumes consecutive searches will be near each other" todo = [self.root()] if self.last_hit: todo.append(self.last_hit.parent) todo.append(self.last_hit) #while todo[0].parent: # todo.insert(0, todo[0].parent) while todo: node = todo.pop() if not in_box(node.box, box): continue if node.name: self.last_hit = node yield node todo.extend(node.children) def insert(self, box, name=None): "makes a new node" target = self.root().choose_leaf(box) target.children.append(Rtree(target, box, name)) target.merge_up(box) if len(target.children) > target.bigm: target.divide_children() def merge_up(self, box): # only grows new_box = merge(self.box, box) if new_box == self.box: return self.box = new_box if self.parent: self.parent.merge_up(self.box) def is_leaf_node(self): return not self.children or bool(self.children[0].name) def choose_leaf(self, box): n = self while not n.is_leaf_node(): n = n.best_node(box) return n def adjust(self): if len(self.children) > self.bigm: self.overflow() if self.parent and len(self.children) < self.lilm: self.underflow() def divide_children(self): children2 = [c for c in self.children] self.children = [] while children2: c1 = children2.pop() newp = Rtree(self, c1.box) newp.children.append(c1) c1.parent = newp self.children.append(newp) if not children2: break c2 = smallest_merge(children2, newp.box) newp.children.append(c2) children2.remove(c2) c2.parent = newp newp.box = merge(c1.box, c2.box) self.box = merge(*(c.box for c in self.children)) self.merge_up(self.box) # Leaves are too small, but grow soon. Probably. def underflow(self): "Not implemented" raise def delete(self, box): "Not implemented." raise def unbalance(self): "histogram of leaf depths" hist = dict((i,0) for i in range(100)) stack = [[self]] depth = 0 while stack: if not stack[-1]: depth -= 1 stack.pop() continue c = stack[-1].pop() assert bool(c.name) ^ bool(c.children) if c.name: hist[depth] += 1 else: stack.append(c.children) depth += 1 return dict((k,v) for k,v in hist.items() if v) def leafiness(self): "histogram of branch size" hist = dict((i,0) for i in range(100)) stack = [self] while stack: c = stack.pop() if c.name: continue hist[len(c.children)] += 1 stack.extend(c.children) return dict((k,v) for k,v in hist.items() if v) def merge(*boxes): "new box surrounding inputs" fns = (min, min, max, max) return tuple(f(p) for f,p in zip(fns, list(zip(*boxes)))) def area(box): return box[2]-box[0] * box[3]-box[1] def smallest_merge(nodes, box): "return best node from nodes" return min((area(merge(n.box, box)), area(n.box), n) for n in nodes)[2] def full_deps(package, tree, reverse=False): "returns every package in dep tree" deps = set() to_crawl = deque([package]) while to_crawl: current = to_crawl.popleft() if current in deps: continue deps.add(current) if not reverse: current_deps = tree[current].links else: current_deps = tree[current].inverse to_crawl.extend(current_deps - deps) return list(deps) def bilink_tree(tree): "adds inverse from links" for p in tree: deps = tree[p].links [tree[d].inverse.add(p) for d in deps] return tree def flatten(list_of_lists): return list(chain(*list_of_lists)) def single_depends(tree, preserve_explicit=False): if preserve_explicit: return (p for p in tree if len(tree[p].inverse) == 1 and not tree[p].explicit) else: return (p for p in tree if len(tree[p].inverse) == 1) def compress_chains(tree): "single depends are absorbed into parent" options, args = parse() if not options.use_compression: return tree while True: singles = single_depends(tree, options.preserve_explicit) try: s = next(singles) except StopIteration: return tree parent = list(tree[s].inverse)[0] if s == parent: #print 'self loop', s tree[s].links.remove(s) tree[s].inverse.remove(s) continue #print 'merge', s, 'into', parent tree[parent].size += tree[s].size #tree[parent].name += ' ' + tree[s].name for dep in tree[s].links: tree[dep].inverse.remove(s) tree[dep].inverse.add(parent) tree[parent].links.update(tree[s].links) tree[parent].links.remove(s) tree.pop(s) def sum_sizes(packages, tree): return sum(tree[p].size for p in packages if p in tree) def shared_size(package, tree): "package and all deps" return sum_sizes(full_deps(package, tree), tree) def biggest_packs(tree): packs = [(shared_size(p, tree), p) for p in tree] return [p for s,p in sorted(packs, reverse=True)] def toplevel_packs(tree): return set(p for p in tree if not tree[p].inverse) def packs_by_size(tree, pack_list): by_sizes = [(tree[n].size, n) for n in pack_list] return sorted(by_sizes, reverse=True) def legal_links(tree): "removes/reports dangling references" valid = set(tree) for k,v in tree.items(): invalid1 = v.links - valid invalid2 = v.inverse - valid if invalid1: print('error: unknown', list(invalid1), 'in', k) v.links = v.links - invalid1 if invalid2: print('error: unknown', list(invalid2), 'in', k) v.inverse = v.inverse - invalid2 return tree #print('worst shared packages:', biggest_packs(tree)[:20]) #print('most crucial packages:', biggest_packs(invert_tree(tree))[:20]) def pt_sizes(tree, min_pt=10, max_pt=100, by_area=False): "size in bytes -> size in points" norm = lambda n: n.size if by_area: norm = lambda n: n.size / len(n.name) sizes = [norm(node) for p,node in tree.items()] min_s,max_s = min(sizes), max(sizes) convert = lambda s: int((max_pt-min_pt)*(s-min_s)/(max_s-min_s) + min_pt) for p, node in tree.items(): tree[p].font_pt = convert(norm(node)) return tree def prioritized(packs): "iter of names, sorted by priority" # first are the most central, with lots of links stats = [(len(node.all), k) for k,node in packs.items()] stats = [n for l,n in sorted(stats, reverse=True)] # but slip in anyone who's deps are met early plotted = set() for n in (n for n in stats if n not in plotted): yield n plotted.add(n) deps_met = set(k for k,node in packs.items() if node.all <= plotted) for n2 in deps_met - plotted: yield n2 plotted.update(deps_met) def ran_rad(): return random() * tau def bbox(center, dim): c,d = center,dim cx,cy = center dx,dy = dim x1,x2 = cx - dx//2, cx + dx//2 y1,y2 = cy - dy , cy + dy//3 return [x1, y1, x2, y2] def in_box(bboxA, bboxB): a1x,a2y,a3x,a4y = bboxA b1x,b2y,b3x,b4y = bboxB return (a3x > b1x if a1xa1x) and (a4y > b2y if a2ya2y) def all_bboxes(packs): return [packs[name].box for name in packs] def normalize(point, origin): "weighted" dx,dy = point[0]-origin[0], point[1]-origin[1] length_sq = dx**2 + dy**2 return dx/length_sq, dy/length_sq def link_pull(name, origin_n, packs): "average of angles of links" origin = packs[origin_n].center norm_ps = lambda ps: [normalize(c, origin) for c in ps if c not in [(0,0), origin]] rot90 = lambda x,y: (-y,x) if randint(0,1) else (y,-x) good_links = packs[name].all skew_links = set(packs.keys()) - good_links g_centers = norm_ps(packs[l].center for l in good_links) s_centers = norm_ps(packs[l].center for l in skew_links) s_centers = [rot90(*xy) for xy in s_centers] centers = g_centers + s_centers if not centers: # new branch, try to avoid existing branches centers = norm_ps(packs[l].center for l in list(packs.keys())) if not centers: return (0,0) centers = [(-x,-y) for x,y in centers] return list(map(sum, list(zip(*centers)))) def xy2rad(x, y): if (x,y) == (0,0): return ran_rad() return math.atan2(y,x) def pol2xy(o, a, r): return int(o[0] + r * cos(a)), int(o[1] + r * sin(a)) def pt2dim(name, pt): x_scale = 0.65 y_scale = 0.85 return int(len(name)*pt*x_scale), int(pt*y_scale) def best_origin(name, pri_hist, packs): "returns largest sibling, or root" possible = [n for n in pri_hist if n in packs[name].all] if not possible: return pri_hist[0] # root package return max((packs[n].size, n) for n in possible)[1] # add search_hyperbola and means of switching def search(cd, origin, heading, scale, rt): "linear binary search recursive closure thingy, returns radius" history = [] def probe(r): "returns true if clear" cd[0] = pol2xy(origin, heading, r) history.append(cd[0]) bb1 = bbox(*cd) #return not any(rt.search_cache(bb1)) return not any(rt.search(bb1)) def search3(step, r): s2 = step // 2 if probe(r - s2): if step < scale: return r-s2 else: return search3(s2, r-s2) if probe(r + s2): step = s2 else: step *= 2 return search3(step, r + step) return search3(scale, scale), history def search_spiral(cd, origin, scale, rt, step=5, aspect=0.5): "returns angle, radius, history" # step controls speed/granularity # really should be a logarithmic spiral # spiral aspect -> final aspect... holy grail? history = [] def probe(heading, r): "returns true if clear" cd[0] = pol2xy(origin, heading, r) history.append(cd[0]) bb1 = bbox(*cd) return not any(rt.search(bb1)) angle = ran_rad() r = scale while not probe(angle, r): x = cos(angle) * r y = sin(angle) * r m = angle + tau/4 x += cos(m) * scale * step / aspect y += sin(m) * scale * step angle = xy2rad(x, y) r = hypot(x, y) return angle, r, history def place(packs, detail=False): "radial placement algo, returns non-overlapping coords" # maybe try two different link_pulls for each placement? # maybe some control inversion to avoid queue checks? pri_cache = [] def pri(): for p in prioritized(packs): pri_cache.append(p) yield p prii = pri() rt = Rtree() p = next(prii) rt.insert(bbox((0,0), packs[p].dim), p) if detail: yield pri_cache[0], [(0,0)] else: yield pri_cache[0], (0,0) for name in prii: node = packs[name] origin_name = best_origin(name, pri_cache, packs) #print('placing', name, 'around', origin_name) origin = packs[origin_name].center # unplaced links and big text need more room scale = len(node.all-set(pri_cache))+1 scale = max(scale, node.dim[1]) #heading = xy2rad(*link_pull(name, origin_name, packs)) #r,history = search(node.cd, origin, heading, scale, rt) heading,r,history = search_spiral(node.cd, origin, scale, rt) center = pol2xy(origin, heading, r) rt.insert(bbox(center, packs[name].dim), name) if detail: yield name, history else: yield name, center #print(rt.show()) #print(copy.deepcopy(rt).unbalance()) #print(copy.deepcopy(rt).leafiness()) def offset_coord(c, d): "corrects textbox origin" return c[0]-d[0]//2, c[1] #+d[1]//2 def xml_wrap(tag, inner=None, **kwargs): kw = ' '.join('%s="%s"' % (str(k), str(v)) for k,v in kwargs.items()) if inner is None: return '<%s %s/>' % (tag, kw) return '<%s %s>%s' % (tag, kw, inner, tag) def control_point(p1, p2, drop=None): dx = abs(p2[0] - p1[0]) lower = (p1,p2)[p1[1] number] + powers[-1:])[0] n2 = str(round(number / (limit//2**10))) if si == 'M': n2 = '%0.1f' % round((number / float(limit//2**10)), 1) return n2 + ' ' + si +'B' def console_dump(tree): options, args = parse() stat_line = human_si(sum(node.size for node in tree.values())) print('Total size:', stat_line) if options.use_compression: tops = toplevel_packs(tree) else: tops = list(tree.keys()) if len(tops) == 1: tops = list(tree.keys()) for s,n in packs_by_size(tree, tops): print(human_si(s), n) return def secondlevel_sizes(tree): # this is slightly lazy and only handles the common case # it ignores oddly shaped dep trees tops = toplevel_packs(tree) seconds = set() [seconds.update(tree[t].links) for t in tops] sec_info = [] for s in seconds: aboves = tree[s].inverse & tops # lazy! size = tree[s].size + sum(tree[i].size for i in aboves) sec_info.append((size/len(aboves), s, size, aboves)) sec_info.sort(reverse=True) for null, s, size, aboves in sec_info: print('%s: %s ' % (s, human_si(size))) print('\t', ' '.join(sorted(aboves))) def exists(app): return call_status('which %s' % app) == 0 def distro_detect(): # todo, replace this with an /etc/issue scanner if exists('pacman'): print ('Autodetected Arch.') return 'arch' if exists('ipkg'): print ('Autodetected ipkg.') return 'ipkg' if any(map(exists, 'dpkg apt-get aptitude'.split())): print ('Autodetected deb.') return 'debian' if exists('rpm'): print ('Autodetected rpm.') return 'redhat' if exists('poldek'): print ('Autodetected PLD.') return 'redhat' if exists('emerge'): print ('Autodetected gentoo.') return 'gentoo' return None def distro_detect2(): "prototype /etc scanner" is_r = lambda f: f.endswith('release') or f.endswith('version') releases = [f for f in next(os.walk('/etc/'))[2] if is_r(f)] releases = [f.partition('_')[0] for f in releases] releases = [f.partition('-')[0] for f in releases] if 'arch' in releases: print ('Autodetected Arch') return 'arch' if 'redhat' in releases or 'fedora' in releases: print ('Autodetected rpm') return 'redhat' if 'debian' in releases: print ('Autodetected deb') return 'debian' if 'gentoo' in releases: print ('Autodetected gentoo') return 'gentoo' print ('Warning: Could not autodetect from /etc') return None def main(): arch = Arch() debian = Debian() redhat = Redhat() gentoo = Gentoo() frugal = Frugal() textfile = Textfile() options, args = parse() filename = os.path.expanduser(options.filename) tree = None if options.mode == 'autodetect': options.mode = distro_detect2() if not options.mode: print ('Trying fallback package manager detection') options.mode = distro_detect() print('Loading package info') if options.mode == 'arch': tree = arch.local_load() if options.mode == 'arch-repo': tree = arch.repo_load() if options.mode == 'debian': tree = debian.local_load() if options.mode == 'redhat': tree = redhat.local_load() if options.mode == 'gentoo': tree = gentoo.local_load() if options.mode == 'frugalware': tree = frugal.local_load() if options.mode == 'frugal-repo': tree = frugal.repo_load() if options.mode == 'textfile': tree = textfile.local_load() if options.mode == 'ipkg': tree = debian.local_load(status_path='/usr/lib/ipkg/status') # ipkg reports bytes, not KB for node in tree.values(): node.size /= 1024 if tree is None: print('Could not load packages. Manually specify --mode.') return stat_line = human_si(sum(v.size for k,v in list(tree.items()))) if options.rip: open(filename + '.txt', 'w').write(repr(tree)) return if options.console: console_dump(tree) return if options.shared: secondlevel_sizes(tree) return print('Placing %i nodes' % len(tree)) tree = pt_sizes(tree, *options.point_size) for n,c in place(tree): tree[n].center = c tree = recenter(tree, all_points(tree)) tree[stat_line] = Node(name=stat_line, font_pt=sum(options.point_size)//2) print('Saving SVG') svgify(tree) if options.svg_only: return print('Rendering PNG') png_conversion = ['inkscape -D -e %(fn)s.png %(fn)s.svg', 'svg2png %(fn)s.svg %(fn)s.png', 'convert %(fn)s.svg %(fn)s.png'] for cmd_line in png_conversion: app = cmd_line.split()[0] if not exists(app): continue call_status(cmd_line % {'fn':filename}) if options.indexed and exists('mogrify'): print('Indexing PNG') call_status('mogrify -quality 100 -type optimize -flatten +matte -colors 127 %s.png' % filename) return # after first success print('No way to convert SVG to PNG.') print('Inkscape, svg2png or imagemagick would be nice.') print('Alternatives: "pacgraph-tk" or "pacgraph --console".') print() console_dump(tree) if __name__ == "__main__": main() #import cProfile; cProfile.run("main()", sort=1)