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