You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

942 lines
34 KiB

#!/usr/bin/env python3
# Sync a suite with a Seed list.
# Copyright (C) 2004, 2005, 2009, 2010, 2011, 2012 Canonical Ltd.
# Author: James Troup <james.troup@canonical.com>
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
# XXX - add indication if all of the binaries of a source packages are
# listed for promotion at once
# i.e. to allow 'change-override -S' usage
__metaclass__ = type
import atexit
from collections import defaultdict, OrderedDict
import copy
import csv
import gzip
try:
from html import escape
except ImportError:
from cgi import escape
import json
from operator import attrgetter
from optparse import OptionParser
import os
import shutil
import sys
import tempfile
from textwrap import dedent
import time
from urllib.parse import quote_plus
import apt_pkg
from launchpadlib.launchpad import Launchpad
from charts import make_chart, make_chart_header
tempdir = None
archive_source = {}
archive_binary = {}
current_source = {}
current_binary = {}
germinate_source = {}
germinate_binary = {}
seed_source = defaultdict(set)
seed_binary = defaultdict(set)
class MIRLink:
def __init__(self, id, status, title, assignee):
self.id = id
self.status = status
self.title = title
self.assignee = assignee
def __str__(self):
if self.status not in ('Fix Committed', 'Fix Released') and self.assignee:
s = "MIR: #%d (%s for %s)" % (self.id, self.status,
self.assignee.display_name)
else:
s = "MIR: #%d (%s)" % (self.id, self.status)
# no need to repeat the standard title
if not self.title.startswith("[MIR]"):
s += " %s" % self.title
return s
def html(self):
h = 'MIR: <a href="https://launchpad.net/bugs/%d">#%d</a> (%s)' % (
self.id, self.id, escape(self.status))
# no need to repeat the standard title
if not self.title.startswith("[MIR]"):
h += " %s" % escape(self.title)
return h
def ensure_tempdir():
global tempdir
if not tempdir:
tempdir = tempfile.mkdtemp(prefix='component-mismatches')
atexit.register(shutil.rmtree, tempdir)
def decompress_open(tagfile):
ensure_tempdir()
decompressed = tempfile.mktemp(dir=tempdir)
fin = gzip.GzipFile(filename=tagfile)
with open(decompressed, 'wb') as fout:
fout.write(fin.read())
return open(decompressed, 'r')
def read_current_source(options):
for suite in options.suites:
for component in options.all_components:
sources_path = "%s/dists/%s/%s/source/Sources.gz" % (
options.archive_dir, suite, component)
for section in apt_pkg.TagFile(decompress_open(sources_path)):
if 'Package' in section and 'Version' in section:
(pkg, version) = (section['Package'], section['Version'])
if pkg not in archive_source:
archive_source[pkg] = (version, component)
else:
if apt_pkg.version_compare(
archive_source[pkg][0], version) < 0:
archive_source[pkg] = (
version, component.split("/")[0])
for pkg, (version, component) in list(archive_source.items()):
if component in options.components:
current_source[pkg] = (version, component)
def read_current_binary(options):
components_with_di = []
for component in options.all_components:
components_with_di.append(component)
components_with_di.append('%s/debian-installer' % component)
for suite in options.suites:
for component in components_with_di:
for arch in options.architectures:
binaries_path = "%s/dists/%s/%s/binary-%s/Packages.gz" % (
options.archive_dir, suite, component, arch)
for section in apt_pkg.TagFile(decompress_open(binaries_path)):
if 'Package' in section and 'Version' in section:
(pkg, version) = (section['Package'],
section['Version'])
if 'source' in section:
src = section['Source'].split(" ", 1)[0]
else:
src = section['Package']
if pkg not in archive_binary:
archive_binary[pkg] = (
version, component.split("/")[0], src)
else:
if apt_pkg.version_compare(
archive_binary[pkg][0], version) < 0:
archive_binary[pkg] = (version, component, src)
for pkg, (version, component, src) in list(archive_binary.items()):
if component in options.components:
current_binary[pkg] = (version, component, src)
def read_germinate(options):
for flavour in reversed(options.flavours.split(",")):
# List of seeds
seeds = ["all"]
try:
filename = "%s/structure_%s_%s_i386" % (
options.germinate_path, flavour, options.suite)
with open(filename) as structure:
for line in structure:
if not line or line.startswith('#') or ':' not in line:
continue
seeds.append(line.split(':')[0])
except IOError:
continue
# ideally supported+build-depends too, but Launchpad's
# cron.germinate doesn't save this
for arch in options.architectures:
for seed in seeds:
filename = "%s/%s_%s_%s_%s" % (
options.germinate_path, seed, flavour, options.suite, arch)
with open(filename) as f:
for line in f:
# Skip header and footer
if (line[0] == "-" or line.startswith("Package") or
line[0] == " "):
continue
# Skip empty lines
line = line.strip()
if not line:
continue
pkg, source, why = [word.strip()
for word in line.split('|')][:3]
if seed == "all":
germinate_binary[pkg] = (
source, why, flavour, arch)
germinate_source[source] = (flavour, arch)
else:
seed_binary[seed].add(pkg)
seed_source[seed].add(source)
def is_included_binary(options, pkg):
if options.include:
for seed in options.include.split(","):
if seed in seed_binary and pkg in seed_binary[seed]:
return True
return False
return True
def is_excluded_binary(options, pkg):
if options.exclude:
seeds = set(seed_binary) - set(options.exclude.split(","))
for seed in seeds:
if seed in seed_binary and pkg in seed_binary[seed]:
return False
for seed in options.exclude.split(","):
if seed in seed_binary and pkg in seed_binary[seed]:
return True
return False
def is_included_source(options, pkg):
if options.include:
for seed in options.include.split(","):
if seed in seed_source and pkg in seed_source[seed]:
return True
return False
return True
def is_excluded_source(options, pkg):
if options.exclude:
seeds = set(seed_source) - set(options.exclude.split(","))
for seed in seeds:
if seed in seed_source and pkg in seed_source[seed]:
return False
for seed in options.exclude.split(","):
if seed in seed_source and pkg in seed_source[seed]:
return True
return False
def get_source(binary):
return current_binary[binary][2]
def find_signer(options, source):
# look at the source package publishing history for the most recent
# package_signer, a copy from debian won't have a package signer
series = options.distro.getSeries(name_or_version=options.suite)
publications = options.archive.getPublishedSources(
distro_series=series, source_name=source,
exact_match=True)
if not publications:
return('no publications found', '')
sorted_pubs = sorted(
[ps for ps in publications if ps.date_published is not None],
key=attrgetter('date_published'), reverse=True)
for pub in sorted_pubs:
if pub.package_signer:
signer = pub.package_signer.name
web_link = pub.package_signer.web_link
return(signer, web_link)
else:
signer = ''
web_link = ''
return (signer, web_link)
def do_reverse(options, source, binaries, why_d):
global signers
try:
signers.keys()
except NameError:
signers = {}
output = []
depend = {}
recommend = {}
build_depend = {}
for binary in binaries:
why = why_d[source][binary]
if why.find("Build-Depend") != -1:
why = why.replace("(Build-Depend)", "").strip()
build_depend[why] = ""
elif why.find("Recommends") != -1:
why = why.replace("(Recommends)", "").strip()
recommend[why] = ""
else:
depend[why] = ""
def do_category(map, category):
keys = []
for k in map:
if k.startswith('Rescued from '):
pkg = k.replace('Rescued from ', '')
else:
pkg = k
# seed names have spaces in them
if ' ' not in pkg:
try:
source = get_source(pkg)
except KeyError:
source = pkg
pass
if source not in signers:
signer, web_link = find_signer(options, source)
if signer and web_link:
signers[source] = (signer, web_link)
if k in current_binary:
keys.append('%s (%s)' % (k, current_binary[k][1].upper()))
elif k in current_source:
keys.append('%s (%s)' % (k, current_source[k][1].upper()))
else:
keys.append(k)
keys.sort()
if keys:
return ["[Reverse-%s: %s]" % (category, ", ".join(keys))]
else:
return []
output.extend(do_category(depend, 'Depends'))
output.extend(do_category(recommend, 'Recommends'))
output.extend(do_category(build_depend, 'Build-Depends'))
return output
def do_dot(why, fd, mir_bugs, suite):
# write dot graph for given why dictionary
written_nodes = set()
fd.write(
'digraph "component-mismatches: movements to main/restricted" {\n')
for s, binwhy in why.items():
for binary, why in binwhy.items():
# ignore binaries from this source, and "rescued"
if why in binwhy or why.startswith('Rescued'):
continue
if "(Recommends)" in why:
relation = " R "
color = "gray"
why = why.replace(" (Recommends)", "")
elif "Build-Depend" in why:
relation = " B"
color = "blue"
why = why.replace(" (Build-Depend)", "")
else:
relation = ""
color = "black"
try:
why = get_source(why)
except KeyError:
# happens for sources which are in universe, or seeds
try:
why = germinate_binary[why][0]
except:
pass
# helper function to write a node
def write_node(name):
# ensure to only write it once
if name in written_nodes:
return name
written_nodes.add(name)
fd.write(' "%s" [label="%s" style="filled" tooltip="%s"' %
(name, name, ', '.join(package_team_mapping[name])))
mirs = mir_bugs.get(name, [])
approved_mirs = [
id for id, status, title, assignee in mirs
if status in ('Fix Committed', 'Fix Released')]
url = None
if name.endswith(' seed'):
fc = "green"
elif name in current_source:
fc = "lightgreen"
url = ("https://launchpad.net/ubuntu/+source/%s" %
quote_plus(name))
elif approved_mirs:
fc = "yellow"
url = "https://launchpad.net/bugs/%i" % approved_mirs[0]
elif mirs:
if mirs[0][1] == 'Incomplete':
fc = "darkkhaki"
else:
fc = "darksalmon"
url = "https://launchpad.net/bugs/%i" % mirs[0][0]
else:
fc = "white"
# Need to use &amp; otherwise the svg will have a syntax error
url = ("https://launchpad.net/ubuntu/+source/%s/+filebug?"
"field.title=%s&amp;field.status=Incomplete"
"&amp;field.tags=%s" %
(quote_plus(name), quote_plus("[MIR] %s" % name),
quote_plus(suite)))
fd.write(' fillcolor="%s"' % fc)
if url:
fd.write(' URL="%s"' % url)
fd.write("]\n")
return name
s_node = write_node(s)
why_node = write_node(why)
# generate relation
fd.write(' "%s" -> "%s" [label="%s" color="%s" '
'fontcolor="%s"]\n' %
(why_node, s_node, relation, color, color))
# add legend
fd.write("""
{
rank="source"
NodeLegend[shape=none, margin=0, label=<
<table border="0" cellborder="1" cellspacing="0" cellpadding="4">
<tr><td>Nodes</td></tr>
<tr><td bgcolor="green">seed</td></tr>
<tr><td bgcolor="lightgreen">in main/restricted </td></tr>
<tr><td bgcolor="yellow">approved MIR (clickable)</td></tr>
<tr><td bgcolor="darksalmon">unapproved MIR (clickable)</td></tr>
<tr><td bgcolor="darkkhaki">Incomplete/stub MIR (clickable)</td></tr>
<tr><td bgcolor="white">No MIR (click to file one)</td></tr>
</table>
>];
EdgeLegend[shape=none, margin=0, label=<
<table border="0" cellborder="1" cellspacing="0" cellpadding="4">
<tr><td>Edges</td></tr>
<tr><td>Depends:</td></tr>
<tr><td><font color="gray">Recommends:</font></td></tr>
<tr><td><font color="blue">Build-Depends: </font></td></tr>
</table>
>];
}
}
""")
def filter_source(component, sources):
return [
s for s in sources
if s in archive_source and archive_source[s][1] == component]
def filter_binary(component, binaries):
return [
b for b in binaries
if b in archive_binary and archive_binary[b][1] == component]
package_team_mapping = defaultdict(set)
def get_teams(options, source):
global package_team_mapping
if os.path.exists(options.package_team_mapping):
with open(options.package_team_mapping) as ptm_file:
for team, packages in list(json.load(ptm_file).items()):
if team == "unsubscribed":
continue
for package in packages:
package_team_mapping[package].add(team)
if source in package_team_mapping:
for team in package_team_mapping[source]:
yield team
elif package_team_mapping:
yield "unsubscribed"
def print_section_text(options, header, body,
source_and_binary=False, binary_only=False):
if body:
print(" %s" % header)
print(" %s" % ("-" * len(header)))
print()
for entry in body:
line = entry[0]
source = line[0]
binaries = " ".join(line[1:])
if source_and_binary:
print(" o %s: %s" % (source, binaries))
elif binary_only:
indent_right = 75 - len(binaries) - len(source) - 2
print(" o %s%s{%s}" % (binaries, " " * indent_right, source))
else:
print(" o %s" % source)
for line in entry[1:]:
print(" %s" % line)
if len(entry) != 1:
print()
if len(body[-1]) == 1:
print()
print("=" * 70)
print()
def print_section_html(options, header, body,
source_and_binary=False, binary_only=False):
if body:
def print_html(*args, **kwargs):
print(*args, file=options.html_output, **kwargs)
def source_link(source):
return (
'<a href="https://launchpad.net/ubuntu/+source/%s">%s</a>' % (
escape(source, quote=True), escape(source)))
print_html("<h2>%s</h2>" % escape(header))
print_html("<table>")
for entry in body:
line = entry[0]
source = line[0]
binaries = " ".join(line[1:])
if source_and_binary:
print_html(
'<tr><th colspan="2">%s: %s' % (
source_link(source), escape(binaries)))
elif binary_only:
print_html('<tr><th>%s</th>' % escape(binaries), end="")
print_html(
"<th><small>%s</small></th></tr>" % source_link(source))
else:
print_html(
'<tr><th colspan="2">%s</th></tr>' % source_link(source))
for line in entry[1:]:
if isinstance(line, MIRLink):
line = line.html()
else:
for item in line.strip('[]').split(' '):
if item.strip(',') in signers:
comma = ''
if item.endswith(','):
comma = ','
pkg = item.strip(',')
else:
pkg = item
# neither of these will help fix the issue
if signers[pkg][0] in ['ps-jenkins',
'ci-train-bot']:
continue
line = line.replace(item, '%s (Uploader: %s)%s' %
(pkg, signers[pkg][0], comma))
line = escape(line)
print_html(
'<tr><td colspan="2"><span class="note">%s'
'</span></td></tr>' % line)
print_html("</table>")
def do_output(options,
orig_source_add, orig_source_remove, binary_add, binary_remove,
mir_bugs):
results = {}
results["time"] = int(options.time * 1000)
global package_team_mapping
package_team_mapping = defaultdict(set)
if os.path.exists(options.package_team_mapping):
with open(options.package_team_mapping) as ptm_file:
for team, packages in (json.load(ptm_file).items()):
if team == "unsubscribed":
continue
for package in packages:
package_team_mapping[package].add(team)
if options.html_output is not None:
print(dedent("""\
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type"
content="text/html; charset=utf-8" />
<title>Component mismatches for %s</title>
<style type="text/css">
body { background: #CCCCB0; color: black; }
a { text-decoration: none; }
table { border-collapse: collapse; border-style: none none;
margin-bottom: 3ex; empty-cells: show; }
table th { text-align: left;
border-style: groove none none none;
border-width: 3px; padding-right: 10px;
font-weight: normal; }
table td { vertical-align: top; text-align: left;
border-style: none none;
border-width: 1px; padding-right: 10px; }
.note { margin-left: 3ex; }
</style>
%s
</head>
<body>
<h1>Component mismatches for %s</h1>
""") % (escape(options.suite), make_chart_header(),
escape(options.suite)), file=options.html_output)
# Additions
binary_only = defaultdict(dict)
both = defaultdict(dict)
source_add = copy.copy(orig_source_add)
source_remove = copy.copy(orig_source_remove)
for pkg in binary_add:
(source, why, flavour, arch) = binary_add[pkg]
if source not in orig_source_add:
binary_only[source][pkg] = why
else:
both[source][pkg] = why
if source in source_add:
source_add.remove(source)
all_output = OrderedDict()
results["source promotions"] = 0
results["binary promotions"] = 0
for component in options.components:
if component == "main":
counterpart = "universe"
elif component == "restricted":
counterpart = "multiverse"
else:
continue
output = []
for source in filter_source(counterpart, sorted(both)):
binaries = sorted(both[source])
entry = [[source] + binaries]
for (id, status, title, assignee) in mir_bugs.get(source, []):
entry.append(MIRLink(id, status, title, assignee))
entry.extend(do_reverse(options, source, binaries, both))
output.append(entry)
all_output["Source and binary movements to %s" % component] = {
"output": output,
"source_and_binary": True,
}
results["source promotions"] += len(output)
output = []
for source in sorted(binary_only):
binaries = filter_binary(counterpart, sorted(binary_only[source]))
if binaries:
entry = [[source] + binaries]
entry.extend(do_reverse(options, source, binaries,
binary_only))
output.append(entry)
all_output["Binary only movements to %s" % component] = {
"output": output,
"binary_only": True,
}
results["binary promotions"] += len(output)
output = []
for source in filter_source(counterpart, sorted(source_add)):
output.append([[source]])
all_output["Source only movements to %s" % component] = {
"output": output,
}
results["source promotions"] += len(output)
if options.dot:
with open(options.dot, 'w') as f:
do_dot(both, f, mir_bugs, options.suite)
# Removals
binary_only = defaultdict(dict)
both = defaultdict(dict)
for pkg in binary_remove:
source = get_source(pkg)
if source not in orig_source_remove:
binary_only[source][pkg] = ""
else:
both[source][pkg] = ""
if source in source_remove:
source_remove.remove(source)
results["source demotions"] = 0
results["binary demotions"] = 0
for component in options.components:
if component == "main":
counterpart = "universe"
elif component == "restricted":
counterpart = "multiverse"
else:
continue
output = []
for source in filter_source(component, sorted(both)):
binaries = sorted(both[source])
output.append([[source] + binaries])
all_output["Source and binary movements to %s" % counterpart] = {
"output": output,
"source_and_binary": True,
}
results["source demotions"] += len(output)
output = []
for source in sorted(binary_only):
binaries = filter_binary(component, sorted(binary_only[source]))
if binaries:
output.append([[source] + binaries])
all_output["Binary only movements to %s" % counterpart] = {
"output": output,
"binary_only": True,
}
results["binary demotions"] += len(output)
output = []
for source in filter_source(component, sorted(source_remove)):
output.append([[source]])
all_output["Source only movements to %s" % counterpart] = {
"output": output,
}
results["source demotions"] += len(output)
for title, output_spec in list(all_output.items()):
source_and_binary = output_spec.get("source_and_binary", False)
binary_only = output_spec.get("binary_only", False)
print_section_text(
options, title, output_spec["output"],
source_and_binary=source_and_binary, binary_only=binary_only)
if options.html_output is not None and package_team_mapping:
by_team = defaultdict(list)
for entry in output_spec["output"]:
source = entry[0][0]
for team in package_team_mapping[source]:
by_team[team].append(entry)
if not package_team_mapping[source]:
by_team["unsubscribed"].append(entry)
for team, entries in sorted(by_team.items()):
print_section_html(
options, "%s (%s)" % (title, team), entries,
source_and_binary=source_and_binary,
binary_only=binary_only)
if options.html_output is not None:
print("<h2>Over time</h2>", file=options.html_output)
print(
make_chart("component-mismatches.csv", [
"source promotions", "binary promotions",
"source demotions", "binary demotions",
]),
file=options.html_output)
print(
"<p><small>Generated: %s</small></p>" % escape(options.timestamp),
file=options.html_output)
print("</body></html>", file=options.html_output)
return results
def do_source_diff(options):
removed = []
added = []
removed = list(set(current_source).difference(set(germinate_source)))
for pkg in germinate_source:
if (pkg not in current_source and
is_included_source(options, pkg) and
not is_excluded_source(options, pkg)):
added.append(pkg)
removed.sort()
added.sort()
return (added, removed)
def do_binary_diff(options):
removed = []
added = {}
removed = list(set(current_binary).difference(set(germinate_binary)))
for pkg in germinate_binary:
if (pkg not in current_binary and
is_included_binary(options, pkg) and
not is_excluded_binary(options, pkg)):
added[pkg] = germinate_binary[pkg]
removed.sort()
return (added, removed)
def get_mir_bugs(options, sources):
'''Return MIR bug information for a set of source packages.
Return a map source -> [(id, status, title, assignee), ...]
'''
result = defaultdict(list)
mir_team = options.launchpad.people['ubuntu-mir']
bug_statuses = ("New", "Incomplete", "Won't Fix", "Confirmed", "Triaged",
"In Progress", "Fix Committed", "Fix Released")
for source in sources:
tasks = options.distro.getSourcePackage(name=source).searchTasks(
bug_subscriber=mir_team, status=bug_statuses)
for task in tasks:
result[source].append((task.bug.id, task.status, task.bug.title,
task.assignee))
return result
def main():
apt_pkg.init()
parser = OptionParser(description='Sync a suite with a Seed list.')
parser.add_option(
"-l", "--launchpad", dest="launchpad_instance", default="production")
parser.add_option('-o', '--output-file', help='output to this file')
parser.add_option('--html-output-file', help='output HTML to this file')
parser.add_option(
'--csv-file', help='record CSV time series data in this file')
parser.add_option(
'--package-team-mapping',
default=os.path.expanduser('~/public_html/package-team-mapping.json'),
help='path to package-team-mapping.json')
parser.add_option('-s', '--suite', help='check this suite')
parser.add_option('-f', '--flavours', default='ubuntu',
help='check these flavours (comma-separated)')
parser.add_option('-i', '--include', help='include these seeds')
parser.add_option('-e', '--exclude', help='exclude these seeds')
parser.add_option('-d', '--dot',
help='generate main promotion graph suitable for dot')
parser.add_option(
'--germinate-path',
default=os.path.expanduser('~/mirror/ubuntu-germinate/'),
help='read Germinate output from this directory')
parser.add_option(
'--archive-dir',
default=os.path.expanduser('~/mirror/ubuntu/'),
help='use Ubuntu archive located in this directory')
options, args = parser.parse_args()
options.launchpad = Launchpad.login_anonymously(
'component-mismatches', options.launchpad_instance)
options.distro = options.launchpad.distributions['ubuntu']
options.archive = options.distro.getArchive(name='primary')
options.component = "main,restricted"
options.components = options.component.split(',')
options.all_components = ["main", "restricted", "universe", "multiverse"]
if options.suite is None:
options.suite = options.distro.current_series.name
# Considering all the packages to have a full installable suite. So:
# -security = release + -security
# -updates = release + -updates + -security
# -proposed = release + updates + security + proposed
if "-" in options.suite:
options.suite, options.pocket = options.suite.split("-")
options.suites = [options.suite]
if options.pocket in ["updates", "security", "proposed"]:
options.suites.append("%s-security" % options.suite)
if options.pocket in ["updates", "proposed"]:
options.suites.append("%s-updates" % options.suite)
if options.pocket in ["proposed"]:
options.suites.append("%s-proposed" % options.suite)
else:
options.suites = [options.suite]
options.series = options.distro.getSeries(name_or_version=options.suites[0])
options.architectures = [a.architecture_tag for a in options.series.architectures]
if options.output_file is not None:
sys.stdout = open('%s.new' % options.output_file, 'w')
if options.html_output_file is not None:
options.html_output = open('%s.new' % options.html_output_file, 'w')
else:
options.html_output = None
# Force encoding to UTF-8 even in non-UTF-8 locales.
import io
sys.stdout = io.TextIOWrapper(
sys.stdout.detach(), encoding="UTF-8", line_buffering=True)
options.time = time.time()
options.timestamp = time.strftime(
'%a %b %e %H:%M:%S %Z %Y', time.gmtime(options.time))
print('Generated: %s' % options.timestamp)
print()
read_germinate(options)
read_current_source(options)
read_current_binary(options)
source_add, source_remove = do_source_diff(options)
binary_add, binary_remove = do_binary_diff(options)
mir_bugs = get_mir_bugs(options, source_add)
results = do_output(
options, source_add, source_remove, binary_add, binary_remove,
mir_bugs)
if options.html_output_file is not None:
options.html_output.close()
os.rename(
'%s.new' % options.html_output_file, options.html_output_file)
if options.output_file is not None:
sys.stdout.close()
os.rename('%s.new' % options.output_file, options.output_file)
if options.csv_file is not None:
if sys.version < "3":
open_mode = "ab"
open_kwargs = {}
else:
open_mode = "a"
open_kwargs = {"newline": ""}
csv_is_new = not os.path.exists(options.csv_file)
with open(options.csv_file, open_mode, **open_kwargs) as csv_file:
# Field names deliberately hardcoded; any changes require
# manually rewriting the output file.
fieldnames = [
"time",
"source promotions",
"binary promotions",
"source demotions",
"binary demotions",
]
csv_writer = csv.DictWriter(csv_file, fieldnames)
if csv_is_new:
csv_writer.writeheader()
csv_writer.writerow(results)
if __name__ == '__main__':
main()