#!/usr/bin/env python # Copyright (c) 2015, Joseph Hunkeler # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. try: import argparse except ImportError: print("Please install argparse.") exit(1) import os import sys from pprint import pprint from string import Template from collections import OrderedDict BAD_VERSION_ERROR="{0} requires Python 2.6 or greater.".format(sys.argv[0]) try: pyinfo = sys.version_info if pyinfo.major <= 2 and pyinfo.minor < 6: print(BAD_VERSION_ERROR) exit(2) except: # Edge case failure print(BAD_VERSION_ERROR) class PackageException(Exception): pass class Package(object): def __init__(self, filename, load=True): self.valid_keywords = [ 'Type', 'Requires', 'Precedes', 'Synopsis', 'Description', 'Environment', 'LdLibrary', 'IncPath', 'Root', 'Path', 'ManPath', 'Default', 'Source' ] self.filename = os.path.join(ENVCONFIG_PATH, filename) self.exists = False self.name = '' self.name_internal = '' self.description = '' self.dependencies = [] self.precedence = [] self.priority = 0 # 0 - 99 self.shell = '' self.env = {} self.script = '' self.invisible = False self.mtime = 0 self.data = {} self.verbose = False if os.path.exists(self.filename): self.exists = True self.name = os.path.basename(self.filename.replace(' ', '_')) if not load: return self.populate() def __repr__(self): return self.name def populate(self): if self.filename is not None \ and self.exists: self.data = self.load() self.get_requirements() self.get_precedence() def get_requirements(self): if not self.data: PackageException('{0}: Package data not loaded.'.format(self.name)) if 'Requires' in self.data: for next_req in self.data['Requires']: try: req = Package(next_req) except: continue self.dependencies.append(req) if not req.exists and self.verbose: print("Requirement warning, {0}: {1} does not exist".format(self.name, os.path.basename(req.filename))) def get_precedence(self): if not self.data: raise PackageException('{0}: Package data not loaded.'.format(self.name)) if 'Precedes' in self.data: for next_req in self.data['Precedes']: req = Package(next_req, load=False) self.precedence.append(req) if not req.exists and self.verbose: print("Precedence warning, {0}: {1} does not exist".format(self.name, os.path.basename(req.filename))) def load(self): pairs = [] comment = '#' delimiter = ':' keyword = '' value = '' with open(self.filename, 'r') as f: for line in f.readlines(): line = line.strip() if not line: continue if not line.find(delimiter): continue if line.startswith(comment): continue keyword = line[0:line.find(delimiter)] if keyword not in self.valid_keywords: continue if keyword == 'Source': continue value = line[line.find(delimiter) + 1:].strip() pairs.append([keyword, value]) # Do source block with open(self.filename, 'r') as f: value = '' shell_type = '' data_block = False for line in f.readlines(): if line.startswith('Source:'): data_block = True shell_type = line[line.find(delimiter) + 1:line.rfind(delimiter)].strip() line = line[line.rfind(delimiter) + 1:-1] if data_block: value += line if line.startswith('"') and line.endswith('\\'): data_block = False pairs.append(['Source', value]) pairs.append(['Shell', shell_type]) pairs = dict(pairs) # Alias the filename to be the same as "Root" value if 'Root' in pairs: pairs[self.name] = pairs['Root'] # Substitute configuration values for key, value in pairs.items(): s = Template(value) pairs[key] = s.safe_substitute(pairs) if 'Requires' in pairs: pairs['Requires'] = str(pairs['Requires']).split() if 'Precedes' in pairs: pairs['Precedes'] = str(pairs['Precedes']).split() return dict(pairs) spaces = '' missing = ' Missing'.rjust(25, '.') def solver(p, style='dependency'): ''' Recursive package dependency resolver ''' flags = '' tree = ' \\_' resolver = p.dependencies if args.no_graphics: tree = ' ' if style != 'dependency': resolver = p.precedence # Wasn't planning on using a global here, but... recursion. global spaces global missing for dep in resolver: if not dep.exists: flags = missing print('{0} {1}{2:20s} {3}'.format(spaces, tree, dep.name, flags)) flags = '' if dep.dependencies: spaces += ' ' # Fall back in on yourself if there are more dependencies # beyond the current 'dep' solver(dep, style) spaces = '' def solver_ex(p, style='dependency'): ''' Recursive package dependency resolver ''' resolver = p.dependencies if style != 'dependency': resolver = p.precedence for dep in resolver: yield dep.name, dep.exists if dep.dependencies: # Fall back in on yourself if there are more dependencies # beyond the current 'dep' solver_ex(dep, style) def show_package_map(packages, style='dependency'): tree = '*--' global missing if args.no_graphics: tree = '' for pkg in pkgs: flags = '' if not pkg.exists: flags = missing print('{0}{1:22s} {2}'.format(tree, pkg.name, flags)) solver(pkg, style) print('') def find_missing(pkgs, style='dependency'): package_records = {} head = '' tail = '' for pkg in pkgs: if tail: for dep_name, dep_exists in list(tail): #print('{0} {1}'.format(dep, flag)) if dep_exists: continue package_records[head].append((dep_name, dep_exists)) head = pkg.name package_records[head] = [(pkg.name, pkg.exists)] #print('{0} {1}'.format(pkg.name, flags)) yield head, package_records[head] tail = solver_ex(pkg, style) def show_missing(package_records, style_flag): for top, deps in package_records.items(): dep_output = '' for index, (dep_name, dep_exists) in enumerate(deps, 1): if not dep_exists: dep_output += dep_name if index < len(deps): dep_output += ', ' if dep_output: print('Missing {0:<11s} in {1:<20s}: {2:<1s}'.format(style_flag, top, dep_output)) if __name__ == '__main__': args = None # ENVCONFIG_PATH = os.path.abspath('/usr/local/envconfig') ENVCONFIG_PATH = os.path.abspath('/usr/local/envconfig') HOME = os.environ['HOME'] ENVRC = os.path.join(HOME, '.envrc') pkgs = [] parser = argparse.ArgumentParser() parser.add_argument('-e', '--every', action='store_true', help='Generate map for all packages') parser.add_argument('-r', '--requires', action='store_true', help='Omit package dependencies') parser.add_argument('-p', '--precedence', action='store_true', help='Omit package precedence') parser.add_argument('-w', '--warnings', action='store_true', help='No tree, only warnings') parser.add_argument('-g', '--no-graphics', action='store_true', help='No tree characters') args = parser.parse_args() if args.every: for r, d, fs in os.walk(ENVCONFIG_PATH): for f in fs: pkgs.append(Package(f)) else: try: with open(ENVRC, 'r') as fp: for record in fp: if record.startswith('#'): continue if record.startswith('Package:'): _, _, record = record.partition(':') else: continue record = record.strip() pkgs.append(Package(record)) except OSError as e: print(e.message) if not args.requires and not args.precedence: print('To see a summary of available options, issue -h or --help') exit(0) if args.warnings: dep_records = {} prec_records = {} if args.requires: dep_records = dict(find_missing(pkgs)) if args.precedence: prec_records = dict(find_missing(pkgs, 'precedence')) if dep_records: show_missing(dep_records, 'requirement') if prec_records: show_missing(prec_records, 'precedence') exit(0) if args.requires: print("#### ####") print("### Dependency Map ###") print("#### ####") show_package_map(pkgs) print('') if args.precedence: print("#### ####") print("### Precedence Map ###") print("#### ####") show_package_map(pkgs, style='precedence') print('') exit(0)