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