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