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