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