]> git.decadent.org.uk Git - dak.git/blob - katie
silly typo + thinkos
[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.24 2001-01-27 03:51:14 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)
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     # Fix the Maintainer: field to be RFC822 compatible
186     (changes["maintainer822"], changes["maintainername"], changes["maintaineremail"]) = utils.fix_maintainer (changes["maintainer"])
187
188     # Override the Distribution: field if appropriate
189     if Cnf["Dinstall::Options::Override-Distribution"] != "":
190         reject_message = reject_message + "Warning: Distribution was overriden from %s to %s.\n" % (changes["distribution"], Cnf["Dinstall::Options::Override-Distribution"])
191         changes["distribution"] = Cnf["Dinstall::Options::Override-Distribution"]
192
193     # Split multi-value fields into a lower-level dictionary
194     for i in ("architecture", "distribution", "binary", "closes"):
195         o = changes.get(i, "")
196         if o != "":
197             del changes[i]
198         changes[i] = {}
199         for j in string.split(o):
200             changes[i][j] = 1
201
202     # Ensure all the values in Closes: are numbers
203     if changes.has_key("closes"):
204         for i in changes["closes"].keys():
205             if re_isanum.match (i) == None:
206                 reject_message = reject_message + "Rejected: `%s' from Closes field isn't a number.\n" % (i)
207
208     # Map frozen to unstable if frozen doesn't exist
209     if changes["distribution"].has_key("frozen") and not Cnf.has_key("Suite::Frozen"):
210         del changes["distribution"]["frozen"]
211         changes["distribution"]["unstable"] = 1;
212         reject_message = reject_message + "Mapping frozen to unstable.\n"
213
214     # Map testing to unstable
215     if changes["distribution"].has_key("testing"):
216         del changes["distribution"]["testing"]
217         changes["distribution"]["unstable"] = 1;
218         reject_message = reject_message + "Mapping testing to unstable.\n"
219
220     # Ensure target distributions exist
221     for i in changes["distribution"].keys():
222         if not Cnf.has_key("Suite::%s" % (i)):
223             reject_message = reject_message + "Rejected: Unknown distribution `%s'.\n" % (i)
224
225     # Ensure there _is_ a target distribution
226     if changes["distribution"].keys() == []:
227         reject_message = reject_message + "Rejected: huh? Distribution field is empty in changes file.\n";
228             
229     # Map unreleased arches from stable to unstable
230     if changes["distribution"].has_key("stable"):
231         for i in changes["architecture"].keys():
232             if not Cnf.has_key("Suite::Stable::Architectures::%s" % (i)):
233                 reject_message = reject_message + "Mapping stable to unstable for unreleased arch `%s'.\n" % (i)
234                 del changes["distribution"]["stable"]
235                 changes["distribution"]["unstable"] = 1;
236     
237     # Map arches not being released from frozen to unstable
238     if changes["distribution"].has_key("frozen"):
239         for i in changes["architecture"].keys():
240             if not Cnf.has_key("Suite::Frozen::Architectures::%s" % (i)):
241                 reject_message = reject_message + "Mapping frozen to unstable for non-releasing arch `%s'.\n" % (i)
242                 del changes["distribution"]["frozen"]
243                 changes["distribution"]["unstable"] = 1;
244
245     # Handle uploads to stable
246     if changes["distribution"].has_key("stable"):
247         # If running from within proposed-updates; assume an install to stable
248         if string.find(os.getcwd(), 'proposed-updates') != -1:
249             # FIXME: should probably remove anything that != stable
250             for i in ("frozen", "unstable"):
251                 if changes["distribution"].has_key(i):
252                     reject_message = reject_message + "Removing %s from distribution list.\n"
253                     del changes["distribution"][i]
254             changes["stable upload"] = 1;
255             # If we can't find a file from the .changes; assume it's a package already in the pool and move into the pool
256             file = files.keys()[0];
257             if os.access(file, os.R_OK) == 0:
258                 pool_dir = Cnf["Dir::PoolDir"] + '/' + utils.poolify(changes["source"], files[file]["component"]);
259                 os.chdir(pool_dir);
260         # Otherwise (normal case) map stable to updates
261         else:
262             reject_message = reject_message + "Mapping stable to updates.\n";
263             del changes["distribution"]["stable"];
264             changes["distribution"]["proposed-updates"] = 1;
265
266     # chopversion = no epoch; chopversion2 = no epoch and no revision (e.g. for .orig.tar.gz comparison)
267     changes["chopversion"] = utils.re_no_epoch.sub('', changes["version"])
268     changes["chopversion2"] = utils.re_no_revision.sub('', changes["chopversion"])
269     
270     if string.find(reject_message, "Rejected:") != -1:
271         return 0
272     else: 
273         return 1
274
275 def check_files():
276     global reject_message
277     
278     archive = utils.where_am_i();
279
280     for file in files.keys():
281         # Check the file is readable
282         if os.access(file,os.R_OK) == 0:
283             reject_message = reject_message + "Rejected: Can't read `%s'.\n" % (file)
284             files[file]["type"] = "unreadable";
285             continue
286         # If it's byhand skip remaining checks
287         if files[file]["section"] == "byhand":
288             files[file]["byhand"] = 1;
289             files[file]["type"] = "byhand";
290         # Checks for a binary package...
291         elif re_isadeb.match(file) != None:
292             files[file]["type"] = "deb";
293
294             # Extract package information using dpkg-deb
295             try:
296                 control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))
297             except:
298                 reject_message = reject_message + "Rejected: %s: debExtractControl() raised %s.\n" % (file, sys.exc_type);
299                 # Can't continue, none of the checks on control would work.
300                 continue;
301
302             # Check for mandatory fields
303             if control.Find("Package") == None:
304                 reject_message = reject_message + "Rejected: %s: No package field in control.\n" % (file)
305             if control.Find("Architecture") == None:
306                 reject_message = reject_message + "Rejected: %s: No architecture field in control.\n" % (file)
307             if control.Find("Version") == None:
308                 reject_message = reject_message + "Rejected: %s: No version field in control.\n" % (file)
309                 
310             # Ensure the package name matches the one give in the .changes
311             if not changes["binary"].has_key(control.Find("Package", "")):
312                 reject_message = reject_message + "Rejected: %s: control file lists name as `%s', which isn't in changes file.\n" % (file, control.Find("Package", ""))
313
314             # Validate the architecture
315             if not Cnf.has_key("Suite::Unstable::Architectures::%s" % (control.Find("Architecture", ""))):
316                 reject_message = reject_message + "Rejected: Unknown architecture '%s'.\n" % (control.Find("Architecture", ""))
317
318             # Check the architecture matches the one given in the .changes
319             if not changes["architecture"].has_key(control.Find("Architecture", "")):
320                 reject_message = reject_message + "Rejected: %s: control file lists arch as `%s', which isn't in changes file.\n" % (file, control.Find("Architecture", ""))
321             # Check the section & priority match those given in the .changes (non-fatal)
322             if control.Find("Section") != None and files[file]["section"] != "" and files[file]["section"] != control.Find("Section"):
323                 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"])
324             if control.Find("Priority") != None and files[file]["priority"] != "" and files[file]["priority"] != control.Find("Priority"):
325                 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"])
326
327             epochless_version = utils.re_no_epoch.sub('', control.Find("Version", ""))
328
329             files[file]["package"] = control.Find("Package");
330             files[file]["architecture"] = control.Find("Architecture");
331             files[file]["version"] = control.Find("Version");
332             files[file]["maintainer"] = control.Find("Maintainer", "");
333             if file[-5:] == ".udeb":
334                 files[file]["dbtype"] = "udeb";
335             elif file[-4:] == ".deb":
336                 files[file]["dbtype"] = "deb";
337             else:
338                 reject_message = reject_message + "Rejected: %s is neither a .deb or a .udeb.\n " % (file);
339             files[file]["fullname"] = "%s_%s_%s.deb" % (control.Find("Package", ""), epochless_version, control.Find("Architecture", ""))
340             files[file]["source"] = control.Find("Source", "");
341             if files[file]["source"] == "":
342                 files[file]["source"] = files[file]["package"];
343         # Checks for a source package...
344         else:
345             m = re_issource.match(file)
346             if m != None:
347                 files[file]["package"] = m.group(1)
348                 files[file]["version"] = m.group(2)
349                 files[file]["type"] = m.group(3)
350                 
351                 # Ensure the source package name matches the Source filed in the .changes
352                 if changes["source"] != files[file]["package"]:
353                     reject_message = reject_message + "Rejected: %s: changes file doesn't say %s for Source\n" % (file, files[file]["package"])
354
355                 # Ensure the source version matches the version in the .changes file
356                 if files[file]["type"] == "orig.tar.gz":
357                     changes_version = changes["chopversion2"]
358                 else:
359                     changes_version = changes["chopversion"]
360                 if changes_version != files[file]["version"]:
361                     reject_message = reject_message + "Rejected: %s: should be %s according to changes file.\n" % (file, changes_version)
362
363                 # Ensure the .changes lists source in the Architecture field
364                 if not changes["architecture"].has_key("source"):
365                     reject_message = reject_message + "Rejected: %s: changes file doesn't list `source' in Architecture field.\n" % (file)
366
367                 # Check the signature of a .dsc file
368                 if files[file]["type"] == "dsc":
369                     check_signature(file)
370
371                 files[file]["fullname"] = file
372
373             # Not a binary or source package?  Assume byhand...
374             else:
375                 files[file]["byhand"] = 1;
376                 files[file]["type"] = "byhand";
377
378         files[file]["oldfiles"] = {}
379         for suite in changes["distribution"].keys():
380             # Skip byhand
381             if files[file].has_key("byhand"):
382                 continue
383
384             if Cnf.has_key("Suite:%s::Components" % (suite)) and not Cnf.has_key("Suite::%s::Components::%s" % (suite, files[file]["component"])):
385                 reject_message = reject_message + "Rejected: unknown component `%s' for suite `%s'.\n" % (files[file]["component"], suite)
386                 continue
387
388             # See if the package is NEW
389             if not in_override_p(files[file]["package"], files[file]["component"], suite, files[file].get("dbtype",""), file):
390                 files[file]["new"] = 1
391                 
392             # Find any old binary packages
393             if files[file]["type"] == "deb":
394                 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"
395                                    % (files[file]["package"], suite, files[file]["architecture"]))
396                 oldfiles = q.dictresult()
397                 for oldfile in oldfiles:
398                     files[file]["oldfiles"][suite] = oldfile
399                     # Check versions [NB: per-suite only; no cross-suite checking done (yet)]
400                     if apt_pkg.VersionCompare(files[file]["version"], oldfile["version"]) != 1:
401                         reject_message = reject_message + "Rejected: %s Old version `%s' >= new version `%s'.\n" % (file, oldfile["version"], files[file]["version"])
402                 # Check for existing copies of the file
403                 if not changes.has_key("stable upload"):
404                     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"]))
405                     if q.getresult() != []:
406                         reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (file)
407
408             # Find any old .dsc files
409             elif files[file]["type"] == "dsc":
410                 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"
411                                    % (files[file]["package"], suite))
412                 oldfiles = q.dictresult()
413                 if len(oldfiles) >= 1:
414                     files[file]["oldfiles"][suite] = oldfiles[0]
415
416             # Validate the component
417             component = files[file]["component"];
418             component_id = db_access.get_component_id(component);
419             if component_id == -1:
420                 reject_message = reject_message + "Rejected: file '%s' has unknown component '%s'.\n" % (file, component);
421                 continue;
422             
423             # Check the md5sum & size against existing files (if any)
424             location = Cnf["Dir::PoolDir"];
425             files[file]["location id"] = db_access.get_location_id (location, component, archive);
426
427             files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"]);
428             files_id = db_access.get_files_id(files[file]["pool name"] + file, files[file]["size"], files[file]["md5sum"], files[file]["location id"]);
429             if files_id == -1:
430                 reject_message = reject_message + "Rejected: INTERNAL ERROR, get_files_id() returned multiple matches for %s.\n" % (file)
431             elif files_id == -2:
432                 reject_message = reject_message + "Rejected: md5sum and/or size mismatch on existing copy of %s.\n" % (file)
433             files[file]["files id"] = files_id
434
435             # Check for packages that have moved from one component to another
436             if files[file]["oldfiles"].has_key(suite) and files[file]["oldfiles"][suite]["name"] != files[file]["component"]:
437                 files[file]["othercomponents"] = files[file]["oldfiles"][suite]["name"];
438
439                 
440     if string.find(reject_message, "Rejected:") != -1:
441         return 0
442     else: 
443         return 1
444
445 ###############################################################################
446
447 def check_dsc ():
448     global dsc, dsc_files, reject_message, reprocess, orig_tar_id, legacy_source_untouchable;
449
450     for file in files.keys():
451         if files[file]["type"] == "dsc":
452             try:
453                 dsc = utils.parse_changes(file)
454             except utils.cant_open_exc:
455                 reject_message = reject_message + "Rejected: can't read changes file '%s'.\n" % (filename)
456                 return 0;
457             except utils.changes_parse_error_exc, line:
458                 reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line)
459                 return 0;
460             try:
461                 dsc_files = utils.build_file_list(dsc, 1)
462             except utils.no_files_exc:
463                 reject_message = reject_message + "Rejected: no Files: field in .dsc file.\n";
464                 continue;
465             except utils.changes_parse_error_exc, line:
466                 reject_message = "Rejected: error parsing .dsc file '%s', can't grok: %s.\n" % (filename, line);
467                 continue;
468
469             # Try and find all files mentioned in the .dsc.  This has
470             # to work harder to cope with the multiple possible
471             # locations of an .orig.tar.gz.
472             for dsc_file in dsc_files.keys():
473                 if files.has_key(dsc_file):
474                     actual_md5 = files[dsc_file]["md5sum"];
475                     actual_size = int(files[dsc_file]["size"]);
476                     found = "%s in incoming" % (dsc_file)
477                     # Check the file does not already exist in the archive
478                     if not changes.has_key("stable upload"):
479                         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));
480
481                         # "It has not broken them.  It has fixed a
482                         # brokenness.  Your crappy hack exploited a
483                         # bug in the old dinstall.
484                         #
485                         # "(Come on!  I thought it was always obvious
486                         # that one just doesn't release different
487                         # files with the same name and version.)"
488                         #                        -- ajk@ on d-devel@l.d.o
489
490                         if q.getresult() != []:
491                             reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (dsc_file)
492                 elif dsc_file[-12:] == ".orig.tar.gz":
493                     # Check in the pool
494                     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));
495                     ql = q.getresult();
496
497
498                     if ql != []:
499                         # Unfortunately, we make get more than one match
500                         # here if, for example, the package was in potato
501                         # but had a -sa upload in woody.  So we need to a)
502                         # choose the right one and b) mark all wrong ones
503                         # as excluded from the source poolification (to
504                         # avoid file overwrites).
505
506                         x = ql[0]; # default to something sane in case we don't match any or have only one
507
508                         if len(ql) > 1:
509                             for i in ql:
510                                 old_file = i[0] + i[1];
511                                 actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
512                                 actual_size = os.stat(old_file)[stat.ST_SIZE];
513                                 if actual_md5 == dsc_files[dsc_file]["md5sum"] and actual_size == int(dsc_files[dsc_file]["size"]):
514                                     x = i;
515                                 else:
516                                     legacy_source_untouchable[i[3]] = "";
517
518                         old_file = x[0] + x[1];
519                         actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
520                         actual_size = os.stat(old_file)[stat.ST_SIZE];
521                         found = old_file;
522                         suite_type = x[2];
523                         dsc_files[dsc_file]["files id"] = x[3]; # need this for updating dsc_files in install()
524                         # See install()...
525                         if suite_type == "legacy" or suite_type == "legacy-mixed":
526                             orig_tar_id = x[3];
527                     else:
528                         # Not there? Check in Incoming...
529                         # [See comment above process_it() for explanation
530                         #  of why this is necessary...]
531                         if os.access(dsc_file, os.R_OK) != 0:
532                             files[dsc_file] = {};
533                             files[dsc_file]["size"] = os.stat(dsc_file)[stat.ST_SIZE];
534                             files[dsc_file]["md5sum"] = dsc_files[dsc_file]["md5sum"];
535                             files[dsc_file]["section"] = files[file]["section"];
536                             files[dsc_file]["priority"] = files[file]["priority"];
537                             files[dsc_file]["component"] = files[file]["component"];
538                             files[dsc_file]["type"] = "orig.tar.gz";
539                             reprocess = 1;
540                             return 1;
541                         else:
542                             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);
543                             continue;
544                 else:
545                     reject_message = reject_message + "Rejected: %s refers to %s, but I can't find it in Incoming.\n" % (file, dsc_file);
546                     continue;
547                 if actual_md5 != dsc_files[dsc_file]["md5sum"]:
548                     reject_message = reject_message + "Rejected: md5sum for %s doesn't match %s.\n" % (found, file);
549                 if actual_size != int(dsc_files[dsc_file]["size"]):
550                     reject_message = reject_message + "Rejected: size for %s doesn't match %s.\n" % (found, file);
551
552     if string.find(reject_message, "Rejected:") != -1:
553         return 0
554     else: 
555         return 1
556
557 ###############################################################################
558
559 # Some cunning stunt broke dpkg-source in dpkg 1.8{,.1}; detect the
560 # resulting bad source packages and reject them.
561
562 # Even more amusingly the fix in 1.8.1.1 didn't actually fix the
563 # problem just changed the symptoms.
564
565 def check_diff ():
566     global dsc, dsc_files, reject_message, reprocess, orig_tar_id;
567
568     for filename in files.keys():
569         if files[filename]["type"] == "diff.gz":
570             file = gzip.GzipFile(filename, 'r');
571             for line in file.readlines():
572                 if re_bad_diff.search(line):
573                     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";
574                     break;
575
576     if string.find(reject_message, "Rejected:") != -1:
577         return 0
578     else: 
579         return 1
580
581 ###############################################################################
582
583 def check_md5sums ():
584     global reject_message;
585
586     for file in files.keys():
587         try:
588             file_handle = utils.open_file(file,"r");
589         except utils.cant_open_exc:
590             pass;
591         else:
592             if apt_pkg.md5sum(file_handle) != files[file]["md5sum"]:
593                 reject_message = reject_message + "Rejected: md5sum check failed for %s.\n" % (file);
594
595 def check_override ():
596     # Only check section & priority on sourceful uploads
597     if not changes["architecture"].has_key("source"):
598         return;
599
600     summary = ""
601     for file in files.keys():
602         if not files[file].has_key("new") and (files[file]["type"] == "dsc" or files[file]["type"] == "deb"):
603             section = files[file]["section"];
604             override_section = files[file]["override section"];
605             if section != override_section and section != "-":
606                 # Ignore this; it's a common mistake and not worth whining about
607                 if section == "non-US/main" and override_section == "non-US":
608                     continue;
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:
985                 sys.stderr.write("W: couldn't reject file '%s' [Got %s].\n" % (file, sys.exc_type));
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, reject_message;
1188
1189     # Reset some globals
1190     reprocess = 1;
1191     changes = {};
1192     dsc = {};
1193     dsc_files = {};
1194     files = {};
1195     orig_tar_id = None;
1196     legacy_source_untouchable = {};
1197     reject_message = "";
1198     orig_tar_id = None;
1199
1200     # Absolutize the filename to avoid the requirement of being in the
1201     # same directory as the .changes file.
1202     changes_file = os.path.abspath(changes_file);
1203
1204     # And since handling of installs to stable munges with the CWD;
1205     # save and restore it.
1206     cwd = os.getcwd();
1207     
1208     check_signature (changes_file);
1209     check_changes (changes_file);
1210     while reprocess:
1211         reprocess = 0;
1212         check_files ();
1213         check_md5sums ();
1214         check_dsc ();
1215         check_diff ();
1216         
1217     action(changes_file);
1218
1219     # Restore CWD
1220     os.chdir(cwd);
1221
1222 ###############################################################################
1223
1224 def main():
1225     global Cnf, projectB, install_bytes, new_ack_old
1226
1227     apt_pkg.init();
1228     
1229     Cnf = apt_pkg.newConfiguration();
1230     apt_pkg.ReadConfigFileISC(Cnf,utils.which_conf_file());
1231
1232     Arguments = [('a',"automatic","Dinstall::Options::Automatic"),
1233                  ('d',"debug","Dinstall::Options::Debug", "IntVal"),
1234                  ('h',"help","Dinstall::Options::Help"),
1235                  ('k',"ack-new","Dinstall::Options::Ack-New"),
1236                  ('m',"manual-reject","Dinstall::Options::Manual-Reject", "HasArg"),
1237                  ('n',"no-action","Dinstall::Options::No-Action"),
1238                  ('p',"no-lock", "Dinstall::Options::No-Lock"),
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         print "\n" + changes_file;
1286         process_it (changes_file);
1287
1288     if install_count:
1289         sets = "set"
1290         if install_count > 1:
1291             sets = "sets"
1292         sys.stderr.write("Installed %d package %s, %s.\n" % (install_count, sets, utils.size_type(int(install_bytes))));
1293
1294     # Write out the list of already-acknowledged NEW packages
1295     if Cnf["Dinstall::Options::Ack-New"]:
1296         new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'w')
1297         for i in new_ack_new.keys():
1298             new_ack_list.write(i+'\n')
1299         new_ack_list.close()
1300     
1301             
1302 if __name__ == '__main__':
1303     main()
1304