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