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