diff options
| -rw-r--r-- | meta/lib/oe/buildhistory_analysis.py | 240 | ||||
| -rwxr-xr-x | scripts/buildhistory-diff | 43 |
2 files changed, 283 insertions, 0 deletions
diff --git a/meta/lib/oe/buildhistory_analysis.py b/meta/lib/oe/buildhistory_analysis.py new file mode 100644 index 0000000000..9f42fe38ac --- /dev/null +++ b/meta/lib/oe/buildhistory_analysis.py | |||
| @@ -0,0 +1,240 @@ | |||
| 1 | # Report significant differences in the buildhistory repository since a specific revision | ||
| 2 | # | ||
| 3 | # Copyright (C) 2012 Intel Corporation | ||
| 4 | # Author: Paul Eggleton <paul.eggleton@linux.intel.com> | ||
| 5 | # | ||
| 6 | # Note: requires GitPython 0.3.1+ | ||
| 7 | # | ||
| 8 | # You can use this from the command line by running scripts/buildhistory-diff | ||
| 9 | # | ||
| 10 | |||
| 11 | import sys | ||
| 12 | import os.path | ||
| 13 | import difflib | ||
| 14 | import git | ||
| 15 | |||
| 16 | |||
| 17 | # How to display fields | ||
| 18 | pkg_list_fields = ['DEPENDS', 'RDEPENDS', 'RRECOMMENDS', 'PACKAGES', 'FILES', 'FILELIST'] | ||
| 19 | pkg_numeric_fields = ['PKGSIZE'] | ||
| 20 | # Fields to monitor | ||
| 21 | pkg_monitor_fields = ['RDEPENDS', 'RRECOMMENDS', 'PACKAGES', 'FILELIST', 'PKGSIZE'] | ||
| 22 | # Percentage change to alert for numeric fields | ||
| 23 | pkg_monitor_numeric_threshold = 20 | ||
| 24 | # Image files to monitor | ||
| 25 | img_monitor_files = ['installed-package-names.txt', 'files-in-image.txt'] | ||
| 26 | |||
| 27 | |||
| 28 | class ChangeRecord: | ||
| 29 | def __init__(self, path, fieldname, oldvalue, newvalue): | ||
| 30 | self.path = path | ||
| 31 | self.fieldname = fieldname | ||
| 32 | self.oldvalue = oldvalue | ||
| 33 | self.newvalue = newvalue | ||
| 34 | self.filechanges = None | ||
| 35 | |||
| 36 | def __str__(self): | ||
| 37 | if self.fieldname in pkg_list_fields: | ||
| 38 | aitems = self.oldvalue.split(' ') | ||
| 39 | bitems = self.newvalue.split(' ') | ||
| 40 | removed = list(set(aitems) - set(bitems)) | ||
| 41 | added = list(set(bitems) - set(aitems)) | ||
| 42 | return '%s: %s:%s%s' % (self.path, self.fieldname, ' removed "%s"' % ' '.join(removed) if removed else '', ' added "%s"' % ' '.join(added) if added else '') | ||
| 43 | elif self.fieldname in pkg_numeric_fields: | ||
| 44 | aval = int(self.oldvalue) | ||
| 45 | bval = int(self.newvalue) | ||
| 46 | percentchg = ((bval - aval) / float(aval)) * 100 | ||
| 47 | return '%s: %s changed from %d to %d (%s%d%%)' % (self.path, self.fieldname, aval, bval, '+' if percentchg > 0 else '', percentchg) | ||
| 48 | elif self.fieldname in img_monitor_files: | ||
| 49 | out = 'Changes to %s (%s):\n ' % (self.path, self.fieldname) | ||
| 50 | if self.filechanges: | ||
| 51 | out += '\n '.join(['%s' % i for i in self.filechanges]) | ||
| 52 | else: | ||
| 53 | alines = self.oldvalue.splitlines() | ||
| 54 | blines = self.newvalue.splitlines() | ||
| 55 | diff = difflib.unified_diff(alines, blines, self.fieldname, self.fieldname, lineterm='') | ||
| 56 | out += '\n '.join(list(diff)) | ||
| 57 | out += '\n --' | ||
| 58 | return out | ||
| 59 | else: | ||
| 60 | return '%s: %s changed from "%s" to "%s"' % (self.path, self.self.fieldname, self.oldvalue, self.newvalue) | ||
| 61 | |||
| 62 | |||
| 63 | class FileChange: | ||
| 64 | changetype_add = 'A' | ||
| 65 | changetype_remove = 'R' | ||
| 66 | changetype_type = 'T' | ||
| 67 | changetype_perms = 'P' | ||
| 68 | changetype_ownergroup = 'O' | ||
| 69 | changetype_link = 'L' | ||
| 70 | |||
| 71 | def __init__(self, path, changetype, oldvalue = None, newvalue = None): | ||
| 72 | self.path = path | ||
| 73 | self.changetype = changetype | ||
| 74 | self.oldvalue = oldvalue | ||
| 75 | self.newvalue = newvalue | ||
| 76 | |||
| 77 | def _ftype_str(self, ftype): | ||
| 78 | if ftype == '-': | ||
| 79 | return 'file' | ||
| 80 | elif ftype == 'd': | ||
| 81 | return 'directory' | ||
| 82 | elif ftype == 'l': | ||
| 83 | return 'symlink' | ||
| 84 | elif ftype == 'c': | ||
| 85 | return 'char device' | ||
| 86 | elif ftype == 'b': | ||
| 87 | return 'block device' | ||
| 88 | elif ftype == 'p': | ||
| 89 | return 'fifo' | ||
| 90 | elif ftype == 's': | ||
| 91 | return 'socket' | ||
| 92 | else: | ||
| 93 | return 'unknown (%s)' % ftype | ||
| 94 | |||
| 95 | def __str__(self): | ||
| 96 | if self.changetype == self.changetype_add: | ||
| 97 | return '%s was added' % self.path | ||
| 98 | elif self.changetype == self.changetype_remove: | ||
| 99 | return '%s was removed' % self.path | ||
| 100 | elif self.changetype == self.changetype_type: | ||
| 101 | return '%s changed type from %s to %s' % (self.path, self._ftype_str(self.oldvalue), self._ftype_str(self.newvalue)) | ||
| 102 | elif self.changetype == self.changetype_perms: | ||
| 103 | return '%s changed permissions from %s to %s' % (self.path, self.oldvalue, self.newvalue) | ||
| 104 | elif self.changetype == self.changetype_ownergroup: | ||
| 105 | return '%s changed owner/group from %s to %s' % (self.path, self.oldvalue, self.newvalue) | ||
| 106 | elif self.changetype == self.changetype_link: | ||
| 107 | return '%s changed symlink target from %s to %s' % (self.path, self.oldvalue, self.newvalue) | ||
| 108 | else: | ||
| 109 | return '%s changed (unknown)' % self.path | ||
| 110 | |||
| 111 | |||
| 112 | def blob_to_dict(blob): | ||
| 113 | alines = blob.data_stream.read().splitlines() | ||
| 114 | adict = {} | ||
| 115 | for line in alines: | ||
| 116 | splitv = [i.strip() for i in line.split('=',1)] | ||
| 117 | if splitv.count > 1: | ||
| 118 | adict[splitv[0]] = splitv[1] | ||
| 119 | return adict | ||
| 120 | |||
| 121 | |||
| 122 | def file_list_to_dict(lines): | ||
| 123 | adict = {} | ||
| 124 | for line in lines: | ||
| 125 | # Leave the last few fields intact so we handle file names containing spaces | ||
| 126 | splitv = line.split(None,4) | ||
| 127 | # Grab the path and remove the leading . | ||
| 128 | path = splitv[4][1:].strip() | ||
| 129 | # Handle symlinks | ||
| 130 | if(' -> ' in path): | ||
| 131 | target = path.split(' -> ')[1] | ||
| 132 | path = path.split(' -> ')[0] | ||
| 133 | adict[path] = splitv[0:3] + [target] | ||
| 134 | else: | ||
| 135 | adict[path] = splitv[0:3] | ||
| 136 | return adict | ||
| 137 | |||
| 138 | |||
| 139 | def compare_file_lists(alines, blines): | ||
| 140 | adict = file_list_to_dict(alines) | ||
| 141 | bdict = file_list_to_dict(blines) | ||
| 142 | filechanges = [] | ||
| 143 | for path, splitv in adict.iteritems(): | ||
| 144 | newsplitv = bdict.pop(path, None) | ||
| 145 | if newsplitv: | ||
| 146 | # Check type | ||
| 147 | oldvalue = splitv[0][0] | ||
| 148 | newvalue = newsplitv[0][0] | ||
| 149 | if oldvalue != newvalue: | ||
| 150 | filechanges.append(FileChange(path, FileChange.changetype_type, oldvalue, newvalue)) | ||
| 151 | # Check permissions | ||
| 152 | oldvalue = splitv[0][1:] | ||
| 153 | newvalue = newsplitv[0][1:] | ||
| 154 | if oldvalue != newvalue: | ||
| 155 | filechanges.append(FileChange(path, FileChange.changetype_perms, oldvalue, newvalue)) | ||
| 156 | # Check owner/group | ||
| 157 | oldvalue = '%s/%s' % (splitv[1], splitv[2]) | ||
| 158 | newvalue = '%s/%s' % (newsplitv[1], newsplitv[2]) | ||
| 159 | if oldvalue != newvalue: | ||
| 160 | filechanges.append(FileChange(path, FileChange.changetype_ownergroup, oldvalue, newvalue)) | ||
| 161 | # Check symlink target | ||
| 162 | if newsplitv[0][0] == 'l': | ||
| 163 | if splitv.count > 3: | ||
| 164 | oldvalue = splitv[3] | ||
| 165 | else: | ||
| 166 | oldvalue = None | ||
| 167 | newvalue = newsplitv[3] | ||
| 168 | if oldvalue != newvalue: | ||
| 169 | filechanges.append(FileChange(path, FileChange.changetype_link, oldvalue, newvalue)) | ||
| 170 | else: | ||
| 171 | filechanges.append(FileChange(path, FileChange.changetype_remove)) | ||
| 172 | |||
| 173 | # Whatever is left over has been added | ||
| 174 | for path in bdict: | ||
| 175 | filechanges.append(FileChange(path, FileChange.changetype_add)) | ||
| 176 | |||
| 177 | return filechanges | ||
| 178 | |||
| 179 | |||
| 180 | def compare_lists(alines, blines): | ||
| 181 | removed = list(set(alines) - set(blines)) | ||
| 182 | added = list(set(blines) - set(alines)) | ||
| 183 | |||
| 184 | filechanges = [] | ||
| 185 | for pkg in removed: | ||
| 186 | filechanges.append(FileChange(pkg, FileChange.changetype_remove)) | ||
| 187 | for pkg in added: | ||
| 188 | filechanges.append(FileChange(pkg, FileChange.changetype_add)) | ||
| 189 | |||
| 190 | return filechanges | ||
| 191 | |||
| 192 | |||
| 193 | def process_changes(repopath, revision1, revision2 = 'HEAD', report_all = False): | ||
| 194 | repo = git.Repo(repopath) | ||
| 195 | assert repo.bare == False | ||
| 196 | commit = repo.commit(revision1) | ||
| 197 | diff = commit.diff(revision2) | ||
| 198 | |||
| 199 | changes = [] | ||
| 200 | for d in diff.iter_change_type('M'): | ||
| 201 | path = os.path.dirname(d.a_blob.path) | ||
| 202 | if path.startswith('packages/'): | ||
| 203 | adict = blob_to_dict(d.a_blob) | ||
| 204 | bdict = blob_to_dict(d.b_blob) | ||
| 205 | |||
| 206 | for key in adict: | ||
| 207 | if report_all or key in pkg_monitor_fields: | ||
| 208 | if adict[key] != bdict[key]: | ||
| 209 | if (not report_all) and key in pkg_numeric_fields: | ||
| 210 | aval = int(adict[key]) | ||
| 211 | bval = int(bdict[key]) | ||
| 212 | percentchg = ((bval - aval) / float(aval)) * 100 | ||
| 213 | if percentchg < pkg_monitor_numeric_threshold: | ||
| 214 | continue | ||
| 215 | chg = ChangeRecord(path, key, adict[key], bdict[key]) | ||
| 216 | changes.append(chg) | ||
| 217 | elif path.startswith('images/'): | ||
| 218 | filename = os.path.basename(d.a_blob.path) | ||
| 219 | if filename in img_monitor_files: | ||
| 220 | if filename == 'files-in-image.txt': | ||
| 221 | alines = d.a_blob.data_stream.read().splitlines() | ||
| 222 | blines = d.b_blob.data_stream.read().splitlines() | ||
| 223 | filechanges = compare_file_lists(alines,blines) | ||
| 224 | if filechanges: | ||
| 225 | chg = ChangeRecord(path, filename, None, None) | ||
| 226 | chg.filechanges = filechanges | ||
| 227 | changes.append(chg) | ||
| 228 | elif filename == 'installed-package-names.txt': | ||
| 229 | alines = d.a_blob.data_stream.read().splitlines() | ||
| 230 | blines = d.b_blob.data_stream.read().splitlines() | ||
| 231 | filechanges = compare_lists(alines,blines) | ||
| 232 | if filechanges: | ||
| 233 | chg = ChangeRecord(path, filename, None, None) | ||
| 234 | chg.filechanges = filechanges | ||
| 235 | changes.append(chg) | ||
| 236 | else: | ||
| 237 | chg = ChangeRecord(path, filename, d.a_blob.data_stream.read(), d.b_blob.data_stream.read()) | ||
| 238 | changes.append(chg) | ||
| 239 | |||
| 240 | return changes | ||
diff --git a/scripts/buildhistory-diff b/scripts/buildhistory-diff new file mode 100755 index 0000000000..6b344ebfaf --- /dev/null +++ b/scripts/buildhistory-diff | |||
| @@ -0,0 +1,43 @@ | |||
| 1 | #!/usr/bin/env python | ||
| 2 | |||
| 3 | # Report significant differences in the buildhistory repository since a specific revision | ||
| 4 | # | ||
| 5 | # Copyright (C) 2012 Intel Corporation | ||
| 6 | # Author: Paul Eggleton <paul.eggleton@linux.intel.com> | ||
| 7 | |||
| 8 | import sys | ||
| 9 | import os.path | ||
| 10 | |||
| 11 | # Ensure PythonGit is installed (buildhistory_analysis needs it) | ||
| 12 | try: | ||
| 13 | import git | ||
| 14 | except ImportError: | ||
| 15 | print("Please install PythonGit 0.3.1 or later in order to use this script") | ||
| 16 | sys.exit(1) | ||
| 17 | |||
| 18 | |||
| 19 | def main(): | ||
| 20 | if (len(sys.argv) < 3): | ||
| 21 | print("Report significant differences in the buildhistory repository") | ||
| 22 | print("Syntax: %s <buildhistory-path> <since-revision> [to-revision]" % os.path.basename(sys.argv[0])) | ||
| 23 | print("If to-revision is not specified, it defaults to HEAD") | ||
| 24 | sys.exit(1) | ||
| 25 | |||
| 26 | # Set path to OE lib dir so we can import the buildhistory_analysis module | ||
| 27 | newpath = os.path.abspath(os.path.dirname(os.path.abspath(sys.argv[0])) + '/../meta/lib') | ||
| 28 | sys.path = sys.path + [newpath] | ||
| 29 | import oe.buildhistory_analysis | ||
| 30 | |||
| 31 | if len(sys.argv) > 3: | ||
| 32 | torev = sys.argv[3] | ||
| 33 | else: | ||
| 34 | torev = 'HEAD' | ||
| 35 | changes = oe.buildhistory_analysis.process_changes(sys.argv[1], sys.argv[2], torev) | ||
| 36 | for chg in changes: | ||
| 37 | print('%s' % chg) | ||
| 38 | |||
| 39 | sys.exit(0) | ||
| 40 | |||
| 41 | |||
| 42 | if __name__ == "__main__": | ||
| 43 | main() | ||
