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