3 # Installs Debian packaes
4 # Copyright (C) 2000, 2001 James Troup <james@nocrew.org>
5 # $Id: katie,v 1.51 2001-07-07 04:01:08 troup Exp $
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.
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.
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
21 # Originally based almost entirely on dinstall by Guy Maor <maor@debian.org>
23 #########################################################################################
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."
29 # Kyle: "But Cartman, we're trying to..."
31 # Cartman: "uhh.. screw you guys... home."
33 #########################################################################################
35 import FCNTL, commands, fcntl, getopt, gzip, os, pg, pwd, re, shutil, stat, string, sys, tempfile, time, traceback
36 import apt_inst, apt_pkg
37 import utils, db_access, logging
39 ###############################################################################
41 re_isanum = re.compile (r"^\d+$");
42 re_changes = re.compile (r"changes$");
43 re_default_answer = re.compile(r"\[(.*)\]");
44 re_fdnic = re.compile("\n\n");
45 re_bad_diff = re.compile("^[\-\+][\-\+][\-\+] /dev/null");
46 re_bin_only_nmu_of_mu = re.compile("\.\d+\.\d+$");
47 re_bin_only_nmu_of_nmu = re.compile("\.\d+$");
49 #########################################################################################
67 orig_tar_location = "";
68 legacy_source_untouchable = {};
72 #########################################################################################
74 def usage (exit_code):
75 print """Usage: dinstall [OPTION]... [CHANGES]...
76 -a, --automatic automatic run
77 -D, --debug=VALUE turn on debugging
78 -h, --help show this help and exit.
79 -k, --ack-new acknowledge new packages !! for cron.daily only !!
80 -m, --manual-reject=MSG manual reject with `msg'
81 -n, --no-action don't do anything
82 -p, --no-lock don't check lockfile !! for cron.daily only !!
83 -u, --distribution=DIST override distribution to `dist'
84 -v, --version display the version number and exit"""
87 #########################################################################################
89 def check_signature (filename):
92 (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))
94 reject_message = reject_message + "Rejected: GPG signature check failed on `%s'.\n%s\n" % (os.path.basename(filename), output)
98 ######################################################################################################
101 # Read in the group maintainer override file
103 self.group_maint = {};
104 if Cnf.get("Dinstall::GroupOverrideFilename"):
105 filename = Cnf["Dir::OverrideDir"] + Cnf["Dinstall::GroupOverrideFilename"];
106 file = utils.open_file(filename, 'r');
107 for line in file.readlines():
108 line = string.strip(utils.re_comments.sub('', line));
110 self.group_maint[line] = 1;
113 def is_an_nmu (self, changes, dsc):
114 (dsc_rfc822, dsc_name, dsc_email) = utils.fix_maintainer (dsc.get("maintainer",Cnf["Dinstall::MyEmailAddress"]));
115 # changes["changedbyname"] == dsc_name is probably never true, but better safe than sorry
116 if dsc_name == changes["maintainername"] and (changes["changedby822"] == "" or changes["changedbyname"] == dsc_name):
119 if dsc.has_key("uploaders"):
120 uploaders = string.split(dsc["uploaders"], ",");
123 (rfc822, name, email) = utils.fix_maintainer (string.strip(i));
124 uploadernames[name] = "";
125 if uploadernames.has_key(changes["changedbyname"]):
128 # Some group maintained packages (e.g. Debian QA) are never NMU's
129 if self.group_maint.has_key(changes["maintainername"]):
134 ######################################################################################################
136 # Ensure that source exists somewhere in the archive for the binary
137 # upload being processed.
139 # (1) exact match => 1.0-3
140 # (2) Bin-only NMU of an MU => 1.0-3.0.1
141 # (3) Bin-only NMU of a sourceful-NMU => 1.0-3.1.1
143 def source_exists (package, source_version):
144 q = projectB.query("SELECT s.version FROM source s WHERE s.source = '%s'" % (package));
146 # Reduce the query results to a list of version numbers
147 ql = map(lambda x: x[0], q.getresult());
150 if ql.count(source_version):
154 orig_source_version = re_bin_only_nmu_of_mu.sub('', source_version);
155 if ql.count(orig_source_version):
159 orig_source_version = re_bin_only_nmu_of_nmu.sub('', source_version);
160 if ql.count(orig_source_version):
166 ######################################################################################################
168 # See if a given package is in the override table
170 def in_override_p (package, component, suite, binary_type, file):
173 if binary_type == "": # must be source
178 # Override suite name; used for example with proposed-updates
179 if Cnf.Find("Suite::%s::OverrideSuite" % (suite)) != "":
180 suite = Cnf["Suite::%s::OverrideSuite" % (suite)];
182 # Avoid <undef> on unknown distributions
183 suite_id = db_access.get_suite_id(suite);
186 component_id = db_access.get_component_id(component);
187 type_id = db_access.get_override_type_id(type);
189 # FIXME: nasty non-US speficic hack
190 if string.lower(component[:7]) == "non-us/":
191 component = component[7:];
193 q = projectB.query("SELECT s.section, p.priority FROM override o, section s, priority p WHERE package = '%s' AND suite = %s AND component = %s AND type = %s AND o.section = s.id AND o.priority = p.id"
194 % (package, suite_id, component_id, type_id));
195 result = q.getresult();
196 # If checking for a source package fall back on the binary override type
197 if type == "dsc" and not result:
198 type_id = db_access.get_override_type_id("deb");
199 q = projectB.query("SELECT s.section, p.priority FROM override o, section s, priority p WHERE package = '%s' AND suite = %s AND component = %s AND type = %s AND o.section = s.id AND o.priority = p.id"
200 % (package, suite_id, component_id, type_id));
201 result = q.getresult();
203 # Remember the section and priority so we can check them later if appropriate
205 files[file]["override section"] = result[0][0];
206 files[file]["override priority"] = result[0][1];
210 #####################################################################################################################
212 def check_changes(filename):
213 global reject_message, changes, files
215 # Default in case we bail out
216 changes["maintainer822"] = Cnf["Dinstall::MyEmailAddress"];
217 changes["changedby822"] = Cnf["Dinstall::MyEmailAddress"];
218 changes["architecture"] = {};
220 # Parse the .changes field into a dictionary
222 changes = utils.parse_changes(filename, 0)
223 except utils.cant_open_exc:
224 reject_message = reject_message + "Rejected: can't read changes file '%s'.\n" % (filename)
226 except utils.changes_parse_error_exc, line:
227 reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line)
230 # Parse the Files field from the .changes into another dictionary
232 files = utils.build_file_list(changes, "");
233 except utils.changes_parse_error_exc, line:
234 reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line);
236 # Check for mandatory fields
237 for i in ("source", "binary", "architecture", "version", "distribution","maintainer", "files"):
238 if not changes.has_key(i):
239 reject_message = reject_message + "Rejected: Missing field `%s' in changes file.\n" % (i)
240 return 0 # Avoid <undef> errors during later tests
242 # Override the Distribution: field if appropriate
243 if Options["Override-Distribution"] != "":
244 reject_message = reject_message + "Warning: Distribution was overriden from %s to %s.\n" % (changes["distribution"], Options["Override-Distribution"])
245 changes["distribution"] = Options["Override-Distribution"]
247 # Split multi-value fields into a lower-level dictionary
248 for i in ("architecture", "distribution", "binary", "closes"):
249 o = changes.get(i, "")
253 for j in string.split(o):
256 # Fix the Maintainer: field to be RFC822 compatible
257 (changes["maintainer822"], changes["maintainername"], changes["maintaineremail"]) = utils.fix_maintainer (changes["maintainer"])
259 # Fix the Changed-By: field to be RFC822 compatible; if it exists.
260 (changes["changedby822"], changes["changedbyname"], changes["changedbyemail"]) = utils.fix_maintainer(changes.get("changed-by",""));
262 # Ensure all the values in Closes: are numbers
263 if changes.has_key("closes"):
264 for i in changes["closes"].keys():
265 if re_isanum.match (i) == None:
266 reject_message = reject_message + "Rejected: `%s' from Closes field isn't a number.\n" % (i)
268 # Map frozen to unstable if frozen doesn't exist
269 if changes["distribution"].has_key("frozen") and not Cnf.has_key("Suite::Frozen"):
270 del changes["distribution"]["frozen"]
271 changes["distribution"]["unstable"] = 1;
272 reject_message = reject_message + "Mapping frozen to unstable.\n"
274 # Map testing to unstable
275 if changes["distribution"].has_key("testing"):
276 del changes["distribution"]["testing"]
277 changes["distribution"]["unstable"] = 1;
278 reject_message = reject_message + "Mapping testing to unstable.\n"
280 # Ensure target distributions exist
281 for i in changes["distribution"].keys():
282 if not Cnf.has_key("Suite::%s" % (i)):
283 reject_message = reject_message + "Rejected: Unknown distribution `%s'.\n" % (i)
285 # Ensure there _is_ a target distribution
286 if changes["distribution"].keys() == []:
287 reject_message = reject_message + "Rejected: huh? Distribution field is empty in changes file.\n";
289 # Map unreleased arches from stable to unstable
290 if changes["distribution"].has_key("stable"):
291 for i in changes["architecture"].keys():
292 if not Cnf.has_key("Suite::Stable::Architectures::%s" % (i)):
293 reject_message = reject_message + "Mapping stable to unstable for unreleased arch `%s'.\n" % (i)
294 del changes["distribution"]["stable"]
295 changes["distribution"]["unstable"] = 1;
297 # Map arches not being released from frozen to unstable
298 if changes["distribution"].has_key("frozen"):
299 for i in changes["architecture"].keys():
300 if not Cnf.has_key("Suite::Frozen::Architectures::%s" % (i)):
301 reject_message = reject_message + "Mapping frozen to unstable for non-releasing arch `%s'.\n" % (i)
302 del changes["distribution"]["frozen"]
303 changes["distribution"]["unstable"] = 1;
305 # Handle uploads to stable
306 if changes["distribution"].has_key("stable"):
307 # If running from within proposed-updates; assume an install to stable
308 if string.find(os.getcwd(), 'proposed-updates') != -1:
309 # FIXME: should probably remove anything that != stable
310 for i in ("frozen", "unstable"):
311 if changes["distribution"].has_key(i):
312 reject_message = reject_message + "Removing %s from distribution list.\n" % (i)
313 del changes["distribution"][i]
314 changes["stable upload"] = 1;
315 # If we can't find a file from the .changes; assume it's a package already in the pool and move into the pool
316 file = files.keys()[0];
317 if os.access(file, os.R_OK) == 0:
318 pool_dir = Cnf["Dir::PoolDir"] + '/' + utils.poolify(changes["source"], files[file]["component"]);
320 # Otherwise (normal case) map stable to updates
322 reject_message = reject_message + "Mapping stable to updates.\n";
323 del changes["distribution"]["stable"];
324 changes["distribution"]["proposed-updates"] = 1;
326 # chopversion = no epoch; chopversion2 = no epoch and no revision (e.g. for .orig.tar.gz comparison)
327 changes["chopversion"] = utils.re_no_epoch.sub('', changes["version"])
328 changes["chopversion2"] = utils.re_no_revision.sub('', changes["chopversion"])
330 if string.find(reject_message, "Rejected:") != -1:
336 global reject_message
338 archive = utils.where_am_i();
340 for file in files.keys():
341 # Check the file is readable
342 if os.access(file,os.R_OK) == 0:
343 if os.path.exists(file):
344 reject_message = reject_message + "Rejected: Can't read `%s'. [permission denied]\n" % (file)
346 reject_message = reject_message + "Rejected: Can't read `%s'. [file not found]\n" % (file)
348 files[file]["type"] = "unreadable";
350 # If it's byhand skip remaining checks
351 if files[file]["section"] == "byhand":
352 files[file]["byhand"] = 1;
353 files[file]["type"] = "byhand";
354 # Checks for a binary package...
355 elif utils.re_isadeb.match(file) != None:
356 files[file]["type"] = "deb";
358 # Extract package information using dpkg-deb
360 control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))
362 reject_message = reject_message + "Rejected: %s: debExtractControl() raised %s.\n" % (file, sys.exc_type);
363 # Can't continue, none of the checks on control would work.
366 # Check for mandatory fields
367 if control.Find("Package") == None:
368 reject_message = reject_message + "Rejected: %s: No package field in control.\n" % (file)
369 if control.Find("Architecture") == None:
370 reject_message = reject_message + "Rejected: %s: No architecture field in control.\n" % (file)
371 if control.Find("Version") == None:
372 reject_message = reject_message + "Rejected: %s: No version field in control.\n" % (file)
374 # Ensure the package name matches the one give in the .changes
375 if not changes["binary"].has_key(control.Find("Package", "")):
376 reject_message = reject_message + "Rejected: %s: control file lists name as `%s', which isn't in changes file.\n" % (file, control.Find("Package", ""))
378 # Validate the architecture
379 if not Cnf.has_key("Suite::Unstable::Architectures::%s" % (control.Find("Architecture", ""))):
380 reject_message = reject_message + "Rejected: Unknown architecture '%s'.\n" % (control.Find("Architecture", ""))
382 # Check the architecture matches the one given in the .changes
383 if not changes["architecture"].has_key(control.Find("Architecture", "")):
384 reject_message = reject_message + "Rejected: %s: control file lists arch as `%s', which isn't in changes file.\n" % (file, control.Find("Architecture", ""))
385 # Check the section & priority match those given in the .changes (non-fatal)
386 if control.Find("Section") != None and files[file]["section"] != "" and files[file]["section"] != control.Find("Section"):
387 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"])
388 if control.Find("Priority") != None and files[file]["priority"] != "" and files[file]["priority"] != control.Find("Priority"):
389 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"])
391 epochless_version = utils.re_no_epoch.sub('', control.Find("Version", ""))
393 files[file]["package"] = control.Find("Package");
394 files[file]["architecture"] = control.Find("Architecture");
395 files[file]["version"] = control.Find("Version");
396 files[file]["maintainer"] = control.Find("Maintainer", "");
397 if file[-5:] == ".udeb":
398 files[file]["dbtype"] = "udeb";
399 elif file[-4:] == ".deb":
400 files[file]["dbtype"] = "deb";
402 reject_message = reject_message + "Rejected: %s is neither a .deb or a .udeb.\n " % (file);
403 files[file]["fullname"] = "%s_%s_%s.deb" % (control.Find("Package", ""), epochless_version, control.Find("Architecture", ""))
404 files[file]["source"] = control.Find("Source", "");
405 if files[file]["source"] == "":
406 files[file]["source"] = files[file]["package"];
407 # Get the source version
408 source = files[file]["source"];
410 if string.find(source, "(") != -1:
411 m = utils.re_extract_src_version.match(source)
413 source_version = m.group(2)
414 if not source_version:
415 source_version = files[file]["version"];
416 files[file]["source package"] = source;
417 files[file]["source version"] = source_version;
419 # Checks for a source package...
421 m = utils.re_issource.match(file)
423 files[file]["package"] = m.group(1)
424 files[file]["version"] = m.group(2)
425 files[file]["type"] = m.group(3)
427 # Ensure the source package name matches the Source filed in the .changes
428 if changes["source"] != files[file]["package"]:
429 reject_message = reject_message + "Rejected: %s: changes file doesn't say %s for Source\n" % (file, files[file]["package"])
431 # Ensure the source version matches the version in the .changes file
432 if files[file]["type"] == "orig.tar.gz":
433 changes_version = changes["chopversion2"]
435 changes_version = changes["chopversion"]
436 if changes_version != files[file]["version"]:
437 reject_message = reject_message + "Rejected: %s: should be %s according to changes file.\n" % (file, changes_version)
439 # Ensure the .changes lists source in the Architecture field
440 if not changes["architecture"].has_key("source"):
441 reject_message = reject_message + "Rejected: %s: changes file doesn't list `source' in Architecture field.\n" % (file)
443 # Check the signature of a .dsc file
444 if files[file]["type"] == "dsc":
445 check_signature(file)
447 files[file]["fullname"] = file
448 files[file]["architecture"] = "source";
450 # Not a binary or source package? Assume byhand...
452 files[file]["byhand"] = 1;
453 files[file]["type"] = "byhand";
455 files[file]["oldfiles"] = {}
456 for suite in changes["distribution"].keys():
458 if files[file].has_key("byhand"):
461 if Cnf.has_key("Suite:%s::Components" % (suite)) and not Cnf.has_key("Suite::%s::Components::%s" % (suite, files[file]["component"])):
462 reject_message = reject_message + "Rejected: unknown component `%s' for suite `%s'.\n" % (files[file]["component"], suite)
465 # See if the package is NEW
466 if not in_override_p(files[file]["package"], files[file]["component"], suite, files[file].get("dbtype",""), file):
467 files[file]["new"] = 1
469 if files[file]["type"] == "deb":
470 # Find any old binary packages
471 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"
472 % (files[file]["package"], suite, files[file]["architecture"]))
473 oldfiles = q.dictresult()
474 for oldfile in oldfiles:
475 files[file]["oldfiles"][suite] = oldfile
476 # Check versions [NB: per-suite only; no cross-suite checking done (yet)]
477 if apt_pkg.VersionCompare(files[file]["version"], oldfile["version"]) != 1:
478 reject_message = reject_message + "Rejected: %s Old version `%s' >= new version `%s'.\n" % (file, oldfile["version"], files[file]["version"])
479 # Check for existing copies of the file
480 if not changes.has_key("stable upload"):
481 q = projectB.query("SELECT b.id FROM binaries b, architecture a WHERE b.package = '%s' AND b.version = '%s' AND a.arch_string = '%s' AND a.id = b.architecture" % (files[file]["package"], files[file]["version"], files[file]["architecture"]))
482 if q.getresult() != []:
483 reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (file)
485 # Check for existent source
486 # FIXME: this is no longer per suite
487 if changes["architecture"].has_key("source"):
488 source_version = files[file]["source version"];
489 if source_version != changes["version"]:
490 reject_message = reject_message + "Rejected: source version (%s) for %s doesn't match changes version %s.\n" % (files[file]["source version"], file, changes["version"]);
492 if not source_exists (files[file]["source package"], source_version):
493 reject_message = reject_message + "Rejected: no source found for %s %s (%s).\n" % (files[file]["source package"], source_version, file);
495 # Find any old .dsc files
496 elif files[file]["type"] == "dsc":
497 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"
498 % (files[file]["package"], suite))
499 oldfiles = q.dictresult()
500 if len(oldfiles) >= 1:
501 files[file]["oldfiles"][suite] = oldfiles[0]
503 # Validate the component
504 component = files[file]["component"];
505 component_id = db_access.get_component_id(component);
506 if component_id == -1:
507 reject_message = reject_message + "Rejected: file '%s' has unknown component '%s'.\n" % (file, component);
510 # Validate the priority
511 if string.find(files[file]["priority"],'/') != -1:
512 reject_message = reject_message + "Rejected: file '%s' has invalid priority '%s' [contains '/'].\n" % (file, files[file]["priority"]);
514 # Check the md5sum & size against existing files (if any)
515 location = Cnf["Dir::PoolDir"];
516 files[file]["location id"] = db_access.get_location_id (location, component, archive);
518 files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"]);
519 files_id = db_access.get_files_id(files[file]["pool name"] + file, files[file]["size"], files[file]["md5sum"], files[file]["location id"]);
521 reject_message = reject_message + "Rejected: INTERNAL ERROR, get_files_id() returned multiple matches for %s.\n" % (file)
523 reject_message = reject_message + "Rejected: md5sum and/or size mismatch on existing copy of %s.\n" % (file)
524 files[file]["files id"] = files_id
526 # Check for packages that have moved from one component to another
527 if files[file]["oldfiles"].has_key(suite) and files[file]["oldfiles"][suite]["name"] != files[file]["component"]:
528 files[file]["othercomponents"] = files[file]["oldfiles"][suite]["name"];
531 if string.find(reject_message, "Rejected:") != -1:
536 ###############################################################################
539 global dsc, dsc_files, reject_message, reprocess, orig_tar_id, orig_tar_location, legacy_source_untouchable;
541 for file in files.keys():
542 if files[file]["type"] == "dsc":
544 dsc = utils.parse_changes(file, 1)
545 except utils.cant_open_exc:
546 reject_message = reject_message + "Rejected: can't read changes file '%s'.\n" % (file)
548 except utils.changes_parse_error_exc, line:
549 reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (file, line)
551 except utils.invalid_dsc_format_exc, line:
552 reject_message = reject_message + "Rejected: syntax error in .dsc file '%s', line %s.\n" % (file, line)
555 dsc_files = utils.build_file_list(dsc, 1)
556 except utils.no_files_exc:
557 reject_message = reject_message + "Rejected: no Files: field in .dsc file.\n";
559 except utils.changes_parse_error_exc, line:
560 reject_message = reject_message + "Rejected: error parsing .dsc file '%s', can't grok: %s.\n" % (file, line);
563 # The dpkg maintainer from hell strikes again! Bumping the
564 # version number of the .dsc breaks extraction by stable's
566 if dsc["format"] != "1.0":
567 reject_message = reject_message + """Rejected: [dpkg-sucks] source package was produced by a broken version
568 of dpkg-dev 1.9.1{3,4}; please rebuild with >= 1.9.15 version
572 # Try and find all files mentioned in the .dsc. This has
573 # to work harder to cope with the multiple possible
574 # locations of an .orig.tar.gz.
575 for dsc_file in dsc_files.keys():
576 if files.has_key(dsc_file):
577 actual_md5 = files[dsc_file]["md5sum"];
578 actual_size = int(files[dsc_file]["size"]);
579 found = "%s in incoming" % (dsc_file)
580 # Check the file does not already exist in the archive
581 if not changes.has_key("stable upload"):
582 q = projectB.query("SELECT f.id FROM files f, location l WHERE (f.filename ~ '/%s$' OR f.filename = '%s') AND l.id = f.location" % (utils.regex_safe(dsc_file), dsc_file));
584 # "It has not broken them. It has fixed a
585 # brokenness. Your crappy hack exploited a
586 # bug in the old dinstall.
588 # "(Come on! I thought it was always obvious
589 # that one just doesn't release different
590 # files with the same name and version.)"
591 # -- ajk@ on d-devel@l.d.o
593 if q.getresult() != []:
594 reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (dsc_file)
595 elif dsc_file[-12:] == ".orig.tar.gz":
597 q = projectB.query("SELECT l.path, f.filename, l.type, f.id, l.id FROM files f, location l WHERE (f.filename ~ '/%s$' OR f.filename = '%s') AND l.id = f.location" % (utils.regex_safe(dsc_file), dsc_file));
601 # Unfortunately, we make get more than one match
602 # here if, for example, the package was in potato
603 # but had a -sa upload in woody. So we need to a)
604 # choose the right one and b) mark all wrong ones
605 # as excluded from the source poolification (to
606 # avoid file overwrites).
608 x = ql[0]; # default to something sane in case we don't match any or have only one
612 old_file = i[0] + i[1];
613 actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
614 actual_size = os.stat(old_file)[stat.ST_SIZE];
615 if actual_md5 == dsc_files[dsc_file]["md5sum"] and actual_size == int(dsc_files[dsc_file]["size"]):
618 legacy_source_untouchable[i[3]] = "";
620 old_file = x[0] + x[1];
621 actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
622 actual_size = os.stat(old_file)[stat.ST_SIZE];
625 dsc_files[dsc_file]["files id"] = x[3]; # need this for updating dsc_files in install()
628 if suite_type == "legacy" or suite_type == "legacy-mixed":
629 orig_tar_location = "legacy";
631 orig_tar_location = x[4];
633 # Not there? Check in Incoming...
634 # [See comment above process_it() for explanation
635 # of why this is necessary...]
636 if os.path.exists(dsc_file):
637 files[dsc_file] = {};
638 files[dsc_file]["size"] = os.stat(dsc_file)[stat.ST_SIZE];
639 files[dsc_file]["md5sum"] = dsc_files[dsc_file]["md5sum"];
640 files[dsc_file]["section"] = files[file]["section"];
641 files[dsc_file]["priority"] = files[file]["priority"];
642 files[dsc_file]["component"] = files[file]["component"];
643 files[dsc_file]["type"] = "orig.tar.gz";
647 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);
650 reject_message = reject_message + "Rejected: %s refers to %s, but I can't find it in Incoming.\n" % (file, dsc_file);
652 if actual_md5 != dsc_files[dsc_file]["md5sum"]:
653 reject_message = reject_message + "Rejected: md5sum for %s doesn't match %s.\n" % (found, file);
654 if actual_size != int(dsc_files[dsc_file]["size"]):
655 reject_message = reject_message + "Rejected: size for %s doesn't match %s.\n" % (found, file);
657 if string.find(reject_message, "Rejected:") != -1:
662 ###############################################################################
664 # Some cunning stunt broke dpkg-source in dpkg 1.8{,.1}; detect the
665 # resulting bad source packages and reject them.
667 # Even more amusingly the fix in 1.8.1.1 didn't actually fix the
668 # problem just changed the symptoms.
671 global dsc, dsc_files, reject_message, reprocess;
673 for filename in files.keys():
674 if files[filename]["type"] == "diff.gz":
675 file = gzip.GzipFile(filename, 'r');
676 for line in file.readlines():
677 if re_bad_diff.search(line):
678 reject_message = reject_message + "Rejected: [dpkg-sucks] source package was produced by a broken version of dpkg-dev 1.8.x; please rebuild with >= 1.8.3 version installed.\n";
681 if string.find(reject_message, "Rejected:") != -1:
686 ###############################################################################
688 def check_md5sums ():
689 global reject_message;
691 for file in files.keys():
693 file_handle = utils.open_file(file,"r");
694 except utils.cant_open_exc:
697 if apt_pkg.md5sum(file_handle) != files[file]["md5sum"]:
698 reject_message = reject_message + "Rejected: md5sum check failed for %s.\n" % (file);
700 def check_override ():
703 # Only check section & priority on sourceful uploads
704 if not changes["architecture"].has_key("source"):
708 for file in files.keys():
709 if not files[file].has_key("new") and files[file]["type"] == "deb":
710 section = files[file]["section"];
711 override_section = files[file]["override section"];
712 if section != override_section and section != "-":
713 # Ignore this; it's a common mistake and not worth whining about
714 if string.lower(section) == "non-us/main" and string.lower(override_section) == "non-us":
716 summary = summary + "%s: section is overridden from %s to %s.\n" % (file, section, override_section);
717 priority = files[file]["priority"];
718 override_priority = files[file]["override priority"];
719 if priority != override_priority and priority != "-":
720 summary = summary + "%s: priority is overridden from %s to %s.\n" % (file, priority, override_priority);
725 Subst["__SUMMARY__"] = summary;
726 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.override-disparity","r").read());
727 utils.send_mail (mail_message, "")
729 #####################################################################################################################
731 # Set up the per-package template substitution mappings
733 def update_subst (changes_filename):
736 if changes.has_key("architecture"):
737 Subst["__ARCHITECTURE__"] = string.join(changes["architecture"].keys(), ' ' );
739 Subst["__ARCHITECTURE__"] = "Unknown";
740 Subst["__CHANGES_FILENAME__"] = os.path.basename(changes_filename);
741 Subst["__FILE_CONTENTS__"] = changes.get("filecontents", "");
743 # For source uploads the Changed-By field wins; otherwise Maintainer wins.
744 if changes["architecture"].has_key("source") and changes["changedby822"] != "" and (changes["changedby822"] != changes["maintainer822"]):
745 Subst["__MAINTAINER_FROM__"] = changes["changedby822"];
746 Subst["__MAINTAINER_TO__"] = changes["changedby822"] + ", " + changes["maintainer822"];
747 Subst["__MAINTAINER__"] = changes.get("changed-by", "Unknown");
749 Subst["__MAINTAINER_FROM__"] = changes["maintainer822"];
750 Subst["__MAINTAINER_TO__"] = changes["maintainer822"];
751 Subst["__MAINTAINER__"] = changes.get("maintainer", "Unknown");
753 Subst["__REJECT_MESSAGE__"] = reject_message;
754 Subst["__SOURCE__"] = changes.get("source", "Unknown");
755 Subst["__VERSION__"] = changes.get("version", "Unknown");
757 #####################################################################################################################
759 def action (changes_filename):
760 byhand = confirm = suites = summary = new = "";
762 # changes["distribution"] may not exist in corner cases
763 # (e.g. unreadable changes files)
764 if not changes.has_key("distribution"):
765 changes["distribution"] = {};
767 for suite in changes["distribution"].keys():
768 if Cnf.has_key("Suite::%s::Confirm"):
769 confirm = confirm + suite + ", "
770 suites = suites + suite + ", "
771 confirm = confirm[:-2]
774 for file in files.keys():
775 if files[file].has_key("byhand"):
777 summary = summary + file + " byhand\n"
778 elif files[file].has_key("new"):
780 summary = summary + "(new) %s %s %s\n" % (file, files[file]["priority"], files[file]["section"])
781 if files[file].has_key("othercomponents"):
782 summary = summary + "WARNING: Already present in %s distribution.\n" % (files[file]["othercomponents"])
783 if files[file]["type"] == "deb":
784 summary = summary + apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))["Description"] + '\n';
786 files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"])
787 destination = Cnf["Dir::PoolRoot"] + files[file]["pool name"] + file
788 summary = summary + file + "\n to " + destination + "\n"
790 short_summary = summary;
792 # This is for direport's benefit...
793 f = re_fdnic.sub("\n .\n", changes.get("changes",""));
795 if confirm or byhand or new:
796 summary = summary + "Changes: " + f;
798 summary = summary + announce (short_summary, 0)
800 (prompt, answer) = ("", "XXX")
801 if Options["No-Action"] or Options["Automatic"]:
804 if string.find(reject_message, "Rejected") != -1:
806 modified_time = time.time()-os.path.getmtime(changes_filename);
807 except: # i.e. ignore errors like 'file does not exist';
809 if modified_time < 86400:
810 print "SKIP (too new)\n" + reject_message,;
811 prompt = "[S]kip, Manual reject, Quit ?";
813 print "REJECT\n" + reject_message,;
814 prompt = "[R]eject, Manual reject, Skip, Quit ?";
815 if Options["Automatic"]:
818 print "NEW to %s\n%s%s" % (suites, reject_message, summary),;
819 prompt = "[S]kip, New ack, Manual reject, Quit ?";
820 if Options["Automatic"] and Options["Ack-New"]:
823 print "BYHAND\n" + reject_message + summary,;
824 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
826 print "CONFIRM to %s\n%s%s" % (confirm, reject_message, summary),
827 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
829 print "INSTALL\n" + reject_message + summary,;
830 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
831 if Options["Automatic"]:
834 while string.find(prompt, answer) == -1:
836 answer = utils.our_raw_input()
837 m = re_default_answer.match(prompt)
840 answer = string.upper(answer[:1])
843 reject (changes_filename, "");
845 manual_reject (changes_filename);
847 install (changes_filename, summary, short_summary);
849 acknowledge_new (changes_filename, summary);
853 #####################################################################################################################
855 def install (changes_filename, summary, short_summary):
856 global install_count, install_bytes, Subst;
858 # Stable uploads are a special case
859 if changes.has_key("stable upload"):
860 stable_install (changes_filename, summary, short_summary);
865 Logger.log(["installing changes",changes_filename]);
867 archive = utils.where_am_i();
869 # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
870 projectB.query("BEGIN WORK");
872 # Add the .dsc file to the DB
873 for file in files.keys():
874 if files[file]["type"] == "dsc":
875 package = dsc["source"]
876 version = dsc["version"] # NB: not files[file]["version"], that has no epoch
877 maintainer = dsc["maintainer"]
878 maintainer = string.replace(maintainer, "'", "\\'")
879 maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
880 filename = files[file]["pool name"] + file;
881 dsc_location_id = files[file]["location id"];
882 if not files[file]["files id"]:
883 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], dsc_location_id)
884 projectB.query("INSERT INTO source (source, version, maintainer, file) VALUES ('%s', '%s', %d, %d)"
885 % (package, version, maintainer_id, files[file]["files id"]))
887 for suite in changes["distribution"].keys():
888 suite_id = db_access.get_suite_id(suite);
889 projectB.query("INSERT INTO src_associations (suite, source) VALUES (%d, currval('source_id_seq'))" % (suite_id))
891 # Add the source files to the DB (files and dsc_files)
892 projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files[file]["files id"]));
893 for dsc_file in dsc_files.keys():
894 filename = files[file]["pool name"] + dsc_file;
895 # If the .orig.tar.gz is already in the pool, it's
896 # files id is stored in dsc_files by check_dsc().
897 files_id = dsc_files[dsc_file].get("files id", None);
899 files_id = db_access.get_files_id(filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], dsc_location_id);
900 # FIXME: needs to check for -1/-2 and or handle exception
902 files_id = db_access.set_files_id (filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], dsc_location_id);
903 projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files_id));
905 # Add the .deb files to the DB
906 for file in files.keys():
907 if files[file]["type"] == "deb":
908 package = files[file]["package"]
909 version = files[file]["version"]
910 maintainer = files[file]["maintainer"]
911 maintainer = string.replace(maintainer, "'", "\\'")
912 maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
913 architecture = files[file]["architecture"]
914 architecture_id = db_access.get_architecture_id (architecture);
915 type = files[file]["dbtype"];
916 dsc_component = files[file]["component"]
917 source = files[file]["source package"]
918 source_version = files[file]["source version"];
919 filename = files[file]["pool name"] + file;
920 if not files[file]["files id"]:
921 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], files[file]["location id"])
922 source_id = db_access.get_source_id (source, source_version);
924 projectB.query("INSERT INTO binaries (package, version, maintainer, source, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, %d, '%s')"
925 % (package, version, maintainer_id, source_id, architecture_id, files[file]["files id"], type));
927 projectB.query("INSERT INTO binaries (package, version, maintainer, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, '%s')"
928 % (package, version, maintainer_id, architecture_id, files[file]["files id"], type));
929 for suite in changes["distribution"].keys():
930 suite_id = db_access.get_suite_id(suite);
931 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES (%d, currval('binaries_id_seq'))" % (suite_id));
933 # If the .orig.tar.gz is in a legacy directory we need to poolify
934 # it, so that apt-get source (and anything else that goes by the
935 # "Directory:" field in the Sources.gz file) works.
936 if orig_tar_id != None and orig_tar_location == "legacy":
937 q = projectB.query("SELECT DISTINCT ON (f.id) l.path, f.filename, f.id as files_id, df.source, df.id as dsc_files_id, f.size, f.md5sum FROM files f, dsc_files df, location l WHERE df.source IN (SELECT source FROM dsc_files WHERE file = %s) AND f.id = df.file AND l.id = f.location AND (l.type = 'legacy' OR l.type = 'legacy-mixed')" % (orig_tar_id));
940 # Is this an old upload superseded by a newer -sa upload? (See check_dsc() for details)
941 if legacy_source_untouchable.has_key(qid["files_id"]):
943 # First move the files to the new location
944 legacy_filename = qid["path"]+qid["filename"];
945 pool_location = utils.poolify (changes["source"], files[file]["component"]);
946 pool_filename = pool_location + os.path.basename(qid["filename"]);
947 destination = Cnf["Dir::PoolDir"] + pool_location
948 utils.move(legacy_filename, destination);
949 # Then Update the DB's files table
950 q = projectB.query("UPDATE files SET filename = '%s', location = '%s' WHERE id = '%s'" % (pool_filename, dsc_location_id, qid["files_id"]));
952 # If this is a sourceful diff only upload that is moving non-legacy
953 # cross-component we need to copy the .orig.tar.gz into the new
954 # component too for the same reasons as above.
956 if changes["architecture"].has_key("source") and orig_tar_id != None and \
957 orig_tar_location != "legacy" and orig_tar_location != dsc_location_id:
958 q = projectB.query("SELECT l.path, f.filename, f.size, f.md5sum FROM files f, location l WHERE f.id = %s AND f.location = l.id" % (orig_tar_id));
959 ql = q.getresult()[0];
960 old_filename = ql[0] + ql[1];
963 new_filename = utils.poolify (changes["source"], dsc_component) + os.path.basename(old_filename);
964 new_files_id = db_access.get_files_id(new_filename, file_size, file_md5sum, dsc_location_id);
965 if new_files_id == None:
966 utils.copy(old_filename, Cnf["Dir::PoolDir"] + new_filename);
967 new_files_id = db_access.set_files_id(new_filename, file_size, file_md5sum, dsc_location_id);
968 projectB.query("UPDATE dsc_files SET file = %s WHERE source = %s AND file = %s" % (new_files_id, source_id, orig_tar_id));
970 # Install the files into the pool
971 for file in files.keys():
972 if files[file].has_key("byhand"):
974 destination = Cnf["Dir::PoolDir"] + files[file]["pool name"] + file
975 destdir = os.path.dirname(destination)
976 utils.move (file, destination)
977 Logger.log(["installed", file, files[file]["type"], files[file]["size"], files[file]["architecture"]]);
978 install_bytes = install_bytes + float(files[file]["size"])
980 # Copy the .changes file across for suite which need it.
981 for suite in changes["distribution"].keys():
982 if Cnf.has_key("Suite::%s::CopyChanges" % (suite)):
983 utils.copy (changes_filename, Cnf["Dir::RootDir"] + Cnf["Suite::%s::CopyChanges" % (suite)]);
985 projectB.query("COMMIT WORK");
988 utils.move (changes_filename, Cnf["Dir::IncomingDir"] + 'DONE/' + os.path.basename(changes_filename))
990 utils.warn("couldn't move changes file '%s' to DONE directory. [Got %s]" % (os.path.basename(changes_filename), sys.exc_type));
992 install_count = install_count + 1;
994 if not Options["No-Mail"]:
995 Subst["__SUITE__"] = "";
996 Subst["__SUMMARY__"] = summary;
997 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.installed","r").read());
998 utils.send_mail (mail_message, "")
999 announce (short_summary, 1)
1002 #####################################################################################################################
1004 def stable_install (changes_filename, summary, short_summary):
1005 global install_count, install_bytes, Subst;
1007 print "Installing to stable."
1009 archive = utils.where_am_i();
1011 # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
1012 projectB.query("BEGIN WORK");
1014 # Add the .dsc file to the DB
1015 for file in files.keys():
1016 if files[file]["type"] == "dsc":
1017 package = dsc["source"]
1018 version = dsc["version"] # NB: not files[file]["version"], that has no epoch
1019 q = projectB.query("SELECT id FROM source WHERE source = '%s' AND version = '%s'" % (package, version))
1022 utils.fubar("[INTERNAL ERROR] couldn't find '%s' (%s) in source table." % (package, version));
1023 source_id = ql[0][0];
1024 suite_id = db_access.get_suite_id('proposed-updates');
1025 projectB.query("DELETE FROM src_associations WHERE suite = '%s' AND source = '%s'" % (suite_id, source_id));
1026 suite_id = db_access.get_suite_id('stable');
1027 projectB.query("INSERT INTO src_associations (suite, source) VALUES ('%s', '%s')" % (suite_id, source_id));
1029 # Add the .deb files to the DB
1030 for file in files.keys():
1031 if files[file]["type"] == "deb":
1032 package = files[file]["package"]
1033 version = files[file]["version"]
1034 architecture = files[file]["architecture"]
1035 q = projectB.query("SELECT b.id FROM binaries b, architecture a WHERE b.package = '%s' AND b.version = '%s' AND (a.arch_string = '%s' OR a.arch_string = 'all') AND b.architecture = a.id" % (package, version, architecture))
1038 utils.fubar("[INTERNAL ERROR] couldn't find '%s' (%s for %s architecture) in binaries table." % (package, version, architecture));
1039 binary_id = ql[0][0];
1040 suite_id = db_access.get_suite_id('proposed-updates');
1041 projectB.query("DELETE FROM bin_associations WHERE suite = '%s' AND bin = '%s'" % (suite_id, binary_id));
1042 suite_id = db_access.get_suite_id('stable');
1043 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES ('%s', '%s')" % (suite_id, binary_id));
1045 projectB.query("COMMIT WORK");
1048 utils.move (changes_filename, Cnf["Dir::Morgue"] + '/katie/' + os.path.basename(changes_filename));
1050 # Update the Stable ChangeLog file
1052 new_changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + ".ChangeLog";
1053 changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + "ChangeLog";
1054 if os.path.exists(new_changelog_filename):
1055 os.unlink (new_changelog_filename);
1057 new_changelog = utils.open_file(new_changelog_filename, 'w');
1058 for file in files.keys():
1059 if files[file]["type"] == "deb":
1060 new_changelog.write("stable/%s/binary-%s/%s\n" % (files[file]["component"], files[file]["architecture"], file));
1061 elif utils.re_issource.match(file) != None:
1062 new_changelog.write("stable/%s/source/%s\n" % (files[file]["component"], file));
1064 new_changelog.write("%s\n" % (file));
1065 chop_changes = re_fdnic.sub("\n", changes["changes"]);
1066 new_changelog.write(chop_changes + '\n\n');
1067 if os.access(changelog_filename, os.R_OK) != 0:
1068 changelog = utils.open_file(changelog_filename, 'r');
1069 new_changelog.write(changelog.read());
1070 new_changelog.close();
1071 if os.access(changelog_filename, os.R_OK) != 0:
1072 os.unlink(changelog_filename);
1073 utils.move(new_changelog_filename, changelog_filename);
1075 install_count = install_count + 1;
1077 if not Options["No-Mail"]:
1078 Subst["__SUITE__"] = " into stable";
1079 Subst["__SUMMARY__"] = summary;
1080 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.installed","r").read());
1081 utils.send_mail (mail_message, "")
1082 announce (short_summary, 1)
1084 #####################################################################################################################
1086 def reject (changes_filename, manual_reject_mail_filename):
1089 print "Rejecting.\n"
1091 base_changes_filename = os.path.basename(changes_filename);
1092 reason_filename = re_changes.sub("reason", base_changes_filename);
1093 reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename);
1095 # Move the .changes files and it's contents into REJECT/ (if we can; errors are ignored)
1097 utils.move (changes_filename, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], base_changes_filename));
1099 utils.warn("couldn't reject changes file '%s'. [Got %s]" % (base_changes_filename, sys.exc_type));
1101 for file in files.keys():
1102 if os.path.exists(file):
1104 utils.move (file, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], file));
1106 utils.warn("couldn't reject file '%s'. [Got %s]" % (file, sys.exc_type));
1109 # If this is not a manual rejection generate the .reason file and rejection mail message
1110 if manual_reject_mail_filename == "":
1111 if os.path.exists(reject_filename):
1112 os.unlink(reject_filename);
1113 fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
1114 os.write(fd, reject_message);
1116 Subst["__MANUAL_REJECT_MESSAGE__"] = "";
1117 Subst["__CC__"] = "X-Katie-Rejection: automatic (moo)";
1118 reject_mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.rejected","r").read());
1119 else: # Have a manual rejection file to use
1120 reject_mail_message = ""; # avoid <undef>'s
1122 # Send the rejection mail if appropriate
1123 if not Options["No-Mail"]:
1124 utils.send_mail (reject_mail_message, manual_reject_mail_filename);
1126 Logger.log(["rejected", changes_filename]);
1128 ##################################################################
1130 def manual_reject (changes_filename):
1133 # Build up the rejection email
1134 user_email_address = utils.whoami() + " <%s@%s>" % (pwd.getpwuid(os.getuid())[0], Cnf["Dinstall::MyHost"])
1135 manual_reject_message = Cnf.get("Dinstall::Options::Manual-Reject", "")
1137 Subst["__MANUAL_REJECT_MESSAGE__"] = manual_reject_message;
1138 Subst["__CC__"] = "Cc: " + Cnf["Dinstall::MyEmailAddress"];
1139 reject_mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.rejected","r").read());
1141 # Write the rejection email out as the <foo>.reason file
1142 reason_filename = re_changes.sub("reason", os.path.basename(changes_filename));
1143 reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename)
1144 if os.path.exists(reject_filename):
1145 os.unlink(reject_filename);
1146 fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
1147 os.write(fd, reject_mail_message);
1150 # If we weren't given one, spawn an editor so the user can add one in
1151 if manual_reject_message == "":
1152 result = os.system("vi +6 %s" % (reject_filename))
1154 utils.fubar("vi invocation failed for `%s'!" % (reject_filename), result);
1156 # Then process it as if it were an automatic rejection
1157 reject (changes_filename, reject_filename)
1159 #####################################################################################################################
1161 def acknowledge_new (changes_filename, summary):
1162 global new_ack_new, Subst;
1164 changes_filename = os.path.basename(changes_filename);
1166 new_ack_new[changes_filename] = 1;
1168 if new_ack_old.has_key(changes_filename):
1169 print "Ack already sent.";
1172 print "Sending new ack.";
1173 if not Options["No-Mail"]:
1174 Subst["__SUMMARY__"] = summary;
1175 new_ack_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.new","r").read());
1176 utils.send_mail(new_ack_message,"");
1178 #####################################################################################################################
1180 def announce (short_summary, action):
1183 # Only do announcements for source uploads with a recent dpkg-dev installed
1184 if float(changes.get("format", 0)) < 1.6 or not changes["architecture"].has_key("source"):
1189 Subst["__SHORT_SUMMARY__"] = short_summary;
1191 for dist in changes["distribution"].keys():
1192 list = Cnf.Find("Suite::%s::Announce" % (dist))
1193 if list == "" or lists_done.has_key(list):
1195 lists_done[list] = 1
1196 summary = summary + "Announcing to %s\n" % (list)
1199 Subst["__ANNOUNCE_LIST_ADDRESS__"] = list;
1200 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.announce","r").read());
1201 utils.send_mail (mail_message, "")
1203 bugs = changes["closes"].keys()
1205 if not nmu.is_an_nmu(changes, dsc):
1206 summary = summary + "Closing bugs: "
1208 summary = summary + "%s " % (bug)
1210 Subst["__BUG_NUMBER__"] = bug;
1211 if changes["distribution"].has_key("stable"):
1212 Subst["__STABLE_WARNING__"] = """
1213 Note that this package is not part of the released stable Debian
1214 distribution. It may have dependencies on other unreleased software,
1215 or other instabilities. Please take care if you wish to install it.
1216 The update will eventually make its way into the next released Debian
1219 Subst["__STABLE_WARNING__"] = "";
1220 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.bug-close","r").read());
1221 utils.send_mail (mail_message, "")
1222 Logger.log(["closing bugs"]+bugs);
1224 summary = summary + "Setting bugs to severity fixed: "
1225 control_message = ""
1227 summary = summary + "%s " % (bug)
1228 control_message = control_message + "tag %s + fixed\n" % (bug)
1229 if action and control_message != "":
1230 Subst["__CONTROL_MESSAGE__"] = control_message;
1231 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.bug-nmu-fixed","r").read());
1232 utils.send_mail (mail_message, "")
1233 Logger.log(["setting bugs to fixed"]+bugs);
1234 summary = summary + "\n"
1238 ###############################################################################
1240 # reprocess is necessary for the case of foo_1.2-1 and foo_1.2-2 in
1241 # Incoming. -1 will reference the .orig.tar.gz, but -2 will not.
1242 # dsccheckdistrib() can find the .orig.tar.gz but it will not have
1243 # processed it during it's checks of -2. If -1 has been deleted or
1244 # otherwise not checked by da-install, the .orig.tar.gz will not have
1245 # been checked at all. To get round this, we force the .orig.tar.gz
1246 # into the .changes structure and reprocess the .changes file.
1248 def process_it (changes_file):
1249 global reprocess, orig_tar_id, orig_tar_location, changes, dsc, dsc_files, files, reject_message;
1251 # Reset some globals
1258 orig_tar_location = "";
1259 legacy_source_untouchable = {};
1260 reject_message = "";
1262 # Absolutize the filename to avoid the requirement of being in the
1263 # same directory as the .changes file.
1264 changes_file = os.path.abspath(changes_file);
1266 # And since handling of installs to stable munges with the CWD;
1267 # save and restore it.
1271 check_signature (changes_file);
1272 check_changes (changes_file);
1281 traceback.print_exc(file=sys.stdout);
1284 update_subst(changes_file);
1285 action(changes_file);
1290 ###############################################################################
1293 global Cnf, Options, projectB, install_bytes, new_ack_old, Subst, nmu, Logger
1297 Cnf = apt_pkg.newConfiguration();
1298 apt_pkg.ReadConfigFileISC(Cnf,utils.which_conf_file());
1300 Arguments = [('a',"automatic","Dinstall::Options::Automatic"),
1301 ('d',"debug","Dinstall::Options::Debug", "IntVal"),
1302 ('h',"help","Dinstall::Options::Help"),
1303 ('k',"ack-new","Dinstall::Options::Ack-New"),
1304 ('m',"manual-reject","Dinstall::Options::Manual-Reject", "HasArg"),
1305 ('n',"no-action","Dinstall::Options::No-Action"),
1306 ('p',"no-lock", "Dinstall::Options::No-Lock"),
1307 ('s',"no-mail", "Dinstall::Options::No-Mail"),
1308 ('u',"override-distribution", "Dinstall::Options::Override-Distribution", "HasArg"),
1309 ('v',"version","Dinstall::Options::Version")];
1311 changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
1312 Options = Cnf.SubTree("Dinstall::Options")
1317 if Options["Version"]:
1318 print "katie version 0.0000000000";
1321 postgresql_user = None; # Default == Connect as user running program.
1323 # -n/--dry-run invalidates some other options which would involve things happening
1324 if Options["No-Action"]:
1325 Options["Automatic"] = "";
1326 Options["Ack-New"] = "";
1327 postgresql_user = Cnf["DB::ROUser"];
1329 projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"]), None, None, postgresql_user);
1331 db_access.init(Cnf, projectB);
1333 # Check that we aren't going to clash with the daily cron job
1335 if not Options["No-Action"] and os.path.exists("%s/Archive_Maintenance_In_Progress" % (Cnf["Dir::RootDir"])) and not Options["No-Lock"]:
1336 utils.fubar("Archive maintenance in progress. Try again later.");
1338 # Obtain lock if not in no-action mode and initialize the log
1340 if not Options["No-Action"]:
1341 lock_fd = os.open(Cnf["Dinstall::LockFile"], os.O_RDWR);
1342 fcntl.lockf(lock_fd, FCNTL.F_TLOCK);
1343 Logger = logging.Logger(Cnf, "katie");
1345 # Read in the list of already-acknowledged NEW packages
1346 new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'r');
1348 for line in new_ack_list.readlines():
1349 new_ack_old[line[:-1]] = 1;
1350 new_ack_list.close();
1352 # Initialize the substitution template mapping global
1354 Subst["__ADMIN_ADDRESS__"] = Cnf["Dinstall::MyAdminAddress"];
1355 Subst["__BUG_SERVER__"] = Cnf["Dinstall::BugServer"];
1356 bcc = "X-Katie: $Revision: 1.51 $"
1357 if Cnf.has_key("Dinstall::Bcc"):
1358 Subst["__BCC__"] = bcc + "\nBcc: %s" % (Cnf["Dinstall::Bcc"]);
1360 Subst["__BCC__"] = bcc;
1361 Subst["__DISTRO__"] = Cnf["Dinstall::MyDistribution"];
1362 Subst["__KATIE_ADDRESS__"] = Cnf["Dinstall::MyEmailAddress"];
1364 # Read in the group-maint override file
1367 # Sort the .changes files so that we process sourceful ones first
1368 changes_files.sort(utils.changes_compare);
1370 # Process the changes files
1371 for changes_file in changes_files:
1372 print "\n" + changes_file;
1373 process_it (changes_file);
1377 if install_count > 1:
1379 sys.stderr.write("Installed %d package %s, %s.\n" % (install_count, sets, utils.size_type(int(install_bytes))));
1380 Logger.log(["total",install_count,install_bytes]);
1382 # Write out the list of already-acknowledged NEW packages
1383 if Options["Ack-New"]:
1384 new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'w')
1385 for i in new_ack_new.keys():
1386 new_ack_list.write(i+'\n')
1387 new_ack_list.close()
1389 if not Options["No-Action"]:
1392 if __name__ == '__main__':