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