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