diff --git a/coreos-assembler b/coreos-assembler index 18b99d8935..0749f030fd 100755 --- a/coreos-assembler +++ b/coreos-assembler @@ -19,7 +19,7 @@ if [ -e /sys/fs/selinux/status ]; then fi cmd=${1:-} -build_commands="init fetch build run clean" +build_commands="init fetch analyze build run clean" other_commands="shell" utility_commands="gf-oemid virt-install" if [ -z "${cmd}" ]; then diff --git a/deps.txt b/deps.txt index 52ce40edd7..01f2a55359 100644 --- a/deps.txt +++ b/deps.txt @@ -11,6 +11,9 @@ rpm-ostree # rpmdistro-gitoverlay deps dnf-plugins-core createrepo_c dnf-utils fedpkg openssh-clients rpmdistro-gitoverlay +# coreos-assembler analyze +python3-dnf + # Currently a transitive req of rpmdistro-gitoverlay via mock, but we # expect people to use these explicitly in their repo configurations. distribution-gpg-keys diff --git a/src/cmd-analyze b/src/cmd-analyze new file mode 100644 index 0000000000..3a0b60cd4f --- /dev/null +++ b/src/cmd-analyze @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail + +dn=$(dirname $0) +. ${dn}/cmdlib.sh + +print_help() { + cat 1>&2 <<'EOF' +Usage: coreos-assembler analyze --help + +EOF +} + +# Call getopt to validate the provided input. +options=$(getopt --options hf --longoptions help,force -- "$@") +[ $? -eq 0 ] || { + print_help + exit 1 +} +eval set -- "$options" +while true; do + case "$1" in + -h | --help) + print_help + exit 0 + ;; + *) + print_help + fatal "init: unrecognized option: $1" + exit 1 + ;; + esac + shift +done + +# LOOP: if rpm-ostree-entrypoint.yaml exist; do +# append PKG[] with "*.yaml"."packages*" +# if "*.yaml"."include" exist; LOOP +# else; improper coreos-assembler init + +# analyze.py $PKG $REPOS $FLAGS diff --git a/src/dnf-analyze b/src/dnf-analyze new file mode 100644 index 0000000000..71efb96b82 --- /dev/null +++ b/src/dnf-analyze @@ -0,0 +1,135 @@ +#!/usr/bin/python3 + +# GLOBAL +import dnf,argparse,os,math +interpretedLabel = ["python", "perl"] +interpretedPkg = [] +query = dnf.query.Query + +# CLI ENTRYPOINT +parser = argparse.ArgumentParser() +parser.add_argument('--packages',help='Comma Separated list of packages to analyze.',action='store', required=True) +parser.add_argument('--repo-dir',help='Directory which stores all .repo definitions.',action='store', required=True) +parser.add_argument('--log-dir',help='If set, log to file and stdout.',action='store') +parser.add_argument('--size',help="Display total dependency tree size.",action='store_true') +parser.add_argument('--exit-on',help="Emphasize interpreted dependencies through 'exit 1'",action='store_true') +args = parser.parse_args() + +# CLI VERIFY +def directoryExists(path): + if os.path.isdir(path): + if os.access(path, os.W_OK): + return True + return False + +if not directoryExists(args.repo_dir): + print('Bad Repo Directory.') + exit(1) +if not directoryExists(args.log_dir): + print('Bad Log Directory: Defaulting to /var/log') + args.log_dir = '/var/log' +if (args.size != True) or (args.size != False): + print('Size must be set to True or False.') + exit(1) +if (args.exit_on != True) or (args.exit_on != False): + print('Exit On must be set to True or False.') + exit(1) + +# CLASS +class Package(object): + def printTree(self): + # Make an indentation format for easier read. + ind = '-' * self.generation + # Print dnf.package.Package.name. + print(ind + self.pkg_meta.name) + # Print size in MB if args.size. + if args.size: + print(ind + 'size: ' + (self.pkg_meta.downloadsize / 2^20)) + # Print number of depends. + print(ind + 'depends: ' + len(self.children)) + # Print if it is interpreted. + print(ind + 'isInterpreted: ' + str(self.interpreted)) + for x in self.children: + x.printTree() + + def findChildren(self): + # First, prevent recursive scans i.e. glib requires glib translations which require glib. + # However, this cannot be done in a way where each node is aware of the rest of the tree; this would result in an incomplete dependency tree. + toScan = [] + for x in self.pkg_meta.requires: + # Convert hawkey.ReqDep to string. + i = str(x) + if i not in self.scanned: + self.scanned.append(i) + toScan.append(i) + # Scan the packages which do not already exist in this tree path but pass in the whole tree path to the next child. + for x in toScan: + j = query.filter(name=x,latest=True) + self.children.append(Package(query,self.generation,j[0],self.scanned)) + + def interpreted(self): + # For each package listed as examples for interpreted languages... + for x in interpretedLabel: + # If one is found in the Package.pkg_meta.name, then label this Package object as an interpreted object. + if x in self.pkg_meta.name: + self.interpreted = True + # If this package already hasn't been scanned, labeled interpreted, and placed in interpretedPkg, place in interpretedPkg. + if self.pkg_meta.name not in interpretedPkg: + interpretedPkg.append(self.pkg_meta.name) + + def __init__(self,generation,pkg,scanned): + # ATTR + self.generation = generation + 1 + self.pkg_meta = pkg + self.children = [] + self.scanned = scanned + self.interpreted = False + + # METHOD + interpreted() + findChildren() + +# Split comma delimited packages into an array. +base_packages = args.packages.split(",") + +# Init DNF object. +base = dnf.Base() + +# DNF config. +base_config = base.conf +base_config.assumeyes = "true" +base_config.best = "true" +base_config.cachedir = "/var/cache/dnf" +base_config.logdir = args.log +base_config.reposdir = args.repo_dir + +# Parse repos in dir and fill sack without system rpm scanning. +base.read_all_repos() +base.fill_sack(load_system_repo=False,load_available_repos=True) + +# Init query object on new sack. From here on out, no changes are made to query. +query = base.sack.query() + +# Replace each element in the base_packages array with a Packages object. +for x in range(0,len(base_packages)): + i = base_packages[x] + # Assuming best first, get latest package. + j = query.filter(name=i,latest=True) + # Use first item in j JIC. + base_packages[x] = Package(query,0,j[0],"") + +# Major print functions. +for x in base_backages: + x.printTree() + +if len(interpreted) > 0: + for x in interpreted: + print('Interpreted Package: ' + x) + print('Total Interpreted Packages: ' + len(interpreted)) + if args.exit_on: + exit(1) + +# TODO: print which base packages introduce interpreted dependencies. +# TODO: print which packages require interpreted dependencies. + +exit(0)