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