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