]> git.decadent.org.uk Git - dak.git/blob - dak/cruft_report.py
Stop using silly names, and migrate to a saner directory structure.
[dak.git] / dak / cruft_report.py
1 #!/usr/bin/env python
2
3 # Check for obsolete binary packages
4 # Copyright (C) 2000, 2001, 2002, 2003, 2004  James Troup <james@nocrew.org>
5 # $Id: rene,v 1.23 2005-04-16 09:19:20 rmurray Exp $
6
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20
21 ################################################################################
22
23 # ``If you're claiming that's a "problem" that needs to be "fixed",
24 #   you might as well write some letters to God about how unfair entropy
25 #   is while you're at it.'' -- 20020802143104.GA5628@azure.humbug.org.au
26
27 ## TODO:  fix NBS looping for version, implement Dubious NBS, fix up output of duplicate source package stuff, improve experimental ?, add support for non-US ?, add overrides, avoid ANAIS for duplicated packages
28
29 ################################################################################
30
31 import commands, pg, os, string, sys, time;
32 import utils, db_access;
33 import apt_pkg;
34
35 ################################################################################
36
37 Cnf = None;
38 projectB = None;
39 suite_id = None;
40 no_longer_in_suite = {}; # Really should be static to add_nbs, but I'm lazy
41
42 source_binaries = {};
43 source_versions = {};
44
45 ################################################################################
46
47 def usage(exit_code=0):
48     print """Usage: rene
49 Check for obsolete or duplicated packages.
50
51   -h, --help                show this help and exit.
52   -m, --mode=MODE           chose the MODE to run in (full or daily).
53   -s, --suite=SUITE         check suite SUITE."""
54     sys.exit(exit_code)
55
56 ################################################################################
57
58 def add_nbs(nbs_d, source, version, package):
59     # Ensure the package is still in the suite (someone may have already removed it)
60     if no_longer_in_suite.has_key(package):
61         return;
62     else:
63         q = projectB.query("SELECT b.id FROM binaries b, bin_associations ba WHERE ba.bin = b.id AND ba.suite = %s AND b.package = '%s' LIMIT 1" % (suite_id, package));
64         if not q.getresult():
65             no_longer_in_suite[package] = "";
66             return;
67
68     nbs_d.setdefault(source, {})
69     nbs_d[source].setdefault(version, {})
70     nbs_d[source][version][package] = "";
71
72 ################################################################################
73
74 # Check for packages built on architectures they shouldn't be.
75 def do_anais(architecture, binaries_list, source):
76     if architecture == "any" or architecture == "all":
77         return "";
78
79     anais_output = "";
80     architectures = {};
81     for arch in architecture.split():
82         architectures[arch.strip()] = "";
83     for binary in binaries_list:
84         q = projectB.query("SELECT a.arch_string, b.version FROM binaries b, bin_associations ba, architecture a WHERE ba.suite = %s AND ba.bin = b.id AND b.architecture = a.id AND b.package = '%s'" % (suite_id, binary));
85         ql = q.getresult();
86         versions = [];
87         for i in ql:
88             arch = i[0];
89             version = i[1];
90             if architectures.has_key(arch):
91                 versions.append(version);
92         versions.sort(apt_pkg.VersionCompare);
93         if versions:
94             latest_version = versions.pop()
95         else:
96             latest_version = None;
97         # Check for 'invalid' architectures
98         versions_d = {}
99         for i in ql:
100             arch = i[0];
101             version = i[1];
102             if not architectures.has_key(arch):
103                 versions_d.setdefault(version, [])
104                 versions_d[version].append(arch)
105
106         if versions_d != {}:
107             anais_output += "\n (*) %s_%s [%s]: %s\n" % (binary, latest_version, source, architecture);
108             versions = versions_d.keys();
109             versions.sort(apt_pkg.VersionCompare);
110             for version in versions:
111                 arches = versions_d[version];
112                 arches.sort();
113                 anais_output += "    o %s: %s\n" % (version, ", ".join(arches));
114     return anais_output;
115
116 ################################################################################
117
118 def do_nviu():
119     experimental_id = db_access.get_suite_id("experimental");
120     if experimental_id == -1:
121         return;
122     # Check for packages in experimental obsoleted by versions in unstable
123     q = projectB.query("""
124 SELECT s.source, s.version AS experimental, s2.version AS unstable
125   FROM src_associations sa, source s, source s2, src_associations sa2
126   WHERE sa.suite = %s AND sa2.suite = %d AND sa.source = s.id
127    AND sa2.source = s2.id AND s.source = s2.source
128    AND versioncmp(s.version, s2.version) < 0""" % (experimental_id,
129                                                    db_access.get_suite_id("unstable")));
130     ql = q.getresult();
131     if ql:
132         nviu_to_remove = [];
133         print "Newer version in unstable";
134         print "-------------------------";
135         print ;
136         for i in ql:
137             (source, experimental_version, unstable_version) = i;
138             print " o %s (%s, %s)" % (source, experimental_version, unstable_version);
139             nviu_to_remove.append(source);
140         print
141         print "Suggested command:"
142         print " melanie -m \"[rene] NVIU\" -s experimental %s" % (" ".join(nviu_to_remove));
143         print
144
145 ################################################################################
146
147 def do_nbs(real_nbs):
148     output = "Not Built from Source\n";
149     output += "---------------------\n\n";
150
151     nbs_to_remove = [];
152     nbs_keys = real_nbs.keys();
153     nbs_keys.sort();
154     for source in nbs_keys:
155         output += " * %s_%s builds: %s\n" % (source,
156                                        source_versions.get(source, "??"),
157                                        source_binaries.get(source, "(source does not exist)"));
158         output += "      but no longer builds:\n"
159         versions = real_nbs[source].keys();
160         versions.sort(apt_pkg.VersionCompare);
161         for version in versions:
162             packages = real_nbs[source][version].keys();
163             packages.sort();
164             for pkg in packages:
165                 nbs_to_remove.append(pkg);
166             output += "        o %s: %s\n" % (version, ", ".join(packages));
167
168         output += "\n";
169
170     if nbs_to_remove:
171         print output;
172
173         print "Suggested command:"
174         print " melanie -m \"[rene] NBS\" -b %s" % (" ".join(nbs_to_remove));
175         print
176
177 ################################################################################
178
179 def do_dubious_nbs(dubious_nbs):
180     print "Dubious NBS";
181     print "-----------";
182     print ;
183
184     dubious_nbs_keys = dubious_nbs.keys();
185     dubious_nbs_keys.sort();
186     for source in dubious_nbs_keys:
187         print " * %s_%s builds: %s" % (source,
188                                        source_versions.get(source, "??"),
189                                        source_binaries.get(source, "(source does not exist)"));
190         print "      won't admit to building:"
191         versions = dubious_nbs[source].keys();
192         versions.sort(apt_pkg.VersionCompare);
193         for version in versions:
194             packages = dubious_nbs[source][version].keys();
195             packages.sort();
196             print "        o %s: %s" % (version, ", ".join(packages));
197
198         print ;
199
200 ################################################################################
201
202 def do_obsolete_source(duplicate_bins, bin2source):
203     obsolete = {}
204     for key in duplicate_bins.keys():
205         (source_a, source_b) = key.split('~')
206         for source in [ source_a, source_b ]:
207             if not obsolete.has_key(source):
208                 if not source_binaries.has_key(source):
209                     # Source has already been removed
210                     continue;
211                 else:
212                     obsolete[source] = map(string.strip,
213                                            source_binaries[source].split(','))
214             for binary in duplicate_bins[key]:
215                 if bin2source.has_key(binary) and bin2source[binary]["source"] == source:
216                     continue
217                 if binary in obsolete[source]:
218                     obsolete[source].remove(binary)
219
220     to_remove = []
221     output = "Obsolete source package\n"
222     output += "-----------------------\n\n"
223     obsolete_keys = obsolete.keys()
224     obsolete_keys.sort()
225     for source in obsolete_keys:
226         if not obsolete[source]:
227             to_remove.append(source)
228             output += " * %s (%s)\n" % (source, source_versions[source])
229             for binary in map(string.strip, source_binaries[source].split(',')):
230                 if bin2source.has_key(binary):
231                     output += "    o %s (%s) is built by %s.\n" \
232                           % (binary, bin2source[binary]["version"],
233                              bin2source[binary]["source"])
234                 else:
235                     output += "    o %s is not built.\n" % binary
236             output += "\n"
237
238     if to_remove:
239         print output;
240
241         print "Suggested command:"
242         print " melanie -S -p -m \"[rene] obsolete source package\" %s" % (" ".join(to_remove));
243         print
244
245 ################################################################################
246
247 def main ():
248     global Cnf, projectB, suite_id, source_binaries, source_versions;
249
250     Cnf = utils.get_conf();
251
252     Arguments = [('h',"help","Rene::Options::Help"),
253                  ('m',"mode","Rene::Options::Mode", "HasArg"),
254                  ('s',"suite","Rene::Options::Suite","HasArg")];
255     for i in [ "help" ]:
256         if not Cnf.has_key("Rene::Options::%s" % (i)):
257             Cnf["Rene::Options::%s" % (i)] = "";
258     Cnf["Rene::Options::Suite"] = Cnf["Dinstall::DefaultSuite"];
259
260     if not Cnf.has_key("Rene::Options::Mode"):
261         Cnf["Rene::Options::Mode"] = "daily";
262
263     apt_pkg.ParseCommandLine(Cnf, Arguments, sys.argv);
264
265     Options = Cnf.SubTree("Rene::Options")
266     if Options["Help"]:
267         usage();
268
269     # Set up checks based on mode
270     if Options["Mode"] == "daily":
271         checks = [ "nbs", "nviu", "obsolete source" ];
272     elif Options["Mode"] == "full":
273         checks = [ "nbs", "nviu", "obsolete source", "dubious nbs", "bnb", "bms", "anais" ];
274     else:
275         utils.warn("%s is not a recognised mode - only 'full' or 'daily' are understood." % (Options["Mode"]));
276         usage(1);
277
278     projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"]));
279     db_access.init(Cnf, projectB);
280
281     bin_pkgs = {};
282     src_pkgs = {};
283     bin2source = {}
284     bins_in_suite = {};
285     nbs = {};
286     source_versions = {};
287
288     anais_output = "";
289     duplicate_bins = {};
290
291     suite = Options["Suite"]
292     suite_id = db_access.get_suite_id(suite);
293
294     bin_not_built = {};
295
296     if "bnb" in checks:
297         # Initalize a large hash table of all binary packages
298         before = time.time();
299         sys.stderr.write("[Getting a list of binary packages in %s..." % (suite));
300         q = projectB.query("SELECT distinct b.package FROM binaries b, bin_associations ba WHERE ba.suite = %s AND ba.bin = b.id" % (suite_id));
301         ql = q.getresult();
302         sys.stderr.write("done. (%d seconds)]\n" % (int(time.time()-before)));
303         for i in ql:
304             bins_in_suite[i[0]] = "";
305
306     # Checks based on the Sources files
307     components = Cnf.ValueList("Suite::%s::Components" % (suite));
308     for component in components:
309         filename = "%s/dists/%s/%s/source/Sources.gz" % (Cnf["Dir::Root"], suite, component);
310         # apt_pkg.ParseTagFile needs a real file handle and can't handle a GzipFile instance...
311         temp_filename = utils.temp_filename();
312         (result, output) = commands.getstatusoutput("gunzip -c %s > %s" % (filename, temp_filename));
313         if (result != 0):
314             sys.stderr.write("Gunzip invocation failed!\n%s\n" % (output));
315             sys.exit(result);
316         sources = utils.open_file(temp_filename);
317         Sources = apt_pkg.ParseTagFile(sources);
318         while Sources.Step():
319             source = Sources.Section.Find('Package');
320             source_version = Sources.Section.Find('Version');
321             architecture = Sources.Section.Find('Architecture');
322             binaries = Sources.Section.Find('Binary');
323             binaries_list = map(string.strip, binaries.split(','));
324
325             if "bnb" in checks:
326                 # Check for binaries not built on any architecture.
327                 for binary in binaries_list:
328                     if not bins_in_suite.has_key(binary):
329                         bin_not_built.setdefault(source, {})
330                         bin_not_built[source][binary] = "";
331
332             if "anais" in checks:
333                 anais_output += do_anais(architecture, binaries_list, source);
334
335             # Check for duplicated packages and build indices for checking "no source" later
336             source_index = component + '/' + source;
337             if src_pkgs.has_key(source):
338                 print " %s is a duplicated source package (%s and %s)" % (source, source_index, src_pkgs[source]);
339             src_pkgs[source] = source_index;
340             for binary in binaries_list:
341                 if bin_pkgs.has_key(binary):
342                     key_list = [ source, bin_pkgs[binary] ]
343                     key_list.sort()
344                     key = '~'.join(key_list)
345                     duplicate_bins.setdefault(key, [])
346                     duplicate_bins[key].append(binary);
347                 bin_pkgs[binary] = source;
348             source_binaries[source] = binaries;
349             source_versions[source] = source_version;
350
351         sources.close();
352         os.unlink(temp_filename);
353
354     # Checks based on the Packages files
355     for component in components + ['main/debian-installer']:
356         architectures = filter(utils.real_arch, Cnf.ValueList("Suite::%s::Architectures" % (suite)));
357         for architecture in architectures:
358             filename = "%s/dists/%s/%s/binary-%s/Packages.gz" % (Cnf["Dir::Root"], suite, component, architecture);
359             # apt_pkg.ParseTagFile needs a real file handle
360             temp_filename = utils.temp_filename();
361             (result, output) = commands.getstatusoutput("gunzip -c %s > %s" % (filename, temp_filename));
362             if (result != 0):
363                 sys.stderr.write("Gunzip invocation failed!\n%s\n" % (output));
364                 sys.exit(result);
365             packages = utils.open_file(temp_filename);
366             Packages = apt_pkg.ParseTagFile(packages);
367             while Packages.Step():
368                 package = Packages.Section.Find('Package');
369                 source = Packages.Section.Find('Source', "");
370                 version = Packages.Section.Find('Version');
371                 if source == "":
372                     source = package;
373                 if bin2source.has_key(package) and \
374                        apt_pkg.VersionCompare(version, bin2source[package]["version"]) > 0:
375                     bin2source[package]["version"] = version
376                     bin2source[package]["source"] = source
377                 else:
378                     bin2source[package] = {}
379                     bin2source[package]["version"] = version
380                     bin2source[package]["source"] = source
381                 if source.find("(") != -1:
382                     m = utils.re_extract_src_version.match(source);
383                     source = m.group(1);
384                     version = m.group(2);
385                 if not bin_pkgs.has_key(package):
386                     nbs.setdefault(source,{})
387                     nbs[source].setdefault(package, {})
388                     nbs[source][package][version] = "";
389                 else:
390                     previous_source = bin_pkgs[package]
391                     if previous_source != source:
392                         key_list = [ source, previous_source ]
393                         key_list.sort()
394                         key = '~'.join(key_list)
395                         duplicate_bins.setdefault(key, [])
396                         if package not in duplicate_bins[key]:
397                             duplicate_bins[key].append(package)
398             packages.close();
399             os.unlink(temp_filename);
400     
401     if "obsolete source" in checks:
402         do_obsolete_source(duplicate_bins, bin2source)
403
404     # Distinguish dubious (version numbers match) and 'real' NBS (they don't)
405     dubious_nbs = {};
406     real_nbs = {};
407     for source in nbs.keys():
408         for package in nbs[source].keys():
409             versions = nbs[source][package].keys();
410             versions.sort(apt_pkg.VersionCompare);
411             latest_version = versions.pop();
412             source_version = source_versions.get(source,"0");
413             if apt_pkg.VersionCompare(latest_version, source_version) == 0:
414                 add_nbs(dubious_nbs, source, latest_version, package);
415             else:
416                 add_nbs(real_nbs, source, latest_version, package);
417
418     if "nviu" in checks:
419         do_nviu();
420
421     if "nbs" in checks:
422         do_nbs(real_nbs);
423
424     ###
425
426     if Options["Mode"] == "full":
427         print "="*75
428         print
429
430     if "bnb" in checks:
431         print "Unbuilt binary packages";
432         print "-----------------------";
433         print
434         keys = bin_not_built.keys();
435         keys.sort();
436         for source in keys:
437             binaries = bin_not_built[source].keys();
438             binaries.sort();
439             print " o %s: %s" % (source, ", ".join(binaries));
440         print ;
441
442     if "bms" in checks:
443         print "Built from multiple source packages";
444         print "-----------------------------------";
445         print ;
446         keys = duplicate_bins.keys();
447         keys.sort();
448         for key in keys:
449             (source_a, source_b) = key.split("~");
450             print " o %s & %s => %s" % (source_a, source_b, ", ".join(duplicate_bins[key]));
451         print ;
452
453     if "anais" in checks:
454         print "Architecture Not Allowed In Source";
455         print "----------------------------------";
456         print anais_output;
457         print ;
458
459     if "dubious nbs" in checks:
460         do_dubious_nbs(dubious_nbs);
461
462
463 ################################################################################
464
465 if __name__ == '__main__':
466     main()