File: //bin/apt-listchanges
#!/usr/bin/python3
# vim:set fileencoding=utf-8 et ts=4 sts=4 sw=4:
#
# apt-listchanges - Show changelog entries between the installed versions
# of a set of packages and the versions contained in
# corresponding .deb files
#
# Copyright (C) 2000-2006 Matt Zimmerman <mdz@debian.org>
# Copyright (C) 2006 Pierre Habouzit <madcoder@debian.org>
# Copyright (C) 2016 Robert Luberda <robert@debian.org>
#
# 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
#
import sys, os, os.path
import functools
import apt_pkg
import signal
import subprocess
import traceback
sys.path += [os.path.dirname(sys.argv[0]) + '/apt-listchanges', '/usr/share/apt-listchanges']
import ALCLog
from ALChacks import _
import apt_listchanges, DebianFiles, ALCApt, ALCConfig, ALCSeenDb
def main(config):
config.read('/etc/apt/listchanges.conf')
debs = config.getopt(sys.argv)
if config.dump_seen:
ALCSeenDb.make_seen_db(config, True).dump()
sys.exit(0);
apt_pkg.init_system()
if config.apt_mode:
debs = ALCApt.AptPipeline(config).read()
if not debs:
sys.exit(0)
# Force quiet (loggable) mode if not running interactively
if not sys.stdout.isatty() and not config.quiet:
config.quiet = 1
try:
frontend = apt_listchanges.make_frontend(config, len(debs)+1)
except apt_listchanges.EUnknownFrontend:
ALCLog.error(_("Unknown frontend: %s") % config.frontend)
sys.exit(1)
if frontend == None:
sys.exit(0)
if frontend.needs_tty_stdin() and not sys.stdin.isatty():
try:
# Give any forked processes (eg. lynx) a normal stdin;
# See Debian Bug #343423. (Note: with $APT_HOOK_INFO_FD
# support introduced in version 3.2, stdin should point to
# a terminal already, so there should be no need to reopen it).
tty = open('/dev/tty', 'rb+', buffering=0)
os.close(0)
os.dup2(tty.fileno(), 0)
tty.close()
except Exception as ex:
ALCLog.warning(_("Cannot reopen /dev/tty for stdin: %s") % str(ex))
status = None
if not config.show_all and config.since is None:
status = DebianFiles.ControlParser()
status.readfile('/var/lib/dpkg/status')
status.makeindex('Package')
seen_db = ALCSeenDb.make_seen_db(config)
# Mapping of source->binary packages
source_packages = {}
deb_number = 0
for deb in debs:
if deb_number % 8 == 0:
frontend.update_progress()
pkg = DebianFiles.Package(deb)
source_packages.setdefault(pkg.source, []).append(pkg)
deb_number += 1
all_news = []
all_changelogs = []
notes = []
# Main loop
for srcpackage, binpackages in source_packages.items():
(news, changelogs) = _process_srcpackage(config, seen_db, notes, status,
srcpackage, binpackages)
if news:
all_news.append(news)
if changelogs:
all_changelogs.append(changelogs)
bincount = len(binpackages)
frontend.update_progress(bincount + int(deb_number/8) - int((deb_number+bincount)/8))
deb_number += bincount
frontend.progress_done()
seen_db.close_db()
for batch in (all_news, all_changelogs):
batch.sort(key=lambda x: (x.urgency, x.package))
news = _join_changes(all_news, source_packages,
config.headers, lambda package: _('News for %s') % package)
changes = _join_changes(all_changelogs, source_packages,
config.headers, lambda package: _('Changes for %s') % package)
if config.verbose and notes:
joined_notes = _("Informational notes") + ":\n\n" + '\n'.join(notes)
if config.which == "news":
news += joined_notes
else:
changes += joined_notes
if news or changes:
_display(frontend, news, lambda: _('apt-listchanges: News'))
_display(frontend, changes, lambda: _('apt-listchanges: Changelogs'))
apt_listchanges.confirm_or_exit(config, frontend)
if apt_listchanges.can_send_emails(config):
hostname = subprocess.getoutput('hostname')
_send_email(news, lambda: _("apt-listchanges: news for %s") % hostname)
_send_email(changes, lambda: _("apt-listchanges: changelogs for %s") % hostname)
# Write out seen db
seen_db.apply_changes()
elif not config.apt_mode and not source_packages.keys():
ALCLog.error(_("Didn't find any valid .deb archives"))
sys.exit(1)
def _determinefromversion(config, seen_db, notes, status, srcpackage, binpackages):
if config.show_all:
return None
if srcpackage in seen_db:
return seen_db[srcpackage]
if config.since is not None:
return config.since
result = None
for pkg in binpackages:
binpackage = pkg.binary
statusentry = status.find('Package', binpackage)
if not statusentry or not statusentry.installed():
# Package not installed or seen
notes.append(_("%s: will be newly installed") % binpackage)
elif not result or apt_pkg.version_compare(result, statusentry.version()) < 0:
result = statusentry.version()
return result
def _drop_binnmu_suffix(version):
pos = version.rfind('+')
if pos != -1 and len(version) in range(pos+3, pos+7) and version[pos+1] == 'b':
return version[:pos]
return version
def _process_srcpackage(config, seen_db, notes, status, srcpackage, binpackages):
fromversion = _determinefromversion(config, seen_db, notes, status, srcpackage, binpackages)
if not fromversion and not config.show_all:
return (None, None)
if len(binpackages) > 1:
binpackages = sorted(binpackages,
key=functools.cmp_to_key(lambda x,y: apt_pkg.version_compare(y.Version, x.Version)))
maxversion = binpackages[0].Version # XXX take the real version or we'll lose binNMUs
processpkgs = []
for pkg in binpackages:
version = pkg.Version
binpackage = pkg.binary
if fromversion and apt_pkg.version_compare(fromversion, version) >= 0:
notes.append(_("%(pkg)s: Version %(version)s has already been seen")
% {'pkg': binpackage, 'version': version})
break
if version != maxversion and _drop_binnmu_suffix(version) != _drop_binnmu_suffix(maxversion):
notes.append(_("%(pkg)s: Version %(version)s is lower than version of "
+ "related packages (%(maxversion)s)")
% {'pkg': binpackage, 'version': version, 'maxversion' : maxversion})
break
processpkgs.append(pkg)
# if config.debug and len(processpkgs) < len(binpackages):
# ALCLog.debug("Ignored packages: %s" % ' '.join('%s=%s' % (x.binary, x.Version)
# for x in set(binpackages) - set(processpkgs)))
if not processpkgs:
return (None, None)
(all_news, all_changelogs, all_binnmus) = (None, None, None)
for pkg in processpkgs:
(news, changelog, binnmu) = pkg.extract_changes(config.which, fromversion, config.reverse)
if news and not all_news:
all_news = news
if changelog and not all_changelogs:
all_changelogs = changelog
if binnmu and not all_binnmus:
all_binnmus = binnmu
if all_changelogs or (all_news and config.which == "news"):
break
if not config.no_network and not all_changelogs and config.which != "news":
for pkg in processpkgs:
all_changelogs = pkg.extract_changes_via_apt(fromversion, config.reverse)
if all_changelogs:
break
# Merge binnmu entries with regular changelog entries.
# Assumption: the binnmu version is greater than the last non-binnmu version.
if all_binnmus:
if all_changelogs:
all_changelogs.merge_binnmu(all_binnmus, config.reverse)
else:
all_changelogs = all_binnmus
if all_news or all_changelogs:
seen_db[srcpackage] = maxversion
return (all_news, all_changelogs)
def _join_changes(all_changes, source_packages, show_headers, header_package_getter):
if not show_headers:
return ''.join([x.changes for x in all_changes if x.changes])
changes = ''
for rec in all_changes:
if rec.changes:
package = rec.package
header = header_package_getter(package)
if next((x for x in source_packages[package] if x.binary != package), None):
# Differing source and binary packages
header += ' (%s)' % ' '.join(x.binary for x in source_packages[package])
changes += '--- %s ---\n%s' % (header, rec.changes)
return changes
def _display(frontend, changes, title_getter):
if changes:
frontend.set_title(title_getter())
frontend.display_output(changes)
def _send_email(changes, subject_getter):
if changes:
apt_listchanges.mail_changes(config, changes, subject_getter())
def _setup_signals():
def signal_handler(signum, frame):
ALCLog.error(_('Received signal %d, exiting') % signum)
sys.exit(apt_listchanges.BREAK_APT_EXIT_CODE)
for s in [ signal.SIGHUP, signal.SIGQUIT, signal.SIGTERM ]:
signal.signal(s, signal_handler)
if __name__ == '__main__':
_setup_signals()
config = ALCConfig.ALCConfig()
try:
main(config)
except KeyboardInterrupt:
sys.exit(apt_listchanges.BREAK_APT_EXIT_CODE)
except ALCApt.AptPipelineError as ex:
ALCLog.error(str(ex))
sys.exit(apt_listchanges.BREAK_APT_EXIT_CODE)
except ALCSeenDb.DbError as ex:
ALCLog.error(str(ex))
sys.exit(1)
except Exception:
traceback.print_exc()
apt_listchanges.confirm_or_exit(config, apt_listchanges.ttyconfirm(config))
sys.exit(1)