#!/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 string import Template 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 not load: return self.preload() def __repr__(self): return self.name def preload(self): if os.path.exists(self.filename): self.exists = True self.name = os.path.basename(self.filename.replace(' ', '_')) 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 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('') if __name__ == '__main__': args = None 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: for pkg in pkgs: pkg.verbose = True if not pkg.exists: # Normally the object performs its own checks to make sure # it can read precedence, but we're overriding that functionality. print('General failure, {0}: {1} does not exist'.format(pkg.name)) if args.requires: pkg.get_requirements() if args.precedence: pkg.get_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)