]> git.decadent.org.uk Git - dak.git/blob - katie
Fix for crash on non-existent files.
[dak.git] / katie
1 #!/usr/bin/env python
2
3 # Installs Debian packaes
4 # Copyright (C) 2000, 2001  James Troup <james@nocrew.org>
5 # $Id: katie,v 1.37 2001-04-03 21:28:20 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 # Originally based almost entirely on dinstall by Guy Maor <maor@debian.org>
22
23 #########################################################################################
24
25 #    Cartman: "I'm trying to make the best of a bad situation, I don't
26 #              need to hear crap from a bunch of hippy freaks living in
27 #              denial.  Screw you guys, I'm going home."
28 #
29 #    Kyle: "But Cartman, we're trying to..."
30 #
31 #    Cartman: "uhh.. screw you guys... home."
32
33 #########################################################################################
34
35 import FCNTL, commands, fcntl, getopt, gzip, os, pg, pwd, re, shutil, stat, string, sys, tempfile, time
36 import apt_inst, apt_pkg
37 import utils, db_access
38
39 ###############################################################################
40
41 re_isanum = re.compile (r'^\d+$');
42 re_changes = re.compile (r'changes$');
43 re_default_answer = re.compile(r"\[(.*)\]");
44 re_fdnic = re.compile("\n\n");
45 re_bad_diff = re.compile("^[\-\+][\-\+][\-\+] /dev/null");
46
47 #########################################################################################
48
49 # Globals
50 Cnf = None;
51 reject_message = "";
52 changes = {};
53 dsc = {};
54 dsc_files = {};
55 files = {};
56 projectB = None;
57 new_ack_new = {};
58 new_ack_old = {};
59 install_count = 0;
60 install_bytes = 0.0;
61 reprocess = 0;
62 orig_tar_id = None;
63 orig_tar_location = "";
64 legacy_source_untouchable = {};
65 Subst = {};
66
67 #########################################################################################
68
69 def usage (exit_code):
70     print """Usage: dinstall [OPTION]... [CHANGES]...
71   -a, --automatic           automatic run
72   -D, --debug=VALUE         turn on debugging
73   -h, --help                show this help and exit.
74   -k, --ack-new             acknowledge new packages !! for cron.daily only !!
75   -m, --manual-reject=MSG   manual reject with `msg'
76   -n, --no-action           don't do anything
77   -p, --no-lock             don't check lockfile !! for cron.daily only !!
78   -u, --distribution=DIST   override distribution to `dist'
79   -v, --version             display the version number and exit"""
80     sys.exit(exit_code)
81
82 #########################################################################################
83
84 def check_signature (filename):
85     global reject_message
86
87     (result, output) = commands.getstatusoutput("gpg --emulate-md-encode-bug --batch --no-options --no-default-keyring --always-trust --keyring=%s --keyring=%s < %s >/dev/null" % (Cnf["Dinstall::PGPKeyring"], Cnf["Dinstall::GPGKeyring"], filename))
88     if (result != 0):
89         reject_message = "Rejected: GPG signature check failed on `%s'.\n%s\n" % (os.path.basename(filename), output)
90         return 0
91     return 1
92
93 #####################################################################################################################
94
95 # See if a given package is in the override table
96
97 def in_override_p (package, component, suite, binary_type, file):
98     global files;
99     
100     if binary_type == "": # must be source
101         type = "dsc";
102     else:
103         type = binary_type;
104
105     # Override suite name; used for example with proposed-updates
106     if Cnf.Find("Suite::%s::OverrideSuite" % (suite)) != "":
107         suite = Cnf["Suite::%s::OverrideSuite" % (suite)];
108
109     # Avoid <undef> on unknown distributions
110     suite_id = db_access.get_suite_id(suite);
111     if suite_id == -1:
112         return None;
113     component_id = db_access.get_component_id(component);
114     type_id = db_access.get_override_type_id(type);
115
116     # FIXME: nasty non-US speficic hack
117     if string.lower(component[:7]) == "non-us/":
118         component = component[7:];
119
120     q = projectB.query("SELECT s.section, p.priority FROM override o, section s, priority p WHERE package = '%s' AND suite = %s AND component = %s AND type = %s AND o.section = s.id AND o.priority = p.id"
121                        % (package, suite_id, component_id, type_id));
122     result = q.getresult();
123     # If checking for a source package fall back on the binary override type
124     if type == "dsc" and not result:
125         type_id = db_access.get_override_type_id("deb");
126         q = projectB.query("SELECT s.section, p.priority FROM override o, section s, priority p WHERE package = '%s' AND suite = %s AND component = %s AND type = %s AND o.section = s.id AND o.priority = p.id"
127                            % (package, suite_id, component_id, type_id));
128         result = q.getresult();
129
130     # Remember the section and priority so we can check them later if appropriate
131     if result != []:
132         files[file]["override section"] = result[0][0];
133         files[file]["override priority"] = result[0][1];
134         
135     return result;
136
137 #####################################################################################################################
138
139 def check_changes(filename):
140     global reject_message, changes, files
141
142     # Default in case we bail out
143     changes["maintainer822"] = Cnf["Dinstall::MyEmailAddress"]; 
144
145     # Parse the .changes field into a dictionary
146     try:
147         changes = utils.parse_changes(filename, 0)
148     except utils.cant_open_exc:
149         reject_message = "Rejected: can't read changes file '%s'.\n" % (filename)
150         return 0;
151     except utils.changes_parse_error_exc, line:
152         reject_message = "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line)
153         return 0;
154
155     # Parse the Files field from the .changes into another dictionary
156     try:
157         files = utils.build_file_list(changes, "");
158     except utils.changes_parse_error_exc, line:
159         reject_message = "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line);
160
161     # Check for mandatory fields
162     for i in ("source", "binary", "architecture", "version", "distribution","maintainer", "files"):
163         if not changes.has_key(i):
164             reject_message = "Rejected: Missing field `%s' in changes file.\n" % (i)
165             return 0    # Avoid <undef> errors during later tests
166
167     # Override the Distribution: field if appropriate
168     if Cnf["Dinstall::Options::Override-Distribution"] != "":
169         reject_message = reject_message + "Warning: Distribution was overriden from %s to %s.\n" % (changes["distribution"], Cnf["Dinstall::Options::Override-Distribution"])
170         changes["distribution"] = Cnf["Dinstall::Options::Override-Distribution"]
171
172     # Split multi-value fields into a lower-level dictionary
173     for i in ("architecture", "distribution", "binary", "closes"):
174         o = changes.get(i, "")
175         if o != "":
176             del changes[i]
177         changes[i] = {}
178         for j in string.split(o):
179             changes[i][j] = 1
180
181     # Fix the Maintainer: field to be RFC822 compatible
182     (changes["maintainer822"], changes["maintainername"], changes["maintaineremail"]) = utils.fix_maintainer (changes["maintainer"])
183
184     # Fix the Changed-By: field to be RFC822 compatible; if it exists.
185     (changes["changedby822"], changes["changedbyname"], changes["changedbyemail"]) = utils.fix_maintainer(changes.get("changed-by",""));
186
187     # For source uploads the Changed-By field wins; otherwise Maintainer wins.
188     if changes["architecture"].has_key("source"):
189         changes["uploader822"] = "To: %s\nCc: %s" % (changes["changedby822"], changes["maintainer822"]);
190          #         changes["uploadername"], changes["uploaderemail"]) = (changes["changedby822"], changes["changedbyname"], changes["changedbyemail"]);
191         
192     # Ensure all the values in Closes: are numbers
193     if changes.has_key("closes"):
194         for i in changes["closes"].keys():
195             if re_isanum.match (i) == None:
196                 reject_message = reject_message + "Rejected: `%s' from Closes field isn't a number.\n" % (i)
197
198     # Map frozen to unstable if frozen doesn't exist
199     if changes["distribution"].has_key("frozen") and not Cnf.has_key("Suite::Frozen"):
200         del changes["distribution"]["frozen"]
201         changes["distribution"]["unstable"] = 1;
202         reject_message = reject_message + "Mapping frozen to unstable.\n"
203
204     # Map testing to unstable
205     if changes["distribution"].has_key("testing"):
206         del changes["distribution"]["testing"]
207         changes["distribution"]["unstable"] = 1;
208         reject_message = reject_message + "Mapping testing to unstable.\n"
209
210     # Ensure target distributions exist
211     for i in changes["distribution"].keys():
212         if not Cnf.has_key("Suite::%s" % (i)):
213             reject_message = reject_message + "Rejected: Unknown distribution `%s'.\n" % (i)
214
215     # Ensure there _is_ a target distribution
216     if changes["distribution"].keys() == []:
217         reject_message = reject_message + "Rejected: huh? Distribution field is empty in changes file.\n";
218             
219     # Map unreleased arches from stable to unstable
220     if changes["distribution"].has_key("stable"):
221         for i in changes["architecture"].keys():
222             if not Cnf.has_key("Suite::Stable::Architectures::%s" % (i)):
223                 reject_message = reject_message + "Mapping stable to unstable for unreleased arch `%s'.\n" % (i)
224                 del changes["distribution"]["stable"]
225                 changes["distribution"]["unstable"] = 1;
226     
227     # Map arches not being released from frozen to unstable
228     if changes["distribution"].has_key("frozen"):
229         for i in changes["architecture"].keys():
230             if not Cnf.has_key("Suite::Frozen::Architectures::%s" % (i)):
231                 reject_message = reject_message + "Mapping frozen to unstable for non-releasing arch `%s'.\n" % (i)
232                 del changes["distribution"]["frozen"]
233                 changes["distribution"]["unstable"] = 1;
234
235     # Handle uploads to stable
236     if changes["distribution"].has_key("stable"):
237         # If running from within proposed-updates; assume an install to stable
238         if string.find(os.getcwd(), 'proposed-updates') != -1:
239             # FIXME: should probably remove anything that != stable
240             for i in ("frozen", "unstable"):
241                 if changes["distribution"].has_key(i):
242                     reject_message = reject_message + "Removing %s from distribution list.\n"
243                     del changes["distribution"][i]
244             changes["stable upload"] = 1;
245             # If we can't find a file from the .changes; assume it's a package already in the pool and move into the pool
246             file = files.keys()[0];
247             if os.access(file, os.R_OK) == 0:
248                 pool_dir = Cnf["Dir::PoolDir"] + '/' + utils.poolify(changes["source"], files[file]["component"]);
249                 os.chdir(pool_dir);
250         # Otherwise (normal case) map stable to updates
251         else:
252             reject_message = reject_message + "Mapping stable to updates.\n";
253             del changes["distribution"]["stable"];
254             changes["distribution"]["proposed-updates"] = 1;
255
256     # chopversion = no epoch; chopversion2 = no epoch and no revision (e.g. for .orig.tar.gz comparison)
257     changes["chopversion"] = utils.re_no_epoch.sub('', changes["version"])
258     changes["chopversion2"] = utils.re_no_revision.sub('', changes["chopversion"])
259     
260     if string.find(reject_message, "Rejected:") != -1:
261         return 0
262     else: 
263         return 1
264
265 def check_files():
266     global reject_message
267     
268     archive = utils.where_am_i();
269
270     for file in files.keys():
271         # Check the file is readable
272         if os.access(file,os.R_OK) == 0:
273             reject_message = reject_message + "Rejected: Can't read `%s'.\n" % (file)
274             files[file]["type"] = "unreadable";
275             continue
276         # If it's byhand skip remaining checks
277         if files[file]["section"] == "byhand":
278             files[file]["byhand"] = 1;
279             files[file]["type"] = "byhand";
280         # Checks for a binary package...
281         elif utils.re_isadeb.match(file) != None:
282             files[file]["type"] = "deb";
283
284             # Extract package information using dpkg-deb
285             try:
286                 control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))
287             except:
288                 reject_message = reject_message + "Rejected: %s: debExtractControl() raised %s.\n" % (file, sys.exc_type);
289                 # Can't continue, none of the checks on control would work.
290                 continue;
291
292             # Check for mandatory fields
293             if control.Find("Package") == None:
294                 reject_message = reject_message + "Rejected: %s: No package field in control.\n" % (file)
295             if control.Find("Architecture") == None:
296                 reject_message = reject_message + "Rejected: %s: No architecture field in control.\n" % (file)
297             if control.Find("Version") == None:
298                 reject_message = reject_message + "Rejected: %s: No version field in control.\n" % (file)
299                 
300             # Ensure the package name matches the one give in the .changes
301             if not changes["binary"].has_key(control.Find("Package", "")):
302                 reject_message = reject_message + "Rejected: %s: control file lists name as `%s', which isn't in changes file.\n" % (file, control.Find("Package", ""))
303
304             # Validate the architecture
305             if not Cnf.has_key("Suite::Unstable::Architectures::%s" % (control.Find("Architecture", ""))):
306                 reject_message = reject_message + "Rejected: Unknown architecture '%s'.\n" % (control.Find("Architecture", ""))
307
308             # Check the architecture matches the one given in the .changes
309             if not changes["architecture"].has_key(control.Find("Architecture", "")):
310                 reject_message = reject_message + "Rejected: %s: control file lists arch as `%s', which isn't in changes file.\n" % (file, control.Find("Architecture", ""))
311             # Check the section & priority match those given in the .changes (non-fatal)
312             if control.Find("Section") != None and files[file]["section"] != "" and files[file]["section"] != control.Find("Section"):
313                 reject_message = reject_message + "Warning: %s control file lists section as `%s', but changes file has `%s'.\n" % (file, control.Find("Section", ""), files[file]["section"])
314             if control.Find("Priority") != None and files[file]["priority"] != "" and files[file]["priority"] != control.Find("Priority"):
315                 reject_message = reject_message + "Warning: %s control file lists priority as `%s', but changes file has `%s'.\n" % (file, control.Find("Priority", ""), files[file]["priority"])
316
317             epochless_version = utils.re_no_epoch.sub('', control.Find("Version", ""))
318
319             files[file]["package"] = control.Find("Package");
320             files[file]["architecture"] = control.Find("Architecture");
321             files[file]["version"] = control.Find("Version");
322             files[file]["maintainer"] = control.Find("Maintainer", "");
323             if file[-5:] == ".udeb":
324                 files[file]["dbtype"] = "udeb";
325             elif file[-4:] == ".deb":
326                 files[file]["dbtype"] = "deb";
327             else:
328                 reject_message = reject_message + "Rejected: %s is neither a .deb or a .udeb.\n " % (file);
329             files[file]["fullname"] = "%s_%s_%s.deb" % (control.Find("Package", ""), epochless_version, control.Find("Architecture", ""))
330             files[file]["source"] = control.Find("Source", "");
331             if files[file]["source"] == "":
332                 files[file]["source"] = files[file]["package"];
333         # Checks for a source package...
334         else:
335             m = utils.re_issource.match(file)
336             if m != None:
337                 files[file]["package"] = m.group(1)
338                 files[file]["version"] = m.group(2)
339                 files[file]["type"] = m.group(3)
340                 
341                 # Ensure the source package name matches the Source filed in the .changes
342                 if changes["source"] != files[file]["package"]:
343                     reject_message = reject_message + "Rejected: %s: changes file doesn't say %s for Source\n" % (file, files[file]["package"])
344
345                 # Ensure the source version matches the version in the .changes file
346                 if files[file]["type"] == "orig.tar.gz":
347                     changes_version = changes["chopversion2"]
348                 else:
349                     changes_version = changes["chopversion"]
350                 if changes_version != files[file]["version"]:
351                     reject_message = reject_message + "Rejected: %s: should be %s according to changes file.\n" % (file, changes_version)
352
353                 # Ensure the .changes lists source in the Architecture field
354                 if not changes["architecture"].has_key("source"):
355                     reject_message = reject_message + "Rejected: %s: changes file doesn't list `source' in Architecture field.\n" % (file)
356
357                 # Check the signature of a .dsc file
358                 if files[file]["type"] == "dsc":
359                     check_signature(file)
360
361                 files[file]["fullname"] = file
362
363             # Not a binary or source package?  Assume byhand...
364             else:
365                 files[file]["byhand"] = 1;
366                 files[file]["type"] = "byhand";
367
368         files[file]["oldfiles"] = {}
369         for suite in changes["distribution"].keys():
370             # Skip byhand
371             if files[file].has_key("byhand"):
372                 continue
373
374             if Cnf.has_key("Suite:%s::Components" % (suite)) and not Cnf.has_key("Suite::%s::Components::%s" % (suite, files[file]["component"])):
375                 reject_message = reject_message + "Rejected: unknown component `%s' for suite `%s'.\n" % (files[file]["component"], suite)
376                 continue
377
378             # See if the package is NEW
379             if not in_override_p(files[file]["package"], files[file]["component"], suite, files[file].get("dbtype",""), file):
380                 files[file]["new"] = 1
381                 
382             # Find any old binary packages
383             if files[file]["type"] == "deb":
384                 q = projectB.query("SELECT b.id, b.version, f.filename, l.path, c.name FROM binaries b, bin_associations ba, suite s, location l, component c, architecture a, files f WHERE b.package = '%s' AND s.suite_name = '%s' AND a.arch_string = '%s' AND ba.bin = b.id AND ba.suite = s.id AND b.architecture = a.id AND f.location = l.id AND l.component = c.id AND b.file = f.id"
385                                    % (files[file]["package"], suite, files[file]["architecture"]))
386                 oldfiles = q.dictresult()
387                 for oldfile in oldfiles:
388                     files[file]["oldfiles"][suite] = oldfile
389                     # Check versions [NB: per-suite only; no cross-suite checking done (yet)]
390                     if apt_pkg.VersionCompare(files[file]["version"], oldfile["version"]) != 1:
391                         reject_message = reject_message + "Rejected: %s Old version `%s' >= new version `%s'.\n" % (file, oldfile["version"], files[file]["version"])
392                 # Check for existing copies of the file
393                 if not changes.has_key("stable upload"):
394                     q = projectB.query("SELECT b.id FROM binaries b, architecture a WHERE b.package = '%s' AND b.version = '%s' AND a.arch_string = '%s' AND a.id = b.architecture" % (files[file]["package"], files[file]["version"], files[file]["architecture"]))
395                     if q.getresult() != []:
396                         reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (file)
397
398             # Find any old .dsc files
399             elif files[file]["type"] == "dsc":
400                 q = projectB.query("SELECT s.id, s.version, f.filename, l.path, c.name FROM source s, src_associations sa, suite su, location l, component c, files f WHERE s.source = '%s' AND su.suite_name = '%s' AND sa.source = s.id AND sa.suite = su.id AND f.location = l.id AND l.component = c.id AND f.id = s.file"
401                                    % (files[file]["package"], suite))
402                 oldfiles = q.dictresult()
403                 if len(oldfiles) >= 1:
404                     files[file]["oldfiles"][suite] = oldfiles[0]
405
406             # Validate the component
407             component = files[file]["component"];
408             component_id = db_access.get_component_id(component);
409             if component_id == -1:
410                 reject_message = reject_message + "Rejected: file '%s' has unknown component '%s'.\n" % (file, component);
411                 continue;
412
413             # Validate the priority
414             if string.find(files[file]["priority"],'/') != -1:
415                 reject_message = reject_message + "Rejected: file '%s' has invalid priority '%s' [contains '/'].\n" % (file, files[file]["priority"]);
416             
417             # Check the md5sum & size against existing files (if any)
418             location = Cnf["Dir::PoolDir"];
419             files[file]["location id"] = db_access.get_location_id (location, component, archive);
420
421             files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"]);
422             files_id = db_access.get_files_id(files[file]["pool name"] + file, files[file]["size"], files[file]["md5sum"], files[file]["location id"]);
423             if files_id == -1:
424                 reject_message = reject_message + "Rejected: INTERNAL ERROR, get_files_id() returned multiple matches for %s.\n" % (file)
425             elif files_id == -2:
426                 reject_message = reject_message + "Rejected: md5sum and/or size mismatch on existing copy of %s.\n" % (file)
427             files[file]["files id"] = files_id
428
429             # Check for packages that have moved from one component to another
430             if files[file]["oldfiles"].has_key(suite) and files[file]["oldfiles"][suite]["name"] != files[file]["component"]:
431                 files[file]["othercomponents"] = files[file]["oldfiles"][suite]["name"];
432
433                 
434     if string.find(reject_message, "Rejected:") != -1:
435         return 0
436     else: 
437         return 1
438
439 ###############################################################################
440
441 def check_dsc ():
442     global dsc, dsc_files, reject_message, reprocess, orig_tar_id, orig_tar_location, legacy_source_untouchable;
443
444     for file in files.keys():
445         if files[file]["type"] == "dsc":
446             try:
447                 dsc = utils.parse_changes(file, 1)
448             except utils.cant_open_exc:
449                 reject_message = reject_message + "Rejected: can't read changes file '%s'.\n" % (file)
450                 return 0;
451             except utils.changes_parse_error_exc, line:
452                 reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (file, line)
453                 return 0;
454             except utils.invalid_dsc_format_exc, line:
455                 reject_message = reject_message + "Rejected: syntax error in .dsc file '%s', line %s.\n" % (file, line)
456                 return 0;
457             try:
458                 dsc_files = utils.build_file_list(dsc, 1)
459             except utils.no_files_exc:
460                 reject_message = reject_message + "Rejected: no Files: field in .dsc file.\n";
461                 continue;
462             except utils.changes_parse_error_exc, line:
463                 reject_message = "Rejected: error parsing .dsc file '%s', can't grok: %s.\n" % (file, line);
464                 continue;
465
466             # Try and find all files mentioned in the .dsc.  This has
467             # to work harder to cope with the multiple possible
468             # locations of an .orig.tar.gz.
469             for dsc_file in dsc_files.keys():
470                 if files.has_key(dsc_file):
471                     actual_md5 = files[dsc_file]["md5sum"];
472                     actual_size = int(files[dsc_file]["size"]);
473                     found = "%s in incoming" % (dsc_file)
474                     # Check the file does not already exist in the archive
475                     if not changes.has_key("stable upload"):
476                         q = projectB.query("SELECT f.id FROM files f, location l WHERE (f.filename ~ '/%s$' OR f.filename = '%s') AND l.id = f.location" % (utils.regex_safe(dsc_file), dsc_file));
477
478                         # "It has not broken them.  It has fixed a
479                         # brokenness.  Your crappy hack exploited a
480                         # bug in the old dinstall.
481                         #
482                         # "(Come on!  I thought it was always obvious
483                         # that one just doesn't release different
484                         # files with the same name and version.)"
485                         #                        -- ajk@ on d-devel@l.d.o
486
487                         if q.getresult() != []:
488                             reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (dsc_file)
489                 elif dsc_file[-12:] == ".orig.tar.gz":
490                     # Check in the pool
491                     q = projectB.query("SELECT l.path, f.filename, l.type, f.id, l.id FROM files f, location l WHERE (f.filename ~ '/%s$' OR f.filename = '%s') AND l.id = f.location" % (utils.regex_safe(dsc_file), dsc_file));
492                     ql = q.getresult();
493
494                     if ql != []:
495                         # Unfortunately, we make get more than one match
496                         # here if, for example, the package was in potato
497                         # but had a -sa upload in woody.  So we need to a)
498                         # choose the right one and b) mark all wrong ones
499                         # as excluded from the source poolification (to
500                         # avoid file overwrites).
501
502                         x = ql[0]; # default to something sane in case we don't match any or have only one
503
504                         if len(ql) > 1:
505                             for i in ql:
506                                 old_file = i[0] + i[1];
507                                 actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
508                                 actual_size = os.stat(old_file)[stat.ST_SIZE];
509                                 if actual_md5 == dsc_files[dsc_file]["md5sum"] and actual_size == int(dsc_files[dsc_file]["size"]):
510                                     x = i;
511                                 else:
512                                     legacy_source_untouchable[i[3]] = "";
513
514                         old_file = x[0] + x[1];
515                         actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
516                         actual_size = os.stat(old_file)[stat.ST_SIZE];
517                         found = old_file;
518                         suite_type = x[2];
519                         dsc_files[dsc_file]["files id"] = x[3]; # need this for updating dsc_files in install()
520                         # See install()...
521                         orig_tar_id = x[3];
522                         if suite_type == "legacy" or suite_type == "legacy-mixed":
523                             orig_tar_location = "legacy";
524                         else:
525                             orig_tar_location = x[4];
526                     else:
527                         # Not there? Check in Incoming...
528                         # [See comment above process_it() for explanation
529                         #  of why this is necessary...]
530                         if os.access(dsc_file, os.R_OK) != 0:
531                             files[dsc_file] = {};
532                             files[dsc_file]["size"] = os.stat(dsc_file)[stat.ST_SIZE];
533                             files[dsc_file]["md5sum"] = dsc_files[dsc_file]["md5sum"];
534                             files[dsc_file]["section"] = files[file]["section"];
535                             files[dsc_file]["priority"] = files[file]["priority"];
536                             files[dsc_file]["component"] = files[file]["component"];
537                             files[dsc_file]["type"] = "orig.tar.gz";
538                             reprocess = 1;
539                             return 1;
540                         else:
541                             reject_message = reject_message + "Rejected: %s refers to %s, but I can't find it in Incoming or in the pool.\n" % (file, dsc_file);
542                             continue;
543                 else:
544                     reject_message = reject_message + "Rejected: %s refers to %s, but I can't find it in Incoming.\n" % (file, dsc_file);
545                     continue;
546                 if actual_md5 != dsc_files[dsc_file]["md5sum"]:
547                     reject_message = reject_message + "Rejected: md5sum for %s doesn't match %s.\n" % (found, file);
548                 if actual_size != int(dsc_files[dsc_file]["size"]):
549                     reject_message = reject_message + "Rejected: size for %s doesn't match %s.\n" % (found, file);
550
551     if string.find(reject_message, "Rejected:") != -1:
552         return 0
553     else: 
554         return 1
555
556 ###############################################################################
557
558 # Some cunning stunt broke dpkg-source in dpkg 1.8{,.1}; detect the
559 # resulting bad source packages and reject them.
560
561 # Even more amusingly the fix in 1.8.1.1 didn't actually fix the
562 # problem just changed the symptoms.
563
564 def check_diff ():
565     global dsc, dsc_files, reject_message, reprocess;
566
567     for filename in files.keys():
568         if files[filename]["type"] == "diff.gz":
569             file = gzip.GzipFile(filename, 'r');
570             for line in file.readlines():
571                 if re_bad_diff.search(line):
572                     reject_message = reject_message + "Rejected: [dpkg-sucks] source package was produced by a broken version of dpkg-dev 1.8.x; please rebuild with >= 1.8.3 version installed.\n";
573                     break;
574
575     if string.find(reject_message, "Rejected:") != -1:
576         return 0
577     else: 
578         return 1
579
580 ###############################################################################
581
582 def check_md5sums ():
583     global reject_message;
584
585     for file in files.keys():
586         try:
587             file_handle = utils.open_file(file,"r");
588         except utils.cant_open_exc:
589             pass;
590         else:
591             if apt_pkg.md5sum(file_handle) != files[file]["md5sum"]:
592                 reject_message = reject_message + "Rejected: md5sum check failed for %s.\n" % (file);
593
594 def check_override ():
595     global Subst;
596     
597     # Only check section & priority on sourceful uploads
598     if not changes["architecture"].has_key("source"):
599         return;
600
601     summary = ""
602     for file in files.keys():
603         if not files[file].has_key("new") and files[file]["type"] == "deb":
604             section = files[file]["section"];
605             override_section = files[file]["override section"];
606             if section != override_section and section != "-":
607                 # Ignore this; it's a common mistake and not worth whining about
608                 if string.lower(section) == "non-us/main" and string.lower(override_section) == "non-us":
609                     continue;
610                 summary = summary + "%s: section is overridden from %s to %s.\n" % (file, section, override_section);
611             priority = files[file]["priority"];
612             override_priority = files[file]["override priority"];
613             if priority != override_priority and priority != "-":
614                 summary = summary + "%s: priority is overridden from %s to %s.\n" % (file, priority, override_priority);
615
616     if summary == "":
617         return;
618
619     Subst["__SUMMARY__"] = summary;
620     mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.override-disparity","r").read());
621     utils.send_mail (mail_message, "")
622
623 #####################################################################################################################
624
625 # Set up the per-package template substitution mappings
626
627 def update_subst (changes_filename):
628     global Subst;
629
630     if changes.has_key("architecture"):
631         Subst["__ARCHITECTURE__"] = string.join(changes["architecture"].keys(), ' ' );
632     else:
633         Subst["__ARCHITECTURE__"] = "Unknown";
634     Subst["__CHANGES_FILENAME__"] = os.path.basename(changes_filename);
635     Subst["__FILE_CONTENTS__"] = changes.get("filecontents", "");
636     Subst["__MAINTAINER_ADDRESS__"] = changes["maintainer822"];
637     Subst["__MAINTAINER__"] = changes.get("maintainer", "Unknown");
638     Subst["__REJECT_MESSAGE__"] = reject_message;
639     Subst["__SOURCE__"] = changes.get("source", "Unknown");
640     Subst["__VERSION__"] = changes.get("version", "Unknown");
641
642 #####################################################################################################################
643
644 def action (changes_filename):
645     byhand = confirm = suites = summary = new = "";
646
647     # changes["distribution"] may not exist in corner cases
648     # (e.g. unreadable changes files)
649     if not changes.has_key("distribution"):
650         changes["distribution"] = {};
651     
652     for suite in changes["distribution"].keys():
653         if Cnf.has_key("Suite::%s::Confirm"):
654             confirm = confirm + suite + ", "
655         suites = suites + suite + ", "
656     confirm = confirm[:-2]
657     suites = suites[:-2]
658
659     for file in files.keys():
660         if files[file].has_key("byhand"):
661             byhand = 1
662             summary = summary + file + " byhand\n"
663         elif files[file].has_key("new"):
664             new = 1
665             summary = summary + "(new) %s %s %s\n" % (file, files[file]["priority"], files[file]["section"])
666             if files[file].has_key("othercomponents"):
667                 summary = summary + "WARNING: Already present in %s distribution.\n" % (files[file]["othercomponents"])
668             if files[file]["type"] == "deb":
669                 summary = summary + apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))["Description"] + '\n';
670         else:
671             files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"])
672             destination = Cnf["Dir::PoolRoot"] + files[file]["pool name"] + file
673             summary = summary + file + "\n  to " + destination + "\n"
674
675     short_summary = summary;
676
677     # This is for direport's benefit...
678     f = re_fdnic.sub("\n .\n", changes.get("changes",""));
679
680     if confirm or byhand or new:
681         summary = summary + "Changes: " + f;
682
683     summary = summary + announce (short_summary, 0)
684     
685     (prompt, answer) = ("", "XXX")
686     if Cnf["Dinstall::Options::No-Action"] or Cnf["Dinstall::Options::Automatic"]:
687         answer = 'S'
688
689     if string.find(reject_message, "Rejected") != -1:
690         try:
691             modified_time = time.time()-os.path.getmtime(changes_filename);
692         except: # i.e. ignore errors like 'file does not exist';
693             modified_time = 0;
694         if modified_time < 86400:
695             print "SKIP (too new)\n" + reject_message,;
696             prompt = "[S]kip, Manual reject, Quit ?";
697         else:
698             print "REJECT\n" + reject_message,;
699             prompt = "[R]eject, Manual reject, Skip, Quit ?";
700             if Cnf["Dinstall::Options::Automatic"]:
701                 answer = 'R';
702     elif new:
703         print "NEW to %s\n%s%s" % (suites, reject_message, summary),;
704         prompt = "[S]kip, New ack, Manual reject, Quit ?";
705         if Cnf["Dinstall::Options::Automatic"] and Cnf["Dinstall::Options::Ack-New"]:
706             answer = 'N';
707     elif byhand:
708         print "BYHAND\n" + reject_message + summary,;
709         prompt = "[I]nstall, Manual reject, Skip, Quit ?";
710     elif confirm:
711         print "CONFIRM to %s\n%s%s" % (confirm, reject_message, summary),
712         prompt = "[I]nstall, Manual reject, Skip, Quit ?";
713     else:
714         print "INSTALL\n" + reject_message + summary,;
715         prompt = "[I]nstall, Manual reject, Skip, Quit ?";
716         if Cnf["Dinstall::Options::Automatic"]:
717             answer = 'I';
718
719     while string.find(prompt, answer) == -1:
720         print prompt,;
721         answer = utils.our_raw_input()
722         m = re_default_answer.match(prompt)
723         if answer == "":
724             answer = m.group(1)
725         answer = string.upper(answer[:1])
726
727     if answer == 'R':
728         reject (changes_filename, "");
729     elif answer == 'M':
730         manual_reject (changes_filename);
731     elif answer == 'I':
732         install (changes_filename, summary, short_summary);
733     elif answer == 'N':
734         acknowledge_new (changes_filename, summary);
735     elif answer == 'Q':
736         sys.exit(0)
737
738 #####################################################################################################################
739
740 def install (changes_filename, summary, short_summary):
741     global install_count, install_bytes, Subst;
742
743     # Stable uploads are a special case
744     if changes.has_key("stable upload"):
745         stable_install (changes_filename, summary, short_summary);
746         return;
747     
748     print "Installing."
749
750     archive = utils.where_am_i();
751
752     # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
753     projectB.query("BEGIN WORK");
754
755     # Add the .dsc file to the DB
756     for file in files.keys():
757         if files[file]["type"] == "dsc":
758             package = dsc["source"]
759             version = dsc["version"]  # NB: not files[file]["version"], that has no epoch
760             maintainer = dsc["maintainer"]
761             maintainer = string.replace(maintainer, "'", "\\'")
762             maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
763             filename = files[file]["pool name"] + file;
764             dsc_location_id = files[file]["location id"];
765             if not files[file]["files id"]:
766                 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], dsc_location_id)
767             projectB.query("INSERT INTO source (source, version, maintainer, file) VALUES ('%s', '%s', %d, %d)"
768                            % (package, version, maintainer_id, files[file]["files id"]))
769             
770             for suite in changes["distribution"].keys():
771                 suite_id = db_access.get_suite_id(suite);
772                 projectB.query("INSERT INTO src_associations (suite, source) VALUES (%d, currval('source_id_seq'))" % (suite_id))
773
774             # Add the source files to the DB (files and dsc_files)
775             projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files[file]["files id"]));
776             for dsc_file in dsc_files.keys():
777                 filename = files[file]["pool name"] + dsc_file;
778                 # If the .orig.tar.gz is already in the pool, it's
779                 # files id is stored in dsc_files by check_dsc().
780                 files_id = dsc_files[dsc_file].get("files id", None);
781                 if files_id == None:
782                     files_id = db_access.get_files_id(filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], dsc_location_id);
783                 # FIXME: needs to check for -1/-2 and or handle exception
784                 if files_id == None:
785                     files_id = db_access.set_files_id (filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], dsc_location_id);
786                 projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files_id));
787             
788     # Add the .deb files to the DB
789     for file in files.keys():
790         if files[file]["type"] == "deb":
791             package = files[file]["package"]
792             version = files[file]["version"]
793             maintainer = files[file]["maintainer"]
794             maintainer = string.replace(maintainer, "'", "\\'")
795             maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
796             architecture = files[file]["architecture"]
797             architecture_id = db_access.get_architecture_id (architecture);
798             type = files[file]["dbtype"];
799             dsc_component = files[file]["component"]
800             source = files[file]["source"]
801             source_version = ""
802             if string.find(source, "(") != -1:
803                 m = utils.re_extract_src_version.match(source)
804                 source = m.group(1)
805                 source_version = m.group(2)
806             if not source_version:
807                 source_version = version
808             filename = files[file]["pool name"] + file;
809             if not files[file]["files id"]:
810                 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], files[file]["location id"])
811             source_id = db_access.get_source_id (source, source_version);
812             if source_id:
813                 projectB.query("INSERT INTO binaries (package, version, maintainer, source, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, %d, '%s')"
814                                % (package, version, maintainer_id, source_id, architecture_id, files[file]["files id"], type));
815             else:
816                 projectB.query("INSERT INTO binaries (package, version, maintainer, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, '%s')"
817                                % (package, version, maintainer_id, architecture_id, files[file]["files id"], type));
818             for suite in changes["distribution"].keys():
819                 suite_id = db_access.get_suite_id(suite);
820                 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES (%d, currval('binaries_id_seq'))" % (suite_id));
821
822     # If the .orig.tar.gz is in a legacy directory we need to poolify
823     # it, so that apt-get source (and anything else that goes by the
824     # "Directory:" field in the Sources.gz file) works.
825     if orig_tar_id != None and orig_tar_location == "legacy":
826         q = projectB.query("SELECT DISTINCT ON (f.id) l.path, f.filename, f.id as files_id, df.source, df.id as dsc_files_id, f.size, f.md5sum FROM files f, dsc_files df, location l WHERE df.source IN (SELECT source FROM dsc_files WHERE file = %s) AND f.id = df.file AND l.id = f.location AND (l.type = 'legacy' OR l.type = 'legacy-mixed')" % (orig_tar_id));
827         qd = q.dictresult();
828         for qid in qd:
829             # Is this an old upload superseded by a newer -sa upload?  (See check_dsc() for details)
830             if legacy_source_untouchable.has_key(qid["files_id"]):
831                 continue;
832             # First move the files to the new location
833             legacy_filename = qid["path"]+qid["filename"];
834             pool_location = utils.poolify (changes["source"], files[file]["component"]);
835             pool_filename = pool_location + os.path.basename(qid["filename"]);
836             destination = Cnf["Dir::PoolDir"] + pool_location
837             utils.move(legacy_filename, destination);
838             # Then Update the DB's files table
839             q = projectB.query("UPDATE files SET filename = '%s', location = '%s' WHERE id = '%s'" % (pool_filename, dsc_location_id, qid["files_id"]));
840
841     # If this is a sourceful diff only upload that is moving non-legacy
842     # cross-component we need to copy the .orig.tar.gz into the new
843     # component too for the same reasons as above.
844     #
845     if changes["architecture"].has_key("source") and orig_tar_id != None and \
846        orig_tar_location != "legacy" and orig_tar_location != dsc_location_id:
847         q = projectB.query("SELECT l.path, f.filename, f.size, f.md5sum FROM files f, location l WHERE f.id = %s AND f.location = l.id" % (orig_tar_id));
848         ql = q.getresult()[0];
849         old_filename = ql[0] + ql[1];
850         file_size = ql[2];
851         file_md5sum = ql[3];
852         new_filename = utils.poolify (changes["source"], dsc_component) + os.path.basename(old_filename);
853         new_files_id = db_access.get_files_id(new_filename, file_size, file_md5sum, dsc_location_id);
854         if new_files_id == None:
855             utils.copy(old_filename, Cnf["Dir::PoolDir"] + new_filename);
856             new_files_id = db_access.set_files_id(new_filename, file_size, file_md5sum, dsc_location_id);
857             projectB.query("UPDATE dsc_files SET file = %s WHERE source = %s AND file = %s" % (new_files_id, source_id, orig_tar_id));
858
859     # Install the files into the pool
860     for file in files.keys():
861         if files[file].has_key("byhand"):
862             continue
863         destination = Cnf["Dir::PoolDir"] + files[file]["pool name"] + file
864         destdir = os.path.dirname(destination)
865         utils.move (file, destination)
866         install_bytes = install_bytes + float(files[file]["size"])
867
868     # Copy the .changes file across for suite which need it.
869     for suite in changes["distribution"].keys():
870         if Cnf.has_key("Suite::%s::CopyChanges" % (suite)):
871             utils.copy (changes_filename, Cnf["Dir::RootDir"] + Cnf["Suite::%s::CopyChanges" % (suite)]);
872
873     projectB.query("COMMIT WORK");
874
875     try:
876         utils.move (changes_filename, Cnf["Dir::IncomingDir"] + 'DONE/' + os.path.basename(changes_filename))
877     except:
878         sys.stderr.write("W: couldn't move changes file '%s' to DONE directory [Got %s].\n" % (os.path.basename(changes_filename), sys.exc_type));
879
880     install_count = install_count + 1;
881
882     if not Cnf["Dinstall::Options::No-Mail"]:
883         Subst["__SUITE__"] = "";
884         Subst["__SUMMARY__"] = summary;
885         mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.installed","r").read());
886         utils.send_mail (mail_message, "")
887         announce (short_summary, 1)
888         check_override ();
889
890 #####################################################################################################################
891
892 def stable_install (changes_filename, summary, short_summary):
893     global install_count, install_bytes, Subst;
894     
895     print "Installing to stable."
896
897     archive = utils.where_am_i();
898
899     # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
900     projectB.query("BEGIN WORK");
901
902     # Add the .dsc file to the DB
903     for file in files.keys():
904         if files[file]["type"] == "dsc":
905             package = dsc["source"]
906             version = dsc["version"]  # NB: not files[file]["version"], that has no epoch
907             q = projectB.query("SELECT id FROM source WHERE source = '%s' AND version = '%s'" % (package, version))
908             ql = q.getresult()
909             if ql == []:
910                 sys.stderr.write("INTERNAL ERROR: couldn't find '%s' (%s) in source table.\n" % (package, version));
911                 sys.exit(1);
912             source_id = ql[0][0];
913             suite_id = db_access.get_suite_id('proposed-updates');
914             projectB.query("DELETE FROM src_associations WHERE suite = '%s' AND source = '%s'" % (suite_id, source_id));
915             suite_id = db_access.get_suite_id('stable');
916             projectB.query("INSERT INTO src_associations (suite, source) VALUES ('%s', '%s')" % (suite_id, source_id));
917                 
918     # Add the .deb files to the DB
919     for file in files.keys():
920         if files[file]["type"] == "deb":
921             package = files[file]["package"]
922             version = files[file]["version"]
923             architecture = files[file]["architecture"]
924             q = projectB.query("SELECT b.id FROM binaries b, architecture a WHERE b.package = '%s' AND b.version = '%s' AND (a.arch_string = '%s' OR a.arch_string = 'all') AND b.architecture = a.id" % (package, version, architecture))
925             ql = q.getresult()
926             if ql == []:
927                 sys.stderr.write("INTERNAL ERROR: couldn't find '%s' (%s for %s architecture) in binaries table.\n" % (package, version, architecture));
928                 sys.exit(1);
929             binary_id = ql[0][0];
930             suite_id = db_access.get_suite_id('proposed-updates');
931             projectB.query("DELETE FROM bin_associations WHERE suite = '%s' AND bin = '%s'" % (suite_id, binary_id));
932             suite_id = db_access.get_suite_id('stable');
933             projectB.query("INSERT INTO bin_associations (suite, bin) VALUES ('%s', '%s')" % (suite_id, binary_id));
934
935     projectB.query("COMMIT WORK");
936
937     utils.move (changes_filename, Cnf["Rhona::Morgue"] + os.path.basename(changes_filename));
938
939     # Update the Stable ChangeLog file
940
941     new_changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + ".ChangeLog";
942     changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + "ChangeLog";
943     if os.path.exists(new_changelog_filename):
944         os.unlink (new_changelog_filename);
945     
946     new_changelog = utils.open_file(new_changelog_filename, 'w');
947     for file in files.keys():
948         if files[file]["type"] == "deb":
949             new_changelog.write("stable/%s/binary-%s/%s\n" % (files[file]["component"], files[file]["architecture"], file));
950         elif utils.re_issource.match(file) != None:
951             new_changelog.write("stable/%s/source/%s\n" % (files[file]["component"], file));
952         else:
953             new_changelog.write("%s\n" % (file));
954     chop_changes = re_fdnic.sub("\n", changes["changes"]);
955     new_changelog.write(chop_changes + '\n\n');
956     if os.access(changelog_filename, os.R_OK) != 0:
957         changelog = utils.open_file(changelog_filename, 'r');
958         new_changelog.write(changelog.read());
959     new_changelog.close();
960     if os.access(changelog_filename, os.R_OK) != 0:
961         os.unlink(changelog_filename);
962     utils.move(new_changelog_filename, changelog_filename);
963
964     install_count = install_count + 1;
965
966     if not Cnf["Dinstall::Options::No-Mail"]:
967         Subst["__SUITE__"] = " into stable";
968         Subst["__SUMMARY__"] = summary;
969         utils.send_mail (mail_message, "")
970         announce (short_summary, 1)
971
972 #####################################################################################################################
973
974 def reject (changes_filename, manual_reject_mail_filename):
975     global Subst;
976     
977     print "Rejecting.\n"
978
979     base_changes_filename = os.path.basename(changes_filename);
980     reason_filename = re_changes.sub("reason", base_changes_filename);
981     reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename);
982
983     # Move the .changes files and it's contents into REJECT/ (if we can; errors are ignored)
984     try:
985         utils.move (changes_filename, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], base_changes_filename));
986     except:
987         sys.stderr.write("W: couldn't reject changes file '%s' [Got %s].\n" % (base_changes_filename, sys.exc_type));
988         pass;
989     for file in files.keys():
990         if os.path.exists(file):
991             try:
992                 utils.move (file, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], file));
993             except:
994                 sys.stderr.write("W: couldn't reject file '%s' [Got %s].\n" % (file, sys.exc_type));
995                 pass;
996
997     # If this is not a manual rejection generate the .reason file and rejection mail message
998     if manual_reject_mail_filename == "":
999         if os.path.exists(reject_filename):
1000             os.unlink(reject_filename);
1001         fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
1002         os.write(fd, reject_message);
1003         os.close(fd);
1004         Subst["__MANUAL_REJECT_MESSAGE__"] = "";
1005         reject_mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.rejected","r").read());
1006     else: # Have a manual rejection file to use
1007         reject_mail_message = ""; # avoid <undef>'s
1008         
1009     # Send the rejection mail if appropriate
1010     if not Cnf["Dinstall::Options::No-Mail"]:
1011         utils.send_mail (reject_mail_message, manual_reject_mail_filename);
1012
1013 ##################################################################
1014
1015 def manual_reject (changes_filename):
1016     global Subst;
1017     
1018     # Build up the rejection email 
1019     user_email_address = string.replace(string.split(pwd.getpwuid(os.getuid())[4],',')[0], '.', '')
1020     user_email_address = user_email_address + " <%s@%s>" % (pwd.getpwuid(os.getuid())[0], Cnf["Dinstall::MyHost"])
1021     manual_reject_message = Cnf.get("Dinstall::Options::Manual-Reject", "")
1022
1023     Subst["__MANUAL_REJECT_MESSAGE__"] = manual_reject_message;
1024     reject_mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.rejected","r").read());
1025     
1026     # Write the rejection email out as the <foo>.reason file
1027     reason_filename = re_changes.sub("reason", os.path.basename(changes_filename));
1028     reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename)
1029     if os.path.exists(reject_filename):
1030         os.unlink(reject_filename);
1031     fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
1032     os.write(fd, reject_mail_message);
1033     os.close(fd);
1034     
1035     # If we weren't given one, spawn an editor so the user can add one in
1036     if manual_reject_message == "":
1037         result = os.system("vi +6 %s" % (reject_file))
1038         if result != 0:
1039             sys.stderr.write ("vi invocation failed for `%s'!\n" % (reject_file))
1040             sys.exit(result)
1041
1042     # Then process it as if it were an automatic rejection
1043     reject (changes_filename, reject_filename)
1044
1045 #####################################################################################################################
1046  
1047 def acknowledge_new (changes_filename, summary):
1048     global new_ack_new, Subst;
1049
1050     changes_filename = os.path.basename(changes_filename);
1051
1052     new_ack_new[changes_filename] = 1;
1053
1054     if new_ack_old.has_key(changes_filename):
1055         print "Ack already sent.";
1056         return;
1057
1058     print "Sending new ack.";
1059     if not Cnf["Dinstall::Options::No-Mail"]:
1060         Subst["__SUMMARY__"] = summary;
1061         new_ack_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.new","r").read());
1062         utils.send_mail(new_ack_message,"");
1063
1064 #####################################################################################################################
1065
1066 def announce (short_summary, action):
1067     global Subst;
1068     
1069     # Only do announcements for source uploads with a recent dpkg-dev installed
1070     if float(changes.get("format", 0)) < 1.6 or not changes["architecture"].has_key("source"):
1071         return ""
1072
1073     lists_done = {}
1074     summary = ""
1075     Subst["__SHORT_SUMMARY__"] = short_summary;
1076
1077     for dist in changes["distribution"].keys():
1078         list = Cnf.Find("Suite::%s::Announce" % (dist))
1079         if list == "" or lists_done.has_key(list):
1080             continue
1081         lists_done[list] = 1
1082         summary = summary + "Announcing to %s\n" % (list)
1083
1084         if action:
1085             Subst["__ANNOUNCE_LIST_ADDRESS__"] = list;
1086             mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.announce","r").read());
1087             utils.send_mail (mail_message, "")
1088
1089     (dsc_rfc822, dsc_name, dsc_email) = utils.fix_maintainer (dsc.get("maintainer",Cnf["Dinstall::MyEmailAddress"]));
1090     bugs = changes["closes"].keys()
1091     bugs.sort()
1092     # changes["changedbyname"] == dsc_name is probably never true, but better
1093     # safe than sorry
1094     if dsc_name == changes["maintainername"] and (changes["changedbyname"] == "" or changes["changedbyname"] == dsc_name):
1095         summary = summary + "Closing bugs: "
1096         for bug in bugs:
1097             summary = summary + "%s " % (bug)
1098             if action:
1099                 Subst["__BUG_NUMBER__"] = bug;
1100                 if changes["distribution"].has_key("stable"):
1101                     Subst["__STABLE_WARNING__"] = """
1102 Note that this package is not part of the released stable Debian
1103 distribution.  It may have dependencies on other unreleased software,
1104 or other instabilities.  Please take care if you wish to install it.
1105 The update will eventually make its way into the next released Debian
1106 distribution."""
1107                 else:
1108                     Subst["__STABLE_WARNING__"] = "";
1109                 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.bug-close","r").read());
1110                 utils.send_mail (mail_message, "")
1111     else:                     # NMU
1112         summary = summary + "Setting bugs to severity fixed: "
1113         control_message = ""
1114         for bug in bugs:
1115             summary = summary + "%s " % (bug)
1116             control_message = control_message + "tag %s + fixed\n" % (bug)
1117         if action and control_message != "":
1118             Subst["__CONTROL_MESSAGE__"] = control_message;
1119             mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.bug-nmu-fixed","r").read());
1120             utils.send_mail (mail_message, "")
1121     summary = summary + "\n"
1122
1123     return summary
1124
1125 ###############################################################################
1126
1127 # reprocess is necessary for the case of foo_1.2-1 and foo_1.2-2 in
1128 # Incoming. -1 will reference the .orig.tar.gz, but -2 will not.
1129 # dsccheckdistrib() can find the .orig.tar.gz but it will not have
1130 # processed it during it's checks of -2.  If -1 has been deleted or
1131 # otherwise not checked by da-install, the .orig.tar.gz will not have
1132 # been checked at all.  To get round this, we force the .orig.tar.gz
1133 # into the .changes structure and reprocess the .changes file.
1134
1135 def process_it (changes_file):
1136     global reprocess, orig_tar_id, orig_tar_location, changes, dsc, dsc_files, files, reject_message;
1137
1138     # Reset some globals
1139     reprocess = 1;
1140     changes = {};
1141     dsc = {};
1142     dsc_files = {};
1143     files = {};
1144     orig_tar_id = None;
1145     orig_tar_location = "";
1146     legacy_source_untouchable = {};
1147     reject_message = "";
1148
1149     # Absolutize the filename to avoid the requirement of being in the
1150     # same directory as the .changes file.
1151     changes_file = os.path.abspath(changes_file);
1152
1153     # And since handling of installs to stable munges with the CWD;
1154     # save and restore it.
1155     cwd = os.getcwd();
1156     
1157     check_signature (changes_file);
1158     check_changes (changes_file);
1159     while reprocess:
1160         reprocess = 0;
1161         check_files ();
1162         check_md5sums ();
1163         check_dsc ();
1164         check_diff ();
1165         
1166     update_subst(changes_file);
1167     action(changes_file);
1168
1169     # Restore CWD
1170     os.chdir(cwd);
1171
1172 ###############################################################################
1173
1174 def main():
1175     global Cnf, projectB, install_bytes, new_ack_old, Subst
1176
1177     apt_pkg.init();
1178     
1179     Cnf = apt_pkg.newConfiguration();
1180     apt_pkg.ReadConfigFileISC(Cnf,utils.which_conf_file());
1181
1182     Arguments = [('a',"automatic","Dinstall::Options::Automatic"),
1183                  ('d',"debug","Dinstall::Options::Debug", "IntVal"),
1184                  ('h',"help","Dinstall::Options::Help"),
1185                  ('k',"ack-new","Dinstall::Options::Ack-New"),
1186                  ('m',"manual-reject","Dinstall::Options::Manual-Reject", "HasArg"),
1187                  ('n',"no-action","Dinstall::Options::No-Action"),
1188                  ('p',"no-lock", "Dinstall::Options::No-Lock"),
1189                  ('s',"no-mail", "Dinstall::Options::No-Mail"),
1190                  ('u',"override-distribution", "Dinstall::Options::Override-Distribution", "HasArg"),
1191                  ('v',"version","Dinstall::Options::Version")];
1192     
1193     changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
1194
1195     if Cnf["Dinstall::Options::Help"]:
1196         usage(0);
1197         
1198     if Cnf["Dinstall::Options::Version"]:
1199         print "katie version 0.0000000000";
1200         usage(0);
1201
1202     postgresql_user = None; # Default == Connect as user running program.
1203
1204     # -n/--dry-run invalidates some other options which would involve things happening
1205     if Cnf["Dinstall::Options::No-Action"]:
1206         Cnf["Dinstall::Options::Automatic"] = ""
1207         Cnf["Dinstall::Options::Ack-New"] = ""
1208         postgresql_user = Cnf["DB::ROUser"];
1209
1210     projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"]), None, None, postgresql_user);
1211
1212     db_access.init(Cnf, projectB);
1213
1214     # Check that we aren't going to clash with the daily cron job
1215
1216     if os.path.exists("%s/Archive_Maintenance_In_Progress" % (Cnf["Dir::RootDir"])) and not Cnf["Dinstall::Options::No-Lock"]:
1217         sys.stderr.write("Archive maintenance in progress.  Try again later.\n");
1218         sys.exit(2);
1219     
1220     # Obtain lock if not in no-action mode
1221
1222     if not Cnf["Dinstall::Options::No-Action"]:
1223         lock_fd = os.open(Cnf["Dinstall::LockFile"], os.O_RDWR);
1224         fcntl.lockf(lock_fd, FCNTL.F_TLOCK);
1225
1226     # Read in the list of already-acknowledged NEW packages
1227     new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'r');
1228     new_ack_old = {};
1229     for line in new_ack_list.readlines():
1230         new_ack_old[line[:-1]] = 1;
1231     new_ack_list.close();
1232
1233     # Initialize the substitution template mapping global
1234     Subst = {}
1235     Subst["__ADMIN_ADDRESS__"] = Cnf["Dinstall::MyAdminAddress"];
1236     Subst["__BUG_SERVER__"] = Cnf["Dinstall::BugServer"];
1237     bcc = "X-Katie: $Revision: 1.37 $"
1238     if Cnf.has_key("Dinstall::Bcc"):
1239         Subst["__BCC__"] = bcc + "\nBcc: %s" % (Cnf["Dinstall::Bcc"]);
1240     else:
1241         Subst["__BCC__"] = bcc;
1242     Subst["__DISTRO__"] = Cnf["Dinstall::MyDistribution"];
1243     Subst["__KATIE_ADDRESS__"] = Cnf["Dinstall::MyEmailAddress"];
1244
1245     # Process the changes files
1246     for changes_file in changes_files:
1247         print "\n" + changes_file;
1248         process_it (changes_file);
1249
1250     if install_count:
1251         sets = "set"
1252         if install_count > 1:
1253             sets = "sets"
1254         sys.stderr.write("Installed %d package %s, %s.\n" % (install_count, sets, utils.size_type(int(install_bytes))));
1255
1256     # Write out the list of already-acknowledged NEW packages
1257     if Cnf["Dinstall::Options::Ack-New"]:
1258         new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'w')
1259         for i in new_ack_new.keys():
1260             new_ack_list.write(i+'\n')
1261         new_ack_list.close()
1262     
1263             
1264 if __name__ == '__main__':
1265     main()
1266