Last active
July 3, 2018 07:32
-
-
Save tomschr/23e3038d4678404072e75e4a12b81455 to your computer and use it in GitHub Desktop.
Returns a dependency graph for a given, installed RPM package
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
"""Returns a dependency graph for a given, installed RPM package | |
Usage: | |
rpmdep [-h | --help] | |
rpmdep [-v ...] [options] RPM | |
Required Arguments. | |
RPM The installed RPM package | |
Options: | |
-h, --help Shows this help | |
-v Raise verbosity level | |
--version Prints the version | |
Examples: | |
# Returns the dependency tree of rpm | |
$ rpmdep rpm | |
coreutils | |
info | |
diffutils | |
fillup | |
grep | |
""" | |
__version__ = "0.2.0" | |
__author__ = "Thomas Schraitle <toms AT suse DOT de>" | |
from docopt import docopt, DocoptExit | |
import logging | |
from logging import (# BASIC_FORMAT, | |
CRITICAL, | |
DEBUG, | |
FATAL, | |
ERROR, | |
INFO, | |
NOTSET, | |
WARN, | |
WARNING, | |
) | |
from logging.config import dictConfig | |
import re | |
import subprocess | |
import sys | |
#: Map verbosity to log levels | |
LOGLEVELS = {None: WARNING, # 0 | |
0: WARNING, | |
1: INFO, | |
2: DEBUG, | |
} | |
#: Default logging dict for :class:`logging.config.dictConfig`: | |
DEFAULT_LOGGING_DICT = { | |
'version': 1, | |
'disable_existing_loggers': False, | |
'formatters': { | |
'standard': { | |
# See https://docs.python.org/3.5/library/logging.html#logrecord-attributes | |
'format': '[%(levelname)s] %(name)s::%(funcName)s: %(message)s' | |
}, | |
}, | |
'handlers': { | |
'default': { | |
'level': 'NOTSET', | |
'formatter': 'standard', | |
'class': 'logging.StreamHandler', | |
'stream': 'ext://sys.stderr', | |
}, | |
}, | |
'loggers': { | |
'rpmdep': { | |
'handlers': ['default'], | |
'level': 'INFO', | |
'propagate': True | |
} | |
} | |
} | |
log = logging.getLogger("rpmdep") | |
def parsecli(cliargs=None): | |
"""Parse CLI arguments with docopt | |
:param list cliargs: List of commandline arguments | |
:return: dictionary from docopt | |
:rtype: dict | |
""" | |
version = "%s %s" % ("rpmdep", __version__) | |
args = docopt(__doc__, argv=cliargs, version=version) | |
# This is just needed to introduce some ignore lists later | |
# TODO: Remove this if-clause after ignore list works | |
if not args.get('--ignore'): | |
args['--ignore'] = None | |
args['--ignore'] = re.split("[;,]", args['--ignore']) if args['--ignore'] is not None else None | |
dictConfig(DEFAULT_LOGGING_DICT) | |
# logging.basicConfig(handler=logging.StreamHandler,) | |
log.setLevel(LOGLEVELS.get(args['-v'], logging.DEBUG)) | |
log.debug("CLI result: %s", args) | |
return args | |
def checkargs(args): | |
"""Check arguments | |
:param args: dictionary from docopt parsing | |
""" | |
log.debug("Checking arguments") | |
if not args['RPM']: | |
raise DocoptExit("ERROR: Need a RPM name") | |
def main(cliargs=None): | |
"""Entry point for the application script | |
:param list cliargs: Arguments to parse or None (=use sys.argv) | |
:return: return codes from ``ERROR_CODES`` | |
""" | |
try: | |
args = parsecli(cliargs) | |
checkargs(args) | |
result = process(args) | |
log.info("Done.") | |
return result | |
except KeyboardInterrupt: | |
return 10 | |
def filterdeps(deps): | |
"""Filter dependency list | |
""" | |
ignore = ['bash'] | |
for d in deps: | |
# Handle 'bash >= 4' and return 'bash' | |
d = d.split(" ")[0] | |
if d in ignore or d.startswith("/") or "(" in d: | |
continue | |
yield d | |
def rpmrequires(name, ignores=None, ignoreerror=True): | |
"""Return a list of dependencies for a given name | |
:param name: the RPM name | |
""" | |
ignores = ignores if ignores is not None else [] | |
if name in ignores: | |
log.info("Ignoring %s", name) | |
return [] | |
log.info("Investigating %s...", name) | |
try: | |
result = subprocess.check_output(['rpm', '-q', '--requires', name], | |
universal_newlines=True) | |
except subprocess.CalledProcessError: | |
if not ignoreerror: | |
raise RuntimeError("Cannot find %r RPM" % name) | |
return [] | |
possibledeps = result.rstrip().split("\n") | |
return possibledeps | |
SEENRPMS = set(['distribution-release', 'atk', 'glib2', 'gtk2', 'libglade2', | |
'pango', 'awk', 'insserv', 'xsltproc', 'bash', | |
'ghostscript-library', | |
# general names: | |
'jaxp_parser_impl', 'jre', 'jpackage-utils', 'xml-commons-apis', | |
'xml-commons-resolver', 'libxml2', 'libxslt', 'java', | |
'jakarta-commons-io', 'jakarta-commons-logging' | |
] | |
) | |
def handlerpms(possibledeps, indent="", ignores=None): | |
"""Handle the RPM and sub dependencies | |
:param possibledeps: a list of possible RPM names | |
:param indent: indentation level (spaces) | |
""" | |
for d in filterdeps(possibledeps): | |
# Avoid recursion | |
if d in SEENRPMS: | |
continue | |
print(indent, d) | |
SEENRPMS.add(d) | |
subrpms = rpmrequires(d, ignores=ignores) | |
handlerpms(subrpms, indent=" ", ignores=ignores) | |
def process(args): | |
"""Process RPM | |
""" | |
log.info("Process dependencies...") | |
log.info("Ignored packages %s", args['--ignore']) | |
try: | |
possibledeps = rpmrequires(args['RPM'], args['--ignore']) | |
handlerpms(possibledeps, ignores=args['--ignore']) | |
except RuntimeError as error: | |
log.error(error) | |
return 0 | |
if __name__ == "__main__": | |
sys.exit(main(sys.argv[1:])) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment