File: ALCApt.py

package info (click to toggle)
apt-listchanges 3.24
  • links: PTS, VCS
  • area: main
  • in suites: bullseye, sid
  • size: 1,352 kB
  • sloc: python: 1,710; xml: 650; makefile: 138; sh: 71; perl: 61
file content (180 lines) | stat: -rw-r--r-- 7,418 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# 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