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