Merge lp:~rcj/vmbuilder/mfdiff_fixups into lp:~ubuntu-on-ec2/vmbuilder/mfdiff

Proposed by Robert C Jennings
Status: Merged
Merged at revision: 19
Proposed branch: lp:~rcj/vmbuilder/mfdiff_fixups
Merge into: lp:~ubuntu-on-ec2/vmbuilder/mfdiff
Diff against target: 903 lines (+581/-281)
2 files modified
README (+9/-10)
mfdiff (+572/-271)
To merge this branch: bzr merge lp:~rcj/vmbuilder/mfdiff_fixups
Reviewer Review Type Date Requested Status
Francis Ginther (community) Approve
Dan Watkins (community) Approve
Review via email: mp+326375@code.launchpad.net

Description of the change

Modernization and fixes

Break up main(), modernize code, improve documentation and variable
naming, python3 compat, misc fixes, urllib to requests migration,
support non-x86 architectures, and add logging.

This requires python-six version newer than what is shipping in some
supported Ubuntu releases to use six.viewkeys. (>=1.10 is preferable)

 [ Robert C Jennings ]
 * Strip architecture from binary when searching cache
 * Copy rather than rename changelog files
 * Force output to UTF-8 at all times
 * Output utf-8 rather than ascii
 * Change exception rendering for python3 compatibility
 * Use the current directory for temp changelog files
 * Improve variable naming for changelog_url (was clog_url)
 * Support non-x86 architectures
 * Fix usage text to correct option order
 * Eliminate traceback if binary package has been renamed
   (no longer in apt cache)
 * Use a real temporary file
 * Fixups for code-review
 * Improve names in print_changelogs and store more to the sources dict
 * Break out changelog processing from main
 * More variable renames
 * Rearrange summary printout and changelog printing for later refactor
 * Name, spelling, and doc cleanup
 * Stop processing source package on exception
 * Unlink temp file only if it exists
 * Add more complete function docstrings
 * Additional refactoring to reduce size of main
 * isort requests
 * Update authors/copyright
 * Add logging
 * Simplify cmdline --verbose option
 * Swap urllib for requests
 * Improve variable naming
 * Consistent sorted output
 * python3 compatibility changes
 * Document functions, use 'with' for files, and minor fixes
 * [merge] PEP8 cleanup, example updates, & python-apt updates
   * Correct vi line for tab expansion
   * README: Remove trailing spaces
   * Use host apt-secure keychains
   * APT version comparison moved from apt to apt_pkg
   * Remove unused cmdline option
   * Update README to use modern examples
   * Move comment to proper function
   * remove unused imports/variables
   * sort imports
   * pep8: autopep8 changes
   * pep8: tab -> space conversion

To post a comment you must log in.
Revision history for this message
Robert C Jennings (rcj) wrote :

Break up main(), modernize code, improve documentation and variable naming, python3 compat, misc fixes, urllib to requests migration, support non-x86 architectures, and add logging.

There are a fair number of commits here and this is best reviewed by commit rather than with the MP diff view.

Revision history for this message
Robert C Jennings (rcj) wrote :

This requires python-six version newer than what is shipping in Trusty to use viewkeys. (1.10 is preferable)

Revision history for this message
Dan Watkins (oddbloke) :
review: Approve
Revision history for this message
Francis Ginther (fginther) wrote :

Approve, this MP and resulting mfdiff are identical to a branch I already reviewed and tested.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'README'
--- README 2010-12-13 15:23:43 +0000
+++ README 2017-06-27 19:03:39 +0000
@@ -1,15 +1,14 @@
1"Manifest Diff" (mfdiff) is a tool for determining what changed between1"Manifest Diff" (mfdiff) is a tool for determining what changed between
22 builds by looking at their manifests. A manifest is simply a 22 builds by looking at their manifests. A manifest is simply a
3list of package and versions in a build, obtainable by:3list of package and versions in a build, obtainable by:
4 dpkg-query', '-W', '--showformat=${Package} ${Version}\n'4 dpkg-query', '-W', '--showformat=${Package} ${Version}\n'
55
6Example of running:6Example of running:
7 mf1=mfs/lucid/release-20100827/ubuntu-10.04-server-uec-amd64.manifest7 mf1=mfs/releases/trusty/release-20160314/ubuntu-14.04-server-cloudimg-amd64.manifest
8 mf2=mfs/lucid/release-20100923/ubuntu-10.04-server-uec-amd64.manifest8 mf2=mfs/releases/trusty/release-20170517/ubuntu-14.04-server-cloudimg-amd64.manifest
9 ./mfdiff i386 lucid "${mf1}" "${mf2}"9 ./mfdiff amd64 trusty "${mf1}" "${mf2}"
1010
1111
1212sync manifests from cloud-images.ubuntu.com with:
13sync manifests from uec-images.ubuntu.com with:13 rsync -av cloud-images.ubuntu.com::cloud-images/ --prune-empty-dirs mfs \
14 rsync -av uec-images.ubuntu.com::uec-images/ --prune-empty-dirs mfs \14 --filter "+ */" --filter "+ *.manifest" --filter "- *"
15 --filter "+ */" --filter "+ *.manifest" --filter "- *"
1615
=== modified file 'mfdiff'
--- mfdiff 2014-10-03 11:27:32 +0000
+++ mfdiff 2017-06-27 19:03:39 +0000
@@ -1,9 +1,17 @@
1#!/usr/bin/env python1#!/usr/bin/env python
2# vi:ts=4 noexpandtab2"""
3Given two manifest files for a particular release/arch, find the
4packages which have been added, removed, and changed. Print the
5changelog entries for the changed packages limited to changes between
6the two versions.
7"""
8
9# vi:ts=4 expandtab
3#10#
4# Copyright (C) 2010 Canonical Ltd.11# Copyright (C) 2010, 2017 Canonical Ltd.
5#12#
6# Authors: Scott Moser <scott.moser@canonical.com>13# Authors: Scott Moser <scott.moser@canonical.com>
14# Robert C Jennings <robert.jennings@canonical.com>
7#15#
8# This program is free software: you can redistribute it and/or modify16# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by17# it under the terms of the GNU General Public License as published by
@@ -17,288 +25,581 @@
17# You should have received a copy of the GNU General Public License25# You should have received a copy of the GNU General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.26# along with this program. If not, see <http://www.gnu.org/licenses/>.
1927
28import codecs
29import locale
30import logging
20import os31import os
21import string32import os.path
33import re
34import shutil
22import sys35import sys
36import tempfile
23from optparse import OptionParser37from optparse import OptionParser
24import urllib38
25import logging
26import os.path
27import os
28from debian.changelog import Changelog, ChangeBlock
29import apt39import apt
30import re40import requests
3141from debian.changelog import Changelog
32Usage="""42from six import iteritems, viewkeys
33Usage: %s --apt-sources F arch manifest1 manifest243
34 Compare two manifest files, and show changelog differences.44try:
35"""45 from apt import VersionCompare as version_compare
36logformat="%(asctime)s %(name)s/%(levelname)s: %(message)s"46except ImportError:
47 from apt_pkg import version_compare as version_compare
48
3749
38class MissingChangelogError(Exception):50class MissingChangelogError(Exception):
39 pass51 "The Changelog file could not be found on the server"
52 pass
53
4054
41class ChangelogMissingVersion(Exception):55class ChangelogMissingVersion(Exception):
42 pass56 "The Changelog is missing starting and/or ending versions for the search"
57 pass
58
4359
44class UnknownSourceVersionError(Exception):60class UnknownSourceVersionError(Exception):
45 pass61 "The binary package did not have a source version listed"
4662 pass
47def get_bin2src(changed,cache):63
48 ret = { }64
49 for binpkg in changed:65def get_bin2src(packages, cache):
50 pkg_name = binpkg.split(':')[0]66 """
51 ret[binpkg]=cache[pkg_name].versions[0].source_name67 Find the source package names for a given binary package list
52 return(ret)68
5369 :param list packages: List of binary packages
54# downloads changelog for src/ver and returns path to that70 :param :class:`apt.Cache` cache: Open/updated apt cache
55def getchangelog(src,ver,cache_d):71 :return: List mapping binary package name to source package name
56 # http://changelogs.ubuntu.com/changelogs/pool/main/h/hal/hal_0.5.5.1-1ubuntu2/changelog72 :rtype: dict
57 clog_burl="http://changelogs.ubuntu.com/changelogs/pool"73 """
58 urls=[]74
59 bsize=204875 ret = {}
60 cache_f = "%s/changelog.%s_%s" % (cache_d, src,ver)76 logging.debug('Finding source package names for all binary packages')
6177 for binpkg in packages:
62 if os.path.isfile(cache_f):78 pkg_name = binpkg.split(':')[0]
63 return(cache_f)79 ret[binpkg] = cache[pkg_name].versions[0].source_name
6480 return ret
65 furls=[ ]81
66 num_colon_m=re.compile("[0-9]:")82
6783def get_changelog(source, version, changelog_cache_d):
68 cache_tmp = "%s/.changelog.%s_%s" % (cache_d, src,ver)84 """
69 for pile in ("main", "universe", "multiverse", "restricted"):85 Download changelog for source / version and returns path to that
70 pre=src[0:1]86
71 # packages starting with 'lib' are special87 :param str source: Source package name
72 if src.startswith("lib"): pre=src[0:4]88 :param str version: Source package version
7389 :param str changelog_cache_d: path to store cached changelogs
74 # packages with '1:' versions have different paths90 :raises MissingChangelogError: If changelog file could not be downloaded
75 # update-manager at version 1:0.134.11.91 :return: changelog file for source package & version
76 # would be at main/u/update-manager/update-manager_1:0.134.1192 :rtype: str
77 # instead at main/u/update-manager/update-manager_0.134.11/93 """
78 xver=ver94
79 if num_colon_m.match(ver):95 cache_f = "%s/changelog.%s_%s" % (changelog_cache_d, source, version)
80 xver=ver[2:]96
8197 if os.path.isfile(cache_f):
82 clog_url = "%s/%s/%s/%s/%s_%s/changelog" % \98 logging.debug("Using cached changelog for %s:%s", source, version)
83 ( clog_burl, pile, pre, src, src, xver)99 return cache_f
84100
85 fp = open(cache_tmp,"w")101 furls = []
86 u_fp = urllib.urlopen(clog_url)102 num_colon_m = re.compile("[0-9]:")
87 while True:103
88 buf = u_fp.read(bsize)104 cache_tmp_fd, cache_tmp_path = tempfile.mkstemp(
89 fp.write(buf)105 ".changelog.%s_%s" % (source, version),
90 if len(buf) != bsize: break106 prefix="." + tempfile.gettempprefix(), dir=".")
91 fp.close()107 cache_tmp = os.fdopen(cache_tmp_fd, "w")
92 if u_fp.getcode() == 200:108 for pile in ("main", "universe", "multiverse", "restricted"):
93 os.rename(cache_tmp,cache_f)109 pre = source[0:1]
94 return(cache_f)110 # packages starting with 'lib' are special
95 print "missing %s: %s" % (src,clog_url)111 if source.startswith("lib"):
96 furls.append(clog_url)112 pre = source[0:4]
97 os.unlink(cache_tmp)113
98 raise(MissingChangelogError("Failed to find changelog for %s at version %s.\n tried %s" % (src, ver, ' '.join(furls))))114 # packages with '1:' versions have different paths
99115 # update-manager at version 1:0.134.11.
100def hashmffile(filename):116 # is main/u/update-manager/update-manager_0.134.11/
101 fp=open(filename,"r")117 # rather than main/u/update-manager/update-manager_1:0.134.11
102 lines=fp.readlines()118 url_version = version
103 fp.close()119 if num_colon_m.match(version):
104 ret={ }120 url_version = version[2:]
105 for line in lines:121
106 (pkg,ver)=line.split()122 # Changelog URL example http://changelogs.ubuntu.com/changelogs/\
107 ret[pkg]=ver123 # pool/main/h/hal/hal_0.5.5.1-1ubuntu2/changelog
108 return(ret)124 changelog_url = "http://changelogs.ubuntu.com/changelogs/pool/" \
109125 "%s/%s/%s/%s_%s/changelog" % \
110def filecontents(filename):126 (pile, pre, source, source, url_version)
111 fp=open(filename,"r")127
112 contents=fp.read()128 changelog = requests.get(changelog_url)
113 fp.close()129 if changelog.status_code == 200:
114 return(contents)130 cache_tmp.write(changelog.content)
115131 cache_tmp.close()
116def prep_cacheroot(cache_d, release):132 shutil.copy2(cache_tmp_path, cache_f)
117 mirror = "http://archive.ubuntu.com/ubuntu/"133 os.unlink(cache_tmp_path)
118 pockets = ( release, "%s-updates" % release, "%s-security" % release,134 return cache_f
119 "%s-proposed" % release, )135 else:
120 components = ( "main", "universe" )136 logging.error("missing %s: %s", source, changelog_url)
121 srclines = []137 furls.append(changelog_url)
122 for pocket in pockets:138 if os.path.exists(cache_tmp_path):
123 srclines.append("deb %s %s %s" % (mirror, pocket, ' '.join(components)))139 os.unlink(cache_tmp_path)
124 try:140 raise MissingChangelogError("Failed to find changelog for %s at version "
125 os.makedirs( "%s/etc/apt" % cache_d )141 "%s.\n tried %s" % (source, version,
126 except:142 ' '.join(furls)))
127 pass143
128 asl = open( "%s/etc/apt/sources.list" % cache_d, "w" )144
129 asl.write('\n'.join(srclines))145def manifest_to_dict(filename):
130 asl.close()146 """
131147 Parse manifest file to create a package / version mapping
132def get_cache(cache_d, cache=None):148
133 if cache: return cache149 :param str filename: Name of package manifest file
134 cache = apt.Cache(rootdir=cache_d)150 :return: List of package versions by name
135 cache.update()151 :rtype: dict
136 cache.open()152 """
137 return cache153
138154 ret = {}
139# render a changelog block to something printable155 logging.debug('Reading package manifest from %s', filename)
156 with open(filename, "r") as manifest:
157 for line in manifest:
158 (pkg, ver) = line.split()
159 ret[pkg] = ver
160 return ret
161
162
163def open_apt_cache(arch, release, cache_d=None):
164 """
165 Create, update, and open an apt cache.
166
167 This creates an apt cache directory and write a sources.list file
168 before updating and opening the cache. The caller is responsible
169 for closing the cache.
170
171 :param str arch: Package architecture
172 :param str release: Ubuntu release name (e.g. Xenial)
173 :param str cache_d: apt cache path
174 :returns: tuple of Open/updated apt cache and cache path name
175 :rtype: tuple(:class:`apt.Cache`, str)
176 """
177
178 if not cache_d:
179 cache_d = "./cache.%s-%s" % (release, arch)
180 logging.info("Using %s as the apt cache directory", cache_d)
181
182 if arch in ['amd64', 'i386']:
183 mirror = "http://archive.ubuntu.com/ubuntu/"
184 else:
185 mirror = "http://ports.ubuntu.com/ubuntu-ports/"
186 logging.debug('Configuring apt cache using mirror %s', mirror)
187
188 pockets = (release, "%s-updates" % release, "%s-security" % release,
189 "%s-proposed" % release, )
190 components = ("main", "universe")
191 srclines = []
192 for pocket in pockets:
193 srcline = "deb %s %s %s" % (mirror, pocket, ' '.join(components))
194 logging.debug('Adding source: %s', srcline)
195 srclines.append(srcline)
196 try:
197 os.makedirs("%s/etc/apt" % cache_d)
198 except OSError as oserror:
199 if os.errno.EEXIST != oserror.errno:
200 raise
201 with open("%s/etc/apt/sources.list" % cache_d, "w") as asl:
202 asl.write('\n'.join(srclines))
203
204 apt.apt_pkg.config.set("Apt::Architecture", arch)
205
206 logging.debug('Using host apt keys for signature verification')
207 apt.apt_pkg.config.set("Dir::Etc::Trusted", "/etc/apt/trusted.gpg")
208 apt.apt_pkg.config.set("Dir::Etc::TrustedParts",
209 "/etc/apt/trusted.gpg.d/")
210
211 cache = apt.Cache(rootdir=cache_d)
212 logging.info('Updating apt cache')
213 cache.update()
214 logging.info('Update of apt cache complete')
215 cache.open()
216 return cache, cache_d
217
218
140def render_block(block):219def render_block(block):
141 return('\n'.join([x.encode('ascii', 'replace') for x in block.changes() if x]))220 """
142221 Render a changelog block to something printable (dropping blank lines)
143def print_blocks(blist):222
144 # print a blocklist223 :param :class:`debian.changelog.ChangeBlock` block: Changelog block
145 for block in blist:224 :return: String containing the changelog block text
146 print render_block(block)225 :rtype: str
226 """
227 return '\n'.join([x for x in block.changes() if x])
228
229
230def print_blocks(block_list):
231 """
232 Print a Changelog block list
233
234 :param list block_list: List of :class:`debian.changelog.ChangeBlock`
235 """
236
237 for block in block_list:
238 print(render_block(block).encode('utf-8').decode('utf-8'))
239
240
241def kernel_fixups(manifest_from, manifest_to):
242 """
243 Fix up kernels so the pkg names match
244
245 Kernel package names change from release to release so that they are
246 co-installable, but we need to find matching package names in the
247 two manifests to provide a list of changes between the versions of the
248 package in each manifest.
249 This function will return an altered version of manifest_from with kernel
250 package names changed to match kernel package names in manifest_to. This
251 will support later version comparisons.
252
253 :param dict manifest_from: Dictionary mapping package to version for of
254 the starting manifest
255 :param dict manifest_to: Dictionary mapping package to version for the
256 ending manifest
257 :return: Starting manifest dictionary with altered kernel package names
258 to match names in ending manifest
259 :rtype dict:
260 """
261 kfixups = {}
262 kmatch = re.compile("linux-image-[0-9]")
263 for pkg in manifest_to:
264 # if this is a linux-image-* binary package do some hacks to make it
265 # look like manifest_from is the same (format like
266 # linux-image-2.6.32-32-virtual)
267 if kmatch.match(pkg):
268 logging.debug('Found kernel %s in manifest #2', pkg)
269 img_type = pkg.split("-")[-1]
270 if pkg in manifest_from:
271 logging.debug('Found same kernel in manifest #1')
272 continue
273 for fpkg in manifest_from:
274 if kmatch.match(fpkg) and fpkg.endswith("-%s" % img_type):
275 logging.debug('Found similar kernel %s in manifest #1',
276 fpkg)
277 kfixups[pkg] = fpkg
278
279 for pkg_to, pkg_from in iteritems(kfixups):
280 logging.debug('Substituting kernel %s for %s in manifest #1 to '
281 'enable version comparison', pkg_to, pkg_from)
282 manifest_from[pkg_to] = manifest_from[pkg_from]
283 del manifest_from[pkg_from]
284 return manifest_from
285
286
287def find_added(manifest_from, manifest_to):
288 "Find new packages in manifest_to"
289 new = {}
290 for pkg in sorted(viewkeys(manifest_to) - viewkeys(manifest_from)):
291 logging.debug('New package: %s', pkg)
292 new[pkg] = manifest_to[pkg]
293 return new
294
295
296def find_removed(manifest_from, manifest_to):
297 "Find packages removed from manifest_from"
298 removed = {}
299 for pkg in sorted(viewkeys(manifest_from) - viewkeys(manifest_to)):
300 logging.debug('Removed package: %s', pkg)
301 removed[pkg] = manifest_from[pkg]
302 return removed
303
304
305def find_changed(manifest_from, manifest_to):
306 "Find modified packages"
307 changed = []
308 for pkg in sorted(viewkeys(manifest_from) & viewkeys(manifest_to)):
309 if manifest_from[pkg] != manifest_to[pkg]:
310 logging.debug('Changed package: %s', pkg)
311 changed.append(pkg)
312 return changed
313
314
315def map_source_to_binary(cache, packages):
316 "Create a dictionary of source to list of binary packages"
317 src2bins = {}
318 for bin_pkg in packages:
319 bin_name = bin_pkg.split(':')[0]
320 if bin_name in cache:
321 src2bins.setdefault(
322 cache[bin_name].versions[0].source_name, []).append(bin_pkg)
323 return src2bins
324
325
326def get_pkg_versions(cache, binary):
327 "Get all known versions from the apt cache"
328 pkg_name = binary.split(':')[0]
329 try:
330 return cache[pkg_name].versions
331 except KeyError:
332 raise Exception(
333 "%s not in cache or did not have version info in cache" %
334 pkg_name)
335
336
337def source_version_for_binary(cache, binary, binary_ver):
338 "Find the source version data for a specific binary version"
339 versions = get_pkg_versions(cache, binary)
340 try:
341 return versions[binary_ver].source_version
342 except KeyError:
343 # Strip the architecture name from the binary
344 source_name = cache[binary.split(':')[0]].versions[0].source_name
345 msg = ("Unable to determine source version for %s. "
346 "Binary package %s/%s not in known source version "
347 "list (%s)" % (source_name, binary, binary_ver, versions))
348 raise UnknownSourceVersionError(msg)
349
350
351def filter_changelog(changelog_path, version_low, version_high):
352 """
353 Extract changelog entries within a version range
354
355 The range of changelog entries returned will include all entries
356 after version_low up to, and including, version_high.
357 If either the starting or ending version are not found in the
358 list of changelog entries the result will be incomplete and
359 a non-empty error message is returned to indicate the issue.
360
361 :param str changelog_path: File name of the changelog to process
362 :return: list of changelog blocks and an error_msg if incomplete
363 :rtype tuple(list, str):
364 """
365
366 with open(changelog_path, "r") as fileptr:
367 chlog = Changelog(fileptr.read())
368 change_blocks = []
369 start = False
370 end = False
371 error_msg = ''
372
373 # The changelog blocks are in reverse order; we'll see high before low.
374 for block in chlog:
375 if block.version == version_high:
376 start = True
377 change_blocks = []
378 if block.version == version_low:
379 end = True
380 break
381 change_blocks.append(block)
382 if not start:
383 error_msg = "Missing starting version {} in {}. " \
384 "Changlelog will be incomplete".format(
385 version_high, changelog_path)
386 logging.error(error_msg)
387 if not end:
388 if error_msg:
389 # Start and end were not found, put a newline between their
390 # error messages
391 error_msg += '\n'
392 error_msg += "Missing ending version {} in {}. " \
393 "Changelog output truncated".format(
394 version_low, changelog_path)
395 logging.error(error_msg)
396 return change_blocks, error_msg
397
398
399def print_changelogs(apt_cache, apt_cache_d, manifest_from, manifest_to,
400 changed):
401 """
402 Print changelog entries for each changed package limited to
403 changes in the package between the versions in the two manifests.
404
405 :param :class:`apt.Cache` apt_cache: Open & up-to-date apt cache
406 :param str apt_cache_d: apt cache path
407 :param dict manifest_from: Packages and their versions in the
408 first manifest file
409 :param dict manifest_to: Packages and their versions in the
410 second manifest file
411 :param list changed: Packages which changed between the two manifests
412 """
413
414 srcs = {}
415 errors = []
416 src2bins = map_source_to_binary(apt_cache, changed)
417
418 # Generate changelog data per unique source package
419 for source_name in src2bins:
420 srcs[source_name] = {"changelog_file": "", "changeblocks": []}
421 src = srcs[source_name]
422
423 # Use the first binary listed for a source package
424 binary_name = src2bins[source_name][0]
425
426 # Find the source version data for the binary in manifest #2
427 try:
428 src['version_to'] = source_version_for_binary(
429 apt_cache, binary_name, manifest_to[binary_name])
430 except UnknownSourceVersionError as excp:
431 logging.error(str(excp))
432 errors.append(excp)
433 continue
434
435 # Find the source version data for the binary in manifest #1
436 binver_from = manifest_from[binary_name]
437 try:
438 src['version_from'] = source_version_for_binary(
439 apt_cache, binary_name, binver_from)
440 except UnknownSourceVersionError as excp:
441 if manifest_to[binary_name] == src['version_to']:
442 logging.info('Could not find source version data in apt '
443 'cache. Assuming source %s version %s from '
444 'binary %s', source_name, binver_from,
445 binary_name)
446 src['version_from'] = binver_from
447 else:
448 logging.error(str(excp))
449 errors.append(excp)
450 continue
451
452 # Check for version regression between manifests
453 try:
454 if version_compare(src['version_from'], src['version_to']) > 0:
455 msg = "Package version regression {} -> {}".format(
456 src['version_from'], src['version_to'])
457 raise UnknownSourceVersionError(msg)
458 except UnknownSourceVersionError as excp:
459 errors.append(excp)
460 continue
461
462 # Get the changelog for this source package
463 try:
464 # Use the apt cache directory to store the changelog cache
465 srcs[source_name]["changelog_file"] = get_changelog(
466 source_name, src['version_to'], changelog_cache_d=apt_cache_d)
467 except MissingChangelogError as excp:
468 errors.append(excp)
469 continue
470
471 # Filter the changelog to a list of blocks between the two versions
472 try:
473 srcs[source_name]["changeblocks"], incomplete = filter_changelog(
474 srcs[source_name]["changelog_file"], src['version_from'],
475 src['version_to'])
476 if incomplete:
477 raise ChangelogMissingVersion(incomplete)
478 except ChangelogMissingVersion as exp:
479 errors.append(exp)
480 continue
481
482 # Print changelog ranges for changed packages
483 for source_name in sorted(src2bins):
484 binlist = sorted(src2bins[source_name])
485 binary = binlist[0]
486 print("==== %s: %s => %s ====" %
487 (source_name, manifest_from[binary], manifest_to[binary]))
488 print("==== %s" % ' '.join(binlist))
489 print_blocks(srcs[source_name]["changeblocks"])
490
491 if errors:
492 print("**** Errors ****")
493 for error in errors:
494 print(error)
495
496
497def parse_args():
498 """
499 Parse command line arguments
500
501 :returns: options and remaining arguments from OptionParser.parse_args()
502 :rtype list:
503 """
504
505 parser = OptionParser(usage="Usage: {} arch suite manifest1 manifest2\n"
506 "Compare two manifest files, and show "
507 "changelog differences."
508 .format(os.path.basename(sys.argv[0])))
509 parser.add_option("--cache-dir", dest="cache_d",
510 help="cache dir for info", metavar="DIR", type="string",
511 default=None)
512 parser.add_option("-v", "--verbose", action="count", dest="loglevel",
513 help="increase verbosity", default=0)
514
515 (options, args) = parser.parse_args()
516
517 if len(args) != 4:
518 parser.error('you must provide arch, release, and 2 manifest files')
519
520 return options, args
521
522
523def setup_logging(loglevel=0):
524 """
525 Configure logging
526
527 By default, log WARNING and higher messages
528 :param int: loglevel 0: Warning, 1: Info, 2: Debug
529 """
530
531 loglevel = [logging.WARNING,
532 logging.INFO,
533 logging.DEBUG][min(2, loglevel)]
534
535 logging.basicConfig(
536 level=loglevel,
537 format="%(asctime)s %(name)s/%(levelname)s: %(message)s",
538 stream=sys.stderr)
539
540
541def stdout_force_unicode():
542 """
543 Force output to UTF-8 at all times
544
545 We want to output UTF-8 at all times to preserve Unicode characters
546 in the changelog blocks. For Python 2 we will wrap sys.stdout with
547 an instance of StreamWriter with our preferred coding. Python3 requires
548 no changes.
549
550 When writing output to the terminal we get the encoding of the
551 terminal (utf-8 these days). When we redirect or pipe the output of
552 the program it is generally not possible to know what the input
553 encoding of the receiving program is, the encoding when redirecting
554 to a file will be None (Python 2.7) or UTF-8 (Python 3)
555
556 $ python2.7 -c "import sys; print sys.stdout.encoding" | cat
557 None
558
559 $ python3.4 -c "import sys; print(sys.stdout.encoding)" | cat
560 UTF-8
561
562 Source:
563 https://wiki.python.org/moin/PrintFails#print.2C_write_and_Unicode_in_pre-3.0_Python
564 """
565
566 if sys.version_info[0] < 3:
567 encoding = codecs.getwriter(locale.getpreferredencoding())
568 sys.stdout = encoding(sys.stdout)
569
147570
148def main():571def main():
149 parser = OptionParser(usage=Usage)572 """
150 parser.add_option("--sources-list", dest="sources_list",573 Given two manifest files for a particular release/arch, find the
151 help="sources.list file", metavar="FILE", type="string",574 packages which have been added, removed, and changed. Print the
152 default=None)575 changelog entries for the changed packages limited to changes between
153 parser.add_option("--cache-dir", dest="cache_d",576 the two versions.
154 help="cache dir for info", metavar="DIR", type="string",577 """
155 default=None)578
156 parser.add_option("-v", "--verbose", action="count", dest="loglevel",579 stdout_force_unicode()
157 help="increase verbosity", default=0)580 options, (arch, release, manifest_from_filename,
158581 manifest_to_filename) = parse_args()
159 (options, args) = parser.parse_args()582
160583 setup_logging(options.loglevel)
161 if len(args) != 4:584
162 parser.error('you must provide arch, release, and 2 manifest files')585 # index both manifests
163586 manifest_to = manifest_to_dict(manifest_to_filename)
164 # logging module levels are 0,10,20,30 ...587 manifest_from = kernel_fixups(manifest_to_dict(manifest_from_filename),
165 loglevel = (6 - options.loglevel) * 10588 manifest_to)
166 logging.basicConfig(level=loglevel,format=logformat,stream=sys.stderr)589
167590 new = find_added(manifest_from, manifest_to)
168 (arch,release,mf_from, mf_to) = args591 removed = find_removed(manifest_from, manifest_to)
169592 changed = find_changed(manifest_from, manifest_to)
170 cache_d = options.cache_d593
171 if not options.cache_d:594 print("new: %s" % new)
172 cache_d = "./cache.%s-%s" % (release, arch)595 print("removed: %s" % removed)
173596 print("changed: %s" % changed)
174 # index both manifests597
175 h_from=hashmffile(mf_from)598 # if modified packages, download all changelogs from changelogs.
176 h_to=hashmffile(mf_to)599 if changed:
177600 cache, cache_d = open_apt_cache(arch, release, options.cache_d)
178 # fix up kernels so the pkg names match601 print_changelogs(cache, cache_d, manifest_from, manifest_to, changed)
179 kfixups = { }
180 kmatch = re.compile("linux-image-[0-9]")
181 for pkg,ver in h_to.iteritems():
182 # if this is a linux-image-* binary package do some hacks to make it
183 # look like h_from is the same (format like linux-image-2.6.32-32-virtual)
184 if kmatch.match(pkg):
185 img_type = pkg[pkg.rfind("-")+1:]
186 if pkg in h_from: continue
187 for fpkg, fver in h_from.iteritems():
188 if ( kmatch.match(fpkg) and fpkg.endswith("-%s" % img_type)):
189 kfixups[pkg] = fpkg
190
191 for pkg_to, pkg_from in kfixups.iteritems():
192 h_from[pkg_to] = h_from[pkg_from]
193 del h_from[pkg_from]
194
195 # find new packages in mf2
196 new={ }
197 for pkg,ver in h_to.iteritems():
198 if pkg not in h_from: new[pkg]=h_to[pkg]
199
200 # find packages removed from mf1
201 removed={ }
202 for pkg,ver in h_from.iteritems():
203 if pkg not in h_to: removed[pkg]=h_from[pkg]
204
205 # find modified packages
206 changed=[]
207 for pkg,ver in h_to.iteritems():
208 if pkg in h_from and h_from[pkg] != ver: changed.append(pkg)
209
210 print "new: %s" % new
211 print "removed: %s" % removed
212 print "changed: %s" % changed
213
214
215 prep_cacheroot(cache_d,release)
216 apt.apt_pkg.config.set("Apt::Architecture",arch)
217 cache = None
218
219 errors = [ ]
220
221 # if modified packages
222 # download all changelogs from changelogs.
223 bin2src = { }
224 srcs = { }
225 if len(changed):
226 cache=get_cache(cache_d, cache)
227
228 b2s_hash=get_bin2src(changed,cache)
229
230 for pkg,src in b2s_hash.iteritems():
231 if src not in bin2src:
232 bin2src[src]=[]
233 bin2src[src].append(pkg)
234
235 missing_cl=[]
236 for src,binlist in bin2src.iteritems():
237 if src not in srcs:
238 srcs[src] = { "changelog_file": "", "changeblocks": [ ] }
239
240 try:
241 pkg_name = binlist[0].split(':')[0]
242 versions = cache[pkg_name].versions
243 except KeyError as e:
244 raise Exception("%s not in cache did not have versions info in cache")
245 binver_to=h_to[binlist[0]]
246 try:
247 srcver_to=versions[binver_to].source_version
248 except KeyError as e:
249 raise Exception("bin pkg %s from src %s version %s not in binver list (%s)" %
250 (binlist[0],src,binver_to,versions))
251
252 binver_from=h_from[binlist[0]]
253 try:
254 srcver_from=versions[binver_from].source_version
255 except KeyError as e:
256 if binver_to == srcver_to:
257 srcver_from = binver_from
258 else:
259 raise UnknownSourceVersionError(
260 "unable to determine src version for %s/%s" %
261 (src,binver_from))
262
263 try:
264 clogfile=getchangelog(src,srcver_to,cache_d)
265 srcs[src]["changelog_file"] = clogfile
266 chlog=Changelog(filecontents(clogfile))
267 srcs[src]["changeblocks"] = [ ]
268 start = False
269 end = False
270 for block in chlog:
271 if block.version == srcver_to:
272 start = True
273 if block.version == srcver_from:
274 end = True
275 break
276 if start:
277 srcs[src]["changeblocks"].append(block)
278 if not (start and end):
279 em = ("%s missing %s or %s." %
280 (clogfile, srcver_to, binver_from))
281 if apt.VersionCompare(binver_from, srcver_to) > 0:
282 em = "%s %s" % (em, "from version > to version")
283 raise ChangelogMissingVersion(em)
284
285 except MissingChangelogError as e:
286 errors.append(e)
287 except ChangelogMissingVersion as e:
288 errors.append(e)
289 except UnknownSourceVersionError as e:
290 errors.append(e)
291
292 for src,binlist in bin2src.iteritems():
293 print "==== %s: %s => %s ====" % (src, h_from[binlist[0]], h_to[binlist[0]])
294 print "==== %s" % ' '.join(binlist)
295 print_blocks(srcs[src]["changeblocks"])
296
297 if errors:
298 print "**** Errors ****"
299 for e in errors: print e
300 # load each changelog
301602
302603
303if __name__ == "__main__":604if __name__ == "__main__":
304 main()605 main()

Subscribers

People subscribed via source and target branches