]> git.decadent.org.uk Git - dak.git/blob - utils.py
wrapper for print_exc()
[dak.git] / utils.py
1 #!/usr/bin/env python
2
3 # Utility functions
4 # Copyright (C) 2000, 2001, 2002  James Troup <james@nocrew.org>
5 # $Id: utils.py,v 1.53 2002-11-26 15:49:16 troup 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 import commands, os, pwd, re, socket, shutil, string, sys, tempfile, traceback;
24 import apt_pkg;
25 import db_access;
26
27 ################################################################################
28
29 re_comments = re.compile(r"\#.*")
30 re_no_epoch = re.compile(r"^\d*\:")
31 re_no_revision = re.compile(r"\-[^-]*$")
32 re_arch_from_filename = re.compile(r"/binary-[^/]+/")
33 re_extract_src_version = re.compile (r"(\S+)\s*\((.*)\)")
34 re_isadeb = re.compile (r"(.+?)_(.+?)_(.+)\.u?deb$");
35 re_issource = re.compile (r"(.+)_(.+?)\.(orig\.tar\.gz|diff\.gz|tar\.gz|dsc)$");
36
37 re_single_line_field = re.compile(r"^(\S*)\s*:\s*(.*)");
38 re_multi_line_field = re.compile(r"^\s(.*)");
39 re_taint_free = re.compile(r"^[-+~\.\w]+$");
40
41 re_parse_maintainer = re.compile(r"^\s*(\S.*\S)\s*\<([^\> \t]+)\>");
42
43 changes_parse_error_exc = "Can't parse line in .changes file";
44 invalid_dsc_format_exc = "Invalid .dsc file";
45 nk_format_exc = "Unknown Format: in .changes file";
46 no_files_exc = "No Files: field in .dsc file.";
47 cant_open_exc = "Can't read file.";
48 unknown_hostname_exc = "Unknown hostname";
49 cant_overwrite_exc = "Permission denied; can't overwrite existent file."
50 file_exists_exc = "Destination file exists";
51 send_mail_invalid_args_exc = "Both arguments are non-null.";
52 sendmail_failed_exc = "Sendmail invocation failed";
53 tried_too_hard_exc = "Tried too hard to find a free filename.";
54
55 default_config = "/etc/katie/katie.conf";
56 default_apt_config = "/etc/katie/apt.conf";
57
58 ######################################################################################
59
60 def open_file(filename, mode='r'):
61     try:
62         f = open(filename, mode);
63     except IOError:
64         raise cant_open_exc, filename
65     return f
66
67 ######################################################################################
68
69 def our_raw_input(prompt=""):
70     if prompt:
71         sys.stdout.write(prompt);
72     sys.stdout.flush();
73     try:
74         ret = raw_input();
75         return ret
76     except EOFError:
77         sys.stderr.write('\nUser interrupt (^D).\n');
78         raise SystemExit;
79
80 ######################################################################################
81
82 def str_isnum (s):
83     for c in s:
84         if c not in string.digits:
85             return 0;
86     return 1;
87
88 ######################################################################################
89
90 def extract_component_from_section(section):
91     component = "";
92
93     if section.find('/') != -1:
94         component = section.split('/')[0];
95     if component.lower() == "non-us" and section.count('/') > 0:
96         s = component + '/' + section.split('/')[1];
97         if Cnf.has_key("Component::%s" % s): # Avoid e.g. non-US/libs
98             component = s;
99
100     if section.lower() == "non-us":
101         component = "non-US/main";
102
103     # non-US prefix is case insensitive
104     if component.lower()[:6] == "non-us":
105         component = "non-US"+component[6:];
106
107     # Expand default component
108     if component == "":
109         if Cnf.has_key("Component::%s" % section):
110             component = section;
111         else:
112             component = "main";
113     elif component == "non-US":
114         component = "non-US/main";
115
116     return (section, component);
117
118 ######################################################################################
119
120 # dsc_whitespace_rules turns on strict format checking to avoid
121 # allowing in source packages which are unextracable by the
122 # inappropriately fragile dpkg-source.
123 #
124 # The rules are:
125 #
126 #
127 # o The PGP header consists of "-----BEGIN PGP SIGNED MESSAGE-----"
128 #   followed by any PGP header data and must end with a blank line.
129 #
130 # o The data section must end with a blank line and must be followed by
131 #   "-----BEGIN PGP SIGNATURE-----".
132
133 def parse_changes(filename, dsc_whitespace_rules=0):
134     changes_in = open_file(filename);
135     error = "";
136     changes = {};
137     lines = changes_in.readlines();
138
139     if not lines:
140         raise changes_parse_error_exc, "[Empty changes file]";
141
142     # Reindex by line number so we can easily verify the format of
143     # .dsc files...
144     index = 0;
145     indexed_lines = {};
146     for line in lines:
147         index += 1;
148         indexed_lines[index] = line[:-1];
149
150     inside_signature = 0;
151
152     indices = indexed_lines.keys()
153     index = 0;
154     first = -1;
155     while index < max(indices):
156         index += 1;
157         line = indexed_lines[index];
158         if line == "":
159             if dsc_whitespace_rules:
160                 index += 1;
161                 if index > max(indices):
162                     raise invalid_dsc_format_exc, index;
163                 line = indexed_lines[index];
164                 if not line.startswith("-----BEGIN PGP SIGNATURE"):
165                     raise invalid_dsc_format_exc, index;
166                 inside_signature = 0;
167                 break;
168         if line.startswith("-----BEGIN PGP SIGNATURE"):
169             break;
170         if line.startswith("-----BEGIN PGP SIGNED MESSAGE"):
171             if dsc_whitespace_rules:
172                 inside_signature = 1;
173                 while index < max(indices) and line != "":
174                     index += 1;
175                     line = indexed_lines[index];
176             continue;
177         slf = re_single_line_field.match(line);
178         if slf:
179             field = slf.groups()[0].lower();
180             changes[field] = slf.groups()[1];
181             first = 1;
182             continue;
183         if line == " .":
184             changes[field] += '\n';
185             continue;
186         mlf = re_multi_line_field.match(line);
187         if mlf:
188             if first == -1:
189                 raise changes_parse_error_exc, "'%s'\n [Multi-line field continuing on from nothing?]" % (line);
190             if first == 1 and changes[field] != "":
191                 changes[field] += '\n';
192             first = 0;
193             changes[field] += mlf.groups()[0] + '\n';
194             continue;
195         error += line;
196
197     if dsc_whitespace_rules and inside_signature:
198         raise invalid_dsc_format_exc, index;
199
200     changes_in.close();
201     changes["filecontents"] = "".join(lines);
202
203     if error != "":
204         raise changes_parse_error_exc, error;
205
206     return changes;
207
208 ######################################################################################
209
210 # Dropped support for 1.4 and ``buggy dchanges 3.4'' (?!) compared to di.pl
211
212 def build_file_list(changes, is_a_dsc=0):
213     files = {}
214     format = changes.get("format", "")
215     if format != "":
216         format = float(format)
217     if not is_a_dsc and (format < 1.5 or format > 2.0):
218         raise nk_format_exc, format;
219
220     # No really, this has happened.  Think 0 length .dsc file.
221     if not changes.has_key("files"):
222         raise no_files_exc
223
224     for i in changes["files"].split("\n"):
225         if i == "":
226             break
227         s = i.split();
228         section = priority = "";
229         try:
230             if is_a_dsc:
231                 (md5, size, name) = s
232             else:
233                 (md5, size, section, priority, name) = s
234         except ValueError:
235             raise changes_parse_error_exc, i
236
237         if section == "": section = "-"
238         if priority == "": priority = "-"
239
240         (section, component) = extract_component_from_section(section);
241
242         files[name] = { "md5sum" : md5,
243                         "size" : size,
244                         "section": section,
245                         "priority": priority,
246                         "component": component }
247
248     return files
249
250 ######################################################################################
251
252 # Fix the `Maintainer:' field to be an RFC822 compatible address.
253 # cf. Packaging Manual (4.2.4)
254 #
255 # 06:28|<Culus> 'The standard sucks, but my tool is supposed to
256 #                interoperate with it. I know - I'll fix the suckage
257 #                and make things incompatible!'
258
259 def fix_maintainer (maintainer):
260     m = re_parse_maintainer.match(maintainer);
261     rfc822 = maintainer
262     name = ""
263     email = ""
264     if m != None and len(m.groups()) == 2:
265         name = m.group(1)
266         email = m.group(2)
267         if name.find(',') != -1 or name.find('.') != -1:
268             rfc822 = re_parse_maintainer.sub(r"\2 (\1)", maintainer)
269     return (rfc822, name, email)
270
271 ######################################################################################
272
273 # sendmail wrapper, takes _either_ a message string or a file as arguments
274 def send_mail (message, filename):
275         # Sanity check arguments
276         if message != "" and filename != "":
277             raise send_mail_invalid_args_exc;
278
279         # If we've been passed a string dump it into a temporary file
280         if message != "":
281             filename = tempfile.mktemp();
282             fd = os.open(filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0700);
283             os.write (fd, message);
284             os.close (fd);
285
286         # Invoke sendmail
287         (result, output) = commands.getstatusoutput("%s < %s" % (Cnf["Dinstall::SendmailCommand"], filename));
288         if (result != 0):
289             raise sendmail_failed_exc, output;
290
291         # Clean up any temporary files
292         if message !="":
293             os.unlink (filename);
294
295 ######################################################################################
296
297 def poolify (source, component):
298     if component != "":
299         component += '/';
300     # FIXME: this is nasty
301     component = component.lower().replace('non-us/', 'non-US/');
302     if source[:3] == "lib":
303         return component + source[:4] + '/' + source + '/'
304     else:
305         return component + source[:1] + '/' + source + '/'
306
307 ######################################################################################
308
309 def move (src, dest, overwrite = 0, perms = 0664):
310     if os.path.exists(dest) and os.path.isdir(dest):
311         dest_dir = dest;
312     else:
313         dest_dir = os.path.dirname(dest);
314     if not os.path.exists(dest_dir):
315         umask = os.umask(00000);
316         os.makedirs(dest_dir, 02775);
317         os.umask(umask);
318     #print "Moving %s to %s..." % (src, dest);
319     if os.path.exists(dest) and os.path.isdir(dest):
320         dest += '/' + os.path.basename(src);
321     # Don't overwrite unless forced to
322     if os.path.exists(dest):
323         if not overwrite:
324             raise file_exists_exc;
325         else:
326             if not os.access(dest, os.W_OK):
327                 raise cant_overwrite_exc
328     shutil.copy2(src, dest);
329     os.chmod(dest, perms);
330     os.unlink(src);
331
332 def copy (src, dest, overwrite = 0, perms = 0664):
333     if os.path.exists(dest) and os.path.isdir(dest):
334         dest_dir = dest;
335     else:
336         dest_dir = os.path.dirname(dest);
337     if not os.path.exists(dest_dir):
338         umask = os.umask(00000);
339         os.makedirs(dest_dir, 02775);
340         os.umask(umask);
341     #print "Copying %s to %s..." % (src, dest);
342     if os.path.exists(dest) and os.path.isdir(dest):
343         dest += '/' + os.path.basename(src);
344     # Don't overwrite unless forced to
345     if os.path.exists(dest):
346         if not overwrite:
347             raise file_exists_exc
348         else:
349             if not os.access(dest, os.W_OK):
350                 raise cant_overwrite_exc
351     shutil.copy2(src, dest);
352     os.chmod(dest, perms);
353
354 ######################################################################################
355
356 def where_am_i ():
357     res = socket.gethostbyaddr(socket.gethostname());
358     database_hostname = Cnf.get("Config::" + res[0] + "::DatabaseHostname");
359     if database_hostname:
360         return database_hostname;
361     else:
362         return res[0];
363
364 def which_conf_file ():
365     res = socket.gethostbyaddr(socket.gethostname());
366     if Cnf.get("Config::" + res[0] + "::KatieConfig"):
367         return Cnf["Config::" + res[0] + "::KatieConfig"]
368     else:
369         return default_config;
370
371 def which_apt_conf_file ():
372     res = socket.gethostbyaddr(socket.gethostname());
373     if Cnf.get("Config::" + res[0] + "::AptConfig"):
374         return Cnf["Config::" + res[0] + "::AptConfig"]
375     else:
376         return default_apt_config;
377
378 ######################################################################################
379
380 # Escape characters which have meaning to SQL's regex comparison operator ('~')
381 # (woefully incomplete)
382
383 def regex_safe (s):
384     s = s.replace('+', '\\\\+');
385     s = s.replace('.', '\\\\.');
386     return s
387
388 ######################################################################################
389
390 # Perform a substition of template
391 def TemplateSubst(map, filename):
392     file = open_file(filename);
393     template = file.read();
394     for x in map.keys():
395         template = template.replace(x,map[x]);
396     file.close();
397     return template;
398
399 ######################################################################################
400
401 def fubar(msg, exit_code=1):
402     sys.stderr.write("E: %s\n" % (msg));
403     sys.exit(exit_code);
404
405 def warn(msg):
406     sys.stderr.write("W: %s\n" % (msg));
407
408 ######################################################################################
409
410 # Returns the user name with a laughable attempt at rfc822 conformancy
411 # (read: removing stray periods).
412 def whoami ():
413     return pwd.getpwuid(os.getuid())[4].split(',')[0].replace('.', '');
414
415 ######################################################################################
416
417 def size_type (c):
418     t  = " b";
419     if c > 10000:
420         c = c / 1000;
421         t = " Kb";
422     if c > 10000:
423         c = c / 1000;
424         t = " Mb";
425     return ("%d%s" % (c, t))
426
427 ################################################################################
428
429 def cc_fix_changes (changes):
430     o = changes.get("architecture", "")
431     if o != "":
432         del changes["architecture"]
433     changes["architecture"] = {}
434     for j in o.split():
435         changes["architecture"][j] = 1
436
437 # Sort by source name, source version, 'have source', and then by filename
438 def changes_compare (a, b):
439     try:
440         a_changes = parse_changes(a);
441     except:
442         return -1;
443
444     try:
445         b_changes = parse_changes(b);
446     except:
447         return 1;
448
449     cc_fix_changes (a_changes);
450     cc_fix_changes (b_changes);
451
452     # Sort by source name
453     a_source = a_changes.get("source");
454     b_source = b_changes.get("source");
455     q = cmp (a_source, b_source);
456     if q:
457         return q;
458
459     # Sort by source version
460     a_version = a_changes.get("version");
461     b_version = b_changes.get("version");
462     q = apt_pkg.VersionCompare(a_version, b_version);
463     if q:
464         return q;
465
466     # Sort by 'have source'
467     a_has_source = a_changes["architecture"].get("source");
468     b_has_source = b_changes["architecture"].get("source");
469     if a_has_source and not b_has_source:
470         return -1;
471     elif b_has_source and not a_has_source:
472         return 1;
473
474     # Fall back to sort by filename
475     return cmp(a, b);
476
477 ################################################################################
478
479 def find_next_free (dest, too_many=100):
480     extra = 0;
481     orig_dest = dest;
482     while os.path.exists(dest) and extra < too_many:
483         dest = orig_dest + '.' + repr(extra);
484         extra += 1;
485     if extra >= too_many:
486         raise tried_too_hard_exc;
487     return dest;
488
489 ################################################################################
490
491 def result_join (original, sep = '\t'):
492     list = [];
493     for i in xrange(len(original)):
494         if original[i] == None:
495             list.append("");
496         else:
497             list.append(original[i]);
498     return sep.join(list);
499
500 ################################################################################
501
502 def prefix_multi_line_string(str, prefix):
503     out = "";
504     for line in str.split('\n'):
505         line = line.strip();
506         if line:
507             out += "%s%s\n" % (prefix, line);
508     # Strip trailing new line
509     if out:
510         out = out[:-1];
511     return out;
512
513 ################################################################################
514
515 def validate_changes_file_arg(file, fatal=1):
516     error = None;
517
518     orig_filename = file
519     if file.endswith(".katie"):
520         file = file[:-6]+".changes";
521
522     if not file.endswith(".changes"):
523         error = "invalid file type; not a changes file";
524     else:
525         if not os.access(file,os.R_OK):
526             if os.path.exists(file):
527                 error = "permission denied";
528             else:
529                 error = "file not found";
530
531     if error:
532         if fatal:
533             fubar("%s: %s." % (orig_filename, error));
534         else:
535             warn("Skipping %s - %s" % (orig_filename, error));
536             return None;
537     else:
538         return file;
539
540 ################################################################################
541
542 def real_arch(arch):
543     return (arch != "source" and arch != "all");
544
545 ################################################################################
546
547 def join_with_commas_and(list):
548         if len(list) == 0: return "nothing";
549         if len(list) == 1: return list[0];
550         return ", ".join(list[:-1]) + " and " + list[-1];
551
552 ################################################################################
553
554 def get_conf():
555         return Cnf;
556
557 ################################################################################
558
559 # Handle -a, -c and -s arguments; returns them as SQL constraints
560 def parse_args(Options):
561     # Process suite
562     if Options["Suite"]:
563         suite_ids_list = [];
564         for suite in Options["Suite"].split():
565             suite_id = db_access.get_suite_id(suite);
566             if suite_id == -1:
567                 warn("suite '%s' not recognised." % (suite));
568             else:
569                 suite_ids_list.append(suite_id);
570         if suite_ids_list:
571             con_suites = "AND su.id IN (%s)" % ", ".join(map(str, suite_ids_list));
572         else:
573             fubar("No valid suite given.");
574     else:
575         con_suites = "";
576
577     # Process component
578     if Options["Component"]:
579         component_ids_list = [];
580         for component in Options["Component"].split():
581             component_id = db_access.get_component_id(component);
582             if component_id == -1:
583                 warn("component '%s' not recognised." % (component));
584             else:
585                 component_ids_list.append(component_id);
586         if component_ids_list:
587             con_components = "AND c.id IN (%s)" % ", ".join(map(str, component_ids_list));
588         else:
589             fubar("No valid component given.");
590     else:
591         con_components = "";
592
593     # Process architecture
594     con_architectures = "";
595     if Options["Architecture"]:
596         arch_ids_list = [];
597         check_source = 0;
598         for architecture in Options["Architecture"].split():
599             if architecture == "source":
600                 check_source = 1;
601             else:
602                 architecture_id = db_access.get_architecture_id(architecture);
603                 if architecture_id == -1:
604                     warn("architecture '%s' not recognised." % (architecture));
605                 else:
606                     arch_ids_list.append(architecture_id);
607         if arch_ids_list:
608             con_architectures = "AND a.id IN (%s)" % ", ".join(map(str, arch_ids_list));
609         else:
610             if not check_source:
611                 fubar("No valid architecture given.");
612     else:
613         check_source = 1;
614
615     return (con_suites, con_architectures, con_components, check_source);
616
617 ################################################################################
618
619 # Inspired(tm) by Bryn Keller's print_exc_plus (See
620 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52215)
621
622 def print_exc():
623     tb = sys.exc_info()[2];
624     while tb.tb_next:
625         tb = tb.tb_next;
626     stack = [];
627     frame = tb.tb_frame;
628     while frame:
629         stack.append(frame);
630         frame = frame.f_back;
631     stack.reverse();
632     traceback.print_exc();
633     for frame in stack:
634         print "\nFrame %s in %s at line %s" % (frame.f_code.co_name,
635                                              frame.f_code.co_filename,
636                                              frame.f_lineno);
637         for key, value in frame.f_locals.items():
638             print "\t%20s = " % key,;
639             try:
640                 print value;
641             except:
642                 print "<unable to print>";
643
644 ################################################################################
645
646 def try_with_debug(function):
647     try:
648         function();
649     except SystemExit:
650         raise;
651     except:
652         print_exc();
653
654 ################################################################################
655
656 apt_pkg.init()
657
658 Cnf = apt_pkg.newConfiguration();
659 apt_pkg.ReadConfigFileISC(Cnf,default_config);
660
661 if which_conf_file() != default_config:
662         apt_pkg.ReadConfigFileISC(Cnf,which_conf_file())
663
664 ################################################################################