]> git.decadent.org.uk Git - dak.git/blob - katie
sync
[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.34 2001-03-21 05:37:23 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     Subst["__CHANGES_FILENAME__"] = os.path.basename(changes_filename);
633     Subst["__FILE_CONTENTS__"] = changes.get("filecontents");
634     Subst["__MAINTAINER_ADDRESS__"] = changes["maintainer822"];
635     Subst["__MAINTAINER__"] = changes.get("maintainer");
636     Subst["__REJECT_MESSAGE__"] = reject_message;
637     Subst["__SOURCE__"] = changes.get("source");
638     Subst["__VERSION__"] = changes.get("version");
639
640 #####################################################################################################################
641
642 def action (changes_filename):
643     byhand = confirm = suites = summary = new = "";
644
645     # changes["distribution"] may not exist in corner cases
646     # (e.g. unreadable changes files)
647     if not changes.has_key("distribution"):
648         changes["distribution"] = {};
649     
650     for suite in changes["distribution"].keys():
651         if Cnf.has_key("Suite::%s::Confirm"):
652             confirm = confirm + suite + ", "
653         suites = suites + suite + ", "
654     confirm = confirm[:-2]
655     suites = suites[:-2]
656
657     for file in files.keys():
658         if files[file].has_key("byhand"):
659             byhand = 1
660             summary = summary + file + " byhand\n"
661         elif files[file].has_key("new"):
662             new = 1
663             summary = summary + "(new) %s %s %s\n" % (file, files[file]["priority"], files[file]["section"])
664             if files[file].has_key("othercomponents"):
665                 summary = summary + "WARNING: Already present in %s distribution.\n" % (files[file]["othercomponents"])
666             if files[file]["type"] == "deb":
667                 summary = summary + apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))["Description"] + '\n';
668         else:
669             files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"])
670             destination = Cnf["Dir::PoolRoot"] + files[file]["pool name"] + file
671             summary = summary + file + "\n  to " + destination + "\n"
672
673     short_summary = summary;
674
675     # This is for direport's benefit...
676     f = re_fdnic.sub("\n .\n", changes.get("changes",""));
677
678     if confirm or byhand or new:
679         summary = summary + "Changes: " + f;
680
681     summary = summary + announce (short_summary, 0)
682     
683     (prompt, answer) = ("", "XXX")
684     if Cnf["Dinstall::Options::No-Action"] or Cnf["Dinstall::Options::Automatic"]:
685         answer = 'S'
686
687     if string.find(reject_message, "Rejected") != -1:
688         if time.time()-os.path.getmtime(changes_filename) < 86400:
689             print "SKIP (too new)\n" + reject_message,;
690             prompt = "[S]kip, Manual reject, Quit ?";
691         else:
692             print "REJECT\n" + reject_message,;
693             prompt = "[R]eject, Manual reject, Skip, Quit ?";
694             if Cnf["Dinstall::Options::Automatic"]:
695                 answer = 'R';
696     elif new:
697         print "NEW to %s\n%s%s" % (suites, reject_message, summary),;
698         prompt = "[S]kip, New ack, Manual reject, Quit ?";
699         if Cnf["Dinstall::Options::Automatic"] and Cnf["Dinstall::Options::Ack-New"]:
700             answer = 'N';
701     elif byhand:
702         print "BYHAND\n" + reject_message + summary,;
703         prompt = "[I]nstall, Manual reject, Skip, Quit ?";
704     elif confirm:
705         print "CONFIRM to %s\n%s%s" % (confirm, reject_message, summary),
706         prompt = "[I]nstall, Manual reject, Skip, Quit ?";
707     else:
708         print "INSTALL\n" + reject_message + summary,;
709         prompt = "[I]nstall, Manual reject, Skip, Quit ?";
710         if Cnf["Dinstall::Options::Automatic"]:
711             answer = 'I';
712
713     while string.find(prompt, answer) == -1:
714         print prompt,;
715         answer = utils.our_raw_input()
716         m = re_default_answer.match(prompt)
717         if answer == "":
718             answer = m.group(1)
719         answer = string.upper(answer[:1])
720
721     if answer == 'R':
722         reject (changes_filename, "");
723     elif answer == 'M':
724         manual_reject (changes_filename);
725     elif answer == 'I':
726         install (changes_filename, summary, short_summary);
727     elif answer == 'N':
728         acknowledge_new (changes_filename, summary);
729     elif answer == 'Q':
730         sys.exit(0)
731
732 #####################################################################################################################
733
734 def install (changes_filename, summary, short_summary):
735     global install_count, install_bytes, Subst;
736
737     # Stable uploads are a special case
738     if changes.has_key("stable upload"):
739         stable_install (changes_filename, summary, short_summary);
740         return;
741     
742     print "Installing."
743
744     archive = utils.where_am_i();
745
746     # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
747     projectB.query("BEGIN WORK");
748
749     # Add the .dsc file to the DB
750     for file in files.keys():
751         if files[file]["type"] == "dsc":
752             package = dsc["source"]
753             version = dsc["version"]  # NB: not files[file]["version"], that has no epoch
754             maintainer = dsc["maintainer"]
755             maintainer = string.replace(maintainer, "'", "\\'")
756             maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
757             filename = files[file]["pool name"] + file;
758             dsc_location_id = files[file]["location id"];
759             if not files[file]["files id"]:
760                 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], dsc_location_id)
761             projectB.query("INSERT INTO source (source, version, maintainer, file) VALUES ('%s', '%s', %d, %d)"
762                            % (package, version, maintainer_id, files[file]["files id"]))
763             
764             for suite in changes["distribution"].keys():
765                 suite_id = db_access.get_suite_id(suite);
766                 projectB.query("INSERT INTO src_associations (suite, source) VALUES (%d, currval('source_id_seq'))" % (suite_id))
767
768             # Add the source files to the DB (files and dsc_files)
769             projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files[file]["files id"]));
770             for dsc_file in dsc_files.keys():
771                 filename = files[file]["pool name"] + dsc_file;
772                 # If the .orig.tar.gz is already in the pool, it's
773                 # files id is stored in dsc_files by check_dsc().
774                 files_id = dsc_files[dsc_file].get("files id", None);
775                 if files_id == None:
776                     files_id = db_access.get_files_id(filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], dsc_location_id);
777                 # FIXME: needs to check for -1/-2 and or handle exception
778                 if files_id == None:
779                     files_id = db_access.set_files_id (filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], dsc_location_id);
780                 projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files_id));
781             
782     # Add the .deb files to the DB
783     for file in files.keys():
784         if files[file]["type"] == "deb":
785             package = files[file]["package"]
786             version = files[file]["version"]
787             maintainer = files[file]["maintainer"]
788             maintainer = string.replace(maintainer, "'", "\\'")
789             maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
790             architecture = files[file]["architecture"]
791             architecture_id = db_access.get_architecture_id (architecture);
792             type = files[file]["dbtype"];
793             dsc_component = files[file]["component"]
794             source = files[file]["source"]
795             source_version = ""
796             if string.find(source, "(") != -1:
797                 m = utils.re_extract_src_version.match(source)
798                 source = m.group(1)
799                 source_version = m.group(2)
800             if not source_version:
801                 source_version = version
802             filename = files[file]["pool name"] + file;
803             if not files[file]["files id"]:
804                 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], files[file]["location id"])
805             source_id = db_access.get_source_id (source, source_version);
806             if source_id:
807                 projectB.query("INSERT INTO binaries (package, version, maintainer, source, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, %d, '%s')"
808                                % (package, version, maintainer_id, source_id, architecture_id, files[file]["files id"], type));
809             else:
810                 projectB.query("INSERT INTO binaries (package, version, maintainer, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, '%s')"
811                                % (package, version, maintainer_id, architecture_id, files[file]["files id"], type));
812             for suite in changes["distribution"].keys():
813                 suite_id = db_access.get_suite_id(suite);
814                 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES (%d, currval('binaries_id_seq'))" % (suite_id));
815
816     # If the .orig.tar.gz is in a legacy directory we need to poolify
817     # it, so that apt-get source (and anything else that goes by the
818     # "Directory:" field in the Sources.gz file) works.
819     if orig_tar_id != None and orig_tar_location == "legacy":
820         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));
821         qd = q.dictresult();
822         for qid in qd:
823             # Is this an old upload superseded by a newer -sa upload?  (See check_dsc() for details)
824             if legacy_source_untouchable.has_key(qid["files_id"]):
825                 continue;
826             # First move the files to the new location
827             legacy_filename = qid["path"]+qid["filename"];
828             pool_location = utils.poolify (changes["source"], files[file]["component"]);
829             pool_filename = pool_location + os.path.basename(qid["filename"]);
830             destination = Cnf["Dir::PoolDir"] + pool_location
831             utils.move(legacy_filename, destination);
832             # Then Update the DB's files table
833             q = projectB.query("UPDATE files SET filename = '%s', location = '%s' WHERE id = '%s'" % (pool_filename, dsc_location_id, qid["files_id"]));
834
835     # If this is a sourceful diff only upload that is moving non-legacy
836     # cross-component we need to copy the .orig.tar.gz into the new
837     # component too for the same reasons as above.
838     #
839     if changes["architecture"].has_key("source") and orig_tar_id != None and \
840        orig_tar_location != "legacy" and orig_tar_location != dsc_location_id:
841         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));
842         ql = q.getresult()[0];
843         old_filename = ql[0] + ql[1];
844         file_size = ql[2];
845         file_md5sum = ql[3];
846         new_filename = utils.poolify (changes["source"], dsc_component) + os.path.basename(old_filename);
847         new_files_id = db_access.get_files_id(new_filename, file_size, file_md5sum, dsc_location_id);
848         if new_files_id == None:
849             utils.copy(old_filename, Cnf["Dir::PoolDir"] + new_filename);
850             new_files_id = db_access.set_files_id(new_filename, file_size, file_md5sum, dsc_location_id);
851             projectB.query("UPDATE dsc_files SET file = %s WHERE source = %s AND file = %s" % (new_files_id, source_id, orig_tar_id));
852
853     # Install the files into the pool
854     for file in files.keys():
855         if files[file].has_key("byhand"):
856             continue
857         destination = Cnf["Dir::PoolDir"] + files[file]["pool name"] + file
858         destdir = os.path.dirname(destination)
859         utils.move (file, destination)
860         install_bytes = install_bytes + float(files[file]["size"])
861
862     # Copy the .changes file across for suite which need it.
863     for suite in changes["distribution"].keys():
864         if Cnf.has_key("Suite::%s::CopyChanges" % (suite)):
865             utils.copy (changes_filename, Cnf["Dir::RootDir"] + Cnf["Suite::%s::CopyChanges" % (suite)]);
866
867     projectB.query("COMMIT WORK");
868
869     try:
870         utils.move (changes_filename, Cnf["Dir::IncomingDir"] + 'DONE/' + os.path.basename(changes_filename))
871     except:
872         sys.stderr.write("W: couldn't move changes file '%s' to DONE directory [Got %s].\n" % (os.path.basename(changes_filename), sys.exc_type));
873
874     install_count = install_count + 1;
875
876     if not Cnf["Dinstall::Options::No-Mail"]:
877         Subst["__SUITE__"] = "";
878         Subst["__SUMMARY__"] = summary;
879         mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.installed","r").read());
880         utils.send_mail (mail_message, "")
881         announce (short_summary, 1)
882         check_override ();
883
884 #####################################################################################################################
885
886 def stable_install (changes_filename, summary, short_summary):
887     global install_count, install_bytes, Subst;
888     
889     print "Installing to stable."
890
891     archive = utils.where_am_i();
892
893     # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
894     projectB.query("BEGIN WORK");
895
896     # Add the .dsc file to the DB
897     for file in files.keys():
898         if files[file]["type"] == "dsc":
899             package = dsc["source"]
900             version = dsc["version"]  # NB: not files[file]["version"], that has no epoch
901             q = projectB.query("SELECT id FROM source WHERE source = '%s' AND version = '%s'" % (package, version))
902             ql = q.getresult()
903             if ql == []:
904                 sys.stderr.write("INTERNAL ERROR: couldn't find '%s' (%s) in source table.\n" % (package, version));
905                 sys.exit(1);
906             source_id = ql[0][0];
907             suite_id = db_access.get_suite_id('proposed-updates');
908             projectB.query("DELETE FROM src_associations WHERE suite = '%s' AND source = '%s'" % (suite_id, source_id));
909             suite_id = db_access.get_suite_id('stable');
910             projectB.query("INSERT INTO src_associations (suite, source) VALUES ('%s', '%s')" % (suite_id, source_id));
911                 
912     # Add the .deb files to the DB
913     for file in files.keys():
914         if files[file]["type"] == "deb":
915             package = files[file]["package"]
916             version = files[file]["version"]
917             architecture = files[file]["architecture"]
918             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))
919             ql = q.getresult()
920             if ql == []:
921                 sys.stderr.write("INTERNAL ERROR: couldn't find '%s' (%s for %s architecture) in binaries table.\n" % (package, version, architecture));
922                 sys.exit(1);
923             binary_id = ql[0][0];
924             suite_id = db_access.get_suite_id('proposed-updates');
925             projectB.query("DELETE FROM bin_associations WHERE suite = '%s' AND bin = '%s'" % (suite_id, binary_id));
926             suite_id = db_access.get_suite_id('stable');
927             projectB.query("INSERT INTO bin_associations (suite, bin) VALUES ('%s', '%s')" % (suite_id, binary_id));
928
929     projectB.query("COMMIT WORK");
930
931     utils.move (changes_filename, Cnf["Rhona::Morgue"] + os.path.basename(changes_filename));
932
933     # Update the Stable ChangeLog file
934
935     new_changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + ".ChangeLog";
936     changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + "ChangeLog";
937     if os.path.exists(new_changelog_filename):
938         os.unlink (new_changelog_filename);
939     
940     new_changelog = utils.open_file(new_changelog_filename, 'w');
941     for file in files.keys():
942         if files[file]["type"] == "deb":
943             new_changelog.write("stable/%s/binary-%s/%s\n" % (files[file]["component"], files[file]["architecture"], file));
944         elif utils.re_issource.match(file) != None:
945             new_changelog.write("stable/%s/source/%s\n" % (files[file]["component"], file));
946         else:
947             new_changelog.write("%s\n" % (file));
948     chop_changes = re_fdnic.sub("\n", changes["changes"]);
949     new_changelog.write(chop_changes + '\n\n');
950     if os.access(changelog_filename, os.R_OK) != 0:
951         changelog = utils.open_file(changelog_filename, 'r');
952         new_changelog.write(changelog.read());
953     new_changelog.close();
954     if os.access(changelog_filename, os.R_OK) != 0:
955         os.unlink(changelog_filename);
956     utils.move(new_changelog_filename, changelog_filename);
957
958     install_count = install_count + 1;
959
960     if not Cnf["Dinstall::Options::No-Mail"]:
961         Subst["__SUITE__"] = "into stable";
962         Subst["__SUMMARY__"] = summary;
963         utils.send_mail (mail_message, "")
964         announce (short_summary, 1)
965
966 #####################################################################################################################
967
968 def reject (changes_filename, manual_reject_mail_filename):
969     global Subst;
970     
971     print "Rejecting.\n"
972
973     base_changes_filename = os.path.basename(changes_filename);
974     reason_filename = re_changes.sub("reason", base_changes_filename);
975     reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename);
976
977     # Move the .changes files and it's contents into REJECT/ (if we can; errors are ignored)
978     try:
979         utils.move (changes_filename, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], base_changes_filename));
980     except:
981         sys.stderr.write("W: couldn't reject changes file '%s' [Got %s].\n" % (base_changes_filename, sys.exc_type));
982         pass;
983     for file in files.keys():
984         if os.path.exists(file):
985             try:
986                 utils.move (file, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], file));
987             except:
988                 sys.stderr.write("W: couldn't reject file '%s' [Got %s].\n" % (file, sys.exc_type));
989                 pass;
990
991     # If this is not a manual rejection generate the .reason file and rejection mail message
992     if manual_reject_mail_filename == "":
993         if os.path.exists(reject_filename):
994             os.unlink(reject_filename);
995         fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
996         os.write(fd, reject_message);
997         os.close(fd);
998         Subst["__MANUAL_REJECT_MESSAGE__"] = "";
999         reject_mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.rejected","r").read());
1000     else: # Have a manual rejection file to use
1001         reject_mail_message = ""; # avoid <undef>'s
1002         
1003     # Send the rejection mail if appropriate
1004     if not Cnf["Dinstall::Options::No-Mail"]:
1005         utils.send_mail (reject_mail_message, manual_reject_mail_filename);
1006
1007 ##################################################################
1008
1009 def manual_reject (changes_filename):
1010     global Subst;
1011     
1012     # Build up the rejection email 
1013     user_email_address = string.replace(string.split(pwd.getpwuid(os.getuid())[4],',')[0], '.', '')
1014     user_email_address = user_email_address + " <%s@%s>" % (pwd.getpwuid(os.getuid())[0], Cnf["Dinstall::MyHost"])
1015     manual_reject_message = Cnf.get("Dinstall::Options::Manual-Reject", "")
1016
1017     Subst["__MANUAL_REJECT_MESSAGE__"] = manual_reject_message;
1018     reject_mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.rejected","r").read());
1019     
1020     # Write the rejection email out as the <foo>.reason file
1021     reason_filename = re_changes.sub("reason", os.path.basename(changes_filename));
1022     reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename)
1023     if os.path.exists(reject_filename):
1024         os.unlink(reject_filename);
1025     fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
1026     os.write(fd, reject_mail_message);
1027     os.close(fd);
1028     
1029     # If we weren't given one, spawn an editor so the user can add one in
1030     if manual_reject_message == "":
1031         result = os.system("vi +6 %s" % (reject_file))
1032         if result != 0:
1033             sys.stderr.write ("vi invocation failed for `%s'!\n" % (reject_file))
1034             sys.exit(result)
1035
1036     # Then process it as if it were an automatic rejection
1037     reject (changes_filename, reject_filename)
1038
1039 #####################################################################################################################
1040  
1041 def acknowledge_new (changes_filename, summary):
1042     global new_ack_new, Subst;
1043
1044     changes_filename = os.path.basename(changes_filename);
1045
1046     new_ack_new[changes_filename] = 1;
1047
1048     if new_ack_old.has_key(changes_filename):
1049         print "Ack already sent.";
1050         return;
1051
1052     print "Sending new ack.";
1053     if not Cnf["Dinstall::Options::No-Mail"]:
1054         Subst["__SUMMARY__"] = summary;
1055         new_ack_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.new","r").read());
1056         utils.send_mail(new_ack_message,"");
1057
1058 #####################################################################################################################
1059
1060 def announce (short_summary, action):
1061     global Subst;
1062     
1063     # Only do announcements for source uploads with a recent dpkg-dev installed
1064     if float(changes.get("format", 0)) < 1.6 or not changes["architecture"].has_key("source"):
1065         return ""
1066
1067     lists_done = {}
1068     summary = ""
1069     Subst["__SHORT_SUMMARY__"] = short_summary;
1070
1071     for dist in changes["distribution"].keys():
1072         list = Cnf.Find("Suite::%s::Announce" % (dist))
1073         if list == "" or lists_done.has_key(list):
1074             continue
1075         lists_done[list] = 1
1076         summary = summary + "Announcing to %s\n" % (list)
1077
1078         if action:
1079             Subst["__ANNOUNCE_LIST_ADDRESS__"] = list;
1080             mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.announce","r").read());
1081             utils.send_mail (mail_message, "")
1082
1083     (dsc_rfc822, dsc_name, dsc_email) = utils.fix_maintainer (dsc.get("maintainer",Cnf["Dinstall::MyEmailAddress"]));
1084     bugs = changes["closes"].keys()
1085     bugs.sort()
1086     # changes["changedbyname"] == dsc_name is probably never true, but better
1087     # safe than sorry
1088     if dsc_name == changes["maintainername"] and (changes["changedbyname"] == "" or changes["changedbyname"] == dsc_name):
1089         summary = summary + "Closing bugs: "
1090         for bug in bugs:
1091             summary = summary + "%s " % (bug)
1092             if action:
1093                 Subst["__BUG_NUMBER__"] = bug;
1094                 if changes["distribution"].has_key("stable"):
1095                     Subst["__STABLE_WARNING__"] = """
1096 Note that this package is not part of the released stable Debian
1097 distribution.  It may have dependencies on other unreleased software,
1098 or other instabilities.  Please take care if you wish to install it.
1099 The update will eventually make its way into the next released Debian
1100 distribution."""
1101                 else:
1102                     Subst["__STABLE_WARNING__"] = "";
1103                 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.bug-close","r").read());
1104                 utils.send_mail (mail_message, "")
1105     else:                     # NMU
1106         summary = summary + "Setting bugs to severity fixed: "
1107         control_message = ""
1108         for bug in bugs:
1109             summary = summary + "%s " % (bug)
1110             control_message = control_message + "tag %s + fixed\n" % (bug)
1111         if action and control_message != "":
1112             Subst["__CONTROL_MESSAGE__"] = control_message;
1113             mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.bug-nmu-fixed","r").read());
1114             utils.send_mail (mail_message, "")
1115     summary = summary + "\n"
1116
1117     return summary
1118
1119 ###############################################################################
1120
1121 # reprocess is necessary for the case of foo_1.2-1 and foo_1.2-2 in
1122 # Incoming. -1 will reference the .orig.tar.gz, but -2 will not.
1123 # dsccheckdistrib() can find the .orig.tar.gz but it will not have
1124 # processed it during it's checks of -2.  If -1 has been deleted or
1125 # otherwise not checked by da-install, the .orig.tar.gz will not have
1126 # been checked at all.  To get round this, we force the .orig.tar.gz
1127 # into the .changes structure and reprocess the .changes file.
1128
1129 def process_it (changes_file):
1130     global reprocess, orig_tar_id, orig_tar_location, changes, dsc, dsc_files, files, reject_message;
1131
1132     # Reset some globals
1133     reprocess = 1;
1134     changes = {};
1135     dsc = {};
1136     dsc_files = {};
1137     files = {};
1138     orig_tar_id = None;
1139     orig_tar_location = "";
1140     legacy_source_untouchable = {};
1141     reject_message = "";
1142
1143     # Absolutize the filename to avoid the requirement of being in the
1144     # same directory as the .changes file.
1145     changes_file = os.path.abspath(changes_file);
1146
1147     # And since handling of installs to stable munges with the CWD;
1148     # save and restore it.
1149     cwd = os.getcwd();
1150     
1151     check_signature (changes_file);
1152     check_changes (changes_file);
1153     while reprocess:
1154         reprocess = 0;
1155         check_files ();
1156         check_md5sums ();
1157         check_dsc ();
1158         check_diff ();
1159         
1160     update_subst(changes_file);
1161     action(changes_file);
1162
1163     # Restore CWD
1164     os.chdir(cwd);
1165
1166 ###############################################################################
1167
1168 def main():
1169     global Cnf, projectB, install_bytes, new_ack_old, Subst
1170
1171     apt_pkg.init();
1172     
1173     Cnf = apt_pkg.newConfiguration();
1174     apt_pkg.ReadConfigFileISC(Cnf,utils.which_conf_file());
1175
1176     Arguments = [('a',"automatic","Dinstall::Options::Automatic"),
1177                  ('d',"debug","Dinstall::Options::Debug", "IntVal"),
1178                  ('h',"help","Dinstall::Options::Help"),
1179                  ('k',"ack-new","Dinstall::Options::Ack-New"),
1180                  ('m',"manual-reject","Dinstall::Options::Manual-Reject", "HasArg"),
1181                  ('n',"no-action","Dinstall::Options::No-Action"),
1182                  ('p',"no-lock", "Dinstall::Options::No-Lock"),
1183                  ('s',"no-mail", "Dinstall::Options::No-Mail"),
1184                  ('u',"override-distribution", "Dinstall::Options::Override-Distribution", "HasArg"),
1185                  ('v',"version","Dinstall::Options::Version")];
1186     
1187     changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
1188
1189     if Cnf["Dinstall::Options::Help"]:
1190         usage(0);
1191         
1192     if Cnf["Dinstall::Options::Version"]:
1193         print "katie version 0.0000000000";
1194         usage(0);
1195
1196     postgresql_user = None; # Default == Connect as user running program.
1197
1198     # -n/--dry-run invalidates some other options which would involve things happening
1199     if Cnf["Dinstall::Options::No-Action"]:
1200         Cnf["Dinstall::Options::Automatic"] = ""
1201         Cnf["Dinstall::Options::Ack-New"] = ""
1202         postgresql_user = Cnf["DB::ROUser"];
1203
1204     projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"]), None, None, postgresql_user);
1205
1206     db_access.init(Cnf, projectB);
1207
1208     # Check that we aren't going to clash with the daily cron job
1209
1210     if os.path.exists("%s/Archive_Maintenance_In_Progress" % (Cnf["Dir::RootDir"])) and not Cnf["Dinstall::Options::No-Lock"]:
1211         sys.stderr.write("Archive maintenance in progress.  Try again later.\n");
1212         sys.exit(2);
1213     
1214     # Obtain lock if not in no-action mode
1215
1216     if not Cnf["Dinstall::Options::No-Action"]:
1217         lock_fd = os.open(Cnf["Dinstall::LockFile"], os.O_RDWR);
1218         fcntl.lockf(lock_fd, FCNTL.F_TLOCK);
1219
1220     # Read in the list of already-acknowledged NEW packages
1221     new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'r');
1222     new_ack_old = {};
1223     for line in new_ack_list.readlines():
1224         new_ack_old[line[:-1]] = 1;
1225     new_ack_list.close();
1226
1227     # Initialize the substitution template mapping global
1228     Subst = {}
1229     Subst["__ADMIN_ADDRESS__"] = Cnf["Dinstall::MyAdminAddress"];
1230     Subst["__BUG_SERVER__"] = Cnf["Dinstall::BugServer"];
1231     bcc = "X-Katie: $Revision: 1.34 $"
1232     if Cnf.has_key("Dinstall::Bcc"):
1233         Subst["__BCC__"] = bcc + "\nBcc: %s" % (Cnf["Dinstall::Bcc"]);
1234     else:
1235         Subst["__BCC__"] = bcc;
1236     Subst["__DISTRO__"] = Cnf["Dinstall::MyDistribution"];
1237     Subst["__KATIE_ADDRESS__"] = Cnf["Dinstall::MyEmailAddress"];
1238
1239     # Process the changes files
1240     for changes_file in changes_files:
1241         print "\n" + changes_file;
1242         process_it (changes_file);
1243
1244     if install_count:
1245         sets = "set"
1246         if install_count > 1:
1247             sets = "sets"
1248         sys.stderr.write("Installed %d package %s, %s.\n" % (install_count, sets, utils.size_type(int(install_bytes))));
1249
1250     # Write out the list of already-acknowledged NEW packages
1251     if Cnf["Dinstall::Options::Ack-New"]:
1252         new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'w')
1253         for i in new_ack_new.keys():
1254             new_ack_list.write(i+'\n')
1255         new_ack_list.close()
1256     
1257             
1258 if __name__ == '__main__':
1259     main()
1260