apt-listchanges.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. #!/usr/bin/python3
  2. # vim:set fileencoding=utf-8 et ts=4 sts=4 sw=4:
  3. #
  4. # apt-listchanges - Show changelog entries between the installed versions
  5. # of a set of packages and the versions contained in
  6. # corresponding .deb files
  7. #
  8. # Copyright (C) 2000-2006 Matt Zimmerman <mdz@debian.org>
  9. # Copyright (C) 2006 Pierre Habouzit <madcoder@debian.org>
  10. # Copyright (C) 2016 Robert Luberda <robert@debian.org>
  11. #
  12. # This program is free software; you can redistribute it and/or modify
  13. # it under the terms of the GNU General Public License as published by
  14. # the Free Software Foundation; either version 2 of the License, or
  15. # (at your option) any later version.
  16. #
  17. # This program is distributed in the hope that it will be useful,
  18. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  20. # GNU General Public License for more details.
  21. #
  22. # You should have received a copy of the GNU General Public
  23. # License along with this program; if not, write to the Free
  24. # Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
  25. # MA 02111-1307 USA
  26. #
  27. import sys, os, os.path
  28. import apt_pkg
  29. import signal
  30. import subprocess
  31. import traceback
  32. sys.path += [os.path.dirname(sys.argv[0]) + '/apt-listchanges', '/usr/share/apt-listchanges']
  33. import ALCLog
  34. from ALChacks import _
  35. import apt_listchanges, DebianFiles, ALCApt, ALCConfig, ALCSeenDb
  36. def main(config):
  37. config.read('/etc/apt/listchanges.conf')
  38. debs = config.getopt(sys.argv)
  39. if config.dump_seen:
  40. ALCSeenDb.make_seen_db(config, True).dump()
  41. sys.exit(0);
  42. apt_pkg.init_system()
  43. if config.apt_mode:
  44. debs = ALCApt.AptPipeline(config).read()
  45. if not debs:
  46. sys.exit(0)
  47. if not sys.stdin.isatty():
  48. try:
  49. # Give any forked processes (eg. lynx) a normal stdin;
  50. # See Debian Bug #343423. (Note: with $APT_HOOK_INFO_FD
  51. # support introduced in version 3.2, stdin should point to
  52. # a terminal already, so there should be no need to reopen it).
  53. tty = open('/dev/tty', 'rb+', buffering=0)
  54. os.close(0)
  55. os.dup2(tty.fileno(), 0)
  56. tty.close()
  57. except Exception as ex:
  58. ALCLog.warning(_("Cannot reopen /dev/tty for stdin: %s") % str(ex))
  59. # Force quiet (loggable) mode if not running interactively
  60. if not sys.stdout.isatty() and not config.quiet:
  61. config.quiet = 1
  62. try:
  63. frontend = apt_listchanges.make_frontend(config, len(debs))
  64. except apt_listchanges.EUnknownFrontend:
  65. ALCLog.error(_("Unknown frontend: %s") % config.frontend)
  66. sys.exit(1)
  67. if frontend == None:
  68. sys.exit(0)
  69. if not config.show_all:
  70. status = DebianFiles.ControlParser()
  71. status.readfile('/var/lib/dpkg/status')
  72. status.makeindex('Package')
  73. seen_db = ALCSeenDb.make_seen_db(config)
  74. all_news = {}
  75. all_changelogs = {}
  76. all_binnmus = {}
  77. notes = []
  78. # Mapping of source->binary packages
  79. source_packages = {}
  80. # Flag for each source package, only set if changelogs were actually found
  81. found = {}
  82. # Main loop
  83. for deb in debs:
  84. pkg = DebianFiles.Package(deb)
  85. binpackage = pkg.binary
  86. srcpackage = pkg.source
  87. srcversion = pkg.Version # XXX take the real version or we'll lose binNMUs
  88. frontend.update_progress()
  89. # Show changes later than fromversion
  90. fromversion = None
  91. if not config.show_all:
  92. if srcpackage in seen_db:
  93. fromversion = seen_db[srcpackage]
  94. elif config.since:
  95. fromversion = config.since
  96. else:
  97. statusentry = status.find('Package', binpackage)
  98. if statusentry and statusentry.installed():
  99. fromversion = statusentry.version()
  100. else:
  101. # Package not installed or seen
  102. notes.append(_("%s: will be newly installed") % binpackage)
  103. continue
  104. source_packages.setdefault(srcpackage, []).append(binpackage)
  105. # For packages with non uniform binary versions wrt the source
  106. # version, the version reported for the binary package is the source
  107. # one, which lacks binNMU.
  108. #
  109. # This is why even if we've seen a package we may miss bits of
  110. # changelog in some odd cases
  111. if srcpackage in found and \
  112. apt_pkg.version_compare(srcversion, found[srcpackage]) <= 0:
  113. continue
  114. if not config.show_all and apt_pkg.version_compare(fromversion, srcversion) >= 0:
  115. notes.append(_("%(pkg)s: Version %(version)s has already been seen")
  116. % {'pkg': binpackage, 'version': srcversion})
  117. continue
  118. (news, changelog, binnmu) = pkg.extract_changes(config.which, fromversion, config.reverse)
  119. if news or changelog or binnmu:
  120. found[srcpackage] = srcversion
  121. seen_db[srcpackage] = srcversion
  122. if news:
  123. all_news[srcpackage] = news
  124. if changelog:
  125. all_changelogs[srcpackage] = changelog
  126. if binnmu:
  127. all_binnmus[srcpackage] = binnmu
  128. frontend.progress_done()
  129. seen_db.close_db()
  130. # Merge binnmu entries with regular changelog entries.
  131. # Assumption: the binnmu version is greater than the last non-binnmu version.
  132. for srcpackage in all_binnmus:
  133. if srcpackage in all_changelogs:
  134. all_changelogs[srcpackage].merge_binnmu(all_binnmus[srcpackage], config.reverse)
  135. else:
  136. all_changelogs[srcpackage] = all_binnmus[srcpackage]
  137. all_news = list(all_news.values())
  138. all_changelogs = list(all_changelogs.values())
  139. for batch in (all_news, all_changelogs):
  140. batch.sort(key=lambda x: (x.urgency, x.package))
  141. if config.headers:
  142. changes = ''
  143. news = ''
  144. for rec in all_news:
  145. if not rec.changes:
  146. continue
  147. package = rec.package
  148. header = _('News for %s') % package
  149. if len([x for x in source_packages[package] if x != package]) > 0:
  150. # Differing source and binary packages
  151. header += ' (' + ' '.join(source_packages[package]) + ')'
  152. news += '--- ' + header + ' ---\n' + rec.changes
  153. for rec in all_changelogs:
  154. if not rec.changes:
  155. continue
  156. package = rec.package
  157. header = _('Changes for %s') % package
  158. if len([x for x in source_packages[package] if x != package]) > 0:
  159. # Differing source and binary packages
  160. header += ' (' + ' '.join(source_packages[package]) + ')'
  161. changes += '--- ' + header + ' ---\n' + rec.changes
  162. else:
  163. news = ''.join([x.changes for x in all_news if x.changes])
  164. changes = ''.join([x.changes for x in all_changelogs if x.changes])
  165. if config.verbose and len(notes) > 0:
  166. changes += _("Informational notes") + ":\n\n" + '\n'.join(notes)
  167. if news:
  168. frontend.set_title( _('apt-listchanges: News') )
  169. frontend.display_output(news)
  170. if changes:
  171. frontend.set_title( _('apt-listchanges: Changelogs') )
  172. frontend.display_output(changes)
  173. if news or changes:
  174. apt_listchanges.confirm_or_exit(config, frontend)
  175. hostname = subprocess.getoutput('hostname')
  176. if apt_listchanges.can_send_emails(config):
  177. if changes:
  178. subject = _("apt-listchanges: changelogs for %s") % hostname
  179. apt_listchanges.mail_changes(config, changes, subject)
  180. if news:
  181. subject = _("apt-listchanges: news for %s") % hostname
  182. apt_listchanges.mail_changes(config, news, subject)
  183. # Write out seen db
  184. seen_db.apply_changes()
  185. elif not config.apt_mode and not source_packages.keys():
  186. ALCLog.error(_("Didn't find any valid .deb archives"))
  187. sys.exit(1)
  188. def _setup_signals():
  189. def signal_handler(signum, frame):
  190. ALCLog.error(_('Received signal %d, exiting') % signum)
  191. sys.exit(apt_listchanges.BREAK_APT_EXIT_CODE)
  192. for s in [ signal.SIGHUP, signal.SIGQUIT, signal.SIGTERM ]:
  193. signal.signal(s, signal_handler)
  194. if __name__ == '__main__':
  195. _setup_signals()
  196. config = ALCConfig.ALCConfig()
  197. try:
  198. main(config)
  199. except KeyboardInterrupt:
  200. sys.exit(apt_listchanges.BREAK_APT_EXIT_CODE)
  201. except ALCApt.AptPipelineError as ex:
  202. ALCLog.error(str(ex))
  203. sys.exit(apt_listchanges.BREAK_APT_EXIT_CODE)
  204. except ALCSeenDb.DbError as ex:
  205. ALCLog.error(str(ex))
  206. sys.exit(1)
  207. except Exception:
  208. traceback.print_exc()
  209. apt_listchanges.confirm_or_exit(config, apt_listchanges.ttyconfirm(config))
  210. sys.exit(1)