#! /usr/bin/python # SPDX-License-Identifier: CC0-1.0 import argparse import errno import fnmatch import io import itertools import os import re import shutil import struct import sys import tarfile import tempfile from subprocess import PIPE, Popen, STDOUT # Python 3 shims try: from functools import reduce except: pass try: from itertools import zip_longest as izip_longest except: from itertools import izip_longest # revs: # [ { "path", "cpuid", "pf", "rev", "date" } ] # artifacts: # * content summary (per-file) # * overlay summary (per-fms/pf) # * changelog (per-file?) # * discrepancies (per-fms/pf) log_level = 0 print_date = False file_glob = ["*??-??-??", "*microcode*.dat"] def log_status(msg, level=0): global log_level if log_level >= level: sys.stderr.write(msg + "\n") def log_info(msg, level=2): global log_level if log_level >= level: sys.stderr.write("INFO: " + msg + "\n") def log_warn(msg, level=1): global log_level if log_level >= level: sys.stderr.write("WARNING: " + msg + "\n") def log_error(msg, level=-1): global log_level if log_level >= level: sys.stderr.write("ERROR: " + msg + "\n") def remove_prefix(text, prefix): if isinstance(prefix, str): prefix = [prefix, ] for p in prefix: pfx = p if p.endswith(os.sep) else p + os.sep if text.startswith(pfx): return text[len(pfx):] return text def file_walk(args, yield_dirs=False): for content in args: if os.path.isdir(content): if yield_dirs: yield ("", content) for root, dirs, files in os.walk(content): if yield_dirs: for f in dirs: p = os.path.join(root, f) yield (remove_prefix(p, content), p) for f in files: p = os.path.join(root, f) yield (remove_prefix(p, content), p) elif os.path.exists(content): yield ("", content) else: raise IOError(errno.ENOENT, os.strerror(errno.ENOENT), content) def cpuid_fname(c): # Note that the Extended Family is summed up with the Family, # while the Extended Model is concatenated with the Model. return "%02x-%02x-%02x" % ( ((c >> 20) & 0xff) + ((c >> 8) & 0xf), ((c >> 12) & 0xf0) + ((c >> 4) & 0xf), c & 0xf) def read_revs_dir(path, args, src=None, ret=None): if ret is None: ret = [] ucode_re = re.compile('[0-9a-f]{2}-[0-9a-f]{2}-0[0-9a-f]$') ucode_dat_re = re.compile('microcode.*\.dat$') for rp, ap in file_walk([path, ]): rp_fname = os.path.basename(rp) if not ucode_re.match(rp_fname) and not ucode_dat_re.match(rp_fname): continue # Text-based format data = None if ucode_dat_re.match(rp_fname): data = io.BytesIO() with open(ap, "r") as f: for line in f: if line.startswith("/"): continue vals = line.split(",") for val in vals: val = val.strip() if not val: continue data.write(struct.pack(" 48: f.seek(hdr[7], os.SEEK_CUR) ext_tbl = struct.unpack("IIIII", f.read(20)) log_status("Found %u extended signatures for %s:%#x" % (ext_tbl[0], rp, offs), level=1) cur_offs = offs + hdr[7] + 48 + 20 ext_sig_cnt = 0 while cur_offs < offs + hdr[8] \ and ext_sig_cnt <= ext_tbl[0]: ext_sig = struct.unpack("III", f.read(12)) ignore = args.ignore_ext_dups and \ (ext_sig[0] == hdr[3]) if not ignore: ret.append({"path": rp, "src": src or path, "cpuid": ext_sig[0], "pf": ext_sig[1], "rev": hdr[1], "date": hdr[2], "offs": offs, "ext_offs": cur_offs, "cksum": hdr[4], "ext_cksum": ext_sig[2], "data_size": hdr[7], "total_size": hdr[8]}) log_status(("Got ext sig %#x/%#x for " + "%s:%#x:%#x/%#x%s") % (ext_sig[0], ext_sig[1], rp, offs, hdr[3], hdr[6], " (ignored)" if ignore else ""), level=2) cur_offs += 12 ext_sig_cnt += 1 offs += hdr[8] or 2048 except Exception as e: log_error("a problem occurred while processing %s: %s" % (ap, e), level=1) return ret def read_revs_rpm(path, args, ret=None): if ret is None: ret = [] dir_tmp = tempfile.mkdtemp() log_status("Trying to extract files from RPM \"%s\"..." % path, level=1) rpm2cpio = Popen(args=["rpm2cpio", path], stdout=PIPE, stderr=PIPE, close_fds=True) cpio = Popen(args=["cpio", "-idmv"] + file_glob, cwd=dir_tmp, stdin=rpm2cpio.stdout, stdout=PIPE, stderr=STDOUT) out, cpio_stderr = cpio.communicate() rpm2cpio_out, rpm2cpio_err = rpm2cpio.communicate() rpm2cpio_ret = rpm2cpio.returncode cpio_ret = cpio.returncode log_info("rpm2cpio exit code: %d, cpio exit code: %d" % (rpm2cpio_ret, cpio_ret)) if rpm2cpio_err: log_info("rpm2cpio stderr:\n%s" % rpm2cpio_err, level=3) if out: log_info("cpio output:\n%s" % out, level=3) if cpio_stderr: log_info("cpio stderr:\n%s" % cpio_stderr, level=3) if rpm2cpio_ret == 0 and cpio_ret == 0: ret = read_revs_dir(dir_tmp, args, path) shutil.rmtree(dir_tmp) return ret def read_revs_tar(path, args, ret=None): if ret is None: ret = [] dir_tmp = tempfile.mkdtemp() log_status("Trying to extract files from tarball \"%s\"..." % path, level=1) try: with tarfile.open(path, "r:*") as tar: for ti in tar: if any(fnmatch.fnmatchcase(ti.name, p) for p in file_glob): d = os.path.normpath(os.path.join("/", os.path.dirname(ti.name))) # For now, strip exactl one level d = os.path.join(*(d.split(os.path.sep)[2:])) n = os.path.join(d, os.path.basename(ti.name)) if not os.path.exists(d): os.makedirs(d) t = tar.extractfile(ti) with open(n, "wb") as f: shutil.copyfileobj(t, f) t.close() ret = read_revs_dir(dir_tmp, args, path) except Exception as err: log_error("Error while reading \"%s\" as a tarball: \"%s\"" % (path, str(err))) shutil.rmtree(dir_tmp) return ret def read_revs(path, args, ret=None): if ret is None: ret = [] if os.path.isdir(path): return read_revs_dir(path, args, ret) elif tarfile.is_tarfile(path): return read_revs_tar(path, args, ret) else: return read_revs_rpm(path, args, ret) def gen_mc_map(mc_data, merge=False, merge_path=False): """ Converts an array of microcode file information to a map with path/sig/pf as a key. merge: whether to leave only the newest mc variant in the map or leave all possible variants. """ res = dict() for mc in mc_data: key = (None if merge_path else mc["path"], mc["cpuid"], mc["pf"]) if key not in res: res[key] = dict() cpuid = mc["cpuid"] cur_pf = mc["pf"] pid = 1 while cur_pf > 0: if cur_pf & 1 and not (merge and pid in res[key] and res[key][pid]["rev"][0] >= mc["rev"]): if pid not in res[cpuid] or merge: res[cpuid][pid] = [] res[cpuid][pid].append(mc) cur_pf = cur_pf / 2 pid = pid * 2 return res def gen_fn_map(mc_data, merge=False, merge_path=False): res = dict() for mc in mc_data: key = (None if merge_path else mc["path"], mc["cpuid"], mc["pf"]) if key in res: log_warn("Duplicate path/cpuid/pf: %s/%#x/%#x" % key) else: res[key] = [] if merge and len(res[key]): if mc["rev"] > res[key][0]["rev"]: res[key][0] = mc else: res[key].append(mc) return res def revcmp(a, b): return b["rev"] - a["rev"] class ChangeLogEntry: ADDED = 0 REMOVED = 1 UPDATED = 2 DOWNGRADED = 3 OTHER = 4 def mc_stripped_path(mc): paths = ("usr/share/microcode_ctl/ucode_with_caveats/intel", "usr/share/microcode_ctl/ucode_with_caveats", "usr/share/microcode_ctl", "lib/firmware", "etc/firmware", ) return remove_prefix(mc["path"], paths) class mcnm: MCNM_ABBREV = 0 MCNM_FAMILIES = 1 MCNM_MODELS = 2 MCNM_FAMILIES_MODELS = 3 MCNM_CODENAME = 4 def get_mc_cnames(mc, cmap, mode=mcnm.MCNM_ABBREV, stringify=True, segment=False): if not isinstance(mc, dict): mc = mc_from_mc_key(mc) sig = mc["cpuid"] pf = mc["pf"] res = [] if not cmap: return None if sig not in cmap: log_info("No codename information for sig %#x" % sig) return None cnames = cmap[sig] if mode in (mcnm.MCNM_FAMILIES, mcnm.MCNM_MODELS, mcnm.MCNM_FAMILIES_MODELS): for c in cnames: if not (pf & c["pf_mask"]): continue for m, f in ((mcnm.MCNM_FAMILIES, "families"), (mcnm.MCNM_MODELS, "models")): if m & mode == 0: continue if f not in c or not c[f]: log_info("No %s for sig %#x in %r" % (f, sig, c)) continue res.append(c[f]) return ", ".join(res) or None steppings = dict() suffices = dict() for c in cnames: if pf and not (pf & c["pf_mask"]): continue if mode == mcnm.MCNM_ABBREV and "abbrev" in c and c["abbrev"]: cname = c["abbrev"] else: cname = c["codename"] if segment: cname = c["segment"] + " " + cname if cname not in suffices: suffices[cname] = set() if "variant" in c and c["variant"]: suffices[cname] |= set(c["variant"]) if cname not in steppings: steppings[cname] = set() if c["stepping"]: steppings[cname] |= set(c["stepping"]) for cname in sorted(steppings.keys()): cname_res = [cname] if len(suffices[cname]): cname_res[0] += "-" + "/".join(sorted(suffices[cname])) if len(steppings[cname]): cname_res.append("/".join(sorted(steppings[cname]))) res.append(" ".join(cname_res) if stringify else cname_res) return (", ".join(res) or None) if stringify else res def mc_from_mc_key(k): return dict(zip(("path", "cpuid", "pf"), k)) def mc_path(mc, pf_sfx=True, midword=None, cmap=None, cname_segment=False): if not isinstance(mc, dict): mc = mc_from_mc_key(mc) path = mc_stripped_path(mc) if mc["path"] is not None else None cpuid_fn = cpuid_fname(mc["cpuid"]) fname = os.path.basename(mc["path"] or cpuid_fn) midword = "" if midword is None else " " + midword cname = get_mc_cnames(mc, cmap, segment=cname_segment) cname_str = " (" + cname + ")" if cname else "" if pf_sfx: sfx = "/0x%02x" % mc["pf"] else: sfx = "" if not path or path == os.path.join("intel-ucode", cpuid_fn): return "%s%s%s%s" % (fname, sfx, cname_str, midword) else: return "%s%s%s%s (in %s)" % (cpuid_fn, sfx, cname_str, midword, path) def gen_changelog_file(old, new): pass def mc_cmp(old_mc, new_mc): res = [] old_mc_revs = [x["rev"] for x in old_mc] new_mc_revs = [x["rev"] for x in new_mc] common = set(old_mc_revs) & set(new_mc_revs) old_rev_list = [x for x in sorted(old_mc_revs) if x not in common] new_rev_list = [x for x in sorted(new_mc_revs) if x not in common] if len(old_rev_list) != 1 or len(new_rev_list) != 1: for i in new_mc: if i["rev"] in new_rev_list: res.append((ChangeLogEntry.ADDED, None, i)) for i in old_mc: if i["rev"] in old_rev_list: res.append((ChangeLogEntry.REMOVED, i, None)) else: for old in old_mc: if old["rev"] == old_rev_list[0]: break for new in new_mc: if new["rev"] == new_rev_list[0]: break if new["rev"] > old["rev"]: res.append((ChangeLogEntry.UPDATED, old, new)) elif new["rev"] < old["rev"]: res.append((ChangeLogEntry.DOWNGRADED, old, new)) return res def gen_changelog(old, new): res = [] old_map = gen_fn_map(old) new_map = gen_fn_map(new) old_files = set(old_map.keys()) new_files = set(new_map.keys()) both = old_files & new_files added = new_files - old_files removed = old_files - new_files for f in sorted(added): p = mc_path(new_map[f][0]) for old_f in sorted(removed): old_p = mc_path(old_map[old_f][0]) if p == old_p and f[1] == old_f[1] and f[2] == old_f[2]: log_info("Matched %s (%s and %s)" % (p, old_map[old_f][0]["path"], new_map[f][0]["path"])) added.remove(f) removed.remove(old_f) res += mc_cmp(old_map[old_f], new_map[f]) for f in sorted(added): for i in new_map[f]: res.append((ChangeLogEntry.ADDED, None, i)) for f in sorted(removed): for i in old_map[f]: res.append((ChangeLogEntry.REMOVED, i, None)) for f in sorted(both): res += mc_cmp(old_map[f], new_map[f]) return res def mc_date(mc): if isinstance(mc, dict): mc = mc["date"] return "%04x-%02x-%02x" % (mc & 0xffff, mc >> 24, (mc >> 16) & 0xff) def mc_rev(mc, date=None): ''' While revision is signed for comparison purposes, historically it is printed as unsigned, Oh well. ''' global print_date if mc["rev"] < 0: rev = 2**32 + mc["rev"] else: rev = mc["rev"] if date if date is not None else print_date: return "%#x (%s)" % (rev, mc_date(mc)) else: return "%#x" % rev def print_changelog_rpm(clog, cmap, args): for e, old, new in clog: mc_str = mc_path(new if e == ChangeLogEntry.ADDED else old, midword="microcode", cmap=cmap, cname_segment=args.segment) if e == ChangeLogEntry.ADDED: print("Addition of %s at revision %s" % (mc_str, mc_rev(new))) elif e == ChangeLogEntry.REMOVED: print("Removal of %s at revision %s" % (mc_str, mc_rev(old))) elif e == ChangeLogEntry.UPDATED: print("Update of %s from revision %s up to %s" % (mc_str, mc_rev(old), mc_rev(new))) elif e == ChangeLogEntry.DOWNGRADED: print("Downgrade of %s from revision %s down to %s" % (mc_str, mc_rev(old), mc_rev(new))) elif e == ChangeLogEntry.OTHER: print("Other change in %s:" % old["path"]) print(" old: %#x/%#x: rev %s (offs %#x)" % (old["cpuid"], old["pf"], mc_rev(old), old["offs"])) print(" new: %#x/%#x: rev %s (offs %#x)" % (new["cpuid"], new["pf"], mc_rev(new), new["offs"])) def print_changelog_intel(clog, cmap, args): def clog_sort_key(x): res = str(x[0]) if x[0] != ChangeLogEntry.ADDED: res += "%08x%02x" % (x[1]["cpuid"], x[1]["pf"]) else: res += "0" * 10 if x[0] != ChangeLogEntry.REMOVED: res += "%08x%02x" % (x[2]["cpuid"], x[2]["pf"]) else: res += "0" * 10 return res sorted_clog = sorted(clog, key=clog_sort_key) sections = (("New Platforms", (ChangeLogEntry.ADDED, )), ("Updated Platforms", (ChangeLogEntry.UPDATED, ChangeLogEntry.DOWNGRADED)), ("Removed Platforms", (ChangeLogEntry.REMOVED, ))) def print_line(e, old, new, types): if e not in types: return if not print_line.hdr: print(""" | Processor | Stepping | F-M-S/PI | Old Ver | New Ver | Products |:---------------|:---------|:------------|:---------|:---------|:---------""") print_line.hdr = True mc = new if e == ChangeLogEntry.ADDED else old cnames = get_mc_cnames(mc, cmap, stringify=False, segment=args.segment) or (("???", ""), ) for cn in cnames: cname = cn[0] stepping = cn[1] if len(cn) > 1 else "" print("| %-14s | %-8s | %8s/%02x | %8s | %8s | %s" % (cname, stepping, cpuid_fname(mc["cpuid"]), mc["pf"], ("%08x" % old["rev"]) if e != ChangeLogEntry.ADDED else "", ("%08x" % new["rev"]) if e != ChangeLogEntry.REMOVED else "", get_mc_cnames(mc, cmap, mode=mcnm.MCNM_FAMILIES, segment=args.segment) or "")) for h, types in sections: print("\n### %s" % h) print_line.hdr = False for e, old, new in sorted_clog: print_line(e, old, new, types) def print_changelog(clog, cmap, args): if args.format == "rpm": print_changelog_rpm(clog, cmap, args) elif args.format == "intel": print_changelog_intel(clog, cmap, args) else: log_error(("unknown changelog format: \"%s\". " + "Supported formats are: rpm, intel.") % args.format) class TableStyles: TS_CSV = 0 TS_FANCY = 1 def print_line(line, column_sz): print(" | ".join([str(x).ljust(column_sz[i]) for i, x in zip(itertools.count(), itertools.chain(line, [""] * (len(column_sz) - len(line))))]).rstrip()) def print_table(items, header=[], style=TableStyles.TS_CSV): if style == TableStyles.TS_CSV: for i in items: print(";".join(i)) elif style == TableStyles.TS_FANCY: column_sz = list(reduce(lambda x, y: map(max, izip_longest(x, y, fillvalue=0)), [[len(x) for x in i] for i in itertools.chain(header, items)])) for i in header: print_line(i, column_sz) if header: print("-+-".join(["-" * x for x in column_sz])) for i in items: print_line(i, column_sz) def print_summary(revs, cmap, args): m = gen_fn_map(revs) cnames_mode = mcnm.MCNM_ABBREV if args.abbrev else mcnm.MCNM_CODENAME header = [] if args.header: header.append(["Path", "Offset", "Ext. Offset", "Data Size", "Total Size", "CPUID", "Platform ID Mask", "Revision", "Date", "Checksum", "Codenames"] + (["Models"] if args.models else [])) tbl = [] for k in sorted(m.keys()): for mc in m[k]: tbl.append([mc_stripped_path(mc), "0x%x" % mc["offs"], "0x%x" % mc["ext_offs"] if "ext_offs" in mc else "-", "0x%05x" % mc["data_size"], "0x%05x" % mc["total_size"], "0x%05x" % mc["cpuid"], "0x%02x" % mc["pf"], mc_rev(mc, date=False), mc_date(mc), "0x%08x" % (mc["ext_cksum"] if "ext_cksum" in mc else mc["cksum"]), get_mc_cnames(mc, cmap, cnames_mode, segment=args.segment) or ""] + ([get_mc_cnames(mc, cmap, mcnm.MCNM_FAMILIES_MODELS, segment=args.segment)] if args.models else [])) print_table(tbl, header, style=TableStyles.TS_FANCY) def read_codenames_file(path): ''' Supports two formats: new and old * old: tab-separated. Field order: Segment, (unused), Codename, (dash-separated) Stepping, Platform ID mask, CPUID, (unused) Update link, (unused) Specs link * new: semicolon-separated; support comments. Distinguished by the first line that starts with octothorp. Field order: Segment, Unused, Codename, Stepping, Platform ID mask, CPUID, Abbreviation, Variant(s), Families, Models ''' old_fields = ["segment", "_", "codename", "stepping", "pf_mask", "sig", "_update", "_specs"] new_fields = ["segment", "_", "codename", "stepping", "pf_mask", "sig", "abbrev", "variant", "families", "models"] new_fmt = False field_names = old_fields res = dict() try: with open(path, "r") as f: for line in f: line = line.strip() if len(line) == 0: continue if line[0] == '#': new_fmt = True field_names = new_fields continue fields = line.split(";" if new_fmt else "\t", 1 + len(field_names)) fields = dict(zip(field_names, fields)) if "sig" not in fields: log_warn("Skipping %r (from \"%s\")" % (fields, line)) continue sig = fields["sig"] = int(fields["sig"], 16) fields["pf_mask"] = int(fields["pf_mask"], 16) fields["stepping"] = fields["stepping"].split(",") if "variant" in fields: if fields["variant"]: fields["variant"] = fields["variant"].split(",") else: fields["variant"] = [] if sig not in res: res[sig] = list() res[sig].append(fields) except Exception as e: log_error("a problem occurred while reading code names: %s" % e) return res def print_discrepancies(rev_map, deps, cmap, args): """ rev_map: dict "name": revs deps: list of tuples (name, parent/None) """ sigs = set() for p, r in rev_map.items(): sigs |= set(r.keys()) if args.header: header1 = ["sig"] if args.print_vs: header2 = [""] for p, n, d in deps: header1.append(n) if args.print_vs: add = "" if d: for pd, nd, dd in deps: if pd == d: add = "(vs. %s)" % nd break header2.append(add) if args.models: header1.append("Model names") if args.print_vs: header2.append("") header = [header1] + ([header2] if args.print_vs else []) tbl = [] for s in sorted(sigs): out = [mc_path(s)] print_out = not args.print_filter print_date = args.min_date is None for p, n, d in deps: cur = dict([(x["rev"], x) for x in rev_map[p][s]]) \ if s in rev_map[p] else [] v = "/".join([mc_rev(y) for x, y in sorted(cur.items())]) \ if cur else "-" if d is not None: prev = [x["rev"] for x in rev_map[d][s]] if s in rev_map[d] \ else [] if [x for x in cur if x not in prev]: v += " (*)" print_out = True if args.min_date is not None and s in rev_map[p]: for x in rev_map[p][s]: print_date |= mc_date(x) > args.min_date out.append(v) if print_out and print_date: if args.models: out.append(get_mc_cnames(s, cmap, segment=args.segment) or "") tbl.append(out) print_table(tbl, header, style=TableStyles.TS_FANCY) def cmd_summary(args): revs = [] for p in args.filelist: revs = read_revs(p, args, ret=revs) codenames_map = read_codenames_file(args.codenames) print_summary(revs, codenames_map, args) return 0 def cmd_changelog(args): codenames_map = read_codenames_file(args.codenames) base_path = args.filelist[0] upd_path = args.filelist[1] base = read_revs(base_path, args) upd = read_revs(upd_path, args) print_changelog(gen_changelog(base, upd), codenames_map, args) return 0 def cmd_discrepancies(args): """ filenames: * "<" prefix (possibly multiple times) to refer to a previous entry to compare against * "[name]" prefix is a name reference """ codenames_map = read_codenames_file(args.codenames) rev_map = dict() deps = list() cur = -1 for path in args.filelist: orig_path = path name = None cur += 1 dep = None while True: if path[0] == '<': path = path[1:] dep = cur - 1 if dep is None else dep - 1 elif path[0] == '[' and path.find(']') > 0: pos = path.find(']') name = path[1:pos] path = path[pos + 1:] else: break if name is None: name = path if dep is not None and dep < 0: log_error("Incorrect dep reference for '%s' (points to index %d)" % (orig_path, dep)) return 1 deps.append((path, name, deps[dep][0] if dep is not None else None)) rev_map[path] = gen_fn_map(read_revs(path, args), merge=args.merge, merge_path=True) print_discrepancies(rev_map, deps, codenames_map, args) return 0 def parse_cli(): root_parser = argparse.ArgumentParser(prog="gen_updates", description="Intel CPU Microcode " + "parser") root_parser.add_argument("-C", "--codenames", default='codenames', help="Code names file") root_parser.add_argument("-v", "--verbose", action="count", default=0, help="Increase output verbosity") root_parser.add_argument("-E", "--no-ignore-ext-duplicates", action="store_const", dest="ignore_ext_dups", default=False, const=False, help="Do not ignore duplicates of the main " + "signature in the extended signature header") root_parser.add_argument("-e", "--ignore-ext-duplicates", action="store_const", dest="ignore_ext_dups", const=True, help="Ignore duplicates of the main signature " + "in the extended signature header") root_parser.add_argument("-t", "--print-segment", action="store_const", dest="segment", const=True, help="Print model segment") root_parser.add_argument("-T", "--no-print-segment", action="store_const", dest="segment", const=False, default=False, help="Do not print model segment") cmdparsers = root_parser.add_subparsers(title="Commands", help="main gen_updates commands") parser_s = cmdparsers.add_parser("summary", help="Generate microcode summary") parser_s.add_argument("-a", "--abbreviate", action="store_const", dest="abbrev", const=True, default=True, help="Abbreviate code names") parser_s.add_argument("-A", "--no-abbreviate", action="store_const", dest="abbrev", const=False, help="Do not abbreviate code names") parser_s.add_argument("-m", "--print-models", action="store_const", dest="models", const=True, default=False, help="Print models") parser_s.add_argument("-M", "--no-print-models", action="store_const", dest="models", const=False, help="Do not print models") parser_s.add_argument("-H", "--no-print-header", action="store_const", dest="header", const=False, default=True, help="Do not print hader") parser_s.add_argument("filelist", nargs="*", default=[], help="List or RPMs/directories to process") parser_s.set_defaults(func=cmd_summary) parser_c = cmdparsers.add_parser("changelog", help="Generate changelog") parser_c.add_argument("-F", "--format", choices=["rpm", "intel"], default="rpm", help="Changelog format") parser_c.add_argument("filelist", nargs=2, help="RPMs/directories to compare") parser_c.set_defaults(func=cmd_changelog) parser_d = cmdparsers.add_parser("discrepancies", help="Generate discrepancies") parser_d.add_argument("-s", "--merge-revs", action="store_const", dest="merge", const=True, default=False, help="Merge revisions that come" + " from different files") parser_d.add_argument("-S", "--no-merge-revs", action="store_const", dest="merge", const=False, help="Do not Merge revisions that come" + " from different files") parser_d.add_argument("-v", "--print-vs", action="store_const", dest="print_vs", const=True, default=False, help="Print base version ") parser_d.add_argument("-V", "--no-print-vs", action="store_const", dest="print_vs", const=False, help="Do not Merge revisions that come" + " from different files") parser_d.add_argument("-m", "--print-models", action="store_const", dest="models", const=True, default=True, help="Print model names") parser_d.add_argument("-M", "--no-print-models", action="store_const", dest="models", const=False, help="Do not print model names") parser_d.add_argument("-H", "--no-print-header", action="store_const", dest="header", const=False, default=True, help="Do not print hader") parser_d.add_argument("-a", "--print-all-files", action="store_const", dest="print_filter", const=False, default=True, help="Print all files") parser_d.add_argument("-c", "--print-changed-files", action="store_const", dest="print_filter", const=True, help="Print only changed files") parser_d.add_argument("-d", "--min-date", action="store", help="Minimum date filter") parser_d.add_argument("filelist", nargs='*', help="RPMs/directories to compare") parser_d.set_defaults(func=cmd_discrepancies) args = root_parser.parse_args() if not hasattr(args, "func"): root_parser.print_help() return None global log_level log_level = args.verbose return args def main(): args = parse_cli() if args is None: return 1 return args.func(args) if __name__ == "__main__": sys.exit(main())