]> git.decadent.org.uk Git - dak.git/blob - katie
Ensure a sane minimum set of fields in the .dsc and ensure the .dsc version matches...
[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.55 2001-07-13 19:47:31 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' OR a.arch_string = 'all') AND ba.bin = b.id AND ba.suite = s.id AND b.architecture = a.id AND f.location = l.id AND l.component = c.id AND b.file = f.id"
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             # Enforce mandatory fields
571             for i in ("format", "source", "version", "binary", "maintainer", "architecture", "files"):
572                 if not dsc.has_key(i):
573                     reject_message = reject_message + "Rejected: Missing field `%s' in dsc file.\n" % (i)
574
575             # The dpkg maintainer from hell strikes again! Bumping the
576             # version number of the .dsc breaks extraction by stable's
577             # dpkg-source.
578             if dsc["format"] != "1.0":
579                 reject_message = reject_message + """Rejected: [dpkg-sucks] source package was produced by a broken version
580           of dpkg-dev 1.9.1{3,4}; please rebuild with >= 1.9.15 version
581           installed.
582 """;
583
584             # Ensure the version number in the .dsc matches the version number in the .changes
585             epochless_dsc_version = utils.re_no_epoch.sub('', dsc.get("version"));
586             changes_version = files[file]["version"];
587             if epochless_dsc_version != files[file]["version"]:
588                 reject_message = reject_message + "Rejected: version ('%s') in .dsc does not match version ('%s') in .changes\n" % (epochless_dsc_version, changes_version);
589
590             # Try and find all files mentioned in the .dsc.  This has
591             # to work harder to cope with the multiple possible
592             # locations of an .orig.tar.gz.
593             for dsc_file in dsc_files.keys():
594                 if files.has_key(dsc_file):
595                     actual_md5 = files[dsc_file]["md5sum"];
596                     actual_size = int(files[dsc_file]["size"]);
597                     found = "%s in incoming" % (dsc_file)
598                     # Check the file does not already exist in the archive
599                     if not changes.has_key("stable upload"):
600                         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));
601
602                         # "It has not broken them.  It has fixed a
603                         # brokenness.  Your crappy hack exploited a
604                         # bug in the old dinstall.
605                         #
606                         # "(Come on!  I thought it was always obvious
607                         # that one just doesn't release different
608                         # files with the same name and version.)"
609                         #                        -- ajk@ on d-devel@l.d.o
610
611                         if q.getresult() != []:
612                             reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (dsc_file)
613                 elif dsc_file[-12:] == ".orig.tar.gz":
614                     # Check in the pool
615                     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));
616                     ql = q.getresult();
617
618                     if ql != []:
619                         # Unfortunately, we make get more than one match
620                         # here if, for example, the package was in potato
621                         # but had a -sa upload in woody.  So we need to a)
622                         # choose the right one and b) mark all wrong ones
623                         # as excluded from the source poolification (to
624                         # avoid file overwrites).
625
626                         x = ql[0]; # default to something sane in case we don't match any or have only one
627
628                         if len(ql) > 1:
629                             for i in ql:
630                                 old_file = i[0] + i[1];
631                                 actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
632                                 actual_size = os.stat(old_file)[stat.ST_SIZE];
633                                 if actual_md5 == dsc_files[dsc_file]["md5sum"] and actual_size == int(dsc_files[dsc_file]["size"]):
634                                     x = i;
635                                 else:
636                                     legacy_source_untouchable[i[3]] = "";
637
638                         old_file = x[0] + x[1];
639                         actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
640                         actual_size = os.stat(old_file)[stat.ST_SIZE];
641                         found = old_file;
642                         suite_type = x[2];
643                         dsc_files[dsc_file]["files id"] = x[3]; # need this for updating dsc_files in install()
644                         # See install()...
645                         orig_tar_id = x[3];
646                         if suite_type == "legacy" or suite_type == "legacy-mixed":
647                             orig_tar_location = "legacy";
648                         else:
649                             orig_tar_location = x[4];
650                     else:
651                         # Not there? Check in Incoming...
652                         # [See comment above process_it() for explanation
653                         #  of why this is necessary...]
654                         if os.path.exists(dsc_file):
655                             files[dsc_file] = {};
656                             files[dsc_file]["size"] = os.stat(dsc_file)[stat.ST_SIZE];
657                             files[dsc_file]["md5sum"] = dsc_files[dsc_file]["md5sum"];
658                             files[dsc_file]["section"] = files[file]["section"];
659                             files[dsc_file]["priority"] = files[file]["priority"];
660                             files[dsc_file]["component"] = files[file]["component"];
661                             files[dsc_file]["type"] = "orig.tar.gz";
662                             reprocess = 1;
663                             return 1;
664                         else:
665                             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);
666                             continue;
667                 else:
668                     reject_message = reject_message + "Rejected: %s refers to %s, but I can't find it in Incoming.\n" % (file, dsc_file);
669                     continue;
670                 if actual_md5 != dsc_files[dsc_file]["md5sum"]:
671                     reject_message = reject_message + "Rejected: md5sum for %s doesn't match %s.\n" % (found, file);
672                 if actual_size != int(dsc_files[dsc_file]["size"]):
673                     reject_message = reject_message + "Rejected: size for %s doesn't match %s.\n" % (found, file);
674
675     if string.find(reject_message, "Rejected:") != -1:
676         return 0
677     else: 
678         return 1
679
680 ###############################################################################
681
682 # Some cunning stunt broke dpkg-source in dpkg 1.8{,.1}; detect the
683 # resulting bad source packages and reject them.
684
685 # Even more amusingly the fix in 1.8.1.1 didn't actually fix the
686 # problem just changed the symptoms.
687
688 def check_diff ():
689     global dsc, dsc_files, reject_message, reprocess;
690
691     for filename in files.keys():
692         if files[filename]["type"] == "diff.gz":
693             file = gzip.GzipFile(filename, 'r');
694             for line in file.readlines():
695                 if re_bad_diff.search(line):
696                     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";
697                     break;
698
699     if string.find(reject_message, "Rejected:") != -1:
700         return 0
701     else: 
702         return 1
703
704 ###############################################################################
705
706 def check_md5sums ():
707     global reject_message;
708
709     for file in files.keys():
710         try:
711             file_handle = utils.open_file(file,"r");
712         except utils.cant_open_exc:
713             pass;
714         else:
715             if apt_pkg.md5sum(file_handle) != files[file]["md5sum"]:
716                 reject_message = reject_message + "Rejected: md5sum check failed for %s.\n" % (file);
717
718 def check_override ():
719     global Subst;
720     
721     # Only check section & priority on sourceful uploads
722     if not changes["architecture"].has_key("source"):
723         return;
724
725     summary = ""
726     for file in files.keys():
727         if not files[file].has_key("new") and files[file]["type"] == "deb":
728             section = files[file]["section"];
729             override_section = files[file]["override section"];
730             if section != override_section and section != "-":
731                 # Ignore this; it's a common mistake and not worth whining about
732                 if string.lower(section) == "non-us/main" and string.lower(override_section) == "non-us":
733                     continue;
734                 summary = summary + "%s: section is overridden from %s to %s.\n" % (file, section, override_section);
735             priority = files[file]["priority"];
736             override_priority = files[file]["override priority"];
737             if priority != override_priority and priority != "-":
738                 summary = summary + "%s: priority is overridden from %s to %s.\n" % (file, priority, override_priority);
739
740     if summary == "":
741         return;
742
743     Subst["__SUMMARY__"] = summary;
744     mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.override-disparity","r").read());
745     utils.send_mail (mail_message, "")
746
747 #####################################################################################################################
748
749 # Set up the per-package template substitution mappings
750
751 def update_subst (changes_filename):
752     global Subst;
753
754     # If katie crashed out in the right place, architecture may still be a string.
755     if not changes.has_key("architecture") or not isinstance(changes["architecture"], DictType):
756         changes["architecture"] = { "Unknown" : "" };
757     # and maintainer822 may not exist.
758     if not changes.has_key("maintainer822"):
759         changes["maintainer822"] = Cnf["Dinstall::MyEmailAddress"];
760
761     Subst["__ARCHITECTURE__"] = string.join(changes["architecture"].keys(), ' ' );
762     Subst["__CHANGES_FILENAME__"] = os.path.basename(changes_filename);
763     Subst["__FILE_CONTENTS__"] = changes.get("filecontents", "");
764
765     # For source uploads the Changed-By field wins; otherwise Maintainer wins.
766     if changes["architecture"].has_key("source") and changes["changedby822"] != "" and (changes["changedby822"] != changes["maintainer822"]):
767         Subst["__MAINTAINER_FROM__"] = changes["changedby822"];
768         Subst["__MAINTAINER_TO__"] = changes["changedby822"] + ", " + changes["maintainer822"];
769         Subst["__MAINTAINER__"] = changes.get("changed-by", "Unknown");
770     else:
771         Subst["__MAINTAINER_FROM__"] = changes["maintainer822"];
772         Subst["__MAINTAINER_TO__"] = changes["maintainer822"];
773         Subst["__MAINTAINER__"] = changes.get("maintainer", "Unknown");
774
775     Subst["__REJECT_MESSAGE__"] = reject_message;
776     Subst["__SOURCE__"] = changes.get("source", "Unknown");
777     Subst["__VERSION__"] = changes.get("version", "Unknown");
778
779 #####################################################################################################################
780
781 def action (changes_filename):
782     byhand = confirm = suites = summary = new = "";
783
784     # changes["distribution"] may not exist in corner cases
785     # (e.g. unreadable changes files)
786     if not changes.has_key("distribution") or not isinstance(changes["distribution"], DictType):
787         changes["distribution"] = {};
788     
789     for suite in changes["distribution"].keys():
790         if Cnf.has_key("Suite::%s::Confirm"):
791             confirm = confirm + suite + ", "
792         suites = suites + suite + ", "
793     confirm = confirm[:-2]
794     suites = suites[:-2]
795
796     for file in files.keys():
797         if files[file].has_key("byhand"):
798             byhand = 1
799             summary = summary + file + " byhand\n"
800         elif files[file].has_key("new"):
801             new = 1
802             summary = summary + "(new) %s %s %s\n" % (file, files[file]["priority"], files[file]["section"])
803             if files[file].has_key("othercomponents"):
804                 summary = summary + "WARNING: Already present in %s distribution.\n" % (files[file]["othercomponents"])
805             if files[file]["type"] == "deb":
806                 summary = summary + apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))["Description"] + '\n';
807         else:
808             files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"])
809             destination = Cnf["Dir::PoolRoot"] + files[file]["pool name"] + file
810             summary = summary + file + "\n  to " + destination + "\n"
811
812     short_summary = summary;
813
814     # This is for direport's benefit...
815     f = re_fdnic.sub("\n .\n", changes.get("changes",""));
816
817     if confirm or byhand or new:
818         summary = summary + "Changes: " + f;
819
820     summary = summary + announce (short_summary, 0)
821     
822     (prompt, answer) = ("", "XXX")
823     if Options["No-Action"] or Options["Automatic"]:
824         answer = 'S'
825
826     if string.find(reject_message, "Rejected") != -1:
827         try:
828             modified_time = time.time()-os.path.getmtime(changes_filename);
829         except: # i.e. ignore errors like 'file does not exist';
830             modified_time = 0;
831         if modified_time < 86400:
832             print "SKIP (too new)\n" + reject_message,;
833             prompt = "[S]kip, Manual reject, Quit ?";
834         else:
835             print "REJECT\n" + reject_message,;
836             prompt = "[R]eject, Manual reject, Skip, Quit ?";
837             if Options["Automatic"]:
838                 answer = 'R';
839     elif new:
840         print "NEW to %s\n%s%s" % (suites, reject_message, summary),;
841         prompt = "[S]kip, New ack, Manual reject, Quit ?";
842         if Options["Automatic"] and Options["Ack-New"]:
843             answer = 'N';
844     elif byhand:
845         print "BYHAND\n" + reject_message + summary,;
846         prompt = "[I]nstall, Manual reject, Skip, Quit ?";
847     elif confirm:
848         print "CONFIRM to %s\n%s%s" % (confirm, reject_message, summary),
849         prompt = "[I]nstall, Manual reject, Skip, Quit ?";
850     else:
851         print "INSTALL\n" + reject_message + summary,;
852         prompt = "[I]nstall, Manual reject, Skip, Quit ?";
853         if Options["Automatic"]:
854             answer = 'I';
855
856     while string.find(prompt, answer) == -1:
857         print prompt,;
858         answer = utils.our_raw_input()
859         m = re_default_answer.match(prompt)
860         if answer == "":
861             answer = m.group(1)
862         answer = string.upper(answer[:1])
863
864     if answer == 'R':
865         reject (changes_filename, "");
866     elif answer == 'M':
867         manual_reject (changes_filename);
868     elif answer == 'I':
869         install (changes_filename, summary, short_summary);
870     elif answer == 'N':
871         acknowledge_new (changes_filename, summary);
872     elif answer == 'Q':
873         sys.exit(0)
874
875 #####################################################################################################################
876
877 def install (changes_filename, summary, short_summary):
878     global install_count, install_bytes, Subst;
879
880     # Stable uploads are a special case
881     if changes.has_key("stable upload"):
882         stable_install (changes_filename, summary, short_summary);
883         return;
884     
885     print "Installing."
886
887     Logger.log(["installing changes",changes_filename]);
888
889     archive = utils.where_am_i();
890
891     # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
892     projectB.query("BEGIN WORK");
893
894     # Add the .dsc file to the DB
895     for file in files.keys():
896         if files[file]["type"] == "dsc":
897             package = dsc["source"]
898             version = dsc["version"]  # NB: not files[file]["version"], that has no epoch
899             maintainer = dsc["maintainer"]
900             maintainer = string.replace(maintainer, "'", "\\'")
901             maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
902             filename = files[file]["pool name"] + file;
903             dsc_location_id = files[file]["location id"];
904             if not files[file]["files id"]:
905                 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], dsc_location_id)
906             projectB.query("INSERT INTO source (source, version, maintainer, file) VALUES ('%s', '%s', %d, %d)"
907                            % (package, version, maintainer_id, files[file]["files id"]))
908
909             for suite in changes["distribution"].keys():
910                 suite_id = db_access.get_suite_id(suite);
911                 projectB.query("INSERT INTO src_associations (suite, source) VALUES (%d, currval('source_id_seq'))" % (suite_id))
912
913             # Add the source files to the DB (files and dsc_files)
914             projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files[file]["files id"]));
915             for dsc_file in dsc_files.keys():
916                 filename = files[file]["pool name"] + dsc_file;
917                 # If the .orig.tar.gz is already in the pool, it's
918                 # files id is stored in dsc_files by check_dsc().
919                 files_id = dsc_files[dsc_file].get("files id", None);
920                 if files_id == None:
921                     files_id = db_access.get_files_id(filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], dsc_location_id);
922                 # FIXME: needs to check for -1/-2 and or handle exception
923                 if files_id == None:
924                     files_id = db_access.set_files_id (filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], dsc_location_id);
925                 projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files_id));
926             
927     # Add the .deb files to the DB
928     for file in files.keys():
929         if files[file]["type"] == "deb":
930             package = files[file]["package"]
931             version = files[file]["version"]
932             maintainer = files[file]["maintainer"]
933             maintainer = string.replace(maintainer, "'", "\\'")
934             maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
935             architecture = files[file]["architecture"]
936             architecture_id = db_access.get_architecture_id (architecture);
937             type = files[file]["dbtype"];
938             dsc_component = files[file]["component"]
939             source = files[file]["source package"]
940             source_version = files[file]["source version"];
941             filename = files[file]["pool name"] + file;
942             if not files[file]["files id"]:
943                 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], files[file]["location id"])
944             source_id = db_access.get_source_id (source, source_version);
945             if source_id:
946                 projectB.query("INSERT INTO binaries (package, version, maintainer, source, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, %d, '%s')"
947                                % (package, version, maintainer_id, source_id, architecture_id, files[file]["files id"], type));
948             else:
949                 projectB.query("INSERT INTO binaries (package, version, maintainer, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, '%s')"
950                                % (package, version, maintainer_id, architecture_id, files[file]["files id"], type));
951             for suite in changes["distribution"].keys():
952                 suite_id = db_access.get_suite_id(suite);
953                 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES (%d, currval('binaries_id_seq'))" % (suite_id));
954
955     # If the .orig.tar.gz is in a legacy directory we need to poolify
956     # it, so that apt-get source (and anything else that goes by the
957     # "Directory:" field in the Sources.gz file) works.
958     if orig_tar_id != None and orig_tar_location == "legacy":
959         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));
960         qd = q.dictresult();
961         for qid in qd:
962             # Is this an old upload superseded by a newer -sa upload?  (See check_dsc() for details)
963             if legacy_source_untouchable.has_key(qid["files_id"]):
964                 continue;
965             # First move the files to the new location
966             legacy_filename = qid["path"]+qid["filename"];
967             pool_location = utils.poolify (changes["source"], files[file]["component"]);
968             pool_filename = pool_location + os.path.basename(qid["filename"]);
969             destination = Cnf["Dir::PoolDir"] + pool_location
970             utils.move(legacy_filename, destination);
971             # Then Update the DB's files table
972             q = projectB.query("UPDATE files SET filename = '%s', location = '%s' WHERE id = '%s'" % (pool_filename, dsc_location_id, qid["files_id"]));
973
974     # If this is a sourceful diff only upload that is moving non-legacy
975     # cross-component we need to copy the .orig.tar.gz into the new
976     # component too for the same reasons as above.
977     #
978     if changes["architecture"].has_key("source") and orig_tar_id != None and \
979        orig_tar_location != "legacy" and orig_tar_location != dsc_location_id:
980         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));
981         ql = q.getresult()[0];
982         old_filename = ql[0] + ql[1];
983         file_size = ql[2];
984         file_md5sum = ql[3];
985         new_filename = utils.poolify (changes["source"], dsc_component) + os.path.basename(old_filename);
986         new_files_id = db_access.get_files_id(new_filename, file_size, file_md5sum, dsc_location_id);
987         if new_files_id == None:
988             utils.copy(old_filename, Cnf["Dir::PoolDir"] + new_filename);
989             new_files_id = db_access.set_files_id(new_filename, file_size, file_md5sum, dsc_location_id);
990             projectB.query("UPDATE dsc_files SET file = %s WHERE source = %s AND file = %s" % (new_files_id, source_id, orig_tar_id));
991
992     # Install the files into the pool
993     for file in files.keys():
994         if files[file].has_key("byhand"):
995             continue
996         destination = Cnf["Dir::PoolDir"] + files[file]["pool name"] + file
997         destdir = os.path.dirname(destination)
998         utils.move (file, destination)
999         Logger.log(["installed", file, files[file]["type"], files[file]["size"], files[file]["architecture"]]);
1000         install_bytes = install_bytes + float(files[file]["size"])
1001
1002     # Copy the .changes file across for suite which need it.
1003     for suite in changes["distribution"].keys():
1004         if Cnf.has_key("Suite::%s::CopyChanges" % (suite)):
1005             utils.copy (changes_filename, Cnf["Dir::RootDir"] + Cnf["Suite::%s::CopyChanges" % (suite)]);
1006
1007     projectB.query("COMMIT WORK");
1008
1009     try:
1010         utils.move (changes_filename, Cnf["Dir::IncomingDir"] + 'DONE/' + os.path.basename(changes_filename))
1011     except:
1012         utils.warn("couldn't move changes file '%s' to DONE directory. [Got %s]" % (os.path.basename(changes_filename), sys.exc_type));
1013
1014     install_count = install_count + 1;
1015
1016     if not Options["No-Mail"]:
1017         Subst["__SUITE__"] = "";
1018         Subst["__SUMMARY__"] = summary;
1019         mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.installed","r").read());
1020         utils.send_mail (mail_message, "")
1021         announce (short_summary, 1)
1022         check_override ();
1023
1024 #####################################################################################################################
1025
1026 def stable_install (changes_filename, summary, short_summary):
1027     global install_count, install_bytes, Subst;
1028     
1029     print "Installing to stable."
1030
1031     archive = utils.where_am_i();
1032
1033     # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
1034     projectB.query("BEGIN WORK");
1035
1036     # Add the .dsc file to the DB
1037     for file in files.keys():
1038         if files[file]["type"] == "dsc":
1039             package = dsc["source"]
1040             version = dsc["version"]  # NB: not files[file]["version"], that has no epoch
1041             q = projectB.query("SELECT id FROM source WHERE source = '%s' AND version = '%s'" % (package, version))
1042             ql = q.getresult()
1043             if ql == []:
1044                 utils.fubar("[INTERNAL ERROR] couldn't find '%s' (%s) in source table." % (package, version));
1045             source_id = ql[0][0];
1046             suite_id = db_access.get_suite_id('proposed-updates');
1047             projectB.query("DELETE FROM src_associations WHERE suite = '%s' AND source = '%s'" % (suite_id, source_id));
1048             suite_id = db_access.get_suite_id('stable');
1049             projectB.query("INSERT INTO src_associations (suite, source) VALUES ('%s', '%s')" % (suite_id, source_id));
1050                 
1051     # Add the .deb files to the DB
1052     for file in files.keys():
1053         if files[file]["type"] == "deb":
1054             package = files[file]["package"]
1055             version = files[file]["version"]
1056             architecture = files[file]["architecture"]
1057             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))
1058             ql = q.getresult()
1059             if ql == []:
1060                 utils.fubar("[INTERNAL ERROR] couldn't find '%s' (%s for %s architecture) in binaries table." % (package, version, architecture));
1061             binary_id = ql[0][0];
1062             suite_id = db_access.get_suite_id('proposed-updates');
1063             projectB.query("DELETE FROM bin_associations WHERE suite = '%s' AND bin = '%s'" % (suite_id, binary_id));
1064             suite_id = db_access.get_suite_id('stable');
1065             projectB.query("INSERT INTO bin_associations (suite, bin) VALUES ('%s', '%s')" % (suite_id, binary_id));
1066
1067     projectB.query("COMMIT WORK");
1068
1069     # FIXME
1070     utils.move (changes_filename, Cnf["Dir::Morgue"] + '/katie/' + os.path.basename(changes_filename));
1071
1072     # Update the Stable ChangeLog file
1073
1074     new_changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + ".ChangeLog";
1075     changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + "ChangeLog";
1076     if os.path.exists(new_changelog_filename):
1077         os.unlink (new_changelog_filename);
1078     
1079     new_changelog = utils.open_file(new_changelog_filename, 'w');
1080     for file in files.keys():
1081         if files[file]["type"] == "deb":
1082             new_changelog.write("stable/%s/binary-%s/%s\n" % (files[file]["component"], files[file]["architecture"], file));
1083         elif utils.re_issource.match(file) != None:
1084             new_changelog.write("stable/%s/source/%s\n" % (files[file]["component"], file));
1085         else:
1086             new_changelog.write("%s\n" % (file));
1087     chop_changes = re_fdnic.sub("\n", changes["changes"]);
1088     new_changelog.write(chop_changes + '\n\n');
1089     if os.access(changelog_filename, os.R_OK) != 0:
1090         changelog = utils.open_file(changelog_filename, 'r');
1091         new_changelog.write(changelog.read());
1092     new_changelog.close();
1093     if os.access(changelog_filename, os.R_OK) != 0:
1094         os.unlink(changelog_filename);
1095     utils.move(new_changelog_filename, changelog_filename);
1096
1097     install_count = install_count + 1;
1098
1099     if not Options["No-Mail"]:
1100         Subst["__SUITE__"] = " into stable";
1101         Subst["__SUMMARY__"] = summary;
1102         mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.installed","r").read());
1103         utils.send_mail (mail_message, "")
1104         announce (short_summary, 1)
1105
1106 #####################################################################################################################
1107
1108 def reject (changes_filename, manual_reject_mail_filename):
1109     global Subst;
1110     
1111     print "Rejecting.\n"
1112
1113     base_changes_filename = os.path.basename(changes_filename);
1114     reason_filename = re_changes.sub("reason", base_changes_filename);
1115     reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename);
1116
1117     # Move the .changes files and it's contents into REJECT/ (if we can; errors are ignored)
1118     try:
1119         utils.move (changes_filename, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], base_changes_filename));
1120     except:
1121         utils.warn("couldn't reject changes file '%s'. [Got %s]" % (base_changes_filename, sys.exc_type));
1122         pass;
1123     for file in files.keys():
1124         if os.path.exists(file):
1125             try:
1126                 utils.move (file, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], file));
1127             except:
1128                 utils.warn("couldn't reject file '%s'. [Got %s]" % (file, sys.exc_type));
1129                 pass;
1130
1131     # If this is not a manual rejection generate the .reason file and rejection mail message
1132     if manual_reject_mail_filename == "":
1133         if os.path.exists(reject_filename):
1134             os.unlink(reject_filename);
1135         fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
1136         os.write(fd, reject_message);
1137         os.close(fd);
1138         Subst["__MANUAL_REJECT_MESSAGE__"] = "";
1139         Subst["__CC__"] = "X-Katie-Rejection: automatic (moo)";
1140         reject_mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.rejected","r").read());
1141     else: # Have a manual rejection file to use
1142         reject_mail_message = ""; # avoid <undef>'s
1143         
1144     # Send the rejection mail if appropriate
1145     if not Options["No-Mail"]:
1146         utils.send_mail (reject_mail_message, manual_reject_mail_filename);
1147
1148     Logger.log(["rejected", changes_filename]);
1149
1150 ##################################################################
1151
1152 def manual_reject (changes_filename):
1153     global Subst;
1154     
1155     # Build up the rejection email 
1156     user_email_address = utils.whoami() + " <%s@%s>" % (pwd.getpwuid(os.getuid())[0], Cnf["Dinstall::MyHost"])
1157     manual_reject_message = Cnf.get("Dinstall::Options::Manual-Reject", "")
1158
1159     Subst["__MANUAL_REJECT_MESSAGE__"] = manual_reject_message;
1160     Subst["__CC__"] = "Cc: " + Cnf["Dinstall::MyEmailAddress"];
1161     reject_mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.rejected","r").read());
1162     
1163     # Write the rejection email out as the <foo>.reason file
1164     reason_filename = re_changes.sub("reason", os.path.basename(changes_filename));
1165     reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename)
1166     if os.path.exists(reject_filename):
1167         os.unlink(reject_filename);
1168     fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
1169     os.write(fd, reject_mail_message);
1170     os.close(fd);
1171     
1172     # If we weren't given one, spawn an editor so the user can add one in
1173     if manual_reject_message == "":
1174         result = os.system("vi +6 %s" % (reject_filename))
1175         if result != 0:
1176             utils.fubar("vi invocation failed for `%s'!" % (reject_filename), result);
1177
1178     # Then process it as if it were an automatic rejection
1179     reject (changes_filename, reject_filename)
1180
1181 #####################################################################################################################
1182  
1183 def acknowledge_new (changes_filename, summary):
1184     global new_ack_new, Subst;
1185
1186     changes_filename = os.path.basename(changes_filename);
1187
1188     new_ack_new[changes_filename] = 1;
1189
1190     if new_ack_old.has_key(changes_filename):
1191         print "Ack already sent.";
1192         return;
1193
1194     print "Sending new ack.";
1195     if not Options["No-Mail"]:
1196         Subst["__SUMMARY__"] = summary;
1197         new_ack_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.new","r").read());
1198         utils.send_mail(new_ack_message,"");
1199
1200 #####################################################################################################################
1201
1202 def announce (short_summary, action):
1203     global Subst;
1204     
1205     # Only do announcements for source uploads with a recent dpkg-dev installed
1206     if float(changes.get("format", 0)) < 1.6 or not changes["architecture"].has_key("source"):
1207         return ""
1208
1209     lists_done = {}
1210     summary = ""
1211     Subst["__SHORT_SUMMARY__"] = short_summary;
1212
1213     for dist in changes["distribution"].keys():
1214         list = Cnf.Find("Suite::%s::Announce" % (dist))
1215         if list == "" or lists_done.has_key(list):
1216             continue
1217         lists_done[list] = 1
1218         summary = summary + "Announcing to %s\n" % (list)
1219
1220         if action:
1221             Subst["__ANNOUNCE_LIST_ADDRESS__"] = list;
1222             mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.announce","r").read());
1223             utils.send_mail (mail_message, "")
1224
1225     bugs = changes["closes"].keys()
1226     bugs.sort()
1227     if not nmu.is_an_nmu(changes, dsc):
1228         summary = summary + "Closing bugs: "
1229         for bug in bugs:
1230             summary = summary + "%s " % (bug)
1231             if action:
1232                 Subst["__BUG_NUMBER__"] = bug;
1233                 if changes["distribution"].has_key("stable"):
1234                     Subst["__STABLE_WARNING__"] = """
1235 Note that this package is not part of the released stable Debian
1236 distribution.  It may have dependencies on other unreleased software,
1237 or other instabilities.  Please take care if you wish to install it.
1238 The update will eventually make its way into the next released Debian
1239 distribution."""
1240                 else:
1241                     Subst["__STABLE_WARNING__"] = "";
1242                 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.bug-close","r").read());
1243                 utils.send_mail (mail_message, "")
1244         if action:
1245             Logger.log(["closing bugs"]+bugs);
1246     else:                     # NMU
1247         summary = summary + "Setting bugs to severity fixed: "
1248         control_message = ""
1249         for bug in bugs:
1250             summary = summary + "%s " % (bug)
1251             control_message = control_message + "tag %s + fixed\n" % (bug)
1252         if action and control_message != "":
1253             Subst["__CONTROL_MESSAGE__"] = control_message;
1254             mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.bug-nmu-fixed","r").read());
1255             utils.send_mail (mail_message, "")
1256         if action:
1257             Logger.log(["setting bugs to fixed"]+bugs);
1258     summary = summary + "\n"
1259
1260     return summary
1261
1262 ###############################################################################
1263
1264 # reprocess is necessary for the case of foo_1.2-1 and foo_1.2-2 in
1265 # Incoming. -1 will reference the .orig.tar.gz, but -2 will not.
1266 # dsccheckdistrib() can find the .orig.tar.gz but it will not have
1267 # processed it during it's checks of -2.  If -1 has been deleted or
1268 # otherwise not checked by da-install, the .orig.tar.gz will not have
1269 # been checked at all.  To get round this, we force the .orig.tar.gz
1270 # into the .changes structure and reprocess the .changes file.
1271
1272 def process_it (changes_file):
1273     global reprocess, orig_tar_id, orig_tar_location, changes, dsc, dsc_files, files, reject_message;
1274
1275     # Reset some globals
1276     reprocess = 1;
1277     changes = {};
1278     dsc = {};
1279     dsc_files = {};
1280     files = {};
1281     orig_tar_id = None;
1282     orig_tar_location = "";
1283     legacy_source_untouchable = {};
1284     reject_message = "";
1285
1286     # Absolutize the filename to avoid the requirement of being in the
1287     # same directory as the .changes file.
1288     changes_file = os.path.abspath(changes_file);
1289
1290     # And since handling of installs to stable munges with the CWD;
1291     # save and restore it.
1292     cwd = os.getcwd();
1293     
1294     try:
1295         check_signature (changes_file);
1296         check_changes (changes_file);
1297         while reprocess:
1298             reprocess = 0;
1299             check_files ();
1300             check_md5sums ();
1301             check_dsc ();
1302             check_diff ();
1303     except:
1304         print "ERROR";
1305         traceback.print_exc(file=sys.stdout);
1306         pass;
1307         
1308     update_subst(changes_file);
1309     action(changes_file);
1310
1311     # Restore CWD
1312     os.chdir(cwd);
1313
1314 ###############################################################################
1315
1316 def main():
1317     global Cnf, Options, projectB, install_bytes, new_ack_old, Subst, nmu, Logger
1318
1319     apt_pkg.init();
1320     
1321     Cnf = apt_pkg.newConfiguration();
1322     apt_pkg.ReadConfigFileISC(Cnf,utils.which_conf_file());
1323
1324     Arguments = [('a',"automatic","Dinstall::Options::Automatic"),
1325                  ('d',"debug","Dinstall::Options::Debug", "IntVal"),
1326                  ('h',"help","Dinstall::Options::Help"),
1327                  ('k',"ack-new","Dinstall::Options::Ack-New"),
1328                  ('m',"manual-reject","Dinstall::Options::Manual-Reject", "HasArg"),
1329                  ('n',"no-action","Dinstall::Options::No-Action"),
1330                  ('p',"no-lock", "Dinstall::Options::No-Lock"),
1331                  ('s',"no-mail", "Dinstall::Options::No-Mail"),
1332                  ('u',"override-distribution", "Dinstall::Options::Override-Distribution", "HasArg"),
1333                  ('v',"version","Dinstall::Options::Version")];
1334     
1335     changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
1336     Options = Cnf.SubTree("Dinstall::Options")
1337
1338     if Options["Help"]:
1339         usage(0);
1340         
1341     if Options["Version"]:
1342         print "katie version 0.0000000000";
1343         usage(0);
1344
1345     postgresql_user = None; # Default == Connect as user running program.
1346
1347     # -n/--dry-run invalidates some other options which would involve things happening
1348     if Options["No-Action"]:
1349         Options["Automatic"] = "";
1350         Options["Ack-New"] = "";
1351         postgresql_user = Cnf["DB::ROUser"];
1352
1353     projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"]), None, None, postgresql_user);
1354
1355     db_access.init(Cnf, projectB);
1356
1357     # Check that we aren't going to clash with the daily cron job
1358
1359     if not Options["No-Action"] and os.path.exists("%s/Archive_Maintenance_In_Progress" % (Cnf["Dir::RootDir"])) and not Options["No-Lock"]:
1360         utils.fubar("Archive maintenance in progress.  Try again later.");
1361     
1362     # Obtain lock if not in no-action mode and initialize the log
1363
1364     if not Options["No-Action"]:
1365         lock_fd = os.open(Cnf["Dinstall::LockFile"], os.O_RDWR);
1366         fcntl.lockf(lock_fd, FCNTL.F_TLOCK);
1367         Logger = logging.Logger(Cnf, "katie");
1368
1369     # Read in the list of already-acknowledged NEW packages
1370     new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'r');
1371     new_ack_old = {};
1372     for line in new_ack_list.readlines():
1373         new_ack_old[line[:-1]] = 1;
1374     new_ack_list.close();
1375
1376     # Initialize the substitution template mapping global
1377     Subst = {}
1378     Subst["__ADMIN_ADDRESS__"] = Cnf["Dinstall::MyAdminAddress"];
1379     Subst["__BUG_SERVER__"] = Cnf["Dinstall::BugServer"];
1380     bcc = "X-Katie: $Revision: 1.55 $"
1381     if Cnf.has_key("Dinstall::Bcc"):
1382         Subst["__BCC__"] = bcc + "\nBcc: %s" % (Cnf["Dinstall::Bcc"]);
1383     else:
1384         Subst["__BCC__"] = bcc;
1385     Subst["__DISTRO__"] = Cnf["Dinstall::MyDistribution"];
1386     Subst["__KATIE_ADDRESS__"] = Cnf["Dinstall::MyEmailAddress"];
1387
1388     # Read in the group-maint override file
1389     nmu = nmu_p();
1390
1391     # Sort the .changes files so that we process sourceful ones first
1392     changes_files.sort(utils.changes_compare);
1393
1394     # Process the changes files
1395     for changes_file in changes_files:
1396         print "\n" + changes_file;
1397         process_it (changes_file);
1398
1399     if install_count:
1400         sets = "set"
1401         if install_count > 1:
1402             sets = "sets"
1403         sys.stderr.write("Installed %d package %s, %s.\n" % (install_count, sets, utils.size_type(int(install_bytes))));
1404         Logger.log(["total",install_count,install_bytes]);
1405
1406     # Write out the list of already-acknowledged NEW packages
1407     if Options["Ack-New"]:
1408         new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'w')
1409         for i in new_ack_new.keys():
1410             new_ack_list.write(i+'\n')
1411         new_ack_list.close()
1412     
1413     if not Options["No-Action"]:
1414         Logger.close();
1415             
1416 if __name__ == '__main__':
1417     main()
1418