diff options
author | Jon Turney <jon.turney@dronecode.org.uk> | 2022-01-29 21:11:46 +0300 |
---|---|---|
committer | Jon Turney <jon.turney@dronecode.org.uk> | 2022-02-03 16:28:12 +0300 |
commit | 2677eb4380ec544335aec40b71d12d723e76195d (patch) | |
tree | 22ab9485b8d273e90bf6d3c1fc2ec361d6dc97a4 | |
parent | 88b57728790ae1556d545697c15f315dfa182e75 (diff) |
Add 'unmaintained packages' report
-rwxr-xr-x | calm/calm.py | 14 | ||||
-rwxr-xr-x | calm/package.py | 20 | ||||
-rwxr-xr-x | calm/pkg2html.py | 9 | ||||
-rw-r--r-- | calm/reports.py | 130 |
4 files changed, 167 insertions, 6 deletions
diff --git a/calm/calm.py b/calm/calm.py index cfc05dd..2502997 100755 --- a/calm/calm.py +++ b/calm/calm.py @@ -71,6 +71,7 @@ from . import irk from . import maintainers from . import package from . import pkg2html +from . import reports from . import setup_exe from . import uploads from . import utils @@ -382,10 +383,6 @@ def do_output(args, state): # update packages listings # XXX: perhaps we need a --[no]listing command line option to disable this from being run? pkg2html.update_package_listings(args, state.packages) - # if we are daemonized, allow force regeneration of static content in htdocs - # initially (in case the generation code has changed), but update that - # static content only as needed on subsequent loops - args.force = 0 update_json = False @@ -482,6 +479,15 @@ def do_output(args, state): except (OSError): pass + # write reports + if update_json or args.force: + reports.do_reports(args, state.packages) + + # if we are daemonized, allow force regeneration of static content in htdocs + # initially (in case the generation code has changed), but update that + # static content only as needed on subsequent loops + args.force = 0 + # # daemonization loop diff --git a/calm/package.py b/calm/package.py index a6b20af..9fd734c 100755 --- a/calm/package.py +++ b/calm/package.py @@ -495,8 +495,10 @@ def validate_packages(args, packages): for hints in packages[p].version_hints.values(): valid_requires.update(hints.get('provides', '').split()) - # reset obsolete:d by some other package state + # reset computed package state packages[p].obsolete = False + packages[p].rdepends = set() + packages[p].build_rdepends = set() # perform various package validations for p in sorted(packages.keys()): @@ -762,6 +764,22 @@ def validate_packages(args, packages): lvl = logging.ERROR logging.log(lvl, "package '%s' version '%s' has empty source tar file" % (p, vr)) + # build the set of packages which depends: on this package (rdepends), and + # the set of packages which build-depends: on it (build_rdepends) + for p in packages: + for hints in packages[p].version_hints.values(): + for k, a in [ + ('depends', 'rdepends'), + ('build-depends', 'build_rdepends') + ]: + if k in hints: + dpl = hints[k].split(',') + for dp in dpl: + dp = dp.strip() + dp = re.sub(r'(.*)\s+\(.*\)', r'\1', dp) + if dp in packages: + getattr(packages[dp], a).add(p) + # make another pass to verify a source tarfile exists for every install # tarfile version for p in packages.keys(): diff --git a/calm/pkg2html.py b/calm/pkg2html.py index b43ed71..b2080dc 100755 --- a/calm/pkg2html.py +++ b/calm/pkg2html.py @@ -120,6 +120,13 @@ def ensure_dir_exists(args, path): # +# format a unix epoch time (UTC) +# +def tsformat(ts): + return time.strftime('%Y-%m-%d %H:%M', time.gmtime(ts)) + + +# # # @@ -280,7 +287,7 @@ def update_package_listings(args, packages): name = v + ' (source)' target = "%s-%s-src" % (p.orig_name, v) test = 'test' if 'test' in p.version_hints[v] else 'stable' - ts = time.strftime('%Y-%m-%d %H:%M', time.gmtime(p.tar(v).mtime)) + ts = tsformat(p.tar(v).mtime) print('<tr><td>%s</td><td class="right">%d KiB</td><td>%s</td><td>[<a href="../%s/%s/%s">list of files</a>]</td><td>%s</td></tr>' % (name, size, ts, arch, pn, target, test), file=f) for version in sorted(packages[arch][p].versions(), key=lambda v: SetupVersion(v)): diff --git a/calm/reports.py b/calm/reports.py new file mode 100644 index 0000000..2b7020f --- /dev/null +++ b/calm/reports.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022 Jon Turney +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +import io +import os +import textwrap +import types + +from . import maintainers +from . import package +from . import pkg2html +from . import utils +from .version import SetupVersion + + +def template(title, body, f): + os.fchmod(f.fileno(), 0o755) + + print(textwrap.dedent('''\ + <!DOCTYPE html> + <html> + <head> + <link rel="stylesheet" type="text/css" href="/style.css"/> + <title>{0}</title> + </head> + <body> + <div id="main"> + <h1>{0}</h1>''').format(title), file=f) + + print(body, file=f) + + print(textwrap.dedent('''\ + </div> + </body> + </html>'''), file=f) + + +def linkify(pn, po): + return '<a href="/packages/summary/{0}.html">{1}</a>'.format(pn, po.orig_name) + + +# +# produce a report of unmaintained packages +# +def unmaintained(args, packages, reportsdir): + mlist = maintainers.read(args, None) + pkg_maintainers = maintainers.invert(mlist) + + um_list = [] + + arch = 'x86_64' + # XXX: look into how we can make this 'src', after x86 is dropped + for p in packages[arch]: + po = packages[arch][p] + + if po.kind != package.Kind.source: + continue + + if 'ORPHANED' not in pkg_maintainers[po.orig_name]: + continue + + # the highest version we have + v = sorted(po.versions(), key=lambda v: SetupVersion(v), reverse=True)[0] + + # determine the number of unique rdepends over all subpackages (and + # likewise build_rdepends) + # + # zero rdepends makes this package a candidate for removal, whereas lots + # means it's important to update it. + rdepends = set() + build_rdepends = set() + for subp in po.is_used_by: + rdepends.update(packages[arch][subp].rdepends) + build_rdepends.update(packages[arch][subp].build_rdepends) + + up = types.SimpleNamespace() + up.pn = p + up.po = po + up.v = v + up.ts = po.tar(v).mtime + up.rdepends = len(rdepends) + up.build_rdepends = len(build_rdepends) + + um_list.append(up) + + body = io.StringIO() + print('<p>Packages without a maintainer.</p>', file=body) + + print('<table class="grid">', file=body) + print('<tr><th>last updated</th><th>package</th><th>version</th><th>rdepends</th><th>build_rdepends</th></tr>', file=body) + + for up in sorted(um_list, key=lambda i: (i.rdepends + i.build_rdepends, i.ts), reverse=True): + print('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>' % + (pkg2html.tsformat(up.ts), linkify(up.pn, up.po), up.v, up.rdepends, up.build_rdepends), file=body) + + print('</table>', file=body) + + unmaintained = os.path.join(reportsdir, 'unmaintained.html') + with utils.open_amifc(unmaintained) as f: + template('Unmaintained packages', body.getvalue(), f) + + +def do_reports(args, packages): + if args.dryrun: + return + + reportsdir = os.path.join(args.htdocs, 'reports') + pkg2html.ensure_dir_exists(args, reportsdir) + + unmaintained(args, packages, reportsdir) |