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