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