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