# 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
#
import os
import ALCLog
from ALChacks import _
def _parse_apt_bool(value):
# Based on StringToBool() from apt-pkg/contrib/strutl.cc in apt source
# and should return same result as StringToBool(value, false)
return value.lower() in [ '1', 'yes', 'true', 'with', 'on', 'enable' ]
def _parse_apt_int(value):
# This function should match Configuration::FindI() from apt's
# apt-pkg/contrib/configuration.cc, except for values like '1something'
try:
return int(value)
except Exception:
return 0
class AptPipelineError(Exception):
pass
class AptPipeline(object):
def __init__(self, config):
super().__init__()
self._config = config
def read(self):
if self._config.debug:
ALCLog.debug(_("APT pipeline messages:"))
with self._open_apt_fd() as fd:
self._read_version(fd)
self._read_options(fd)
debs = self._read_packages(fd)
if self._config.debug:
ALCLog.debug(_("Packages list:"))
for d in debs:
ALCLog.debug("\t%s" % d)
ALCLog.debug("")
return debs
def _open_apt_fd(self):
if 'APT_HOOK_INFO_FD' not in os.environ:
raise AptPipelineError(_("APT_HOOK_INFO_FD environment variable is not defined\n"
"(is Dpkg::Tools::Options::/usr/bin/apt-listchanges::InfoFD set to 20?)"))
try:
apt_hook_info_fd_val = int(os.environ['APT_HOOK_INFO_FD'])
except Exception as ex:
raise AptPipelineError(_("Invalid (non-numeric) value of APT_HOOK_INFO_FD"
" environment variable")) from ex
if self._config.debug:
ALCLog.debug(_("Will read apt pipeline messages from file descriptor %d") % apt_hook_info_fd_val)
if apt_hook_info_fd_val == 0: # TODO: remove this backward compatibility code in Debian 10 (Buster)
ALCLog.warning(_("Incorrect value (0) of APT_HOOK_INFO_FD environment variable.\n"
"If the warning persists after restart of the package manager (e.g. aptitude),\n"
"please check if the /etc/apt/apt.conf.d/20listchanges file was properly updated."))
elif apt_hook_info_fd_val < 3:
raise AptPipelineError(_("APT_HOOK_INFO_FD environment variable is incorrectly defined\n"
"(Dpkg::Tools::Options::/usr/bin/apt-listchanges::InfoFD should be greater than 2)."))
try:
return os.fdopen(apt_hook_info_fd_val, 'rt')
except Exception as ex:
raise AptPipelineError(_("Cannot read from file descriptor %(fd)d: %(errmsg)s")
% {'fd': apt_hook_info_fd_val, 'errmsg': str(ex) }) from ex
def _read_version(self, fd):
version = fd.readline().rstrip()
if version != "VERSION 2":
raise AptPipelineError(_("Wrong or missing VERSION from apt pipeline\n"
"(is Dpkg::Tools::Options::/usr/bin/apt-listchanges::Version set to 2?)"))
if self._config.debug:
ALCLog.debug("\t%s" % version)
def _read_options(self, fd):
while True:
line = fd.readline().rstrip()
if self._config.debug:
ALCLog.debug("\t%s" % line)
if not line:
return
if (not self._config.ignore_apt_assume and
line.startswith('APT::Get::Assume-Yes=') and
_parse_apt_bool(line[len('APT::Get::Assume-Yes='):]) ):
self._config.confirm = False
# Set self._config.quiet as well to force non-interactive frontend
self._config.quiet = max(1, self._config.quiet)
elif line.startswith('quiet='):
self._config.quiet = max(_parse_apt_int(line[len('quiet='):]), self._config.quiet)
def _read_packages(self, fd):
filenames = {}
toconfig = []
toremove = []
hasupgrade = False
for pkgline in fd.readlines():
pkgline = pkgline.rstrip()
if self._config.debug:
ALCLog.debug("\t%s" % pkgline)
if not pkgline:
break
(pkgname, oldversion, compare, newversion, filename) = pkgline.split(None, 4)
if compare != '<': # ignore downgrades or re-installations
continue
if filename == '**REMOVE**' or filename == '**ERROR**':
toremove.append(pkgname)
continue
# New installs (oldversion equal to '-') are not ignored to support
# a case when changelog is moved from one package to a dependent
# package built from the same source (see p7zip-full 15.09+dfsg-3).
if oldversion != '-':
hasupgrade = True
if filename == '**CONFIGURE**':
toconfig.append(pkgname)
else:
filenames[pkgname] = filename
# Quit early if no package has been upgraded (e.g. only new installs or removals)
if not hasupgrade:
return []
# Sort by configuration order. THIS IS IMPORTANT. Sometimes, a
# situation exists where package X contains changelog.gz (upstream
# changelog) and depends on package Y which contains
# changelog.Debian.gz (Debian changelog). Until we have a more
# reliable method for determining whether a package is Debian
# native, this allows things to work, since Y will always be
# configured first.
# apt doesn't explicitly configure everything anymore, so sort
# the things to be configured first, and then do everything else
# in alphabetical order. Also, drop from the list everything
# that's to be removed.
for pkg in toremove:
if pkg in filenames:
del filenames[pkg]
ordered_filenames = []
for pkg in toconfig:
if pkg in filenames:
ordered_filenames.append(filenames[pkg])
del filenames[pkg]
ordered_filenames.extend(sorted(filenames.values()))
return ordered_filenames
|