]> git.decadent.org.uk Git - dak.git/blob - fernanda.py
add less to suggests
[dak.git] / fernanda.py
1 #!/usr/bin/env python
2
3 # Script to automate some parts of checking NEW packages
4 # Copyright (C) 2000, 2001, 2002  James Troup <james@nocrew.org>
5 # $Id: fernanda.py,v 1.7 2002-11-26 02:51:52 rdonald 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 # <Omnic> elmo wrote docs?!!?!?!?!?!?!
24 # <aj> as if he wasn't scary enough before!!
25 # * aj imagines a little red furry toy sitting hunched over a computer
26 #   tapping furiously and giggling to himself
27 # <aj> eventually he stops, and his heads slowly spins around and you
28 #      see this really evil grin and then he sees you, and picks up a
29 #      knife from beside the keyboard and throws it at you, and as you
30 #      breathe your last breath, he starts giggling again
31 # <aj> but i should be telling this to my psychiatrist, not you guys,
32 #      right? :)
33
34 ################################################################################
35
36 import errno, os, re, sys
37 import utils
38 import apt_pkg, apt_inst
39 import pg, string, db_access
40
41 ################################################################################
42
43
44 re_package = re.compile(r"^(.+?)_.*");
45 re_doc_directory = re.compile(r".*/doc/([^/]*).*");
46
47
48 re_contrib = re.compile('^contrib/')
49 re_nonfree = re.compile('^non\-free/')
50
51 re_arch = re.compile("Architecture: .*")
52 re_builddep = re.compile("Build-Depends: .*")
53 re_builddepind = re.compile("Build-Depends-Indep: .*")
54
55 re_localhost = re.compile("localhost\.localdomain") 
56 re_version = re.compile('^(.*)\((.*)\)')
57
58 re_newlinespace = re.compile('\n')
59 re_spacestrip = re.compile('(\s)')
60
61 ##############################################################################################################
62
63 # Colour definitions
64
65 # Main
66 main_colour = "\033[36m"
67 # Contrib
68 contrib_colour = "\033[33m"
69 # Non-Free
70 nonfree_colour = "\033[31m"
71 # Arch
72 arch_colour = "\033[32m"
73 # End
74 end_colour = "\033[0m"
75 # Bold 
76 bold_colour = "\033[1m"
77 # Bad maintainer
78 maintainer_colour = arch_colour
79
80 ################################################################################
81
82 Cnf = None;
83 projectB = None;
84
85 Cnf = utils.get_conf()
86 projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"]))
87 db_access.init(Cnf, projectB);
88
89
90
91 ################################################################################
92
93 def usage (exit_code=0):
94     print """Usage: fernanda [PACKAGE]...
95 Check NEW package(s).
96
97   -h, --help                 show this help and exit
98
99 PACKAGE can be a .changes, .dsc, .deb or .udeb filename."""
100
101     sys.exit(exit_code)
102
103 ################################################################################
104
105 def get_depends_parts(depend) :
106     v_match = re_version.match(depend)
107     if v_match:
108         d_parts = { 'name' : v_match.group(1), 'version' : v_match.group(2) }
109     else :
110         d_parts = { 'name' : depend , 'version' : '' }
111     return d_parts
112                                 
113 def get_or_list(depend) :
114     or_list = string.split(depend, "|")
115     return or_list
116                                         
117 def get_comma_list(depend) :
118     dep_list = string.split(depend, ",")
119     return dep_list
120                                                 
121                                                 
122 def split_depends (d_str) :
123     # creates a list of lists of dictionaries of depends (package,version relation)
124     
125     d_str = re_spacestrip.sub('',d_str)
126     depends_tree = []
127     # first split depends string up amongs comma delimiter
128     dep_list = get_comma_list(d_str)
129     d = 0
130     while d < len(dep_list):
131         # put depends into their own list
132         depends_tree.append([dep_list[d]])
133         d = d + 1
134     d = 0
135     while d < len(depends_tree) :
136         k = 0
137         # split up Or'd depends into a multi-item list
138         depends_tree[d] = get_or_list(depends_tree[d][0])
139         while k < len(depends_tree[d]) :
140             # split depends into {package, version relation}
141             depends_tree[d][k] = get_depends_parts(depends_tree[d][k])
142             k = k + 1
143         d = d +1
144     return depends_tree
145                                                                                                                                                                                 
146 def read_control (filename) :
147     
148     recommends = []
149     depends = []
150     section = ''
151     maintainer = ''
152     arch = ''
153
154
155
156     
157     deb_file = utils.open_file(filename);
158     try:
159         extracts = apt_inst.debExtractControl(deb_file)
160         control = apt_pkg.ParseSection(extracts)
161     except:
162
163         print "can't parse control info"
164         control = ''    
165
166         
167     deb_file.close();
168     
169     control_keys = control.keys()
170     
171     
172    
173
174         
175         
176         
177     if control.has_key("Depends"):
178         depends_str = control.Find("Depends")
179         # create list of dependancy lists
180         depends = split_depends(depends_str)
181     
182         
183     if control.has_key("Recommends"):
184         recommends_str = control.Find("Recommends")
185         recommends = split_depends(recommends_str)
186     
187     if control.has_key("Section"):
188         section_str = control.Find("Section")
189    
190         c_match = re_contrib.search(section_str)
191         nf_match = re_nonfree.search(section_str)
192         if c_match :
193             # contrib colour
194             section = contrib_colour + section_str + end_colour
195         elif nf_match :
196             
197             # non-free colour
198             section = nonfree_colour + section_str + end_colour
199         else :
200             # main
201             section = main_colour +  section_str + end_colour
202     if control.has_key("Achitecture"):
203         arch_str = control.Find("Architecture")
204         arch = arch_colour + arch_str + end_colour
205     
206     if control.has_key("Maintainer"):
207         maintainer = control.Find("Maintainer") 
208         localhost = re_localhost.search(maintainer) 
209         if localhost :
210             #highlight bad email
211             maintainer = maintainer_colour + maintainer + end_colour 
212     
213
214     return (control, control_keys, section, depends, recommends, arch, maintainer)
215     
216     
217 def read_dsc (dsc_filename) :
218     buf = []
219     header = ''
220     footer = ''
221     dsc = {}
222     dsc_files = {}
223     
224     dsc_file = utils.open_file(dsc_filename)                
225     try:
226         dsc.update(utils.parse_changes(dsc_filename, dsc_whitespace_rules=0))
227         dsc_files.update(utils.build_file_list(dsc, is_a_dsc=1))
228     except:
229     
230         print "can't parse control info"
231         
232    
233                                                 
234     dsc_file.close();
235
236     filecontents = dsc["filecontents"]
237
238     if dsc.has_key("build-depends"):
239         
240         builddep = split_depends(dsc["build-depends"])
241         builddepstr = create_depends_string(builddep)
242         filecontents = re_builddep.sub("Build-Depends: "+builddepstr, filecontents)
243     
244     
245     if dsc.has_key("build-depends-indep") :
246         builddepindstr = create_depends_string(split_depends(dsc["build-depends-indep"]))
247         filecontents = re_builddepind.sub("Build-Depends-Indep: "+builddepindstr, filecontents)
248
249     
250
251     
252     if dsc.has_key("architecture") :
253
254         if (dsc["architecture"] != "any") : 
255             newarch = arch_colour + dsc["architecture"] + end_colour 
256             filecontents = re_arch.sub("Architecture: "+newarch, filecontents)
257     
258     return (filecontents)
259
260 def create_depends_string(depends_tree) :
261     # just look up unstable for now. possibly pull from .changes later
262     suite = "unstable"
263     result = ""
264     comma_count = 1
265     for l in depends_tree:
266         if (comma_count >= 2) :
267             result = result + ", "
268         or_count = 1
269         for d in l :
270             if (or_count >= 2 ) :
271                 result = result + " | "
272             # doesn't do version lookup yet.
273
274             q = projectB.query("SELECT DISTINCT(b.package), b.version, c.name, su.suite_name FROM  binaries b, files fi, location l, component c, bin_associations ba, suite su WHERE b.package='%s' AND b.file = fi.id AND fi.location = l.id AND l.component = c.id AND ba.bin=b.id AND ba.suite = su.id AND su.suite_name='%s' ORDER BY b.version desc" % (d['name'], suite))
275             ql = q.getresult();
276             if ql:
277                 i = ql[0]
278                 #for i in ql:
279                 
280                 if i[2] == "contrib" :
281                     result = result + contrib_colour + d['name']
282                 elif i[2] == "non-free" :
283                     result = result + nonfree_colour + d['name']
284                 else :
285                     result = result + main_colour + d['name']
286                     
287                 if d['version'] != '' :
288                     result = result +  " (" + d['version'] + ")"
289                 result = result +end_colour
290             else :
291                 result = result + bold_colour + d['name']
292                 if d['version'] != '' :
293                     result = result + " ("  + d['version'] + ")"
294                 result = result  + end_colour
295             or_count = or_count + 1
296         comma_count = comma_count + 1
297     return result
298
299
300 def output_deb_info(filename):
301     
302
303     (control, control_keys, section, depends, recommends, arch, maintainer) = read_control(filename)
304
305     if control == '' :
306         print "no control info" 
307     else :
308
309         
310         for key in control_keys :
311
312             output = " " + key + ": "
313             if key == 'Depends' :
314                 output = output + create_depends_string(depends)
315             elif key == 'Recommends' :
316                 output = output + create_depends_string(recommends)         
317             elif key == 'Section' :
318                 output = output + section
319             elif key == 'Architecture' :
320                 output = output + arch
321             elif key == 'Maintainer' :
322                 output = output + maintainer
323             elif key == 'Description' :
324                 desc = control.Find(key)
325                 desc = re_newlinespace.sub('\n ', desc)
326                 output = output + desc
327             else :
328                 output = output + control.Find(key)
329             print output
330         
331
332 def do_command (command, filename):
333     o = os.popen("%s %s" % (command, filename));
334     print o.read();
335
336 def print_copyright (deb_filename):
337     package = re_package.sub(r'\1', deb_filename);
338     o = os.popen("ar p %s data.tar.gz | tar tzvf - | egrep 'usr(/share)?/doc/[^/]*/copyright' | awk '{ print $6 }' | head -n 1" % (deb_filename));
339     copyright = o.read()[:-1];
340
341     if copyright == "":
342         print "WARNING: No copyright found, please check package manually."
343         return;
344
345     doc_directory = re_doc_directory.sub(r'\1', copyright);
346     if package != doc_directory:
347         print "WARNING: wrong doc directory (expected %s, got %s)." % (package, doc_directory);
348         return;
349
350     o = os.popen("ar p %s data.tar.gz | tar xzOf - %s" % (deb_filename, copyright));
351     print o.read();
352
353 def check_dsc (dsc_filename):
354     print "---- .dsc file for %s ----" % (dsc_filename);
355     (dsc) = read_dsc(dsc_filename)
356     
357     print dsc
358
359     
360
361
362 def check_deb (deb_filename):
363     filename = os.path.basename(deb_filename);
364
365     if filename.endswith(".udeb"):
366         is_a_udeb = 1;
367     else:
368         is_a_udeb = 0;
369
370     print "---- control file for %s ----" % (filename);
371     #do_command ("dpkg -I", deb_filename);
372     output_deb_info(deb_filename)
373     
374     if is_a_udeb:
375         print "---- skipping lintian check for µdeb ----";
376         print ;
377     else:
378         print "---- lintian check for %s ----" % (filename);
379         do_command ("lintian", deb_filename);
380         print "---- linda check for %s ----" % (filename);
381         do_command ("linda", deb_filename);
382
383     print "---- contents of %s ----" % (filename);
384     do_command ("dpkg -c", deb_filename);
385
386     if is_a_udeb:
387         print "---- skipping copyright for µdeb ----";
388     else:
389         print "---- copyright of %s ----" % (filename);
390         print_copyright(deb_filename);
391
392     print "---- file listing of %s ----" % (filename);
393     do_command ("ls -l", deb_filename);
394
395 def display_changes (changes_filename):
396     print "---- .changes file for %s ----" % (changes_filename);
397     file = utils.open_file (changes_filename);
398     for line in file.readlines():
399         print line[:-1]
400     print ;
401     file.close();
402
403 def check_changes (changes_filename):
404     display_changes(changes_filename);
405
406     changes = utils.parse_changes (changes_filename);
407     files = utils.build_file_list(changes);
408     for file in files.keys():
409         if file.endswith(".deb") or file.endswith(".udeb"):
410             check_deb(file);
411         if file.endswith(".dsc"):
412             check_dsc(file);
413         # else: => byhand
414
415 def main ():
416     global Cnf, projectB, db_files, waste, excluded;
417
418 #    Cnf = utils.get_conf()
419
420     Arguments = [('h',"help","Fernanda::Options::Help")];
421     for i in [ "help" ]:
422         if not Cnf.has_key("Frenanda::Options::%s" % (i)):
423             Cnf["Fernanda::Options::%s" % (i)] = "";
424
425     args = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
426     Options = Cnf.SubTree("Fernanda::Options")
427
428     if Options["Help"]:
429         usage();
430
431     stdout_fd = sys.stdout;
432
433     for file in args:
434         try:
435             # Pipe output for each argument through less
436             less_fd = os.popen("less -R -", 'w', 0);  
437             # -R added to display raw control chars for colour
438             sys.stdout = less_fd;
439
440             try:
441                 if file.endswith(".changes"):
442                     check_changes(file);
443                 elif file.endswith(".deb") or file.endswith(".udeb"):
444                     check_deb(file);
445                 elif file.endswith(".dsc"):
446                     check_dsc(file);
447                 else:
448                     utils.fubar("Unrecognised file type: '%s'." % (file));
449             finally:
450                 # Reset stdout here so future less invocations aren't FUBAR
451                 less_fd.close();
452                 sys.stdout = stdout_fd;
453         except IOError, e:
454             if errno.errorcode[e.errno] == 'EPIPE':
455                 utils.warn("[fernanda] Caught EPIPE; skipping.");
456                 pass;
457             else:
458                 raise;
459         except KeyboardInterrupt:
460             utils.warn("[fernanda] Caught C-c; skipping.");
461             pass;
462
463 #######################################################################################
464
465 if __name__ == '__main__':
466     main()
467