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