]> git.decadent.org.uk Git - dak.git/blob - katie
Initial revision
[dak.git] / katie
1 #!/usr/bin/env python
2
3 # Installs Debian packaes
4 # Copyright (C) 2000  James Troup <james@nocrew.org>
5 # $Id: katie,v 1.1.1.1 2000-11-24 00:20:08 troup Exp $
6
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20
21 # Based (almost entirely) on dinstall by Guy Maor <maor@debian.org>
22
23 #########################################################################################
24
25 #    Cartman: "I'm trying to make the best of a bad situation, I don't
26 #              need to hear crap from a bunch of hippy freaks living in
27 #              denial.  Screw you guys, I'm going home."
28 #
29 #    Kyle: "But Cartman, we're trying to..."
30 #
31 #    Cartman: "uhh.. screw you guys... home."
32
33 #########################################################################################
34
35 import FCNTL, commands, fcntl, getopt, os, pg, pwd, re, shutil, stat, string, sys, tempfile, time
36 import apt_inst, apt_pkg
37 import utils, db_access
38
39 ###############################################################################
40
41 re_isanum = re.compile (r'^\d+$');
42 re_isadeb = re.compile (r'.*\.u?deb$');
43 re_issource = re.compile (r'(.+)_(.+?)\.(orig\.tar\.gz|diff\.gz|tar\.gz|dsc)');
44 re_dpackage = re.compile (r'^package:\s*(.*)', re.IGNORECASE);
45 re_darchitecture = re.compile (r'^architecture:\s*(.*)', re.IGNORECASE);
46 re_dversion = re.compile (r'^version:\s*(.*)', re.IGNORECASE);
47 re_dsection = re.compile (r'^section:\s*(.*)', re.IGNORECASE);
48 re_dpriority = re.compile (r'^priority:\s*(.*)', re.IGNORECASE);
49 re_changes = re.compile (r'changes$');
50 re_override_package = re.compile(r'(\S*)\s+.*');
51 re_default_answer = re.compile(r"\[(.*)\]");
52 re_fdnic = re.compile("\n\n");
53
54 ###############################################################################
55
56 #
57 reject_footer = """If you don't understand why your files were rejected, or if the
58 override file requires editing, reply to this email.
59
60 Your rejected files are in incoming/REJECT/.  (Some may also be in
61 incoming/ if your .changes file was unparsable.)  If only some of the
62 files need to repaired, you may move any good files back to incoming/.
63 Please remove any bad files from incoming/REJECT/."""
64 #
65 new_ack_footer = """Your package contains new components which requires manual editing of
66 the override file.  It is ok otherwise, so please be patient.  New
67 packages are usually added to the override file about once a week.
68
69 You may have gotten the distribution wrong.  You'll get warnings above
70 if files already exist in other distributions."""
71 #
72 installed_footer = """If the override file requires editing, file a bug on ftp.debian.org.
73
74 Thank you for your contribution to Debian GNU."""
75
76 #########################################################################################
77
78 # Globals
79 Cnf = None;
80 reject_message = "";
81 changes = {};
82 dsc = {};
83 dsc_files = {};
84 files = {};
85 projectB = None;
86 new_ack_new = {};
87 new_ack_old = {};
88 overrides = {};
89 install_count = 0;
90 install_bytes = 0.0;
91 reprocess = 0;
92 orig_tar_id = None;
93
94 #########################################################################################
95
96 def usage (exit_code):
97     print """Usage: dinstall [OPTION]... [CHANGES]...
98   -a, --automatic           automatic run
99   -d, --debug=VALUE         debug
100   -k, --ack-new             acknowledge new packages
101   -m, --manual-reject=MSG   manual reject with `msg'
102   -n, --dry-run             don't do anything
103   -p, --no-lock             don't check lockfile !! for cron.daily only !!
104   -r, --no-version-check    override version check
105   -u, --distribution=DIST   override distribution to `dist'"""
106     sys.exit(exit_code)
107
108 def check_signature (filename):
109     global reject_message
110
111     (result, output) = commands.getstatusoutput("gpg --emulate-md-encode-bug --batch --no-options --no-default-keyring --always-trust --load-extension rsaref --keyring=%s --keyring=%s < %s >/dev/null" % (Cnf["Dinstall::PGPKeyring"], Cnf["Dinstall::GPGKeyring"], filename))
112     if (result != 0):
113         reject_message = "Rejected: GPG signature check failed on `%s'.\n%s\n" % (filename, output)
114         return 0
115     return 1
116
117 #####################################################################################################################
118
119 def read_override_file (filename, suite, component):
120     global overrides;
121     
122     file = utils.open_file(filename, 'r');
123     for line in file.readlines():
124         line = string.strip(utils.re_comments.sub('', line))
125         override_package = re_override_package.sub(r'\1', line)
126         if override_package != "":
127             overrides[suite][component][override_package] = 1
128     file.close()
129
130
131 # See if a given package is in the override file.  Caches and only loads override files on demand.
132
133 def in_override_p (package, component, suite):
134     global overrides;
135
136     # FIXME: nasty non-US speficic hack
137     if string.lower(component[:7]) == "non-us/":
138         component = component[7:];
139     if not overrides.has_key(suite) or not overrides[suite].has_key(component):
140         if not overrides.has_key(suite):
141             overrides[suite] = {}
142         if not overrides[suite].has_key(component):
143             overrides[suite][component] = {}
144         if Cnf.has_key("Suite::%s::SingleOverrideFile" % (suite)): # legacy mixed suite (i.e. experimental)
145             override_filename = Cnf["Dir::OverrideDir"] + 'override.' + Cnf["Suite::%s::OverrideCodeName" % (suite)];
146             read_override_file (override_filename, suite, component);
147         else: # all others.
148             for src in ("", ".src"):
149                 override_filename = Cnf["Dir::OverrideDir"] + 'override.' + Cnf["Suite::%s::OverrideCodeName" % (suite)] + '.' + component + src;
150                 read_override_file (override_filename, suite, component);
151
152     return overrides[suite][component].get(package, None);
153
154 #####################################################################################################################
155
156 def check_changes(filename):
157     global reject_message, changes, files
158
159     # Parse the .changes field into a dictionary [FIXME - need to trap errors, pass on to reject_message etc.]
160     try:
161         changes = utils.parse_changes(filename)
162     except utils.cant_open_exc:
163         reject_message = "Rejected: can't read changes file '%s'.\n" % (filename)
164         return 0;
165     except utils.changes_parse_error_exc, line:
166         reject_message = "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line)
167         changes["maintainer822"] = Cnf["Dinstall::MyEmailAddress"];
168         return 0;
169
170     # Parse the Files field from the .changes into another dictionary [FIXME need to trap errors as above]
171     files = utils.build_file_list(changes, "")
172
173     # Check for mandatory fields
174     for i in ("source", "binary", "architecture", "version", "distribution","maintainer", "files"):
175         if not changes.has_key(i):
176             reject_message = "Rejected: Missing field `%s' in changes file." % (i)
177             return 0    # Avoid <undef> errors during later tests
178
179     # Fix the Maintainer: field to be RFC822 compatible
180     (changes["maintainer822"], changes["maintainername"], changes["maintaineremail"]) = utils.fix_maintainer (changes["maintainer"])
181
182     # Override the Distribution: field if appropriate
183     if Cnf["Dinstall::Options::Override-Distribution"] != "":
184         reject_message = reject_message + "Warning: Distribution was overriden from %s to %s.\n" % (changes["distribution"], Cnf["Dinstall::Options::Override-Distribution"])
185         changes["distribution"] = Cnf["Dinstall::Options::Override-Distribution"]
186
187     # Split multi-value fields into a lower-level dictionary
188     for i in ("architecture", "distribution", "binary", "closes"):
189         o = changes.get(i, "")
190         if o != "":
191             del changes[i]
192         changes[i] = {}
193         for j in string.split(o):
194             changes[i][j] = 1
195
196     # Ensure all the values in Closes: are numbers
197     if changes.has_key("closes"):
198         for i in changes["closes"].keys():
199             if re_isanum.match (i) == None:
200                 reject_message = reject_message + "Rejected: `%s' from Closes field isn't a number.\n" % (i)
201
202
203     # Map frozen to unstable if frozen doesn't exist
204     if changes["distribution"].has_key("frozen") and not Cnf.has_key("Suite::Frozen"):
205         del changes["distribution"]["frozen"]
206         reject_message = reject_message + "Mapping frozen 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     # Map unreleased arches from stable to unstable
214     if changes["distribution"].has_key("stable"):
215         for i in changes["architecture"].keys():
216             if not Cnf.has_key("Suite::Stable::Architectures::%s" % (i)):
217                 reject_message = reject_message + "Mapping stable to unstable for unreleased arch `%s'.\n" % (i)
218                 del changes["distribution"]["stable"]
219     
220     # Map arches not being released from frozen to unstable
221     if changes["distribution"].has_key("frozen"):
222         for i in changes["architecture"].keys():
223             if not Cnf.has_key("Suite::Frozen::Architectures::%s" % (i)):
224                 reject_message = reject_message + "Mapping frozen to unstable for non-releasing arch `%s'.\n" % (i)
225                 del changes["distribution"]["frozen"]
226
227     # Handle uploads to stable
228     if changes["distribution"].has_key("stable"):
229         # If running from within proposed-updates kill non-stable distributions
230         if string.find(os.getcwd(), 'proposed-updates') != -1:
231             for i in ("frozen", "unstable"):
232                 if changes["distributions"].has_key(i):
233                     reject_message = reject_message + "Removing %s from distribution list.\n"
234                     del changes["distribution"][i]
235         # Otherwise (normal case) map stable to updates
236         else:
237             reject_message = reject_message + "Mapping stable to updates.\n";
238             del changes["distribution"]["stable"];
239             changes["distribution"]["proposed-updates"] = 1;
240
241     # chopversion = no epoch; chopversion2 = no epoch and no revision (e.g. for .orig.tar.gz comparison)
242     changes["chopversion"] = utils.re_no_epoch.sub('', changes["version"])
243     changes["chopversion2"] = utils.re_no_revision.sub('', changes["chopversion"])
244     
245     if string.find(reject_message, "Rejected:") != -1:
246         return 0
247     else: 
248         return 1
249
250 def check_files():
251     global reject_message
252     
253     archive = utils.where_am_i();
254
255     for file in files.keys():
256         # Check the file is readable
257         if os.access(file,os.R_OK) == 0:
258             reject_message = reject_message + "Rejected: Can't read `%s'.\n" % (file)
259             files[file]["type"] = "unreadable";
260             continue
261         # If it's byhand skip remaining checks
262         if files[file]["section"] == "byhand":
263             files[file]["byhand"] = 1;
264             files[file]["type"] = "byhand";
265         # Checks for a binary package...
266         elif re_isadeb.match(file) != None:
267             # Extract package information using dpkg-deb
268             control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))
269
270             # Check for mandatory fields
271             if control.Find("Package") == None:
272                 reject_message = reject_message + "Rejected: %s: No package field in control.\n" % (file)
273             if control.Find("Architecture") == None:
274                 reject_message = reject_message + "Rejected: %s: No architecture field in control.\n" % (file)
275             if control.Find("Version") == None:
276                 reject_message = reject_message + "Rejected: %s: No version field in control.\n" % (file)
277                 
278             # Ensure the package name matches the one give in the .changes
279             if not changes["binary"].has_key(control.Find("Package", "")):
280                 reject_message = reject_message + "Rejected: %s: control file lists name as `%s', which isn't in changes file.\n" % (file, control.Find("Package", ""))
281
282             # Validate the architecture
283             if not Cnf.has_key("Suite::Unstable::Architectures::%s" % (control.Find("Architecture", ""))):
284                 reject_message = reject_message + "Rejected: Unknown architecture '%s'.\n" % (control.Find("Architecture", ""))
285
286             # Check the architecture matches the one given in the .changes
287             if not changes["architecture"].has_key(control.Find("Architecture", "")):
288                 reject_message = reject_message + "Rejected: %s: control file lists arch as `%s', which isn't in changes file.\n" % (file, control.Find("Architecture", ""))
289             # Check the section & priority match those given in the .changes (non-fatal)
290             if control.Find("Section") != None and files[file]["section"] != "" and files[file]["section"] != control.Find("Section"):
291                 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"])
292             if control.Find("Priority") != None and files[file]["priority"] != "" and files[file]["priority"] != control.Find("Priority"):
293                 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"])
294
295             epochless_version = utils.re_no_epoch.sub('', control.Find("Version", ""))
296
297             files[file]["package"] = control.Find("Package");
298             files[file]["architecture"] = control.Find("Architecture");
299             files[file]["version"] = control.Find("Version");
300             files[file]["maintainer"] = control.Find("Maintainer", "");
301             if file[-5:] == ".udeb":
302                 files[file]["dbtype"] = "udeb";
303             elif file[-4:] == ".deb":
304                 files[file]["dbtype"] = "deb";
305             else:
306                 reject_message = reject_message + "Rejected: %s is neither a .deb or a .udeb.\n " % (file);
307             files[file]["type"] = "deb";
308             files[file]["fullname"] = "%s_%s_%s.deb" % (control.Find("Package", ""), epochless_version, control.Find("Architecture", ""))
309             files[file]["source"] = control.Find("Source", "");
310             if files[file]["source"] == "":
311                 files[file]["source"] = files[file]["package"];
312         # Checks for a source package...
313         else:
314             m = re_issource.match(file)
315             if m != None:
316                 files[file]["package"] = m.group(1)
317                 files[file]["version"] = m.group(2)
318                 files[file]["type"] = m.group(3)
319                 
320                 # Ensure the source package name matches the Source filed in the .changes
321                 if changes["source"] != files[file]["package"]:
322                     reject_message = reject_message + "Rejected: %s: changes file doesn't say %s for Source\n" % (file, files[file]["package"])
323
324                 # Ensure the source version matches the version in the .changes file
325                 if files[file]["type"] == "orig.tar.gz":
326                     changes_version = changes["chopversion2"]
327                 else:
328                     changes_version = changes["chopversion"]
329                 if changes_version != files[file]["version"]:
330                     reject_message = reject_message + "Rejected: %s: should be %s according to changes file.\n" % (file, changes_version)
331
332                 # Ensure the .changes lists source in the Architecture field
333                 if not changes["architecture"].has_key("source"):
334                     reject_message = reject_message + "Rejected: %s: changes file doesn't list `source' in Architecture field.\n" % (file)
335
336                 # Check the signature of a .dsc file
337                 if files[file]["type"] == "dsc":
338                     check_signature(file)
339
340                 files[file]["fullname"] = file
341
342             # Not a binary or source package?  Assume byhand...
343             else:
344                 files[file]["byhand"] = 1;
345                 files[file]["type"] = "byhand";
346                 
347         files[file]["oldfiles"] = {}
348         for suite in changes["distribution"].keys():
349             # Skip byhand
350             if files[file].has_key("byhand"):
351                 continue
352
353             if Cnf.has_key("Suite:%s::Components" % (suite)) and not Cnf.has_key("Suite::%s::Components::%s" % (suite, files[file]["component"])):
354                 reject_message = reject_message + "Rejected: unknown component `%s' for suite `%s'.\n" % (files[file]["component"], suite)
355                 continue
356
357             # See if the package is NEW
358             if not in_override_p(files[file]["package"], files[file]["component"], suite):
359                 files[file]["new"] = 1
360                 
361             # Find any old binary packages
362             if files[file]["type"] == "deb":
363                 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"
364                                    % (files[file]["package"], suite, files[file]["architecture"]))
365                 oldfiles = q.dictresult()
366                 for oldfile in oldfiles:
367                     files[file]["oldfiles"][suite] = oldfile
368                     # Check versions [NB: per-suite only; no cross-suite checking done (yet)]
369                     if apt_pkg.VersionCompare(files[file]["version"], oldfile["version"]) != 1:
370                         if Cnf["Dinstall::Options::No-Version-Check"]:
371                             reject_message = reject_message + "Overriden rejection"
372                         else:
373                             reject_message = reject_message + "Rejected"
374                         reject_message = reject_message + ": %s Old version `%s' >= new version `%s'.\n" % (file, oldfile["version"], files[file]["version"])
375             # Find any old .dsc files
376             elif files[file]["type"] == "dsc":
377                 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"
378                                    % (files[file]["package"], suite))
379                 oldfiles = q.dictresult()
380                 if len(oldfiles) >= 1:
381                     files[file]["oldfiles"][suite] = oldfiles[0]
382
383             # Validate the component
384             component = files[file]["component"];
385             component_id = db_access.get_component_id(component);
386             if component_id == -1:
387                 reject_message = reject_message + "Rejected: file '%s' has unknown component '%s'.\n" % (file, component);
388                 continue;
389             
390             # Check the md5sum & size against existing files (if any)
391             location = Cnf["Dir::PoolDir"];
392             files[file]["location id"] = db_access.get_location_id (location, component, archive);
393             files_id = db_access.get_files_id(component + '/' + file, files[file]["size"], files[file]["md5sum"], files[file]["location id"]);
394             if files_id == -1:
395                 reject_message = reject_message + "Rejected: INTERNAL ERROR, get_files_id() returned multiple matches for %s.\n" % (file)
396             elif files_id == -2:
397                 reject_message = reject_message + "Rejected: md5sum and/or size mismatch on existing copy of %s.\n" % (file)
398             files[file]["files id"] = files_id
399
400             # Check for packages that have moved from one component to another
401             if files[file]["oldfiles"].has_key(suite) and files[file]["oldfiles"][suite]["name"] != files[file]["component"]:
402                 files[file]["othercomponents"] = files[file]["oldfiles"][suite]["name"];
403
404                 
405     if string.find(reject_message, "Rejected:") != -1:
406         return 0
407     else: 
408         return 1
409
410 ###############################################################################
411
412 def check_dsc ():
413     global dsc, dsc_files, reject_message, reprocess, orig_tar_id;
414     
415     for file in files.keys():
416         if files[file]["type"] == "dsc":
417             try:
418                 dsc = utils.parse_changes(file)
419             except utils.cant_open_exc:
420                 reject_message = reject_message + "Rejected: can't read changes file '%s'.\n" % (filename)
421                 return 0;
422             except utils.changes_parse_error_exc, line:
423                 reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line)
424                 return 0;
425             try:
426                 dsc_files = utils.build_file_list(dsc, 1)
427             except utils.no_files_exc:
428                 reject_message = reject_message + "Rejected: no Files: field in .dsc file.\n";
429                 continue;
430
431             # Try and find all files mentioned in the .dsc.  This has
432             # to work harder to cope with the multiple possible
433             # locations of an .orig.tar.gz.
434             for dsc_file in dsc_files.keys():
435                 if files.has_key(dsc_file):
436                     actual_md5 = files[dsc_file]["md5sum"]
437                     found = "%s in incoming" % (dsc_file)
438                 elif dsc_file[-12:] == ".orig.tar.gz":
439                     # Check in Incoming
440                     # See comment above process_it() for explanation...
441                     if os.access(dsc_file, os.R_OK) != 0:
442                         files[dsc_file] = {};
443                         files[dsc_file]["size"] = os.stat(dsc_file)[stat.ST_SIZE];
444                         files[dsc_file]["md5sum"] = dsc_files[dsc_file]["md5sum"];
445                         files[dsc_file]["section"] = files[file]["section"];
446                         files[dsc_file]["priority"] = files[file]["priority"];
447                         files[dsc_file]["component"] = files[file]["component"];
448                         reprocess = 1;
449                         return 1;
450                     # Check in the pool
451                     q = projectB.query("SELECT l.path, f.filename, l.type, f.id FROM files f, location l WHERE f.filename ~ '/%s' AND l.id = f.location" % (dsc_file));
452                     ql = q.getresult();
453                     if len(ql) > 0:
454                         old_file = ql[0][0] + ql[0][1];
455                         actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
456                         found = old_file;
457                         suite_type = ql[0][2];
458                         # See install()...
459                         if suite_type == "legacy" or suite_type == "legacy-mixed":
460                             orig_tar_id = ql[0][3];
461                     else:
462                         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);
463                         continue;
464                 else:
465                     reject_message = reject_message + "Rejected: %s refers to %s, but I can't find it in Incoming." % (file, dsc_file);
466                     continue;
467                 if actual_md5 != dsc_files[dsc_file]["md5sum"]:
468                     reject_message = reject_message + "Rejected: md5sum for %s doesn't match %s.\n" % (found, file)
469                 
470     if string.find(reject_message, "Rejected:") != -1:
471         return 0
472     else: 
473         return 1
474
475 ###############################################################################
476
477 def check_md5sums ():
478     global reject_message;
479
480     for file in files.keys():
481         try:
482             file_handle = utils.open_file(file,"r");
483         except utils.cant_open_exc:
484             pass;
485         else:
486             if apt_pkg.md5sum(file_handle) != files[file]["md5sum"]:
487                 reject_message = reject_message + "Rejected: md5sum check failed for %s.\n" % (file);
488
489 #####################################################################################################################
490
491 def action (changes_filename):
492     byhand = confirm = suites = summary = new = "";
493
494     # changes["distribution"] may not exist in corner cases
495     # (e.g. unreadable changes files)
496     if not changes.has_key("distribution"):
497         changes["distribution"] = {};
498     
499     for suite in changes["distribution"].keys():
500         if Cnf.has_key("Suite::%s::Confirm"):
501             confirm = confirm + suite + ", "
502         suites = suites + suite + ", "
503     confirm = confirm[:-2]
504     suites = suites[:-2]
505
506     for file in files.keys():
507         if files[file].has_key("byhand"):
508             byhand = 1
509             summary = summary + file + " byhand\n"
510         elif files[file].has_key("new"):
511             new = 1
512             summary = summary + "(new) %s %s %s\n" % (file, files[file]["priority"], files[file]["section"])
513             if files[file].has_key("othercomponents"):
514                 summary = summary + "WARNING: Already present in %s distribution.\n" % (files[file]["othercomponents"])
515             if files[file]["type"] == "deb":
516                 summary = summary + apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))["Description"] + '\n';
517         else:
518             files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"])
519             destination = Cnf["Dir::PoolRoot"] + files[file]["pool name"] + file
520             summary = summary + file + "\n  to " + destination + "\n"
521
522     short_summary = summary;
523
524     # This is for direport's benefit...
525     f = re_fdnic.sub("\n .\n", changes.get("changes",""));
526
527     if confirm or byhand or new:
528         summary = summary + "Changes: " + f;
529
530     summary = summary + announce (short_summary, 0)
531     
532     (prompt, answer) = ("", "XXX")
533     if Cnf["Dinstall::Options::No-Action"] or Cnf["Dinstall::Options::Automatic"]:
534         answer = 'S'
535
536     if string.find(reject_message, "Rejected") != -1:
537         if time.time()-os.path.getmtime(changes_filename) < 86400:
538             print "SKIP (too new)\n" + reject_message,;
539             prompt = "[S]kip, Manual reject, Quit ?";
540         else:
541             print "REJECT\n" + reject_message,;
542             prompt = "[R]eject, Manual reject, Skip, Quit ?";
543             if Cnf["Dinstall::Options::Automatic"]:
544                 answer = 'R';
545     elif new:
546         print "NEW to %s\n%s%s" % (suites, reject_message, summary),;
547         prompt = "[S]kip, New ack, Manual reject, Quit ?";
548         if Cnf["Dinstall::Options::Automatic"] and Cnf["Dinstall::Options::Ack-New"]:
549             answer = 'N';
550     elif byhand:
551         print "BYHAND\n" + reject_message + summary,;
552         prompt = "[I]nstall, Manual reject, Skip, Quit ?";
553     elif confirm:
554         print "CONFIRM to %s\n%s%s" % (confirm, reject_message, summary),
555         prompt = "[I]nstall, Manual reject, Skip, Quit ?";
556     else:
557         print "INSTALL\n" + reject_message + summary,;
558         prompt = "[I]nstall, Manual reject, Skip, Quit ?";
559         if Cnf["Dinstall::Options::Automatic"]:
560             answer = 'I';
561
562     while string.find(prompt, answer) == -1:
563         print prompt,;
564         answer = utils.our_raw_input()
565         m = re_default_answer.match(prompt)
566         if answer == "":
567             answer = m.group(1)
568         answer = string.upper(answer[:1])
569
570     if answer == 'R':
571         reject (changes_filename, "");
572     elif answer == 'M':
573         manual_reject (changes_filename);
574     elif answer == 'I':
575         install (changes_filename, summary, short_summary);
576     elif answer == 'N':
577         acknowledge_new (changes_filename, summary);
578     elif answer == 'Q':
579         sys.exit(0)
580
581 #####################################################################################################################
582
583 def install (changes_filename, summary, short_summary):
584     global install_count, install_bytes
585     
586     print "Installing."
587
588     archive = utils.where_am_i();
589
590     # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
591     projectB.query("BEGIN WORK");
592
593     # Add the .dsc file to the DB
594     for file in files.keys():
595         if files[file]["type"] == "dsc":
596             package = dsc["source"]
597             version = dsc["version"]  # NB: not files[file]["version"], that has no epoch
598             maintainer = dsc["maintainer"]
599             maintainer = string.replace(maintainer, "'", "\\'")
600             maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
601             filename = files[file]["pool name"] + file;
602             dsc_location_id = files[file]["location id"];
603             if not files[file]["files id"]:
604                 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], dsc_location_id)
605             dsc_file_id = files[file]["files id"]
606             projectB.query("INSERT INTO source (source, version, maintainer, file) VALUES ('%s', '%s', %d, %d)"
607                            % (package, version, maintainer_id, files[file]["files id"]))
608             
609             for suite in changes["distribution"].keys():
610                 suite_id = db_access.get_suite_id(suite);
611                 projectB.query("INSERT INTO src_associations (suite, source) VALUES (%d, currval('source_id_seq'))" % (suite_id))
612
613
614             # Add the .diff.gz and {.orig,}.tar.gz files to the DB (files and dsc_files)
615             for file in files.keys():
616                 if files[file]["type"] == "diff.gz" or files[file]["type"] == "orig.tar.gz" or files[file]["type"] == "tar.gz":
617                     if not files[file]["files id"]:
618                         filename = files[file]["pool name"] + file;
619                         files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], files[file]["location id"])
620                     projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files[file]["files id"]));
621                 
622     # Add the .deb files to the DB
623     for file in files.keys():
624         if files[file]["type"] == "deb":
625             package = files[file]["package"]
626             version = files[file]["version"]
627             maintainer = files[file]["maintainer"]
628             maintainer = string.replace(maintainer, "'", "\\'")
629             maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
630             architecture = files[file]["architecture"]
631             architecture_id = db_access.get_architecture_id (architecture);
632             type = files[file]["dbtype"];
633             component = files[file]["component"]
634             source = files[file]["source"]
635             source_version = ""
636             if string.find(source, "(") != -1:
637                 m = utils.re_extract_src_version.match(source)
638                 source = m.group(1)
639                 source_version = m.group(2)
640             if not source_version:
641                 source_version = version
642             filename = files[file]["pool name"] + file;
643             if not files[file]["files id"]:
644                 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], files[file]["location id"])
645             source_id = db_access.get_source_id (source, source_version);
646             if source_id:
647                 projectB.query("INSERT INTO binaries (package, version, maintainer, source, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, %d, '%s')"
648                                % (package, version, maintainer_id, source_id, architecture_id, files[file]["files id"], type));
649             else:
650                 projectB.query("INSERT INTO binaries (package, version, maintainer, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, '%s')"
651                                % (package, version, maintainer_id, architecture_id, files[file]["files id"], type));
652             for suite in changes["distribution"].keys():
653                 suite_id = db_access.get_suite_id(suite);
654                 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES (%d, currval('binaries_id_seq'))" % (suite_id));
655
656     # Install the files into the pool
657     for file in files.keys():
658         if files[file].has_key("byhand"):
659             continue
660         destination = Cnf["Dir::PoolDir"] + files[file]["pool name"] + file
661         destdir = os.path.dirname(destination)
662         utils.move (file, destination)
663         install_bytes = install_bytes + float(files[file]["size"])
664
665     # Copy the .changes file across for suite which need it.
666     for suite in changes["distribution"].keys():
667         if Cnf.has_key("Suties::%s::CopyChanges" % (suite)):
668             destination = Cnf["Dir::RootDir"] + Cnf["Suite::%s::CopyChanges" % (suite)] + os.path.basename(changes_filename)
669             copy_file (changes_filename, destination)
670
671     # If the .orig.tar.gz is in a legacy directory we need to poolify
672     # it, so that apt-get source (and anything else that goes by the
673     # "Directory:" field in the Sources.gz file) works.
674     if orig_tar_id != None:
675         q = projectB.query("SELECT 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" % (orig_tar_id));
676         qd = q.dictresult();
677         for qid in qd:
678             # First move the files to the new location
679             legacy_filename = qid["path"]+qid["filename"];
680             pool_location = utils.poolify (files[file]["package"], files[file]["component"]);
681             pool_filename = pool_location + os.path.basename(qid["filename"]);
682             destination = Cnf["Dir::PoolDir"] + pool_location
683             utils.move(legacy_filename, destination);
684             # Update the DB: files table
685             new_files_id = db_access.set_files_id(pool_filename, qid["size"], qid["md5sum"], dsc_location_id);
686             # Update the DB: dsc_files table
687             projectB.query("INSERT INTO dsc_files (source, file) VALUES (%s, %s)" % (qid["source"], new_files_id));
688             # Update the DB: source table
689             if legacy_filename[-4:] == ".dsc":
690                 projectB.query("UPDATE source SET file = %s WHERE id = %d" % (new_files_id, qid["source"]));
691
692         for qid in qd:
693             # Remove old data from the DB: dsc_files table
694             projectB.query("DELETE FROM dsc_files WHERE id = %s" % (qid["dsc_files_id"]));
695             # Remove old data from the DB: files table
696             projectB.query("DELETE FROM files WHERE id = %s" % (qid["files_id"]));
697
698     utils.move (changes_filename, Cnf["Dir::IncomingDir"] + 'DONE/' + os.path.basename(changes_filename))
699
700     projectB.query("COMMIT WORK");
701
702     install_count = install_count + 1;
703
704     if not Cnf["Dinstall::Options::No-Mail"]:
705         mail_message = """Return-Path: %s
706 From: %s
707 To: %s
708 Bcc: troup@auric.debian.org
709 Subject: %s INSTALLED
710
711 %s
712 Installing:
713 %s
714
715 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, reject_message, summary, installed_footer)
716         utils.send_mail (mail_message, "")
717         announce (short_summary, 1)
718
719 #####################################################################################################################
720
721 def reject (changes_filename, manual_reject_mail_filename):
722     print "Rejecting.\n"
723
724     base_changes_filename = os.path.basename(changes_filename);
725     reason_filename = re_changes.sub("reason", base_changes_filename);
726     reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename);
727
728     # Move the .changes files and it's contents into REJECT/
729     utils.move (changes_filename, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], base_changes_filename));
730     for file in files.keys():
731         if os.access(file,os.R_OK) == 0:
732             utils.move (file, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], file));
733
734     # If this is not a manual rejection generate the .reason file and rejection mail message
735     if manual_reject_mail_filename == "":
736         if os.path.exists(reject_filename):
737             os.unlink(reject_filename);
738         fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
739         os.write(fd, reject_message);
740         os.close(fd);
741         reject_mail_message = """From: %s
742 To: %s
743 Bcc: troup@auric.debian.org
744 Subject: %s REJECTED
745
746 %s
747 ===
748 %s""" % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, reject_message, reject_footer);
749     else: # Have a manual rejection file to use
750         reject_mail_message = ""; # avoid <undef>'s
751         
752     # Send the rejection mail if appropriate
753     if not Cnf["Dinstall::Options::No-Mail"]:
754         utils.send_mail (reject_mail_message, manual_reject_mail_filename);
755
756 ##################################################################
757
758 def manual_reject (changes_filename):
759     # Build up the rejection email 
760     user_email_address = string.replace(string.split(pwd.getpwuid(os.getuid())[4],',')[0], '.', '')
761     user_email_address = user_email_address + " <%s@%s>" % (pwd.getpwuid(os.getuid())[0], Cnf["Dinstall::MyHost"])
762     manual_reject_message = Cnf.get("Dinstall::Options::Manual-Reject", "")
763
764     reject_mail_message = """From: %s
765 Cc: %s
766 To: %s
767 Bcc: troup@auric.debian.org
768 Subject: %s REJECTED
769
770 %s
771 %s
772 ===
773 %s""" % (user_email_address, Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, manual_reject_message, reject_message, reject_footer)
774     
775     # Write the rejection email out as the <foo>.reason file
776     reason_filename = re_changes.sub("reason", os.path.basename(changes_filename));
777     reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename)
778     if os.path.exists(reject_filename):
779         os.unlink(reject_filename);
780     fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
781     os.write(fd, reject_mail_message);
782     os.close(fd);
783     
784     # If we weren't given one, spawn an editor so the user can add one in
785     if manual_reject_message == "":
786         result = os.system("vi +6 %s" % (reject_file))
787         if result != 0:
788             sys.stderr.write ("vi invocation failed for `%s'!" % (reject_file))
789             sys.exit(result)
790
791     # Then process it as if it were an automatic rejection
792     reject (changes_filename, reject_filename)
793
794 #####################################################################################################################
795  
796 def acknowledge_new (changes_filename, summary):
797     global new_ack_new;
798
799     new_ack_new[changes_filename] = 1;
800
801     if new_ack_old.has_key(changes_filename):
802         print "Ack already sent.";
803         return;
804
805     print "Sending new ack.";
806     if not Cnf["Dinstall::Options::No-Mail"]:
807         new_ack_message = """Return-Path: %s
808 From: %s
809 To: %s
810 Bcc: troup@auric.debian.org
811 Subject: %s is NEW
812
813 %s
814 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, summary, new_ack_footer);
815         utils.send_mail(new_ack_message,"");
816
817 #####################################################################################################################
818
819 def announce (short_summary, action):
820     # Only do announcements for source uploads with a recent dpkg-dev installed
821     if float(changes.get("format", 0)) < 1.6 or not changes["architecture"].has_key("source"):
822         return ""
823
824     lists_done = {}
825     summary = ""
826
827     for dist in changes["distribution"].keys():
828         list = Cnf["Suite::%s::Announce" % (dist)]
829         if lists_done.has_key(list):
830             continue
831         lists_done[list] = 1
832         summary = summary + "Announcing to %s\n" % (list)
833
834         if action:
835             mail_message = """Return-Path: %s
836 From: %s
837 To: %s
838 Bcc: troup@auric.debian.org
839 Subject: Installed %s %s (%s)
840
841 %s
842
843 Installed:
844 %s
845 """ % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], list, changes["source"], changes["version"], string.join(changes["architecture"].keys(), ' ' ),
846        changes["filecontents"], short_summary)
847             utils.send_mail (mail_message, "")
848
849     (dsc_rfc822, dsc_name, dsc_email) = utils.fix_maintainer (dsc.get("maintainer",Cnf["Dinstall::MyEmailAddress"]));
850     bugs = changes["closes"].keys()
851     bugs.sort()
852     if dsc_name == changes["maintainername"]:
853         summary = summary + "Closing bugs: "
854         for bug in bugs:
855             summary = summary + "%s " % (bug)
856             if action:
857                 mail_message = """Return-Path: %s
858 From: %s
859 To: %s-close@bugs.debian.org
860 Bcc: troup@auric.debian.org
861 Subject: Bug#%s: fixed in %s %s
862
863 We believe that the bug you reported is fixed in the latest version of
864 %s, which has been installed in the Debian FTP archive:
865
866 %s""" % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], bug, bug, changes["source"], changes["version"], changes["source"], short_summary)
867
868                 if changes["distribution"].has_key("stable"):
869                     mail_message = mail_message + """Note that this package is not part of the released stable Debian
870 distribution.  It may have dependencies on other unreleased software,
871 or other instabilities.  Please take care if you wish to install it.
872 The update will eventually make its way into the next released Debian
873 distribution."""
874
875                 mail_message = mail_message + """A summary of the changes between this version and the previous one is
876 attached.
877
878 Thank you for reporting the bug, which will now be closed.  If you
879 have further comments please address them to %s@bugs.debian.org,
880 and the maintainer will reopen the bug report if appropriate.
881
882 Debian distribution maintenance software
883 pp.
884 %s (supplier of updated %s package)
885
886 (This message was generated automatically at their request; if you
887 believe that there is a problem with it please contact the archive
888 administrators by mailing ftpmaster@debian.org)
889
890
891 %s""" % (bug, changes["maintainer"], changes["source"], changes["filecontents"])
892
893                 utils.send_mail (mail_message, "")
894     else:                     # NMU
895         summary = summary + "Setting bugs to severity fixed: "
896         control_message = ""
897         for bug in bugs:
898             summary = summary + "%s " % (bug)
899             control_message = control_message + "severity %s fixed\n" % (bug)
900         if action and control_message != "":
901             mail_message = """Return-Path: %s
902 From: %s
903 To: control@bugs.debian.org
904 Bcc: troup@auric.debian.org, %s
905 Subject: Fixed in NMU of %s %s
906
907 %s
908 quit
909
910 This message was generated automatically in response to a
911 non-maintainer upload.  The .changes file follows.
912
913 %s
914 """ % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes["maintainer822"], changes["source"], changes["version"], control_message, changes["filecontents"])
915             utils.send_mail (mail_message, "")
916     summary = summary + "\n"
917
918     return summary
919
920 ###############################################################################
921
922 # reprocess is necessary for the case of foo_1.2-1 and foo_1.2-2 in
923 # Incoming. -1 will reference the .orig.tar.gz, but -2 will not.
924 # dsccheckdistrib() can find the .orig.tar.gz but it will not have
925 # processed it during it's checks of -2.  If -1 has been deleted or
926 # otherwise not checked by da-install, the .orig.tar.gz will not have
927 # been checked at all.  To get round this, we force the .orig.tar.gz
928 # into the .changes structure and reprocess the .changes file.
929
930 def process_it (changes_file):
931     global reprocess, orig_tar_id;
932
933     reprocess = 1;
934     orig_tar_id = None;
935
936     check_signature (changes_file);
937     check_changes (changes_file);
938     while reprocess:
939         reprocess = 0;
940         check_files ();
941         check_md5sums ();
942         check_dsc ();
943         
944     action(changes_file);
945
946 ###############################################################################
947
948 def main():
949     global Cnf, projectB, reject_message, install_bytes, new_ack_old
950
951     apt_pkg.init();
952     
953     Cnf = apt_pkg.newConfiguration();
954     apt_pkg.ReadConfigFileISC(Cnf,utils.which_conf_file());
955
956     Arguments = [('a',"automatic","Dinstall::Options::Automatic"),
957                  ('d',"debug","Dinstall::Options::Debug", "IntVal"),
958                  ('h',"help","Dinstall::Options::Help"),
959                  ('k',"ack-new","Dinstall::Options::Ack-New"),
960                  ('m',"manual-reject","Dinstall::Options::Manual-Reject", "HasArg"),
961                  ('n',"no-action","Dinstall::Options::No-Action"),
962                  ('p',"no-lock", "Dinstall::Options::No-Lock"),
963                  ('r',"no-version-check", "Dinstall::Options::No-Version-Check"),
964                  ('s',"no-mail", "Dinstall::Options::No-Mail"),
965                  ('u',"override-distribution", "Dinstall::Options::Override-Distribution", "HasArg"),
966                  ('v',"version","Dinstall::Options::Version")];
967     
968     changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
969
970     if Cnf["Dinstall::Options::Help"]:
971         usage(0);
972         
973     if Cnf["Dinstall::Options::Version"]:
974         print "katie version 0.0000000000";
975         usage(0);
976
977     postgresql_user = None; # Default == Connect as user running program.
978
979     # -n/--dry-run invalidates some other options which would involve things happening
980     if Cnf["Dinstall::Options::No-Action"]:
981         Cnf["Dinstall::Options::Automatic"] = ""
982         Cnf["Dinstall::Options::Ack-New"] = ""
983         postgresql_user = Cnf["DB::ROUser"];
984
985     projectB = pg.connect('projectb', Cnf["DB::Host"], int(Cnf["DB::Port"]), None, None, postgresql_user);
986
987     db_access.init(Cnf, projectB);
988
989     # Check that we aren't going to clash with the daily cron job
990
991     if os.path.exists("%s/Archive_Maintenance_In_Progress" % (Cnf["Dir::RootDir"])) and not Cnf["Dinstall::Options::No-Lock"]:
992         sys.stderr.write("Archive maintenance in progress.  Try again later.\n");
993         sys.exit(2);
994     
995     # Obtain lock if not in no-action mode
996
997     if not Cnf["Dinstall::Options::No-Action"]:
998         lock_fd = os.open(Cnf["Dinstall::LockFile"], os.O_RDWR);
999         fcntl.lockf(lock_fd, FCNTL.F_TLOCK);
1000
1001     # Read in the list of already-acknowledged NEW packages
1002     new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'r');
1003     new_ack_old = {};
1004     for line in new_ack_list.readlines():
1005         new_ack_old[line[:-1]] = 1;
1006     new_ack_list.close();
1007
1008     # Process the changes files
1009     for changes_file in changes_files:
1010         reject_message = ""
1011         print "\n" + changes_file;
1012         process_it (changes_file);
1013
1014     install_mag = " b";
1015     if install_bytes > 10000:
1016         install_bytes = install_bytes / 1000;
1017         install_mag = " Kb";
1018     if install_bytes > 10000:
1019         install_bytes = install_bytes / 1000;
1020         install_mag = " Mb";
1021     if install_count:
1022         sets = "set"
1023         if install_count > 1:
1024             sets = "sets"
1025         sys.stderr.write("Installed %d package %s, %d%s.\n" % (install_count, sets, int(install_bytes), install_mag))
1026
1027     # Write out the list of already-acknowledged NEW packages
1028     if Cnf["Dinstall::Options::Ack-New"]:
1029         new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'w')
1030         for i in new_ack_new.keys():
1031             new_ack_list.write(i+'\n')
1032         new_ack_list.close()
1033     
1034             
1035 if __name__ == '__main__':
1036     main()
1037