]> git.decadent.org.uk Git - dak.git/blob - katie
typo
[dak.git] / katie
1 #!/usr/bin/env python
2
3 # Installs Debian packages
4 # Copyright (C) 2000, 2001  James Troup <james@nocrew.org>
5 # $Id: katie,v 1.65 2001-11-19 02:02:53 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.65 $";
74
75 ###############################################################################
76
77 def init():
78     global Cnf, Options;
79
80     Cnf = utils.get_conf()
81
82     Arguments = [('a',"automatic","Dinstall::Options::Automatic"),
83                  ('h',"help","Dinstall::Options::Help"),
84                  ('k',"ack-new","Dinstall::Options::Ack-New"),
85                  ('m',"manual-reject","Dinstall::Options::Manual-Reject", "HasArg"),
86                  ('n',"no-action","Dinstall::Options::No-Action"),
87                  ('p',"no-lock", "Dinstall::Options::No-Lock"),
88                  ('s',"no-mail", "Dinstall::Options::No-Mail"),
89                  ('u',"override-distribution", "Dinstall::Options::Override-Distribution", "HasArg"),
90                  ('V',"version","Dinstall::Options::Version")];
91
92     for i in ["automatic", "help", "ack-new", "manual-reject", "no-action",
93               "no-lock", "no-mail", "override-distribution", "version"]:
94         if not Cnf.has_key("Dinstall::Options::%s" % (i)):
95             Cnf["Dinstall::Options::%s" % (i)] = "";
96
97     changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
98     Options = Cnf.SubTree("Dinstall::Options")
99
100     return changes_files;
101
102 #########################################################################################
103
104 def usage (exit_code=0):
105     print """Usage: dinstall [OPTION]... [CHANGES]...
106   -a, --automatic           automatic run
107   -h, --help                show this help and exit.
108   -k, --ack-new             acknowledge new packages !! for cron.daily only !!
109   -m, --manual-reject=MSG   manual reject with `msg'
110   -n, --no-action           don't do anything
111   -p, --no-lock             don't check lockfile !! for cron.daily only !!
112   -s, --no-mail             don't send any mail
113   -u, --distribution=DIST   override distribution to `dist'
114   -V, --version             display the version number and exit"""
115     sys.exit(exit_code)
116
117 #########################################################################################
118
119 def check_signature (filename):
120     global reject_message
121
122     (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))
123     if (result != 0):
124         reject_message = reject_message + "Rejected: GPG signature check failed on `%s'.\n%s\n" % (os.path.basename(filename), output)
125         return 0
126     return 1
127
128 ######################################################################################################
129
130 class nmu_p:
131     # Read in the group maintainer override file
132     def __init__ (self):
133         self.group_maint = {};
134         if Cnf.get("Dinstall::GroupOverrideFilename"):
135             filename = Cnf["Dir::OverrideDir"] + Cnf["Dinstall::GroupOverrideFilename"];
136             file = utils.open_file(filename);
137             for line in file.readlines():
138                 line = string.strip(utils.re_comments.sub('', line));
139                 if line != "":
140                     self.group_maint[line] = 1;
141             file.close();
142
143     def is_an_nmu (self, changes, dsc):
144         (dsc_rfc822, dsc_name, dsc_email) = utils.fix_maintainer (dsc.get("maintainer",Cnf["Dinstall::MyEmailAddress"]));
145         # changes["changedbyname"] == dsc_name is probably never true, but better safe than sorry
146         if dsc_name == changes["maintainername"] and (changes["changedby822"] == "" or changes["changedbyname"] == dsc_name):
147             return 0;
148
149         if dsc.has_key("uploaders"):
150             uploaders = string.split(dsc["uploaders"], ",");
151             uploadernames = {};
152             for i in uploaders:
153                 (rfc822, name, email) = utils.fix_maintainer (string.strip(i));
154                 uploadernames[name] = "";
155             if uploadernames.has_key(changes["changedbyname"]):
156                 return 0;
157
158         # Some group maintained packages (e.g. Debian QA) are never NMU's
159         if self.group_maint.has_key(changes["maintaineremail"]):
160             return 0;
161
162         return 1;
163
164 ######################################################################################################
165
166 # Ensure that source exists somewhere in the archive for the binary
167 # upload being processed.
168 #
169 # (1) exact match                      => 1.0-3
170 # (2) Bin-only NMU of an MU            => 1.0-3.0.1
171 # (3) Bin-only NMU of a sourceful-NMU  => 1.0-3.1.1
172
173 def source_exists (package, source_version):
174     q = projectB.query("SELECT s.version FROM source s WHERE s.source = '%s'" % (package));
175
176     # Reduce the query results to a list of version numbers
177     ql = map(lambda x: x[0], q.getresult());
178
179     # Try (1)
180     if ql.count(source_version):
181         return 1;
182
183     # Try (2)
184     orig_source_version = re_bin_only_nmu_of_mu.sub('', source_version);
185     if ql.count(orig_source_version):
186         return 1;
187
188     # Try (3)
189     orig_source_version = re_bin_only_nmu_of_nmu.sub('', source_version);
190     if ql.count(orig_source_version):
191         return 1;
192
193     # No source found...
194     return 0;
195
196 ######################################################################################################
197
198 # See if a given package is in the override table
199
200 def in_override_p (package, component, suite, binary_type, file):
201     global files;
202
203     if binary_type == "": # must be source
204         type = "dsc";
205     else:
206         type = binary_type;
207
208     # Override suite name; used for example with proposed-updates
209     if Cnf.Find("Suite::%s::OverrideSuite" % (suite)) != "":
210         suite = Cnf["Suite::%s::OverrideSuite" % (suite)];
211
212     # Avoid <undef> on unknown distributions
213     suite_id = db_access.get_suite_id(suite);
214     if suite_id == -1:
215         return None;
216     component_id = db_access.get_component_id(component);
217     type_id = db_access.get_override_type_id(type);
218
219     # FIXME: nasty non-US speficic hack
220     if string.lower(component[:7]) == "non-us/":
221         component = component[7:];
222
223     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"
224                        % (package, suite_id, component_id, type_id));
225     result = q.getresult();
226     # If checking for a source package fall back on the binary override type
227     if type == "dsc" and not result:
228         type_id = db_access.get_override_type_id("deb");
229         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"
230                            % (package, suite_id, component_id, type_id));
231         result = q.getresult();
232
233     # Remember the section and priority so we can check them later if appropriate
234     if result != []:
235         files[file]["override section"] = result[0][0];
236         files[file]["override priority"] = result[0][1];
237
238     return result;
239
240 #####################################################################################################################
241
242 def check_changes(filename):
243     global reject_message, changes, files
244
245     # Default in case we bail out
246     changes["maintainer822"] = Cnf["Dinstall::MyEmailAddress"];
247     changes["changedby822"] = Cnf["Dinstall::MyEmailAddress"];
248     changes["architecture"] = {};
249
250     # Parse the .changes field into a dictionary
251     try:
252         changes = utils.parse_changes(filename, 0)
253     except utils.cant_open_exc:
254         reject_message = reject_message + "Rejected: can't read changes file '%s'.\n" % (filename)
255         return 0;
256     except utils.changes_parse_error_exc, line:
257         reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line)
258         return 0;
259
260     # Parse the Files field from the .changes into another dictionary
261     try:
262         files = utils.build_file_list(changes, "");
263     except utils.changes_parse_error_exc, line:
264         reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line);
265     except utils.nk_format_exc, format:
266         reject_message = reject_message + "Rejected: unknown format '%s' of changes file '%s'.\n" % (format, filename);
267         return 0;
268
269     # Check for mandatory fields
270     for i in ("source", "binary", "architecture", "version", "distribution", "maintainer", "files"):
271         if not changes.has_key(i):
272             reject_message = reject_message + "Rejected: Missing field `%s' in changes file.\n" % (i)
273             return 0    # Avoid <undef> errors during later tests
274
275     # Override the Distribution: field if appropriate
276     if Options["Override-Distribution"] != "":
277         reject_message = reject_message + "Warning: Distribution was overriden from %s to %s.\n" % (changes["distribution"], Options["Override-Distribution"])
278         changes["distribution"] = Options["Override-Distribution"]
279
280     # Split multi-value fields into a lower-level dictionary
281     for i in ("architecture", "distribution", "binary", "closes"):
282         o = changes.get(i, "")
283         if o != "":
284             del changes[i]
285         changes[i] = {}
286         for j in string.split(o):
287             changes[i][j] = 1
288
289     # Fix the Maintainer: field to be RFC822 compatible
290     (changes["maintainer822"], changes["maintainername"], changes["maintaineremail"]) = utils.fix_maintainer (changes["maintainer"])
291
292     # Fix the Changed-By: field to be RFC822 compatible; if it exists.
293     (changes["changedby822"], changes["changedbyname"], changes["changedbyemail"]) = utils.fix_maintainer(changes.get("changed-by",""));
294
295     # Ensure all the values in Closes: are numbers
296     if changes.has_key("closes"):
297         for i in changes["closes"].keys():
298             if re_isanum.match (i) == None:
299                 reject_message = reject_message + "Rejected: `%s' from Closes field isn't a number.\n" % (i)
300
301     # Ensure there _is_ a target distribution
302     if not changes["distribution"].keys():
303         reject_message = reject_message + "Rejected: huh? Distribution field is empty in changes file.\n";
304
305     # Map frozen to unstable if frozen doesn't exist
306     if changes["distribution"].has_key("frozen") and not Cnf.has_key("Suite::Frozen"):
307         del changes["distribution"]["frozen"]
308         changes["distribution"]["unstable"] = 1;
309         reject_message = reject_message + "Mapping frozen to unstable.\n"
310
311     # Map testing to unstable
312     if changes["distribution"].has_key("testing"):
313         if len(changes["distribution"].keys()) > 1:
314             del changes["distribution"]["testing"];
315             reject_message = reject_message + "Warning: Ignoring testing as a target suite.\n";
316         else:
317             reject_message = reject_message + "Rejected: invalid distribution 'testing'.\n";
318
319     # Ensure target distributions exist
320     for i in changes["distribution"].keys():
321         if not Cnf.has_key("Suite::%s" % (i)):
322             reject_message = reject_message + "Rejected: Unknown distribution `%s'.\n" % (i)
323
324     # Map unreleased arches from stable to unstable
325     if changes["distribution"].has_key("stable"):
326         for i in changes["architecture"].keys():
327             if not Cnf.has_key("Suite::Stable::Architectures::%s" % (i)):
328                 reject_message = reject_message + "Mapping stable to unstable for unreleased arch `%s'.\n" % (i)
329                 del changes["distribution"]["stable"]
330                 changes["distribution"]["unstable"] = 1;
331
332     # Map arches not being released from frozen to unstable
333     if changes["distribution"].has_key("frozen"):
334         for i in changes["architecture"].keys():
335             if not Cnf.has_key("Suite::Frozen::Architectures::%s" % (i)):
336                 reject_message = reject_message + "Mapping frozen to unstable for non-releasing arch `%s'.\n" % (i)
337                 del changes["distribution"]["frozen"]
338                 changes["distribution"]["unstable"] = 1;
339
340     # Handle uploads to stable
341     if changes["distribution"].has_key("stable"):
342         # If running from within proposed-updates; assume an install to stable
343         if string.find(os.getcwd(), 'proposed-updates') != -1:
344             # Remove non-stable target distributions
345             for dist in changes["distribution"].keys():
346                 if dist != "stable":
347                     reject_message = reject_message + "Removing %s from distribution list.\n" % (dist);
348                     del changes["distribution"][dist];
349             changes["stable install"] = 1;
350             # If we can't find a file from the .changes; assume it's a package already in the pool and move into the pool
351             file = files.keys()[0];
352             if os.access(file, os.R_OK) == 0:
353                 pool_dir = Cnf["Dir::PoolDir"] + '/' + utils.poolify(changes["source"], files[file]["component"]);
354                 changes["installing from the pool"] = 1;
355                 os.chdir(pool_dir);
356         # Otherwise (normal case) map stable to updates
357         else:
358             reject_message = reject_message + "Mapping stable to updates.\n";
359             del changes["distribution"]["stable"];
360             changes["distribution"]["proposed-updates"] = 1;
361
362     # chopversion = no epoch; chopversion2 = no epoch and no revision (e.g. for .orig.tar.gz comparison)
363     changes["chopversion"] = utils.re_no_epoch.sub('', changes["version"])
364     changes["chopversion2"] = utils.re_no_revision.sub('', changes["chopversion"])
365
366     if string.find(reject_message, "Rejected:") != -1:
367         return 0
368     else:
369         return 1
370
371 def check_files():
372     global reject_message
373
374     archive = utils.where_am_i();
375
376     for file in files.keys():
377         # Check the file is readable
378         if os.access(file,os.R_OK) == 0:
379             if os.path.exists(file):
380                 reject_message = reject_message + "Rejected: Can't read `%s'. [permission denied]\n" % (file)
381             else:
382                 reject_message = reject_message + "Rejected: Can't read `%s'. [file not found]\n" % (file)
383
384             files[file]["type"] = "unreadable";
385             continue
386         # If it's byhand skip remaining checks
387         if files[file]["section"] == "byhand":
388             files[file]["byhand"] = 1;
389             files[file]["type"] = "byhand";
390         # Checks for a binary package...
391         elif utils.re_isadeb.match(file) != None:
392             files[file]["type"] = "deb";
393
394             # Extract package information using dpkg-deb
395             try:
396                 control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file)));
397             except:
398                 reject_message = reject_message + "Rejected: %s: debExtractControl() raised %s.\n" % (file, sys.exc_type);
399                 # Can't continue, none of the checks on control would work.
400                 continue;
401
402             # Check for mandatory fields
403             if control.Find("Package") == None:
404                 reject_message = reject_message + "Rejected: %s: No package field in control.\n" % (file)
405             if control.Find("Architecture") == None:
406                 reject_message = reject_message + "Rejected: %s: No architecture field in control.\n" % (file)
407             if control.Find("Version") == None:
408                 reject_message = reject_message + "Rejected: %s: No version field in control.\n" % (file)
409
410             # Ensure the package name matches the one give in the .changes
411             if not changes["binary"].has_key(control.Find("Package", "")):
412                 reject_message = reject_message + "Rejected: %s: control file lists name as `%s', which isn't in changes file.\n" % (file, control.Find("Package", ""))
413
414             # Validate the architecture
415             if not Cnf.has_key("Suite::Unstable::Architectures::%s" % (control.Find("Architecture", ""))):
416                 reject_message = reject_message + "Rejected: Unknown architecture '%s'.\n" % (control.Find("Architecture", ""))
417
418             # Check the architecture matches the one given in the .changes
419             if not changes["architecture"].has_key(control.Find("Architecture", "")):
420                 reject_message = reject_message + "Rejected: %s: control file lists arch as `%s', which isn't in changes file.\n" % (file, control.Find("Architecture", ""))
421             # Check the section & priority match those given in the .changes (non-fatal)
422             if control.Find("Section") != None and files[file]["section"] != "" and files[file]["section"] != control.Find("Section"):
423                 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"])
424             if control.Find("Priority") != None and files[file]["priority"] != "" and files[file]["priority"] != control.Find("Priority"):
425                 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"])
426
427             epochless_version = utils.re_no_epoch.sub('', control.Find("Version", ""))
428
429             files[file]["package"] = control.Find("Package");
430             files[file]["architecture"] = control.Find("Architecture");
431             files[file]["version"] = control.Find("Version");
432             files[file]["maintainer"] = control.Find("Maintainer", "");
433             if file[-5:] == ".udeb":
434                 files[file]["dbtype"] = "udeb";
435             elif file[-4:] == ".deb":
436                 files[file]["dbtype"] = "deb";
437             else:
438                 reject_message = reject_message + "Rejected: %s is neither a .deb or a .udeb.\n " % (file);
439             files[file]["fullname"] = "%s_%s_%s.deb" % (control.Find("Package", ""), epochless_version, control.Find("Architecture", ""))
440             files[file]["source"] = control.Find("Source", "");
441             if files[file]["source"] == "":
442                 files[file]["source"] = files[file]["package"];
443             # Get the source version
444             source = files[file]["source"];
445             source_version = ""
446             if string.find(source, "(") != -1:
447                 m = utils.re_extract_src_version.match(source)
448                 source = m.group(1)
449                 source_version = m.group(2)
450             if not source_version:
451                 source_version = files[file]["version"];
452             files[file]["source package"] = source;
453             files[file]["source version"] = source_version;
454
455         # Checks for a source package...
456         else:
457             m = utils.re_issource.match(file)
458             if m != None:
459                 files[file]["package"] = m.group(1)
460                 files[file]["version"] = m.group(2)
461                 files[file]["type"] = m.group(3)
462
463                 # Ensure the source package name matches the Source filed in the .changes
464                 if changes["source"] != files[file]["package"]:
465                     reject_message = reject_message + "Rejected: %s: changes file doesn't say %s for Source\n" % (file, files[file]["package"])
466
467                 # Ensure the source version matches the version in the .changes file
468                 if files[file]["type"] == "orig.tar.gz":
469                     changes_version = changes["chopversion2"]
470                 else:
471                     changes_version = changes["chopversion"]
472                 if changes_version != files[file]["version"]:
473                     reject_message = reject_message + "Rejected: %s: should be %s according to changes file.\n" % (file, changes_version)
474
475                 # Ensure the .changes lists source in the Architecture field
476                 if not changes["architecture"].has_key("source"):
477                     reject_message = reject_message + "Rejected: %s: changes file doesn't list `source' in Architecture field.\n" % (file)
478
479                 # Check the signature of a .dsc file
480                 if files[file]["type"] == "dsc":
481                     check_signature(file)
482
483                 files[file]["fullname"] = file
484                 files[file]["architecture"] = "source";
485
486             # Not a binary or source package?  Assume byhand...
487             else:
488                 files[file]["byhand"] = 1;
489                 files[file]["type"] = "byhand";
490
491         files[file]["oldfiles"] = {}
492         for suite in changes["distribution"].keys():
493             # Skip byhand
494             if files[file].has_key("byhand"):
495                 continue
496
497             if Cnf.has_key("Suite:%s::Components" % (suite)) and not Cnf.has_key("Suite::%s::Components::%s" % (suite, files[file]["component"])):
498                 reject_message = reject_message + "Rejected: unknown component `%s' for suite `%s'.\n" % (files[file]["component"], suite)
499                 continue
500
501             # See if the package is NEW
502             if not in_override_p(files[file]["package"], files[file]["component"], suite, files[file].get("dbtype",""), file):
503                 files[file]["new"] = 1
504
505             if files[file]["type"] == "deb":
506                 # Find any old binary packages
507                 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"
508                                    % (files[file]["package"], suite, files[file]["architecture"]))
509                 oldfiles = q.dictresult()
510                 for oldfile in oldfiles:
511                     files[file]["oldfiles"][suite] = oldfile
512                     # Check versions [NB: per-suite only; no cross-suite checking done (yet)]
513                     if apt_pkg.VersionCompare(files[file]["version"], oldfile["version"]) != 1:
514                         reject_message = reject_message + "Rejected: %s Old version `%s' >= new version `%s'.\n" % (file, oldfile["version"], files[file]["version"])
515                 # Check for existing copies of the file
516                 if not changes.has_key("stable install"):
517                     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"]))
518                     if q.getresult() != []:
519                         reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (file)
520
521                 # Check for existent source
522                 # FIXME: this is no longer per suite
523                 if changes["architecture"].has_key("source"):
524                     source_version = files[file]["source version"];
525                     if source_version != changes["version"]:
526                         reject_message = reject_message + "Rejected: source version (%s) for %s doesn't match changes version %s.\n" % (files[file]["source version"], file, changes["version"]);
527                 else:
528                     if not source_exists (files[file]["source package"], source_version):
529                         reject_message = reject_message + "Rejected: no source found for %s %s (%s).\n" % (files[file]["source package"], source_version, file);
530
531             # Find any old .dsc files
532             elif files[file]["type"] == "dsc":
533                 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"
534                                    % (files[file]["package"], suite))
535                 oldfiles = q.dictresult()
536                 if len(oldfiles) >= 1:
537                     files[file]["oldfiles"][suite] = oldfiles[0]
538
539             # Validate the component
540             component = files[file]["component"];
541             component_id = db_access.get_component_id(component);
542             if component_id == -1:
543                 reject_message = reject_message + "Rejected: file '%s' has unknown component '%s'.\n" % (file, component);
544                 continue;
545
546             # Validate the priority
547             if string.find(files[file]["priority"],'/') != -1:
548                 reject_message = reject_message + "Rejected: file '%s' has invalid priority '%s' [contains '/'].\n" % (file, files[file]["priority"]);
549
550             # Check the md5sum & size against existing files (if any)
551             location = Cnf["Dir::PoolDir"];
552             files[file]["location id"] = db_access.get_location_id (location, component, archive);
553
554             files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"]);
555             files_id = db_access.get_files_id(files[file]["pool name"] + file, files[file]["size"], files[file]["md5sum"], files[file]["location id"]);
556             if files_id == -1:
557                 reject_message = reject_message + "Rejected: INTERNAL ERROR, get_files_id() returned multiple matches for %s.\n" % (file)
558             elif files_id == -2:
559                 reject_message = reject_message + "Rejected: md5sum and/or size mismatch on existing copy of %s.\n" % (file)
560             files[file]["files id"] = files_id
561
562             # Check for packages that have moved from one component to another
563             if files[file]["oldfiles"].has_key(suite) and files[file]["oldfiles"][suite]["name"] != files[file]["component"]:
564                 files[file]["othercomponents"] = files[file]["oldfiles"][suite]["name"];
565
566
567     if string.find(reject_message, "Rejected:") != -1:
568         return 0
569     else:
570         return 1
571
572 ###############################################################################
573
574 def check_dsc ():
575     global dsc, dsc_files, reject_message, reprocess, orig_tar_id, orig_tar_location, legacy_source_untouchable;
576
577     for file in files.keys():
578         if files[file]["type"] == "dsc":
579             try:
580                 dsc = utils.parse_changes(file, 1)
581             except utils.cant_open_exc:
582                 reject_message = reject_message + "Rejected: can't read changes file '%s'.\n" % (file)
583                 return 0;
584             except utils.changes_parse_error_exc, line:
585                 reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (file, line)
586                 return 0;
587             except utils.invalid_dsc_format_exc, line:
588                 reject_message = reject_message + "Rejected: syntax error in .dsc file '%s', line %s.\n" % (file, line)
589                 return 0;
590             try:
591                 dsc_files = utils.build_file_list(dsc, 1)
592             except utils.no_files_exc:
593                 reject_message = reject_message + "Rejected: no Files: field in .dsc file.\n";
594                 continue;
595             except utils.changes_parse_error_exc, line:
596                 reject_message = reject_message + "Rejected: error parsing .dsc file '%s', can't grok: %s.\n" % (file, line);
597                 continue;
598
599             # Enforce mandatory fields
600             for i in ("format", "source", "version", "binary", "maintainer", "architecture", "files"):
601                 if not dsc.has_key(i):
602                     reject_message = reject_message + "Rejected: Missing field `%s' in dsc file.\n" % (i)
603
604             # The dpkg maintainer from hell strikes again! Bumping the
605             # version number of the .dsc breaks extraction by stable's
606             # dpkg-source.
607             if dsc["format"] != "1.0":
608                 reject_message = reject_message + """Rejected: [dpkg-sucks] source package was produced by a broken version
609           of dpkg-dev 1.9.1{3,4}; please rebuild with >= 1.9.15 version
610           installed.
611 """;
612
613             # Ensure the version number in the .dsc matches the version number in the .changes
614             epochless_dsc_version = utils.re_no_epoch.sub('', dsc.get("version"));
615             changes_version = files[file]["version"];
616             if epochless_dsc_version != files[file]["version"]:
617                 reject_message = reject_message + "Rejected: version ('%s') in .dsc does not match version ('%s') in .changes\n" % (epochless_dsc_version, changes_version);
618
619             # Ensure source is newer than existing source in target suites
620             package = dsc.get("source");
621             new_version = dsc.get("version");
622             for suite in changes["distribution"].keys():
623                 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"
624                                    % (package, suite));
625                 ql = map(lambda x: x[0], q.getresult());
626                 for old_version in ql:
627                     if apt_pkg.VersionCompare(new_version, old_version) != 1:
628                         reject_message = reject_message + "Rejected: %s Old version `%s' >= new version `%s'.\n" % (file, old_version, new_version)
629
630             # Try and find all files mentioned in the .dsc.  This has
631             # to work harder to cope with the multiple possible
632             # locations of an .orig.tar.gz.
633             for dsc_file in dsc_files.keys():
634                 if files.has_key(dsc_file):
635                     actual_md5 = files[dsc_file]["md5sum"];
636                     actual_size = int(files[dsc_file]["size"]);
637                     found = "%s in incoming" % (dsc_file)
638                     # Check the file does not already exist in the archive
639                     if not changes.has_key("stable install"):
640                         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));
641
642                         # "It has not broken them.  It has fixed a
643                         # brokenness.  Your crappy hack exploited a
644                         # bug in the old dinstall.
645                         #
646                         # "(Come on!  I thought it was always obvious
647                         # that one just doesn't release different
648                         # files with the same name and version.)"
649                         #                        -- ajk@ on d-devel@l.d.o
650
651                         if q.getresult() != []:
652                             reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (dsc_file)
653                 elif dsc_file[-12:] == ".orig.tar.gz":
654                     # Check in the pool
655                     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));
656                     ql = q.getresult();
657
658                     if ql != []:
659                         # Unfortunately, we make get more than one match
660                         # here if, for example, the package was in potato
661                         # but had a -sa upload in woody.  So we need to a)
662                         # choose the right one and b) mark all wrong ones
663                         # as excluded from the source poolification (to
664                         # avoid file overwrites).
665
666                         x = ql[0]; # default to something sane in case we don't match any or have only one
667
668                         if len(ql) > 1:
669                             for i in ql:
670                                 old_file = i[0] + i[1];
671                                 actual_md5 = apt_pkg.md5sum(utils.open_file(old_file));
672                                 actual_size = os.stat(old_file)[stat.ST_SIZE];
673                                 if actual_md5 == dsc_files[dsc_file]["md5sum"] and actual_size == int(dsc_files[dsc_file]["size"]):
674                                     x = i;
675                                 else:
676                                     legacy_source_untouchable[i[3]] = "";
677
678                         old_file = x[0] + x[1];
679                         actual_md5 = apt_pkg.md5sum(utils.open_file(old_file));
680                         actual_size = os.stat(old_file)[stat.ST_SIZE];
681                         found = old_file;
682                         suite_type = x[2];
683                         dsc_files[dsc_file]["files id"] = x[3]; # need this for updating dsc_files in install()
684                         # See install()...
685                         orig_tar_id = x[3];
686                         if suite_type == "legacy" or suite_type == "legacy-mixed":
687                             orig_tar_location = "legacy";
688                         else:
689                             orig_tar_location = x[4];
690                     else:
691                         # Not there? Check in Incoming...
692                         # [See comment above process_it() for explanation
693                         #  of why this is necessary...]
694                         if os.path.exists(dsc_file):
695                             files[dsc_file] = {};
696                             files[dsc_file]["size"] = os.stat(dsc_file)[stat.ST_SIZE];
697                             files[dsc_file]["md5sum"] = dsc_files[dsc_file]["md5sum"];
698                             files[dsc_file]["section"] = files[file]["section"];
699                             files[dsc_file]["priority"] = files[file]["priority"];
700                             files[dsc_file]["component"] = files[file]["component"];
701                             files[dsc_file]["type"] = "orig.tar.gz";
702                             reprocess = 1;
703                             return 1;
704                         else:
705                             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);
706                             continue;
707                 else:
708                     reject_message = reject_message + "Rejected: %s refers to %s, but I can't find it in Incoming.\n" % (file, dsc_file);
709                     continue;
710                 if actual_md5 != dsc_files[dsc_file]["md5sum"]:
711                     reject_message = reject_message + "Rejected: md5sum for %s doesn't match %s.\n" % (found, file);
712                 if actual_size != int(dsc_files[dsc_file]["size"]):
713                     reject_message = reject_message + "Rejected: size for %s doesn't match %s.\n" % (found, file);
714
715     if string.find(reject_message, "Rejected:") != -1:
716         return 0
717     else:
718         return 1
719
720 ###############################################################################
721
722 # Some cunning stunt broke dpkg-source in dpkg 1.8{,.1}; detect the
723 # resulting bad source packages and reject them.
724
725 # Even more amusingly the fix in 1.8.1.1 didn't actually fix the
726 # problem just changed the symptoms.
727
728 def check_diff ():
729     global dsc, dsc_files, reject_message, reprocess;
730
731     for filename in files.keys():
732         if files[filename]["type"] == "diff.gz":
733             file = gzip.GzipFile(filename, 'r');
734             for line in file.readlines():
735                 if re_bad_diff.search(line):
736                     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";
737                     break;
738
739     if string.find(reject_message, "Rejected:") != -1:
740         return 0
741     else:
742         return 1
743
744 ###############################################################################
745
746 def check_md5sums ():
747     global reject_message;
748
749     for file in files.keys():
750         try:
751             file_handle = utils.open_file(file);
752         except utils.cant_open_exc:
753             pass;
754         else:
755             if apt_pkg.md5sum(file_handle) != files[file]["md5sum"]:
756                 reject_message = reject_message + "Rejected: md5sum check failed for %s.\n" % (file);
757
758 def check_override ():
759     global Subst;
760
761     # Only check section & priority on sourceful non-stable installs
762     if not changes["architecture"].has_key("source") or changes.has_key("stable install"):
763         return;
764
765     summary = ""
766     for file in files.keys():
767         if not files[file].has_key("new") and files[file]["type"] == "deb":
768             section = files[file]["section"];
769             override_section = files[file]["override section"];
770             if section != override_section and section != "-":
771                 # Ignore this; it's a common mistake and not worth whining about
772                 if string.lower(section) == "non-us/main" and string.lower(override_section) == "non-us":
773                     continue;
774                 summary = summary + "%s: section is overridden from %s to %s.\n" % (file, section, override_section);
775             priority = files[file]["priority"];
776             override_priority = files[file]["override priority"];
777             if priority != override_priority and priority != "-":
778                 summary = summary + "%s: priority is overridden from %s to %s.\n" % (file, priority, override_priority);
779
780     if summary == "":
781         return;
782
783     Subst["__SUMMARY__"] = summary;
784     mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.override-disparity","r").read());
785     utils.send_mail (mail_message, "")
786
787 #####################################################################################################################
788
789 # Set up the per-package template substitution mappings
790
791 def update_subst (changes_filename):
792     global Subst;
793
794     # If katie crashed out in the right place, architecture may still be a string.
795     if not changes.has_key("architecture") or not isinstance(changes["architecture"], DictType):
796         changes["architecture"] = { "Unknown" : "" };
797     # and maintainer822 may not exist.
798     if not changes.has_key("maintainer822"):
799         changes["maintainer822"] = Cnf["Dinstall::MyEmailAddress"];
800
801     Subst["__ARCHITECTURE__"] = string.join(changes["architecture"].keys(), ' ' );
802     Subst["__CHANGES_FILENAME__"] = os.path.basename(changes_filename);
803     Subst["__FILE_CONTENTS__"] = changes.get("filecontents", "");
804
805     # For source uploads the Changed-By field wins; otherwise Maintainer wins.
806     if changes["architecture"].has_key("source") and changes["changedby822"] != "" and (changes["changedby822"] != changes["maintainer822"]):
807         Subst["__MAINTAINER_FROM__"] = changes["changedby822"];
808         Subst["__MAINTAINER_TO__"] = changes["changedby822"] + ", " + changes["maintainer822"];
809         Subst["__MAINTAINER__"] = changes.get("changed-by", "Unknown");
810     else:
811         Subst["__MAINTAINER_FROM__"] = changes["maintainer822"];
812         Subst["__MAINTAINER_TO__"] = changes["maintainer822"];
813         Subst["__MAINTAINER__"] = changes.get("maintainer", "Unknown");
814
815     Subst["__REJECT_MESSAGE__"] = reject_message;
816     Subst["__SOURCE__"] = changes.get("source", "Unknown");
817     Subst["__VERSION__"] = changes.get("version", "Unknown");
818
819 #####################################################################################################################
820
821 def action (changes_filename):
822     byhand = summary = new = "";
823
824     # changes["distribution"] may not exist in corner cases
825     # (e.g. unreadable changes files)
826     if not changes.has_key("distribution") or not isinstance(changes["distribution"], DictType):
827         changes["distribution"] = {};
828
829     confirm = []
830     suites = []
831     for suite in changes["distribution"].keys():
832         if Cnf.has_key("Suite::%s::Confirm"):
833             confirm.append(suite)
834         suites.append(suite)
835
836     file_keys = files.keys();
837     file_keys.sort();
838     for file in file_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)))["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" % (string.join(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" % (string.join(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 not 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 not 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);
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 not 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 not 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"]);
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