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