~ubuntu-archive/ubuntu-archive-scripts/trunk

238.1.1 by Michael Hudson-Doyle
bare bones of find-rcbuggy-problem-packages script. only checks autopkgtest regressions for now.
1
#!/usr/bin/python3
2
238.1.4 by Michael Hudson-Doyle
long comment, report everything at end
3
# This script highlights packages that are being kept out of Debian
4
# testing by release critical bugs whose removal from the Ubuntu devel
5
# series would aid the migration of other packages out of proposed.
6
#
7
# The packages that are being kept out of Debian testing by release
8
# critical bugs can be found fairly easily by looking at the output
9
# from Debian's britney runs. We do this first (and call these
10
# "rc-gone packages").
11
#
12
# Such packages can inhibit the migration of other packages in two
13
# ways:
14
#
15
# 1) autopkgtest regressions
16
# 2) by becoming uninstallable
17
#
18
# The first is fairly easy to find: scan through Ubuntu's excuses.yaml
19
# and look for the rc-gone package in the list of autopkgtest
20
# regressions for any package.
21
#
22
# The second is a bit more mind-bending to detect. If the package is
238.1.10 by Michael Hudson-Doyle
html output
23
# caught up in a transition, a rebuild will be attempted as a matter
238.1.4 by Michael Hudson-Doyle
long comment, report everything at end
24
# of course, and if this succeeds and passes its autopkgtests then
25
# removing the package will not aid proposed migration. So we look for
26
# rc-gone packages in proposed that are missing builds or failing
27
# autopkgtests. For such source packages, we see if any of their
28
# binary packages are reported as being made uninstallable by the
29
# migration of any source package in proposed. If so, removing the
30
# rc-gone package will help proposed migration.
31
238.1.3 by Michael Hudson-Doyle
make arguments more explicit
32
import argparse
33
import os
238.1.4 by Michael Hudson-Doyle
long comment, report everything at end
34
import shlex
238.1.2 by Michael Hudson-Doyle
find rc-gone packages blocking transitions
35
import subprocess
238.1.1 by Michael Hudson-Doyle
bare bones of find-rcbuggy-problem-packages script. only checks autopkgtest regressions for now.
36
238.1.4 by Michael Hudson-Doyle
long comment, report everything at end
37
import attr
238.1.10 by Michael Hudson-Doyle
html output
38
from jinja2 import Environment, FileSystemLoader
238.1.1 by Michael Hudson-Doyle
bare bones of find-rcbuggy-problem-packages script. only checks autopkgtest regressions for now.
39
import yaml
40
238.1.10 by Michael Hudson-Doyle
html output
41
env = Environment(
42
    loader=FileSystemLoader(os.path.dirname(os.path.abspath(__file__)) + '/templates'),
43
    autoescape=True,
44
    extensions=['jinja2.ext.i18n'],
45
)
46
env.install_null_translations(True)
47
238.1.3 by Michael Hudson-Doyle
make arguments more explicit
48
def parse_args():
49
    parser = argparse.ArgumentParser()
50
    parser.add_argument('--ubuntu-excuses', action='store')
51
    parser.add_argument('--ubuntu-update_output', action='store')
52
    parser.add_argument('--debian-excuses', action='store')
238.1.10 by Michael Hudson-Doyle
html output
53
    parser.add_argument('--output', action='store')
238.1.3 by Michael Hudson-Doyle
make arguments more explicit
54
    return parser.parse_args()
55
56
args = parse_args()
57
238.1.7 by Michael Hudson-Doyle
track which suite a package needs to be removed from
58
def run_output(*cmd, **extra):
243.1.1 by Michael Hudson-Doyle
python 3.5 compat
59
    encoding = extra.pop('encoding', 'ascii')
60
    kw = dict(check=True, stdout=subprocess.PIPE)
238.1.7 by Michael Hudson-Doyle
track which suite a package needs to be removed from
61
    kw.update(extra)
62
    cp = subprocess.run(cmd, **kw)
243.1.1 by Michael Hudson-Doyle
python 3.5 compat
63
    return cp.stdout.decode(encoding).strip()
238.1.7 by Michael Hudson-Doyle
track which suite a package needs to be removed from
64
244.1.2 by Michael Hudson-Doyle
reimplement reverse-depends with a bunch of grep-dctrl
65
all_arches = set()
66
244.1.1 by Michael Hudson-Doyle
try to be more correct about which architecture a package becomes uninstallable on
67
def extract_bin_pkg_arch_to_blocked_src_pkgs(output_fp):
68
    # Extract a mapping from binary package name / architectures to
69
    # source package the migration of which would make that package
70
    # uninstallable (and for convenience, return all architectures
71
    # used as keys -- usually there will only be one or two).
238.1.4 by Michael Hudson-Doyle
long comment, report everything at end
72
73
    # We're looking for sequences of lines like this:
74
75
    # skipped: camlp5 (0, 3, 57)
76
    #     got: 13+0: a-1:a-0:a-0:i-6:p-0:s-6
77
    #      * s390x: hol-light, libaac-tactics-ocaml-dev, libcoq-ocaml-dev, libledit-ocaml-dev, ocaml-ulex08
78
79
    # (Britney tries to migrate batches of packages but it always
80
    # tries each package on its own as well).
81
244.1.1 by Michael Hudson-Doyle
try to be more correct about which architecture a package becomes uninstallable on
82
    # For each potential migration, britney checks architectures in
83
    # sequence and stops when it finds one that regresses (or proceeds
84
    # with the migration if it doesn't find one). This means that we
85
    # can miss blocking packages here --- for example if migrating $src
86
    # would make say $binpkg/amd64 uninstallable, but britney happens
87
    # to check arm64 -- where $binpkg does not exist -- and there are
88
    # regressions there, we will never find out about the problem
89
    # $binpkg causes.  This isn't really too bad because clearly in
90
    # this case the migration of $src is blocked by other things that
91
    # need to be resolved, but it does mean that packages might appear
92
    # and disapper from the report depending on the order that britney
93
    # checks architectures in (which is not consistent from run to
94
    # run). C'est la vie.
95
96
    bin_pkg_arch_to_blocked_src_pkgs = {}
238.1.2 by Michael Hudson-Doyle
find rc-gone packages blocking transitions
97
    srcpkg = None
244.1.2 by Michael Hudson-Doyle
reimplement reverse-depends with a bunch of grep-dctrl
98
    arch_prefix = "Arch order is: "
238.1.4 by Michael Hudson-Doyle
long comment, report everything at end
99
    for line in output_fp:
244.1.2 by Michael Hudson-Doyle
reimplement reverse-depends with a bunch of grep-dctrl
100
        line = line.strip()
101
        if line.startswith(arch_prefix):
102
            all_arches.update(line[len(arch_prefix):].split(', '))
103
        parts = line.split(None, 2)
238.1.2 by Michael Hudson-Doyle
find rc-gone packages blocking transitions
104
        if len(parts) >= 2:
244.1.1 by Michael Hudson-Doyle
try to be more correct about which architecture a package becomes uninstallable on
105
            if parts[0] in {"Trying", "trying"}:
106
                srcpkg = None
238.1.2 by Michael Hudson-Doyle
find rc-gone packages blocking transitions
107
            if parts[0] == 'skipped:':
108
                srcpkg = None
238.1.4 by Michael Hudson-Doyle
long comment, report everything at end
109
                # If parts[2] is '(' then this line is about trying to
110
                # migrate a single package, which is what we are
111
                # looking for.
238.1.2 by Michael Hudson-Doyle
find rc-gone packages blocking transitions
112
                if parts[2].startswith('('):
113
                    srcpkg = parts[1]
244.1.1 by Michael Hudson-Doyle
try to be more correct about which architecture a package becomes uninstallable on
114
            if srcpkg is not None and srcpkg[0] != '-' and parts[0] == '*':
238.1.4 by Michael Hudson-Doyle
long comment, report everything at end
115
                # parts[1] is "${arch}:"
116
                # parts[2:] is a comma+space separated list of binary package names.
244.1.1 by Michael Hudson-Doyle
try to be more correct about which architecture a package becomes uninstallable on
117
                arch = parts[1][:-1]
244.1.2 by Michael Hudson-Doyle
reimplement reverse-depends with a bunch of grep-dctrl
118
                for binpkg in parts[2].split(', '):
244.1.1 by Michael Hudson-Doyle
try to be more correct about which architecture a package becomes uninstallable on
119
                    bin_pkg_arch_to_blocked_src_pkgs.setdefault(
244.1.2 by Michael Hudson-Doyle
reimplement reverse-depends with a bunch of grep-dctrl
120
                        (binpkg, arch), set()).add(srcpkg)
121
    return bin_pkg_arch_to_blocked_src_pkgs
122
123
124
def chdist_grep_dctrl_packages(arch, *args):
125
    return run_output(
126
        "chdist", "grep-dctrl-packages", "{}-{}".format(series, arch),
127
        "-nsPackage", *args, check=False).splitlines()
128
129
130
def chdist_grep_dctrl_sources(arch, *args):
131
    return run_output(
132
        "chdist", "grep-dctrl-sources", "{}-{}".format(series, arch),
133
        "-nsPackage", *args, check=False).splitlines()
238.1.4 by Michael Hudson-Doyle
long comment, report everything at end
134
238.1.1 by Michael Hudson-Doyle
bare bones of find-rcbuggy-problem-packages script. only checks autopkgtest regressions for now.
135
238.1.4 by Michael Hudson-Doyle
long comment, report everything at end
136
@attr.s
137
class RCGone:
138
    source_package_name = attr.ib()
139
    bugs = attr.ib()
140
    block_by_regression = attr.ib(default=attr.Factory(set))
244.1.1 by Michael Hudson-Doyle
try to be more correct about which architecture a package becomes uninstallable on
141
    block_by_uninstallability = attr.ib(default=attr.Factory(set))
238.1.7 by Michael Hudson-Doyle
track which suite a package needs to be removed from
142
    suites = attr.ib(default=attr.Factory(set))
238.1.10 by Michael Hudson-Doyle
html output
143
    _rdeps_lines = attr.ib(default=None)
244.1.2 by Michael Hudson-Doyle
reimplement reverse-depends with a bunch of grep-dctrl
144
    _binpkgs = attr.ib(default=None)
145
146
    def binary_pkgs(self):
147
        if self._binpkgs is None:
148
            self._binpkgs = set()
149
            for arch in all_arches:
150
                arch_binpkgs = chdist_grep_dctrl_packages(
151
                    arch, "-wS", self.source_package_name)
152
                self._binpkgs.update({(binpkg, arch) for binpkg in arch_binpkgs})
153
        return self._binpkgs
154
155
    def reverse_depends(self):
156
        if self._rdeps_lines is None:
157
            # These are maps rdep -> binpkg -> arches
158
            reverse_recommends = {}
159
            reverse_depends = {}
160
            # This just maps rbdep -> binpkgs
161
            reverse_build_depends = {}
162
            for binpkg, arch in self.binary_pkgs():
163
                for rec in chdist_grep_dctrl_packages(
164
                        arch, "-wFRecommends", binpkg):
165
                    if (rec, arch) in self.binary_pkgs():
166
                        continue
167
                    reverse_recommends.setdefault(rec, {}).setdefault(binpkg, set()).add(arch)
168
                for dep in chdist_grep_dctrl_packages(
169
                        arch, "-wFDepends", binpkg):
170
                    if (dep, arch) in self.binary_pkgs():
171
                        continue
172
                    reverse_depends.setdefault(dep, {}).setdefault(binpkg, set()).add(arch)
173
                for bdeb in chdist_grep_dctrl_sources(
174
                    arch, "-wFBuild-Depends", binpkg, "--or", "-wFBuild-Depends-Indep", binpkg):
175
                    reverse_build_depends.setdefault(bdeb, set()).add(binpkg)
176
            self._rdeps_lines = []
177
            if reverse_depends:
178
                self._rdeps_lines.append("Reverse-Depends")
179
                for rdep in sorted(reverse_depends):
180
                    for binpkg in sorted(reverse_depends[rdep]):
181
                        if reverse_depends[rdep][binpkg] == all_arches:
182
                            arches = "all architectures"
183
                        else:
184
                            arches = ", ".join(reverse_depends[rdep][binpkg])
185
                        self._rdeps_lines.append(" ".join(["*", rdep, "for", binpkg, "on", arches]))
186
            if reverse_recommends:
187
                self._rdeps_lines.append("Reverse-Recommends")
188
                for rdep in sorted(reverse_recommends):
189
                    for binpkg in sorted(reverse_recommends[rdep]):
190
                        if reverse_recommends[rdep][binpkg] == all_arches:
191
                            arches = "all architectures"
192
                        else:
193
                            arches = ", ".join(reverse_recommends[rdep][binpkg])
194
                        self._rdeps_lines.append(" ".join(["*", rdep, "for", binpkg, "on", arches]))
195
            if reverse_build_depends:
196
                self._rdeps_lines.append("Reverse-Build-Depends")
197
                for rdep in sorted(reverse_build_depends):
198
                    for binpkg in sorted(reverse_build_depends[rdep]):
199
                        self._rdeps_lines.append(" ".join(["*", rdep, "for", binpkg]))
200
        return self._rdeps_lines
238.1.10 by Michael Hudson-Doyle
html output
201
202
    @property
203
    def comment(self):
204
        comment = "removed from testing (Debian bug"
238.1.11 by Michael Hudson-Doyle
make html a bit nicer
205
        if len(self.bugs) > 1:
238.1.10 by Michael Hudson-Doyle
html output
206
            comment += "s"
238.1.11 by Michael Hudson-Doyle
make html a bit nicer
207
        comment += " " + ", ".join('#' + b for b in self.bugs) + ")"
238.1.10 by Michael Hudson-Doyle
html output
208
        if self.block_by_regression:
240 by Steve Langasek
language tweak in the fix-rcbuggy-problem-packages report
209
            comment += ', blocks {} by autopkgtest regression'.format(', '.join(sorted(self.block_by_regression)))
238.1.10 by Michael Hudson-Doyle
html output
210
        if self.block_by_uninstallability:
211
            comment += ', blocks {} by uninstallability'.format(', '.join(sorted(self.block_by_uninstallability)))
212
        return comment
213
214
    @property
215
    def rdeps_text_short(self):
244.1.2 by Michael Hudson-Doyle
reimplement reverse-depends with a bunch of grep-dctrl
216
        return "\n".join(self.reverse_depends()[:10])
238.1.10 by Michael Hudson-Doyle
html output
217
218
    @property
219
    def rdeps_text_more(self):
244.1.2 by Michael Hudson-Doyle
reimplement reverse-depends with a bunch of grep-dctrl
220
        return "\n".join(self.reverse_depends()[10:])
238.1.10 by Michael Hudson-Doyle
html output
221
222
    @property
223
    def removal_commands(self):
224
        suites = self.suites
225
        if not suites:
226
            suites = [series]
227
        cmds = []
228
        for suite in suites:
247 by Steve Langasek
Include -y in the rcbuggy-problem-packages output for multiline cut'n'paste
229
            cmd = " ".join(["remove-package", "-s", suite, "-y", "-m", shlex.quote(self.comment), self.source_package_name])
244.1.2 by Michael Hudson-Doyle
reimplement reverse-depends with a bunch of grep-dctrl
230
            if self.reverse_depends():
238.1.10 by Michael Hudson-Doyle
html output
231
                cmd = '#' + cmd
232
            cmds.append(cmd)
233
        return cmds
234
238.1.12 by Michael Hudson-Doyle
MOAR CSS
235
    @property
236
    def css_class(self):
244.1.2 by Michael Hudson-Doyle
reimplement reverse-depends with a bunch of grep-dctrl
237
        if self.reverse_depends():
238.1.12 by Michael Hudson-Doyle
MOAR CSS
238
            return 'removal-not-ok'
239
        else:
240
            return 'removal-ok'
238.1.4 by Michael Hudson-Doyle
long comment, report everything at end
241
238.1.2 by Michael Hudson-Doyle
find rc-gone packages blocking transitions
242
238.1.7 by Michael Hudson-Doyle
track which suite a package needs to be removed from
243
if 'SERIES' in os.environ:
244
    series = os.environ['SERIES']
245
else:
246
    series = run_output('distro-info', '-d')
247
238.1.10 by Michael Hudson-Doyle
html output
248
249
def main():
238.1.11 by Michael Hudson-Doyle
make html a bit nicer
250
    print("loading data")
288 by Iain Lane
Fix find-rcbuggy-problem-packages for .xz excuses
251
    if args.ubuntu_excuses.endswith('.xz'):
252
        import lzma
253
        excuses_opener = lzma.open
254
    else:
255
        excuses_opener = open
256
    with excuses_opener(args.ubuntu_excuses) as fp:
238.1.11 by Michael Hudson-Doyle
make html a bit nicer
257
        ubuntu_excuses = yaml.load(fp, Loader=yaml.CSafeLoader)
258
    with open(args.ubuntu_update_output) as fp:
244.1.2 by Michael Hudson-Doyle
reimplement reverse-depends with a bunch of grep-dctrl
259
        bin_pkg_arch_to_blocked_src_pkgs = extract_bin_pkg_arch_to_blocked_src_pkgs(fp)
238.1.11 by Michael Hudson-Doyle
make html a bit nicer
260
    with open(args.debian_excuses) as fp:
261
        debian_excuses = yaml.load(fp, Loader=yaml.CSafeLoader)
262
263
    print("finding rcgone packages")
264
    rc_gones = {}
265
266
    for source in debian_excuses['sources']:
267
        if source['old-version'] == '-':
268
            info = source['policy_info']
269
            rc_bugs = info.get('rc-bugs', {})
270
            if rc_bugs.get('verdict') == "REJECTED_PERMANENTLY":
271
                bugs = []
272
                for k in 'shared-bugs', 'unique-source-bugs', 'unique-target-bugs':
273
                    bugs.extend(rc_bugs[k])
274
                rc_gones[source['item-name']] = RCGone(
275
                    source_package_name=source['item-name'],
276
                    bugs=bugs)
277
    in_proposed_by_autopkgtest_or_missing_binaries = set()
278
    print("checking autopkgtests")
279
    for source in ubuntu_excuses['sources']:
280
        item = source['item-name']
281
        if 'autopkgtest' in source['reason']:
282
            in_proposed_by_autopkgtest_or_missing_binaries.add(item)
283
            for package, results in sorted(source['policy_info']['autopkgtest'].items()):
249 by Michael Hudson-Doyle
a corner case
284
                if '/' not in package:
285
                    # This only happens when all tests are still running
286
                    continue
248 by Michael Hudson-Doyle
do not suggest packages with -0ubuntu1 in the version for fast removal
287
                package, version = package.split('/')
288
                if package not in rc_gones or '-0ubuntu' in version:
238.1.11 by Michael Hudson-Doyle
make html a bit nicer
289
                    continue
290
                for arch, result in sorted(results.items()):
291
                    outcome, log, history, wtf1, wtf2 = result
238.1.13 by Michael Hudson-Doyle
we do not care about a package blocking its own migration!
292
                    if outcome == "REGRESSION" and package != item:
238.1.11 by Michael Hudson-Doyle
make html a bit nicer
293
                        rc_gones[package].block_by_regression.add(item)
294
                        break
248 by Michael Hudson-Doyle
do not suggest packages with -0ubuntu1 in the version for fast removal
295
        if 'missing-builds' in source and '-0ubuntu' not in source['new-version']:
238.1.11 by Michael Hudson-Doyle
make html a bit nicer
296
            in_proposed_by_autopkgtest_or_missing_binaries.add(item)
297
        if item in rc_gones:
298
            if source['new-version'] != '-':
299
                rc_gones[item].suites.add(series+"-proposed")
300
            if source['old-version'] != '-':
301
                rc_gones[item].suites.add(series)
302
    print("checking uninstallability")
244.1.2 by Michael Hudson-Doyle
reimplement reverse-depends with a bunch of grep-dctrl
303
    for rc_gone in rc_gones.values():
304
        if rc_gone.source_package_name not in in_proposed_by_autopkgtest_or_missing_binaries:
238.1.11 by Michael Hudson-Doyle
make html a bit nicer
305
            continue
244.1.2 by Michael Hudson-Doyle
reimplement reverse-depends with a bunch of grep-dctrl
306
        for bin_pkg, arch in set(rc_gone.binary_pkgs()):
307
            rc_gone.block_by_uninstallability.update(bin_pkg_arch_to_blocked_src_pkgs.get((bin_pkg, arch), set()))
238.1.11 by Michael Hudson-Doyle
make html a bit nicer
308
    print("finding reverse-deps")
309
    packages = []
310
    for _, rc_gone in sorted(rc_gones.items()):
311
        if not rc_gone.block_by_regression and not rc_gone.block_by_uninstallability:
312
            continue
244.1.2 by Michael Hudson-Doyle
reimplement reverse-depends with a bunch of grep-dctrl
313
        rc_gone.reverse_depends()
238.1.11 by Michael Hudson-Doyle
make html a bit nicer
314
        packages.append(rc_gone)
315
238.1.10 by Michael Hudson-Doyle
html output
316
    print("rendering")
317
    t = env.get_template('rcbuggy-problem-packages.html')
318
    with open(args.output, 'w', encoding='utf-8') as fp:
319
        fp.write(t.render(
320
            packages=packages,
321
            now=ubuntu_excuses["generated-date"].strftime("%Y.%m.%d %H:%M:%S")))
322
323
324
if __name__ == '__main__':
325
    main()