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