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