]> git.decadent.org.uk Git - dak.git/blob - lisa
Fix lisa not to FUBAR Distribution fields
[dak.git] / lisa
1 #!/usr/bin/env python
2
3 # Handles NEW and BYHAND packages
4 # Copyright (C) 2001  James Troup <james@nocrew.org>
5 # $Id: lisa,v 1.5 2002-03-26 22:05:47 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 # 23:12|<aj> I will not hush!
24 # 23:12|<elmo> :>
25 # 23:12|<aj> Where there is injustice in the world, I shall be there!
26 # 23:13|<aj> I shall not be silenced!
27 # 23:13|<aj> The world shall know!
28 # 23:13|<aj> The world *must* know!
29 # 23:13|<elmo> oh dear, he's gone back to powerpuff girls... ;-)
30 # 23:13|<aj> yay powerpuff girls!!
31 # 23:13|<aj> buttercup's my favourite, who's yours?
32 # 23:14|<aj> you're backing away from the keyboard right now aren't you?
33 # 23:14|<aj> *AREN'T YOU*?!
34 # 23:15|<aj> I will not be treated like this.
35 # 23:15|<aj> I shall have my revenge.
36 # 23:15|<aj> I SHALL!!!
37
38 ################################################################################
39
40 # TODO
41 # ----
42
43 # We don't check error codes very thoroughly; the old 'trust jennifer'
44 # chess nut... db_access calls in particular
45
46 # Possible TODO
47 # -------------
48
49 # Handle multiple different section/priorities (?)
50 # hardcoded debianness (debian-installer, source priority etc.) (?)
51 # Slang/ncurses interface (?)
52 # write changed sections/priority back to katie for later processing (?)
53
54 ################################################################################
55
56 import copy, errno, os, readline, string, stat, sys, tempfile;
57 import apt_pkg, apt_inst;
58 import db_access, fernanda, katie, logging, utils;
59
60 # Globals
61 lisa_version = "$Revision: 1.5 $";
62
63 Cnf = None;
64 Options = None;
65 Katie = None;
66 projectB = None;
67 Logger = None;
68
69 Priorities = None;
70 Sections = None;
71
72 ################################################################################
73 ################################################################################
74 ################################################################################
75
76 def determine_new (changes, files):
77     new = {};
78
79     # Build up a list of potentially new things
80     for file in files.keys():
81         f = files[file];
82         # Skip byhand elements
83         if f["type"] == "byhand":
84             continue;
85         pkg = f["package"];
86         priority = f["priority"];
87         section = f["section"];
88         # FIXME: unhardcode
89         if section == "non-US/main":
90             section = "non-US";
91         type = get_type(f);
92         component = f["component"];
93
94         if type == "dsc":
95             priority = "source";
96         if not new.has_key(pkg):
97             new[pkg] = {};
98             new[pkg]["priority"] = priority;
99             new[pkg]["section"] = section;
100             new[pkg]["type"] = type;
101             new[pkg]["component"] = component;
102             new[pkg]["files"] = [];
103         else:
104             old_type = new[pkg]["type"];
105             if old_type != type:
106                 # source gets trumped by deb or udeb
107                 if old_type == "dsc":
108                     new[pkg]["priority"] = priority;
109                     new[pkg]["section"] = section;
110                     new[pkg]["type"] = type;
111                     new[pkg]["component"] = component;
112         new[pkg]["files"].append(file);
113         if f.has_key("othercomponents"):
114             new[pkg]["othercomponents"] = f["othercomponents"];
115
116     for suite in changes["suite"].keys():
117         suite_id = db_access.get_suite_id(suite);
118         for pkg in new.keys():
119             component_id = db_access.get_component_id(new[pkg]["component"]);
120             type_id = db_access.get_override_type_id(new[pkg]["type"]);
121             q = projectB.query("SELECT package FROM override WHERE package = '%s' AND suite = %s AND component = %s AND type = %s" % (pkg, suite_id, component_id, type_id));
122             ql = q.getresult();
123             if ql:
124                 for file in new[pkg]["files"]:
125                     if files[file].has_key("new"):
126                         del files[file]["new"];
127                 del new[pkg];
128
129     if changes["suite"].has_key("stable"):
130         print "WARNING: overrides will be added for stable!";
131     for pkg in new.keys():
132         if new[pkg].has_key("othercomponents"):
133             print "WARNING: %s already present in %s distribution." % (pkg, new[pkg]["othercomponents"]);
134
135     return new;
136
137 ################################################################################
138
139 # Sort by 'have source', by ctime, by source name, by source version number, by filename
140
141 def changes_compare_by_time (a, b):
142     try:
143         a_changes = utils.parse_changes(a, 0)
144     except:
145         return 1;
146
147     try:
148         b_changes = utils.parse_changes(b, 0)
149     except:
150         return -1;
151
152     utils.cc_fix_changes (a_changes);
153     utils.cc_fix_changes (b_changes);
154
155     # Sort by 'have source'
156
157     a_has_source = a_changes["architecture"].get("source")
158     b_has_source = b_changes["architecture"].get("source")
159     if a_has_source and not b_has_source:
160         return -1;
161     elif b_has_source and not a_has_source:
162         return 1;
163
164     # Sort by ctime
165     a_ctime = os.stat(a)[stat.ST_CTIME];
166     b_ctime = os.stat(b)[stat.ST_CTIME];
167     q = cmp (a_ctime, b_ctime);
168     if q:
169         return q;
170
171     # Sort by source name
172
173     a_source = a_changes.get("source");
174     b_source = b_changes.get("source");
175     q = cmp (a_source, b_source);
176     if q:
177         return q;
178
179     # Sort by source version
180
181     a_version = a_changes.get("version");
182     b_version = b_changes.get("version");
183     q = apt_pkg.VersionCompare(a_version, b_version);
184     if q:
185         return q
186
187     # Fall back to sort by filename
188
189     return cmp(a, b);
190
191 ################################################################################
192
193 class Section_Completer:
194     def __init__ (self):
195         self.sections = [];
196         q = projectB.query("SELECT section FROM section");
197         for i in q.getresult():
198             self.sections.append(i[0]);
199
200     def complete(self, text, state):
201         if state == 0:
202             self.matches = [];
203             n = len(text);
204             for word in self.sections:
205                 if word[:n] == text:
206                     self.matches.append(word);
207         try:
208             return self.matches[state]
209         except IndexError:
210             return None
211
212 ############################################################
213
214 class Priority_Completer:
215     def __init__ (self):
216         self.priorities = [];
217         q = projectB.query("SELECT priority FROM priority");
218         for i in q.getresult():
219             self.priorities.append(i[0]);
220
221     def complete(self, text, state):
222         if state == 0:
223             self.matches = [];
224             n = len(text);
225             for word in self.priorities:
226                 if word[:n] == text:
227                     self.matches.append(word);
228         try:
229             return self.matches[state]
230         except IndexError:
231             return None
232
233 ################################################################################
234
235 def check_valid (new):
236     for pkg in new.keys():
237         section = new[pkg]["section"];
238         priority = new[pkg]["priority"];
239         type = new[pkg]["type"];
240         new[pkg]["section id"] = db_access.get_section_id(section);
241         new[pkg]["priority id"] = db_access.get_priority_id(new[pkg]["priority"]);
242         # Sanity checks
243         if (section == "debian-installer" and type != "udeb") or \
244            (section != "debian-installer" and type == "udeb"):
245             new[pkg]["section id"] = -1;
246         if (priority == "source" and type != "dsc") or \
247            (priority != "source" and type == "dsc"):
248             new[pkg]["priority id"] = -1;
249
250 ################################################################################
251
252 def print_new (new, indexed, file=sys.stdout):
253     check_valid(new);
254     ret_code = 0;
255     index = 0;
256     for pkg in new.keys():
257         index = index + 1;
258         section = new[pkg]["section"];
259         priority = new[pkg]["priority"];
260         if new[pkg]["section id"] == -1:
261             section = section + "[!]";
262             ret_code = 1;
263         if new[pkg]["priority id"] == -1:
264             priority = priority + "[!]";
265             ret_code = 1;
266         if indexed:
267             line = "(%s): %-20s %-20s %-20s" % (index, pkg, priority, section);
268         else:
269             line = "%-20s %-20s %-20s" % (pkg, priority, section);
270         line = string.strip(line)+'\n';
271         file.write(line);
272     return ret_code;
273
274 ################################################################################
275
276 def get_type (f):
277     # Determine the type
278     if f.has_key("dbtype"):
279         type = f["dbtype"];
280     elif f["type"] == "orig.tar.gz" or f["type"] == "tar.gz" or f["type"] == "diff.gz" or f["type"] == "dsc":
281         type = "dsc";
282     else:
283         utils.fubar("invalid type (%s) for new.  Dazed, confused and sure as heck not continuing." % (type));
284
285     # Validate the override type
286     type_id = db_access.get_override_type_id(type);
287     if type_id == -1:
288         utils.fubar("invalid type (%s) for new.  Say wha?" % (type));
289
290     return type;
291
292 ################################################################################
293
294 def index_range (index):
295     if index == 1:
296         return "1";
297     else:
298         return "1-%s" % (index);
299
300 ################################################################################
301 ################################################################################
302
303 def spawn_editor (new):
304     # Write the current data to a temporary file
305     temp_filename = tempfile.mktemp();
306     fd = os.open(temp_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0700);
307     os.close(fd);
308     temp_file = utils.open_file(temp_filename, 'w');
309     print_new (new, 0, temp_file);
310     temp_file.close();
311     # Spawn an editor on that file
312     editor = os.environ.get("EDITOR","vi")
313     result = os.system("%s %s" % (editor, temp_filename))
314     if result != 0:
315         utils.fubar ("vi invocation failed for `%s'!" % (temp_filename), result)
316     # Read the (edited) data back in
317     file = utils.open_file(temp_filename);
318     for line in file.readlines():
319         line = string.strip(line[:-1]);
320         if line == "":
321             continue;
322         s = string.split(line);
323         # Pad the list if necessary
324         s[len(s):3] = [None] * (3-len(s));
325         (pkg, priority, section) = s[:3];
326         if not new.has_key(pkg):
327             utils.warn("Ignoring unknown package '%s'" % (pkg));
328         else:
329             # Strip off any invalid markers, print_new will readd them.
330             if section[-3:] == "[!]":
331                 section = section[:-3];
332             if priority[-3:] == "[!]":
333                 priority = priority[:-3];
334             for file in new[pkg]["files"]:
335                 Katie.pkg.files[file]["section"] = section;
336                 Katie.pkg.files[file]["priority"] = priority;
337             new[pkg]["section"] = section;
338             new[pkg]["priority"] = priority;
339     os.unlink(temp_filename);
340
341 ################################################################################
342
343 def edit_index (new, index):
344     priority = new[index]["priority"]
345     section = new[index]["section"]
346     type = new[index]["type"];
347     done = 0
348     while not done:
349         print string.join([index, priority, section], '\t');
350
351         answer = "XXX";
352         if type != "dsc":
353             prompt = "[B]oth, Priority, Section, Done ? ";
354         else:
355             prompt = "[S]ection, Done ? ";
356         edit_priority = edit_section = 0;
357
358         while string.find(prompt, answer) == -1:
359             answer = utils.our_raw_input(prompt);
360             m = katie.re_default_answer.match(prompt)
361             if answer == "":
362                 answer = m.group(1)
363             answer = string.upper(answer[:1])
364
365         if answer == 'P':
366             edit_priority = 1;
367         elif answer == 'S':
368             edit_section = 1;
369         elif answer == 'B':
370             edit_priority = edit_section = 1;
371         elif answer == 'D':
372             done = 1;
373
374         # Edit the priority
375         if edit_priority:
376             readline.set_completer(Priorities.complete);
377             got_priority = 0;
378             while not got_priority:
379                 new_priority = string.strip(utils.our_raw_input("New priority: "));
380                 if Priorities.priorities.count(new_priority) == 0:
381                     print "E: '%s' is not a valid priority, try again." % (new_priority);
382                 else:
383                     got_priority = 1;
384                     priority = new_priority;
385
386         # Edit the section
387         if edit_section:
388             readline.set_completer(Sections.complete);
389             got_section = 0;
390             while not got_section:
391                 new_section = string.strip(utils.our_raw_input("New section: "));
392                 if Sections.sections.count(new_section) == 0:
393                     print "E: '%s' is not a valid section, try again." % (new_section);
394                 else:
395                     got_section = 1;
396                     section = new_section;
397
398         # Reset the readline completer
399         readline.set_completer(None);
400
401     for file in new[index]["files"]:
402         Katie.pkg.files[file]["section"] = section;
403         Katie.pkg.files[file]["priority"] = priority;
404     new[index]["priority"] = priority;
405     new[index]["section"] = section;
406     return new;
407
408 ################################################################################
409
410 def edit_overrides (new):
411     print;
412     done = 0
413     while not done:
414         print_new (new, 1);
415         new_index = {};
416         index = 0;
417         for i in new.keys():
418             index = index + 1;
419             new_index[index] = i;
420
421         prompt = "(%s) edit override <n>, Editor, Done ? " % (index_range(index));
422
423         got_answer = 0
424         while not got_answer:
425             answer = utils.our_raw_input(prompt);
426             answer = string.upper(answer[:1]);
427             if answer == "E" or answer == "D":
428                 got_answer = 1;
429             elif katie.re_isanum.match (answer):
430                 answer = int(answer);
431                 if (answer < 1) or (answer > index):
432                     print "%s is not a valid index (%s).  Please retry." % (index_range(index), answer);
433                 else:
434                     got_answer = 1;
435
436         if answer == 'E':
437             spawn_editor(new);
438         elif answer == 'D':
439             done = 1;
440         else:
441             edit_index (new, new_index[answer]);
442
443     return new;
444
445 ################################################################################
446
447 def check_pkg ():
448     try:
449         less_fd = os.popen("less -", 'w', 0);
450         stdout_fd = sys.stdout;
451         try:
452             sys.stdout = less_fd;
453             fernanda.display_changes(Katie.pkg.changes_file);
454             files = Katie.pkg.files;
455             for file in files.keys():
456                 if files[file].has_key("new"):
457                     type = files[file]["type"];
458                     if type == "deb":
459                         fernanda.check_deb(file);
460                     elif type == "dsc":
461                         fernanda.check_dsc(file);
462         finally:
463             sys.stdout = stdout_fd;
464     except IOError, e:
465         if errno.errorcode[e.errno] == 'EPIPE':
466             utils.warn("[fernanda] Caught EPIPE; skipping.");
467             pass;
468         else:
469             raise;
470     except KeyboardInterrupt:
471         utils.warn("[fernanda] Caught C-c; skipping.");
472         pass;
473
474 ################################################################################
475
476 ## FIXME: horribly Debian specific
477
478 def do_bxa_notification():
479     files = Katie.pkg.files;
480     summary = "";
481     for file in files.keys():
482         if files[file]["type"] == "deb":
483             control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file)));
484             summary = summary + "\n";
485             summary = summary + "Package: %s\n" % (control.Find("Package"));
486             summary = summary + "Description: %s\n" % (control.Find("Description"));
487     Katie.Subst["__BINARY_DESCRIPTIONS__"] = summary;
488     bxa_mail = utils.TemplateSubst(Katie.Subst,open(Cnf["Dir::TemplatesDir"]+"/lisa.bxa_notification","r").read());
489     utils.send_mail(bxa_mail,"");
490
491 ################################################################################
492
493 def add_overrides (new):
494     changes = Katie.pkg.changes;
495     files = Katie.pkg.files;
496
497     projectB.query("BEGIN WORK");
498     for suite in changes["suite"].keys():
499         suite_id = db_access.get_suite_id(suite);
500         for pkg in new.keys():
501             component_id = db_access.get_component_id(new[pkg]["component"]);
502             type_id = db_access.get_override_type_id(new[pkg]["type"]);
503             priority_id = new[pkg]["priority id"];
504             section_id = new[pkg]["section id"];
505             projectB.query("INSERT INTO override (suite, component, type, package, priority, section) VALUES (%s, %s, %s, '%s', %s, %s)" % (suite_id, component_id, type_id, pkg, priority_id, section_id));
506             for file in new[pkg]["files"]:
507                 if files[file].has_key("new"):
508                     del files[file]["new"];
509             del new[pkg];
510
511     projectB.query("COMMIT WORK");
512
513     if Cnf.FindI("Dinstall::BXANotify"):
514         do_bxa_notification();
515
516 ################################################################################
517
518 def do_new():
519     print "NEW\n";
520     files = Katie.pkg.files;
521     changes = Katie.pkg.changes;
522
523     # Make a copy of distribution we can happily trample on
524     changes["suite"] = copy.copy(changes["distribution"]);
525
526     # Fix up the list of target suites
527     for suite in changes["suite"].keys():
528         override = Cnf.Find("Suite::%s::OverrideSuite" % (suite));
529         if override:
530             del changes["suite"][suite];
531             changes["suite"][override] = 1;
532     # Validate suites
533     for suite in changes["suite"].keys():
534         suite_id = db_access.get_suite_id(suite);
535         if suite_id == -1:
536             utils.fubar("%s has invalid suite '%s' (possibly overriden).  say wha?" % (changes, suite));
537
538     # The main NEW processing loop
539     done = 0;
540     while not done:
541         # Find out what's new
542         new = determine_new(changes, files);
543
544         if not new:
545             break;
546
547         answer = "XXX";
548         if Options["No-Action"] or Options["Automatic"]:
549             answer = 'S';
550
551         broken = print_new(new, 0);
552         prompt = "";
553         if not broken:
554             prompt = "Add overrides, ";
555         else:
556             print "W: [!] marked entries must be fixed before package can be processed.";
557             if answer == 'A':
558                 answer = 'E';
559         prompt = prompt + "Edit overrides, Check, Manual reject, [S]kip, Quit ?";
560
561         while string.find(prompt, answer) == -1:
562             answer = utils.our_raw_input(prompt);
563             m = katie.re_default_answer.search(prompt);
564             if answer == "":
565                 answer = m.group(1)
566             answer = string.upper(answer[:1])
567
568         if answer == 'A':
569             done = add_overrides (new);
570         elif answer == 'C':
571             check_pkg();
572         elif answer == 'E':
573             new = edit_overrides (new);
574         elif answer == 'M':
575             Katie.do_reject(1, Options["Manual-Reject"]);
576             os.unlink(Katie.pkg.changes_file[:-8]+".katie");
577             done = 1;
578         elif answer == 'S':
579             done = 1;
580         elif answer == 'Q':
581             sys.exit(0)
582
583 ################################################################################
584 ################################################################################
585 ################################################################################
586
587 def usage (exit_code=0):
588     print """Usage: lisa [OPTION]... [CHANGES]...
589   -a, --automatic           automatic run
590   -h, --help                show this help and exit.
591   -m, --manual-reject=MSG   manual reject with `msg'
592   -n, --no-action           don't do anything
593   -s, --sort=TYPE           sort type ('time' or 'normal')
594   -V, --version             display the version number and exit"""
595     sys.exit(exit_code)
596
597 ################################################################################
598
599 def init():
600     global Cnf, Options, Logger, Katie, projectB, Sections, Priorities;
601
602     Cnf = utils.get_conf();
603
604     Arguments = [('a',"automatic","Lisa::Options::Automatic"),
605                  ('h',"help","Lisa::Options::Help"),
606                  ('m',"manual-reject","Lisa::Options::Manual-Reject", "HasArg"),
607                  ('n',"no-action","Lisa::Options::No-Action"),
608                  ('s',"sort","Lisa::Options::Sort","HasArg"),
609                  ('V',"version","Lisa::Options::Version")];
610
611     for i in ["automatic", "help", "manual-reject", "no-action", "version"]:
612         if not Cnf.has_key("Lisa::Options::%s" % (i)):
613             Cnf["Lisa::Options::%s" % (i)] = "";
614     if not Cnf.has_key("Lisa::Options::Sort"):
615         Cnf["Lisa::Options::Sort"] = "time";
616
617     changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
618     Options = Cnf.SubTree("Lisa::Options")
619
620     if Options["Help"]:
621         usage();
622
623     if Options["Version"]:
624         print "lisa %s" % (lisa_version);
625         sys.exit(0);
626
627     if Options["Sort"] != "time" and Options["Sort"] != "normal":
628         utils.fubar("Unrecognised sort type '%s'. (Recognised sort types are: time and normal)" % (Options["Sort"]));
629
630     Katie = katie.Katie(Cnf);
631
632     if not Options["No-Action"]:
633         Logger = Katie.Logger = logging.Logger(Cnf, "lisa");
634
635     projectB = Katie.projectB;
636
637     Sections = Section_Completer();
638     Priorities = Priority_Completer();
639     readline.parse_and_bind("tab: complete");
640
641     return changes_files;
642
643 ################################################################################
644
645 def do_byhand():
646     done = 0;
647     while not done:
648         files = Katie.pkg.files;
649         will_install = 1;
650         byhand = [];
651
652         for file in files.keys():
653             if files[file]["type"] == "byhand":
654                 if os.path.exists(file):
655                     print "W: %s still present; please process byhand components and try again." % (file);
656                     will_install = 0;
657                 else:
658                     byhand.append(file);
659
660         answer = "XXXX";
661         if Options["No-Action"]:
662             answer = "S";
663         if will_install:
664             if Options["Automatic"] and not Options["No-Action"]:
665                 answer = 'A';
666             prompt = "[A]ccept, Manual reject, Skip, Quit ?";
667         else:
668             prompt = "Manual reject, [S]kip, Quit ?";
669
670         while string.find(prompt, answer) == -1:
671             answer = utils.our_raw_input(prompt);
672             m = katie.re_default_answer.search(prompt);
673             if answer == "":
674                 answer = m.group(1);
675             answer = string.upper(answer[:1]);
676
677         if answer == 'A':
678             done = 1;
679             for file in byhand:
680                 del files[file];
681         elif answer == 'M':
682             Katie.do_reject(1, Options["Manual-Reject"]);
683             os.unlink(Katie.pkg.changes_file[:-8]+".katie");
684             done = 1;
685         elif answer == 'S':
686             done = 1;
687         elif answer == 'Q':
688             sys.exit(0);
689
690 ################################################################################
691
692 def do_accept():
693     print "ACCEPT";
694     if not Options["No-Action"]:
695         (summary, short_summary) = Katie.build_summaries();
696         Katie.accept(summary, short_summary);
697         os.unlink(Katie.pkg.changes_file[:-8]+".katie");
698
699 def check_status(files):
700     new = byhand = 0;
701     for file in files.keys():
702         if files[file]["type"] == "byhand":
703             byhand = 1;
704         elif files[file].has_key("new"):
705             new = 1;
706     return (new, byhand);
707
708 def do_pkg(changes_file):
709     Katie.pkg.changes_file = changes_file;
710     Katie.init_vars();
711     Katie.update_vars();
712     Katie.update_subst();
713     files = Katie.pkg.files;
714
715     (new, byhand) = check_status(files);
716     if new or byhand:
717         if new:
718             do_new();
719         if byhand:
720             do_byhand();
721         (new, byhand) = check_status(files);
722
723     if not new and not byhand:
724         do_accept();
725
726 ################################################################################
727
728 def end():
729     accept_count = Katie.accept_count;
730     accept_bytes = Katie.accept_bytes;
731
732     if accept_count:
733         sets = "set"
734         if accept_count > 1:
735             sets = "sets"
736         sys.stderr.write("Accepted %d package %s, %s.\n" % (accept_count, sets, utils.size_type(int(accept_bytes))));
737         Logger.log(["total",accept_count,accept_bytes]);
738
739     if not Options["No-Action"]:
740         Logger.close();
741
742 ################################################################################
743
744 def main():
745     changes_files = init();
746
747     # Sort the changes files
748     if Options["Sort"] == "time":
749         changes_files.sort(changes_compare_by_time);
750     else:
751         changes_files.sort(utils.changes_compare);
752
753     # Kill me now? **FIXME**
754     Cnf["Dinstall::Options::No-Mail"] = "";
755     bcc = "X-Lisa: %s" % (lisa_version);
756     if Cnf.has_key("Dinstall::Bcc"):
757         Katie.Subst["__BCC__"] = bcc + "\nBcc: %s" % (Cnf["Dinstall::Bcc"]);
758     else:
759         Katie.Subst["__BCC__"] = bcc;
760
761     for changes_file in changes_files:
762         print "\n" + changes_file;
763         do_pkg (changes_file);
764
765     end();
766
767 ################################################################################
768
769 if __name__ == '__main__':
770     main()