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