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