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