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