]> git.decadent.org.uk Git - dak.git/blob - katie
auric fixes
[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.4 2000-11-27 03:15:26 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             # Find any old .dsc files
379             elif files[file]["type"] == "dsc":
380                 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"
381                                    % (files[file]["package"], suite))
382                 oldfiles = q.dictresult()
383                 if len(oldfiles) >= 1:
384                     files[file]["oldfiles"][suite] = oldfiles[0]
385
386             # Validate the component
387             component = files[file]["component"];
388             component_id = db_access.get_component_id(component);
389             if component_id == -1:
390                 reject_message = reject_message + "Rejected: file '%s' has unknown component '%s'.\n" % (file, component);
391                 continue;
392             
393             # Check the md5sum & size against existing files (if any)
394             location = Cnf["Dir::PoolDir"];
395             files[file]["location id"] = db_access.get_location_id (location, component, archive);
396             files_id = db_access.get_files_id(component + '/' + file, files[file]["size"], files[file]["md5sum"], files[file]["location id"]);
397             if files_id == -1:
398                 reject_message = reject_message + "Rejected: INTERNAL ERROR, get_files_id() returned multiple matches for %s.\n" % (file)
399             elif files_id == -2:
400                 reject_message = reject_message + "Rejected: md5sum and/or size mismatch on existing copy of %s.\n" % (file)
401             files[file]["files id"] = files_id
402
403             # Check for packages that have moved from one component to another
404             if files[file]["oldfiles"].has_key(suite) and files[file]["oldfiles"][suite]["name"] != files[file]["component"]:
405                 files[file]["othercomponents"] = files[file]["oldfiles"][suite]["name"];
406
407                 
408     if string.find(reject_message, "Rejected:") != -1:
409         return 0
410     else: 
411         return 1
412
413 ###############################################################################
414
415 def check_dsc ():
416     global dsc, dsc_files, reject_message, reprocess, orig_tar_id;
417     
418     for file in files.keys():
419         if files[file]["type"] == "dsc":
420             try:
421                 dsc = utils.parse_changes(file)
422             except utils.cant_open_exc:
423                 reject_message = reject_message + "Rejected: can't read changes file '%s'.\n" % (filename)
424                 return 0;
425             except utils.changes_parse_error_exc, line:
426                 reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line)
427                 return 0;
428             try:
429                 dsc_files = utils.build_file_list(dsc, 1)
430             except utils.no_files_exc:
431                 reject_message = reject_message + "Rejected: no Files: field in .dsc file.\n";
432                 continue;
433
434             # Try and find all files mentioned in the .dsc.  This has
435             # to work harder to cope with the multiple possible
436             # locations of an .orig.tar.gz.
437             for dsc_file in dsc_files.keys():
438                 if files.has_key(dsc_file):
439                     actual_md5 = files[dsc_file]["md5sum"]
440                     found = "%s in incoming" % (dsc_file)
441                 elif dsc_file[-12:] == ".orig.tar.gz":
442                     # Check in Incoming
443                     # See comment above process_it() for explanation...
444                     if os.access(dsc_file, os.R_OK) != 0:
445                         files[dsc_file] = {};
446                         files[dsc_file]["size"] = os.stat(dsc_file)[stat.ST_SIZE];
447                         files[dsc_file]["md5sum"] = dsc_files[dsc_file]["md5sum"];
448                         files[dsc_file]["section"] = files[file]["section"];
449                         files[dsc_file]["priority"] = files[file]["priority"];
450                         files[dsc_file]["component"] = files[file]["component"];
451                         reprocess = 1;
452                         return 1;
453                     # Check in the pool
454                     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));
455                     ql = q.getresult();
456                     if len(ql) > 0:
457                         old_file = ql[0][0] + ql[0][1];
458                         actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
459                         found = old_file;
460                         suite_type = ql[0][2];
461                         # See install()...
462                         if suite_type == "legacy" or suite_type == "legacy-mixed":
463                             orig_tar_id = ql[0][3];
464                     else:
465                         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);
466                         continue;
467                 else:
468                     reject_message = reject_message + "Rejected: %s refers to %s, but I can't find it in Incoming." % (file, dsc_file);
469                     continue;
470                 if actual_md5 != dsc_files[dsc_file]["md5sum"]:
471                     reject_message = reject_message + "Rejected: md5sum for %s doesn't match %s.\n" % (found, file)
472                 
473     if string.find(reject_message, "Rejected:") != -1:
474         return 0
475     else: 
476         return 1
477
478 ###############################################################################
479
480 def check_md5sums ():
481     global reject_message;
482
483     for file in files.keys():
484         try:
485             file_handle = utils.open_file(file,"r");
486         except utils.cant_open_exc:
487             pass;
488         else:
489             if apt_pkg.md5sum(file_handle) != files[file]["md5sum"]:
490                 reject_message = reject_message + "Rejected: md5sum check failed for %s.\n" % (file);
491
492 #####################################################################################################################
493
494 def action (changes_filename):
495     byhand = confirm = suites = summary = new = "";
496
497     # changes["distribution"] may not exist in corner cases
498     # (e.g. unreadable changes files)
499     if not changes.has_key("distribution"):
500         changes["distribution"] = {};
501     
502     for suite in changes["distribution"].keys():
503         if Cnf.has_key("Suite::%s::Confirm"):
504             confirm = confirm + suite + ", "
505         suites = suites + suite + ", "
506     confirm = confirm[:-2]
507     suites = suites[:-2]
508
509     for file in files.keys():
510         if files[file].has_key("byhand"):
511             byhand = 1
512             summary = summary + file + " byhand\n"
513         elif files[file].has_key("new"):
514             new = 1
515             summary = summary + "(new) %s %s %s\n" % (file, files[file]["priority"], files[file]["section"])
516             if files[file].has_key("othercomponents"):
517                 summary = summary + "WARNING: Already present in %s distribution.\n" % (files[file]["othercomponents"])
518             if files[file]["type"] == "deb":
519                 summary = summary + apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))["Description"] + '\n';
520         else:
521             files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"])
522             destination = Cnf["Dir::PoolRoot"] + files[file]["pool name"] + file
523             summary = summary + file + "\n  to " + destination + "\n"
524
525     short_summary = summary;
526
527     # This is for direport's benefit...
528     f = re_fdnic.sub("\n .\n", changes.get("changes",""));
529
530     if confirm or byhand or new:
531         summary = summary + "Changes: " + f;
532
533     summary = summary + announce (short_summary, 0)
534     
535     (prompt, answer) = ("", "XXX")
536     if Cnf["Dinstall::Options::No-Action"] or Cnf["Dinstall::Options::Automatic"]:
537         answer = 'S'
538
539     if string.find(reject_message, "Rejected") != -1:
540         if time.time()-os.path.getmtime(changes_filename) < 86400:
541             print "SKIP (too new)\n" + reject_message,;
542             prompt = "[S]kip, Manual reject, Quit ?";
543         else:
544             print "REJECT\n" + reject_message,;
545             prompt = "[R]eject, Manual reject, Skip, Quit ?";
546             if Cnf["Dinstall::Options::Automatic"]:
547                 answer = 'R';
548     elif new:
549         print "NEW to %s\n%s%s" % (suites, reject_message, summary),;
550         prompt = "[S]kip, New ack, Manual reject, Quit ?";
551         if Cnf["Dinstall::Options::Automatic"] and Cnf["Dinstall::Options::Ack-New"]:
552             answer = 'N';
553     elif byhand:
554         print "BYHAND\n" + reject_message + summary,;
555         prompt = "[I]nstall, Manual reject, Skip, Quit ?";
556     elif confirm:
557         print "CONFIRM to %s\n%s%s" % (confirm, reject_message, summary),
558         prompt = "[I]nstall, Manual reject, Skip, Quit ?";
559     else:
560         print "INSTALL\n" + reject_message + summary,;
561         prompt = "[I]nstall, Manual reject, Skip, Quit ?";
562         if Cnf["Dinstall::Options::Automatic"]:
563             answer = 'I';
564
565     while string.find(prompt, answer) == -1:
566         print prompt,;
567         answer = utils.our_raw_input()
568         m = re_default_answer.match(prompt)
569         if answer == "":
570             answer = m.group(1)
571         answer = string.upper(answer[:1])
572
573     if answer == 'R':
574         reject (changes_filename, "");
575     elif answer == 'M':
576         manual_reject (changes_filename);
577     elif answer == 'I':
578         install (changes_filename, summary, short_summary);
579     elif answer == 'N':
580         acknowledge_new (changes_filename, summary);
581     elif answer == 'Q':
582         sys.exit(0)
583
584 #####################################################################################################################
585
586 def install (changes_filename, summary, short_summary):
587     global install_count, install_bytes
588     
589     print "Installing."
590
591     archive = utils.where_am_i();
592
593     # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
594     projectB.query("BEGIN WORK");
595
596     # Add the .dsc file to the DB
597     for file in files.keys():
598         if files[file]["type"] == "dsc":
599             package = dsc["source"]
600             version = dsc["version"]  # NB: not files[file]["version"], that has no epoch
601             maintainer = dsc["maintainer"]
602             maintainer = string.replace(maintainer, "'", "\\'")
603             maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
604             filename = files[file]["pool name"] + file;
605             dsc_location_id = files[file]["location id"];
606             if not files[file]["files id"]:
607                 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], dsc_location_id)
608             dsc_file_id = files[file]["files id"]
609             projectB.query("INSERT INTO source (source, version, maintainer, file) VALUES ('%s', '%s', %d, %d)"
610                            % (package, version, maintainer_id, files[file]["files id"]))
611             
612             for suite in changes["distribution"].keys():
613                 suite_id = db_access.get_suite_id(suite);
614                 projectB.query("INSERT INTO src_associations (suite, source) VALUES (%d, currval('source_id_seq'))" % (suite_id))
615
616
617             # Add the .diff.gz and {.orig,}.tar.gz files to the DB (files and dsc_files)
618             for file in files.keys():
619                 if files[file]["type"] == "diff.gz" or files[file]["type"] == "orig.tar.gz" or files[file]["type"] == "tar.gz":
620                     if not files[file]["files id"]:
621                         filename = files[file]["pool name"] + file;
622                         files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], files[file]["location id"])
623                     projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files[file]["files id"]));
624                 
625     # Add the .deb files to the DB
626     for file in files.keys():
627         if files[file]["type"] == "deb":
628             package = files[file]["package"]
629             version = files[file]["version"]
630             maintainer = files[file]["maintainer"]
631             maintainer = string.replace(maintainer, "'", "\\'")
632             maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
633             architecture = files[file]["architecture"]
634             architecture_id = db_access.get_architecture_id (architecture);
635             type = files[file]["dbtype"];
636             component = files[file]["component"]
637             source = files[file]["source"]
638             source_version = ""
639             if string.find(source, "(") != -1:
640                 m = utils.re_extract_src_version.match(source)
641                 source = m.group(1)
642                 source_version = m.group(2)
643             if not source_version:
644                 source_version = version
645             filename = files[file]["pool name"] + file;
646             if not files[file]["files id"]:
647                 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], files[file]["location id"])
648             source_id = db_access.get_source_id (source, source_version);
649             if source_id:
650                 projectB.query("INSERT INTO binaries (package, version, maintainer, source, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, %d, '%s')"
651                                % (package, version, maintainer_id, source_id, architecture_id, files[file]["files id"], type));
652             else:
653                 projectB.query("INSERT INTO binaries (package, version, maintainer, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, '%s')"
654                                % (package, version, maintainer_id, architecture_id, files[file]["files id"], type));
655             for suite in changes["distribution"].keys():
656                 suite_id = db_access.get_suite_id(suite);
657                 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES (%d, currval('binaries_id_seq'))" % (suite_id));
658
659     # Install the files into the pool
660     for file in files.keys():
661         if files[file].has_key("byhand"):
662             continue
663         destination = Cnf["Dir::PoolDir"] + files[file]["pool name"] + file
664         destdir = os.path.dirname(destination)
665         utils.move (file, destination)
666         install_bytes = install_bytes + float(files[file]["size"])
667
668     # Copy the .changes file across for suite which need it.
669     for suite in changes["distribution"].keys():
670         if Cnf.has_key("Suite::%s::CopyChanges" % (suite)):
671             utils.copy (changes_filename, Cnf["Dir::RootDir"] + Cnf["Suite::%s::CopyChanges" % (suite)]);
672
673     # If the .orig.tar.gz is in a legacy directory we need to poolify
674     # it, so that apt-get source (and anything else that goes by the
675     # "Directory:" field in the Sources.gz file) works.
676     if orig_tar_id != None:
677         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));
678         qd = q.dictresult();
679         for qid in qd:
680             # First move the files to the new location
681             legacy_filename = qid["path"]+qid["filename"];
682             pool_location = utils.poolify (files[file]["package"], files[file]["component"]);
683             pool_filename = pool_location + os.path.basename(qid["filename"]);
684             destination = Cnf["Dir::PoolDir"] + pool_location
685             utils.move(legacy_filename, destination);
686             # Update the DB: files table
687             new_files_id = db_access.set_files_id(pool_filename, qid["size"], qid["md5sum"], dsc_location_id);
688             # Update the DB: dsc_files table
689             projectB.query("INSERT INTO dsc_files (source, file) VALUES (%s, %s)" % (qid["source"], new_files_id));
690             # Update the DB: source table
691             if legacy_filename[-4:] == ".dsc":
692                 projectB.query("UPDATE source SET file = %s WHERE id = %d" % (new_files_id, qid["source"]));
693
694         for qid in qd:
695             # Remove old data from the DB: dsc_files table
696             projectB.query("DELETE FROM dsc_files WHERE id = %s" % (qid["dsc_files_id"]));
697             # Remove old data from the DB: files table
698             projectB.query("DELETE FROM files WHERE id = %s" % (qid["files_id"]));
699
700     utils.move (changes_filename, Cnf["Dir::IncomingDir"] + 'DONE/' + os.path.basename(changes_filename))
701
702     projectB.query("COMMIT WORK");
703
704     install_count = install_count + 1;
705
706     if not Cnf["Dinstall::Options::No-Mail"]:
707         mail_message = """Return-Path: %s
708 From: %s
709 To: %s
710 Bcc: troup@auric.debian.org
711 Subject: %s INSTALLED
712
713 %s
714 Installing:
715 %s
716
717 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, reject_message, summary, installed_footer)
718         utils.send_mail (mail_message, "")
719         announce (short_summary, 1)
720
721 #####################################################################################################################
722
723 def reject (changes_filename, manual_reject_mail_filename):
724     print "Rejecting.\n"
725
726     base_changes_filename = os.path.basename(changes_filename);
727     reason_filename = re_changes.sub("reason", base_changes_filename);
728     reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename);
729
730     # Move the .changes files and it's contents into REJECT/
731     utils.move (changes_filename, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], base_changes_filename));
732     for file in files.keys():
733         if os.access(file,os.R_OK) == 0:
734             utils.move (file, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], file));
735
736     # If this is not a manual rejection generate the .reason file and rejection mail message
737     if manual_reject_mail_filename == "":
738         if os.path.exists(reject_filename):
739             os.unlink(reject_filename);
740         fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
741         os.write(fd, reject_message);
742         os.close(fd);
743         reject_mail_message = """From: %s
744 To: %s
745 Bcc: troup@auric.debian.org
746 Subject: %s REJECTED
747
748 %s
749 ===
750 %s""" % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, reject_message, reject_footer);
751     else: # Have a manual rejection file to use
752         reject_mail_message = ""; # avoid <undef>'s
753         
754     # Send the rejection mail if appropriate
755     if not Cnf["Dinstall::Options::No-Mail"]:
756         utils.send_mail (reject_mail_message, manual_reject_mail_filename);
757
758 ##################################################################
759
760 def manual_reject (changes_filename):
761     # Build up the rejection email 
762     user_email_address = string.replace(string.split(pwd.getpwuid(os.getuid())[4],',')[0], '.', '')
763     user_email_address = user_email_address + " <%s@%s>" % (pwd.getpwuid(os.getuid())[0], Cnf["Dinstall::MyHost"])
764     manual_reject_message = Cnf.get("Dinstall::Options::Manual-Reject", "")
765
766     reject_mail_message = """From: %s
767 Cc: %s
768 To: %s
769 Bcc: troup@auric.debian.org
770 Subject: %s REJECTED
771
772 %s
773 %s
774 ===
775 %s""" % (user_email_address, Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, manual_reject_message, reject_message, reject_footer)
776     
777     # Write the rejection email out as the <foo>.reason file
778     reason_filename = re_changes.sub("reason", os.path.basename(changes_filename));
779     reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename)
780     if os.path.exists(reject_filename):
781         os.unlink(reject_filename);
782     fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
783     os.write(fd, reject_mail_message);
784     os.close(fd);
785     
786     # If we weren't given one, spawn an editor so the user can add one in
787     if manual_reject_message == "":
788         result = os.system("vi +6 %s" % (reject_file))
789         if result != 0:
790             sys.stderr.write ("vi invocation failed for `%s'!" % (reject_file))
791             sys.exit(result)
792
793     # Then process it as if it were an automatic rejection
794     reject (changes_filename, reject_filename)
795
796 #####################################################################################################################
797  
798 def acknowledge_new (changes_filename, summary):
799     global new_ack_new;
800
801     new_ack_new[changes_filename] = 1;
802
803     if new_ack_old.has_key(changes_filename):
804         print "Ack already sent.";
805         return;
806
807     print "Sending new ack.";
808     if not Cnf["Dinstall::Options::No-Mail"]:
809         new_ack_message = """Return-Path: %s
810 From: %s
811 To: %s
812 Bcc: troup@auric.debian.org
813 Subject: %s is NEW
814
815 %s
816 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, summary, new_ack_footer);
817         utils.send_mail(new_ack_message,"");
818
819 #####################################################################################################################
820
821 def announce (short_summary, action):
822     # Only do announcements for source uploads with a recent dpkg-dev installed
823     if float(changes.get("format", 0)) < 1.6 or not changes["architecture"].has_key("source"):
824         return ""
825
826     lists_done = {}
827     summary = ""
828
829     for dist in changes["distribution"].keys():
830         list = Cnf.Find("Suite::%s::Announce" % (dist))
831         if list == None or lists_done.has_key(list):
832             continue
833         lists_done[list] = 1
834         summary = summary + "Announcing to %s\n" % (list)
835
836         if action:
837             mail_message = """Return-Path: %s
838 From: %s
839 To: %s
840 Bcc: troup@auric.debian.org
841 Subject: Installed %s %s (%s)
842
843 %s
844
845 Installed:
846 %s
847 """ % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], list, changes["source"], changes["version"], string.join(changes["architecture"].keys(), ' ' ),
848        changes["filecontents"], short_summary)
849             utils.send_mail (mail_message, "")
850
851     (dsc_rfc822, dsc_name, dsc_email) = utils.fix_maintainer (dsc.get("maintainer",Cnf["Dinstall::MyEmailAddress"]));
852     bugs = changes["closes"].keys()
853     bugs.sort()
854     if dsc_name == changes["maintainername"]:
855         summary = summary + "Closing bugs: "
856         for bug in bugs:
857             summary = summary + "%s " % (bug)
858             if action:
859                 mail_message = """Return-Path: %s
860 From: %s
861 To: %s-close@bugs.debian.org
862 Bcc: troup@auric.debian.org
863 Subject: Bug#%s: fixed in %s %s
864
865 We believe that the bug you reported is fixed in the latest version of
866 %s, which has been installed in the Debian FTP archive:
867
868 %s""" % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], bug, bug, changes["source"], changes["version"], changes["source"], short_summary)
869
870                 if changes["distribution"].has_key("stable"):
871                     mail_message = mail_message + """Note that this package is not part of the released stable Debian
872 distribution.  It may have dependencies on other unreleased software,
873 or other instabilities.  Please take care if you wish to install it.
874 The update will eventually make its way into the next released Debian
875 distribution."""
876
877                 mail_message = mail_message + """A summary of the changes between this version and the previous one is
878 attached.
879
880 Thank you for reporting the bug, which will now be closed.  If you
881 have further comments please address them to %s@bugs.debian.org,
882 and the maintainer will reopen the bug report if appropriate.
883
884 Debian distribution maintenance software
885 pp.
886 %s (supplier of updated %s package)
887
888 (This message was generated automatically at their request; if you
889 believe that there is a problem with it please contact the archive
890 administrators by mailing ftpmaster@debian.org)
891
892
893 %s""" % (bug, changes["maintainer"], changes["source"], changes["filecontents"])
894
895                 utils.send_mail (mail_message, "")
896     else:                     # NMU
897         summary = summary + "Setting bugs to severity fixed: "
898         control_message = ""
899         for bug in bugs:
900             summary = summary + "%s " % (bug)
901             control_message = control_message + "severity %s fixed\n" % (bug)
902         if action and control_message != "":
903             mail_message = """Return-Path: %s
904 From: %s
905 To: control@bugs.debian.org
906 Bcc: troup@auric.debian.org, %s
907 Subject: Fixed in NMU of %s %s
908
909 %s
910 quit
911
912 This message was generated automatically in response to a
913 non-maintainer upload.  The .changes file follows.
914
915 %s
916 """ % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes["maintainer822"], changes["source"], changes["version"], control_message, changes["filecontents"])
917             utils.send_mail (mail_message, "")
918     summary = summary + "\n"
919
920     return summary
921
922 ###############################################################################
923
924 # reprocess is necessary for the case of foo_1.2-1 and foo_1.2-2 in
925 # Incoming. -1 will reference the .orig.tar.gz, but -2 will not.
926 # dsccheckdistrib() can find the .orig.tar.gz but it will not have
927 # processed it during it's checks of -2.  If -1 has been deleted or
928 # otherwise not checked by da-install, the .orig.tar.gz will not have
929 # been checked at all.  To get round this, we force the .orig.tar.gz
930 # into the .changes structure and reprocess the .changes file.
931
932 def process_it (changes_file):
933     global reprocess, orig_tar_id;
934
935     reprocess = 1;
936     orig_tar_id = None;
937
938     check_signature (changes_file);
939     check_changes (changes_file);
940     while reprocess:
941         reprocess = 0;
942         check_files ();
943         check_md5sums ();
944         check_dsc ();
945         
946     action(changes_file);
947
948 ###############################################################################
949
950 def main():
951     global Cnf, projectB, reject_message, install_bytes, new_ack_old
952
953     apt_pkg.init();
954     
955     Cnf = apt_pkg.newConfiguration();
956     apt_pkg.ReadConfigFileISC(Cnf,utils.which_conf_file());
957
958     Arguments = [('a',"automatic","Dinstall::Options::Automatic"),
959                  ('d',"debug","Dinstall::Options::Debug", "IntVal"),
960                  ('h',"help","Dinstall::Options::Help"),
961                  ('k',"ack-new","Dinstall::Options::Ack-New"),
962                  ('m',"manual-reject","Dinstall::Options::Manual-Reject", "HasArg"),
963                  ('n',"no-action","Dinstall::Options::No-Action"),
964                  ('p',"no-lock", "Dinstall::Options::No-Lock"),
965                  ('r',"no-version-check", "Dinstall::Options::No-Version-Check"),
966                  ('s',"no-mail", "Dinstall::Options::No-Mail"),
967                  ('u',"override-distribution", "Dinstall::Options::Override-Distribution", "HasArg"),
968                  ('v',"version","Dinstall::Options::Version")];
969     
970     changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
971
972     if Cnf["Dinstall::Options::Help"]:
973         usage(0);
974         
975     if Cnf["Dinstall::Options::Version"]:
976         print "katie version 0.0000000000";
977         usage(0);
978
979     postgresql_user = None; # Default == Connect as user running program.
980
981     # -n/--dry-run invalidates some other options which would involve things happening
982     if Cnf["Dinstall::Options::No-Action"]:
983         Cnf["Dinstall::Options::Automatic"] = ""
984         Cnf["Dinstall::Options::Ack-New"] = ""
985         postgresql_user = Cnf["DB::ROUser"];
986
987     projectB = pg.connect('projectb', Cnf["DB::Host"], int(Cnf["DB::Port"]), None, None, postgresql_user);
988
989     db_access.init(Cnf, projectB);
990
991     # Check that we aren't going to clash with the daily cron job
992
993     if os.path.exists("%s/Archive_Maintenance_In_Progress" % (Cnf["Dir::RootDir"])) and not Cnf["Dinstall::Options::No-Lock"]:
994         sys.stderr.write("Archive maintenance in progress.  Try again later.\n");
995         sys.exit(2);
996     
997     # Obtain lock if not in no-action mode
998
999     if not Cnf["Dinstall::Options::No-Action"]:
1000         lock_fd = os.open(Cnf["Dinstall::LockFile"], os.O_RDWR);
1001         fcntl.lockf(lock_fd, FCNTL.F_TLOCK);
1002
1003     # Read in the list of already-acknowledged NEW packages
1004     new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'r');
1005     new_ack_old = {};
1006     for line in new_ack_list.readlines():
1007         new_ack_old[line[:-1]] = 1;
1008     new_ack_list.close();
1009
1010     # Process the changes files
1011     for changes_file in changes_files:
1012         reject_message = ""
1013         print "\n" + changes_file;
1014         process_it (changes_file);
1015
1016     install_mag = " b";
1017     if install_bytes > 10000:
1018         install_bytes = install_bytes / 1000;
1019         install_mag = " Kb";
1020     if install_bytes > 10000:
1021         install_bytes = install_bytes / 1000;
1022         install_mag = " Mb";
1023     if install_count:
1024         sets = "set"
1025         if install_count > 1:
1026             sets = "sets"
1027         sys.stderr.write("Installed %d package %s, %d%s.\n" % (install_count, sets, int(install_bytes), install_mag))
1028
1029     # Write out the list of already-acknowledged NEW packages
1030     if Cnf["Dinstall::Options::Ack-New"]:
1031         new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'w')
1032         for i in new_ack_new.keys():
1033             new_ack_list.write(i+'\n')
1034         new_ack_list.close()
1035     
1036             
1037 if __name__ == '__main__':
1038     main()
1039