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