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