3 # Installs Debian packaes
4 # Copyright (C) 2000, 2001 James Troup <james@nocrew.org>
5 # $Id: katie,v 1.59 2001-09-13 23:56:29 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
41 ###############################################################################
43 re_isanum = re.compile (r"^\d+$");
44 re_changes = re.compile (r"changes$");
45 re_default_answer = re.compile(r"\[(.*)\]");
46 re_fdnic = re.compile("\n\n");
47 re_bad_diff = re.compile("^[\-\+][\-\+][\-\+] /dev/null");
48 re_bin_only_nmu_of_mu = re.compile("\.\d+\.\d+$");
49 re_bin_only_nmu_of_nmu = re.compile("\.\d+$");
51 #########################################################################################
69 orig_tar_location = "";
70 legacy_source_untouchable = {};
74 #########################################################################################
76 def usage (exit_code):
77 print """Usage: dinstall [OPTION]... [CHANGES]...
78 -a, --automatic automatic run
79 -D, --debug=VALUE turn on debugging
80 -h, --help show this help and exit.
81 -k, --ack-new acknowledge new packages !! for cron.daily only !!
82 -m, --manual-reject=MSG manual reject with `msg'
83 -n, --no-action don't do anything
84 -p, --no-lock don't check lockfile !! for cron.daily only !!
85 -u, --distribution=DIST override distribution to `dist'
86 -v, --version display the version number and exit"""
89 #########################################################################################
91 def check_signature (filename):
94 (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))
96 reject_message = reject_message + "Rejected: GPG signature check failed on `%s'.\n%s\n" % (os.path.basename(filename), output)
100 ######################################################################################################
103 # Read in the group maintainer override file
105 self.group_maint = {};
106 if Cnf.get("Dinstall::GroupOverrideFilename"):
107 filename = Cnf["Dir::OverrideDir"] + Cnf["Dinstall::GroupOverrideFilename"];
108 file = utils.open_file(filename, 'r');
109 for line in file.readlines():
110 line = string.strip(utils.re_comments.sub('', line));
112 self.group_maint[line] = 1;
115 def is_an_nmu (self, changes, dsc):
116 (dsc_rfc822, dsc_name, dsc_email) = utils.fix_maintainer (dsc.get("maintainer",Cnf["Dinstall::MyEmailAddress"]));
117 # changes["changedbyname"] == dsc_name is probably never true, but better safe than sorry
118 if dsc_name == changes["maintainername"] and (changes["changedby822"] == "" or changes["changedbyname"] == dsc_name):
121 if dsc.has_key("uploaders"):
122 uploaders = string.split(dsc["uploaders"], ",");
125 (rfc822, name, email) = utils.fix_maintainer (string.strip(i));
126 uploadernames[name] = "";
127 if uploadernames.has_key(changes["changedbyname"]):
130 # Some group maintained packages (e.g. Debian QA) are never NMU's
131 if self.group_maint.has_key(changes["maintaineremail"]):
136 ######################################################################################################
138 # Ensure that source exists somewhere in the archive for the binary
139 # upload being processed.
141 # (1) exact match => 1.0-3
142 # (2) Bin-only NMU of an MU => 1.0-3.0.1
143 # (3) Bin-only NMU of a sourceful-NMU => 1.0-3.1.1
145 def source_exists (package, source_version):
146 q = projectB.query("SELECT s.version FROM source s WHERE s.source = '%s'" % (package));
148 # Reduce the query results to a list of version numbers
149 ql = map(lambda x: x[0], q.getresult());
152 if ql.count(source_version):
156 orig_source_version = re_bin_only_nmu_of_mu.sub('', source_version);
157 if ql.count(orig_source_version):
161 orig_source_version = re_bin_only_nmu_of_nmu.sub('', source_version);
162 if ql.count(orig_source_version):
168 ######################################################################################################
170 # See if a given package is in the override table
172 def in_override_p (package, component, suite, binary_type, file):
175 if binary_type == "": # must be source
180 # Override suite name; used for example with proposed-updates
181 if Cnf.Find("Suite::%s::OverrideSuite" % (suite)) != "":
182 suite = Cnf["Suite::%s::OverrideSuite" % (suite)];
184 # Avoid <undef> on unknown distributions
185 suite_id = db_access.get_suite_id(suite);
188 component_id = db_access.get_component_id(component);
189 type_id = db_access.get_override_type_id(type);
191 # FIXME: nasty non-US speficic hack
192 if string.lower(component[:7]) == "non-us/":
193 component = component[7:];
195 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"
196 % (package, suite_id, component_id, type_id));
197 result = q.getresult();
198 # If checking for a source package fall back on the binary override type
199 if type == "dsc" and not result:
200 type_id = db_access.get_override_type_id("deb");
201 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"
202 % (package, suite_id, component_id, type_id));
203 result = q.getresult();
205 # Remember the section and priority so we can check them later if appropriate
207 files[file]["override section"] = result[0][0];
208 files[file]["override priority"] = result[0][1];
212 #####################################################################################################################
214 def check_changes(filename):
215 global reject_message, changes, files
217 # Default in case we bail out
218 changes["maintainer822"] = Cnf["Dinstall::MyEmailAddress"];
219 changes["changedby822"] = Cnf["Dinstall::MyEmailAddress"];
220 changes["architecture"] = {};
222 # Parse the .changes field into a dictionary
224 changes = utils.parse_changes(filename, 0)
225 except utils.cant_open_exc:
226 reject_message = reject_message + "Rejected: can't read changes file '%s'.\n" % (filename)
228 except utils.changes_parse_error_exc, line:
229 reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line)
232 # Parse the Files field from the .changes into another dictionary
234 files = utils.build_file_list(changes, "");
235 except utils.changes_parse_error_exc, line:
236 reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line);
237 except utils.nk_format_exc, format:
238 reject_message = reject_message + "Rejected: unknown format '%s' of changes file '%s'.\n" % (format, filename);
241 # Check for mandatory fields
242 for i in ("source", "binary", "architecture", "version", "distribution", "maintainer", "files"):
243 if not changes.has_key(i):
244 reject_message = reject_message + "Rejected: Missing field `%s' in changes file.\n" % (i)
245 return 0 # Avoid <undef> errors during later tests
247 # Override the Distribution: field if appropriate
248 if Options["Override-Distribution"] != "":
249 reject_message = reject_message + "Warning: Distribution was overriden from %s to %s.\n" % (changes["distribution"], Options["Override-Distribution"])
250 changes["distribution"] = Options["Override-Distribution"]
252 # Split multi-value fields into a lower-level dictionary
253 for i in ("architecture", "distribution", "binary", "closes"):
254 o = changes.get(i, "")
258 for j in string.split(o):
261 # Fix the Maintainer: field to be RFC822 compatible
262 (changes["maintainer822"], changes["maintainername"], changes["maintaineremail"]) = utils.fix_maintainer (changes["maintainer"])
264 # Fix the Changed-By: field to be RFC822 compatible; if it exists.
265 (changes["changedby822"], changes["changedbyname"], changes["changedbyemail"]) = utils.fix_maintainer(changes.get("changed-by",""));
267 # Ensure all the values in Closes: are numbers
268 if changes.has_key("closes"):
269 for i in changes["closes"].keys():
270 if re_isanum.match (i) == None:
271 reject_message = reject_message + "Rejected: `%s' from Closes field isn't a number.\n" % (i)
273 # Ensure there _is_ a target distribution
274 if changes["distribution"].keys() == []:
275 reject_message = reject_message + "Rejected: huh? Distribution field is empty in changes file.\n";
277 # Map frozen to unstable if frozen doesn't exist
278 if changes["distribution"].has_key("frozen") and not Cnf.has_key("Suite::Frozen"):
279 del changes["distribution"]["frozen"]
280 changes["distribution"]["unstable"] = 1;
281 reject_message = reject_message + "Mapping frozen to unstable.\n"
283 # Map testing to unstable
284 if changes["distribution"].has_key("testing"):
285 if len(changes["distribution"].keys()) > 1:
286 del changes["distribution"]["testing"];
287 reject_message = reject_message + "Warning: Ignoring testing as a target suite.\n";
289 reject_message = reject_message + "Rejected: invalid distribution 'testing'.\n";
291 # Ensure target distributions exist
292 for i in changes["distribution"].keys():
293 if not Cnf.has_key("Suite::%s" % (i)):
294 reject_message = reject_message + "Rejected: Unknown distribution `%s'.\n" % (i)
296 # Map unreleased arches from stable to unstable
297 if changes["distribution"].has_key("stable"):
298 for i in changes["architecture"].keys():
299 if not Cnf.has_key("Suite::Stable::Architectures::%s" % (i)):
300 reject_message = reject_message + "Mapping stable to unstable for unreleased arch `%s'.\n" % (i)
301 del changes["distribution"]["stable"]
302 changes["distribution"]["unstable"] = 1;
304 # Map arches not being released from frozen to unstable
305 if changes["distribution"].has_key("frozen"):
306 for i in changes["architecture"].keys():
307 if not Cnf.has_key("Suite::Frozen::Architectures::%s" % (i)):
308 reject_message = reject_message + "Mapping frozen to unstable for non-releasing arch `%s'.\n" % (i)
309 del changes["distribution"]["frozen"]
310 changes["distribution"]["unstable"] = 1;
312 # Handle uploads to stable
313 if changes["distribution"].has_key("stable"):
314 # If running from within proposed-updates; assume an install to stable
315 if string.find(os.getcwd(), 'proposed-updates') != -1:
316 # FIXME: should probably remove anything that != stable
317 for i in ("frozen", "unstable"):
318 if changes["distribution"].has_key(i):
319 reject_message = reject_message + "Removing %s from distribution list.\n" % (i)
320 del changes["distribution"][i]
321 changes["stable upload"] = 1;
322 # If we can't find a file from the .changes; assume it's a package already in the pool and move into the pool
323 file = files.keys()[0];
324 if os.access(file, os.R_OK) == 0:
325 pool_dir = Cnf["Dir::PoolDir"] + '/' + utils.poolify(changes["source"], files[file]["component"]);
327 # Otherwise (normal case) map stable to updates
329 reject_message = reject_message + "Mapping stable to updates.\n";
330 del changes["distribution"]["stable"];
331 changes["distribution"]["proposed-updates"] = 1;
333 # chopversion = no epoch; chopversion2 = no epoch and no revision (e.g. for .orig.tar.gz comparison)
334 changes["chopversion"] = utils.re_no_epoch.sub('', changes["version"])
335 changes["chopversion2"] = utils.re_no_revision.sub('', changes["chopversion"])
337 if string.find(reject_message, "Rejected:") != -1:
343 global reject_message
345 archive = utils.where_am_i();
347 for file in files.keys():
348 # Check the file is readable
349 if os.access(file,os.R_OK) == 0:
350 if os.path.exists(file):
351 reject_message = reject_message + "Rejected: Can't read `%s'. [permission denied]\n" % (file)
353 reject_message = reject_message + "Rejected: Can't read `%s'. [file not found]\n" % (file)
355 files[file]["type"] = "unreadable";
357 # If it's byhand skip remaining checks
358 if files[file]["section"] == "byhand":
359 files[file]["byhand"] = 1;
360 files[file]["type"] = "byhand";
361 # Checks for a binary package...
362 elif utils.re_isadeb.match(file) != None:
363 files[file]["type"] = "deb";
365 # Extract package information using dpkg-deb
367 control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))
369 reject_message = reject_message + "Rejected: %s: debExtractControl() raised %s.\n" % (file, sys.exc_type);
370 # Can't continue, none of the checks on control would work.
373 # Check for mandatory fields
374 if control.Find("Package") == None:
375 reject_message = reject_message + "Rejected: %s: No package field in control.\n" % (file)
376 if control.Find("Architecture") == None:
377 reject_message = reject_message + "Rejected: %s: No architecture field in control.\n" % (file)
378 if control.Find("Version") == None:
379 reject_message = reject_message + "Rejected: %s: No version field in control.\n" % (file)
381 # Ensure the package name matches the one give in the .changes
382 if not changes["binary"].has_key(control.Find("Package", "")):
383 reject_message = reject_message + "Rejected: %s: control file lists name as `%s', which isn't in changes file.\n" % (file, control.Find("Package", ""))
385 # Validate the architecture
386 if not Cnf.has_key("Suite::Unstable::Architectures::%s" % (control.Find("Architecture", ""))):
387 reject_message = reject_message + "Rejected: Unknown architecture '%s'.\n" % (control.Find("Architecture", ""))
389 # Check the architecture matches the one given in the .changes
390 if not changes["architecture"].has_key(control.Find("Architecture", "")):
391 reject_message = reject_message + "Rejected: %s: control file lists arch as `%s', which isn't in changes file.\n" % (file, control.Find("Architecture", ""))
392 # Check the section & priority match those given in the .changes (non-fatal)
393 if control.Find("Section") != None and files[file]["section"] != "" and files[file]["section"] != control.Find("Section"):
394 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"])
395 if control.Find("Priority") != None and files[file]["priority"] != "" and files[file]["priority"] != control.Find("Priority"):
396 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"])
398 epochless_version = utils.re_no_epoch.sub('', control.Find("Version", ""))
400 files[file]["package"] = control.Find("Package");
401 files[file]["architecture"] = control.Find("Architecture");
402 files[file]["version"] = control.Find("Version");
403 files[file]["maintainer"] = control.Find("Maintainer", "");
404 if file[-5:] == ".udeb":
405 files[file]["dbtype"] = "udeb";
406 elif file[-4:] == ".deb":
407 files[file]["dbtype"] = "deb";
409 reject_message = reject_message + "Rejected: %s is neither a .deb or a .udeb.\n " % (file);
410 files[file]["fullname"] = "%s_%s_%s.deb" % (control.Find("Package", ""), epochless_version, control.Find("Architecture", ""))
411 files[file]["source"] = control.Find("Source", "");
412 if files[file]["source"] == "":
413 files[file]["source"] = files[file]["package"];
414 # Get the source version
415 source = files[file]["source"];
417 if string.find(source, "(") != -1:
418 m = utils.re_extract_src_version.match(source)
420 source_version = m.group(2)
421 if not source_version:
422 source_version = files[file]["version"];
423 files[file]["source package"] = source;
424 files[file]["source version"] = source_version;
426 # Checks for a source package...
428 m = utils.re_issource.match(file)
430 files[file]["package"] = m.group(1)
431 files[file]["version"] = m.group(2)
432 files[file]["type"] = m.group(3)
434 # Ensure the source package name matches the Source filed in the .changes
435 if changes["source"] != files[file]["package"]:
436 reject_message = reject_message + "Rejected: %s: changes file doesn't say %s for Source\n" % (file, files[file]["package"])
438 # Ensure the source version matches the version in the .changes file
439 if files[file]["type"] == "orig.tar.gz":
440 changes_version = changes["chopversion2"]
442 changes_version = changes["chopversion"]
443 if changes_version != files[file]["version"]:
444 reject_message = reject_message + "Rejected: %s: should be %s according to changes file.\n" % (file, changes_version)
446 # Ensure the .changes lists source in the Architecture field
447 if not changes["architecture"].has_key("source"):
448 reject_message = reject_message + "Rejected: %s: changes file doesn't list `source' in Architecture field.\n" % (file)
450 # Check the signature of a .dsc file
451 if files[file]["type"] == "dsc":
452 check_signature(file)
454 files[file]["fullname"] = file
455 files[file]["architecture"] = "source";
457 # Not a binary or source package? Assume byhand...
459 files[file]["byhand"] = 1;
460 files[file]["type"] = "byhand";
462 files[file]["oldfiles"] = {}
463 for suite in changes["distribution"].keys():
465 if files[file].has_key("byhand"):
468 if Cnf.has_key("Suite:%s::Components" % (suite)) and not Cnf.has_key("Suite::%s::Components::%s" % (suite, files[file]["component"])):
469 reject_message = reject_message + "Rejected: unknown component `%s' for suite `%s'.\n" % (files[file]["component"], suite)
472 # See if the package is NEW
473 if not in_override_p(files[file]["package"], files[file]["component"], suite, files[file].get("dbtype",""), file):
474 files[file]["new"] = 1
476 if files[file]["type"] == "deb":
477 # Find any old binary packages
478 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' OR a.arch_string = 'all') 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"
479 % (files[file]["package"], suite, files[file]["architecture"]))
480 oldfiles = q.dictresult()
481 for oldfile in oldfiles:
482 files[file]["oldfiles"][suite] = oldfile
483 # Check versions [NB: per-suite only; no cross-suite checking done (yet)]
484 if apt_pkg.VersionCompare(files[file]["version"], oldfile["version"]) != 1:
485 reject_message = reject_message + "Rejected: %s Old version `%s' >= new version `%s'.\n" % (file, oldfile["version"], files[file]["version"])
486 # Check for existing copies of the file
487 if not changes.has_key("stable upload"):
488 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"]))
489 if q.getresult() != []:
490 reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (file)
492 # Check for existent source
493 # FIXME: this is no longer per suite
494 if changes["architecture"].has_key("source"):
495 source_version = files[file]["source version"];
496 if source_version != changes["version"]:
497 reject_message = reject_message + "Rejected: source version (%s) for %s doesn't match changes version %s.\n" % (files[file]["source version"], file, changes["version"]);
499 if not source_exists (files[file]["source package"], source_version):
500 reject_message = reject_message + "Rejected: no source found for %s %s (%s).\n" % (files[file]["source package"], source_version, file);
502 # Find any old .dsc files
503 elif files[file]["type"] == "dsc":
504 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"
505 % (files[file]["package"], suite))
506 oldfiles = q.dictresult()
507 if len(oldfiles) >= 1:
508 files[file]["oldfiles"][suite] = oldfiles[0]
510 # Validate the component
511 component = files[file]["component"];
512 component_id = db_access.get_component_id(component);
513 if component_id == -1:
514 reject_message = reject_message + "Rejected: file '%s' has unknown component '%s'.\n" % (file, component);
517 # Validate the priority
518 if string.find(files[file]["priority"],'/') != -1:
519 reject_message = reject_message + "Rejected: file '%s' has invalid priority '%s' [contains '/'].\n" % (file, files[file]["priority"]);
521 # Check the md5sum & size against existing files (if any)
522 location = Cnf["Dir::PoolDir"];
523 files[file]["location id"] = db_access.get_location_id (location, component, archive);
525 files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"]);
526 files_id = db_access.get_files_id(files[file]["pool name"] + file, files[file]["size"], files[file]["md5sum"], files[file]["location id"]);
528 reject_message = reject_message + "Rejected: INTERNAL ERROR, get_files_id() returned multiple matches for %s.\n" % (file)
530 reject_message = reject_message + "Rejected: md5sum and/or size mismatch on existing copy of %s.\n" % (file)
531 files[file]["files id"] = files_id
533 # Check for packages that have moved from one component to another
534 if files[file]["oldfiles"].has_key(suite) and files[file]["oldfiles"][suite]["name"] != files[file]["component"]:
535 files[file]["othercomponents"] = files[file]["oldfiles"][suite]["name"];
538 if string.find(reject_message, "Rejected:") != -1:
543 ###############################################################################
546 global dsc, dsc_files, reject_message, reprocess, orig_tar_id, orig_tar_location, legacy_source_untouchable;
548 for file in files.keys():
549 if files[file]["type"] == "dsc":
551 dsc = utils.parse_changes(file, 1)
552 except utils.cant_open_exc:
553 reject_message = reject_message + "Rejected: can't read changes file '%s'.\n" % (file)
555 except utils.changes_parse_error_exc, line:
556 reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (file, line)
558 except utils.invalid_dsc_format_exc, line:
559 reject_message = reject_message + "Rejected: syntax error in .dsc file '%s', line %s.\n" % (file, line)
562 dsc_files = utils.build_file_list(dsc, 1)
563 except utils.no_files_exc:
564 reject_message = reject_message + "Rejected: no Files: field in .dsc file.\n";
566 except utils.changes_parse_error_exc, line:
567 reject_message = reject_message + "Rejected: error parsing .dsc file '%s', can't grok: %s.\n" % (file, line);
570 # Enforce mandatory fields
571 for i in ("format", "source", "version", "binary", "maintainer", "architecture", "files"):
572 if not dsc.has_key(i):
573 reject_message = reject_message + "Rejected: Missing field `%s' in dsc file.\n" % (i)
575 # The dpkg maintainer from hell strikes again! Bumping the
576 # version number of the .dsc breaks extraction by stable's
578 if dsc["format"] != "1.0":
579 reject_message = reject_message + """Rejected: [dpkg-sucks] source package was produced by a broken version
580 of dpkg-dev 1.9.1{3,4}; please rebuild with >= 1.9.15 version
584 # Ensure the version number in the .dsc matches the version number in the .changes
585 epochless_dsc_version = utils.re_no_epoch.sub('', dsc.get("version"));
586 changes_version = files[file]["version"];
587 if epochless_dsc_version != files[file]["version"]:
588 reject_message = reject_message + "Rejected: version ('%s') in .dsc does not match version ('%s') in .changes\n" % (epochless_dsc_version, changes_version);
590 # Ensure source is newer than existing source in target suites
591 package = dsc.get("source");
592 new_version = dsc.get("version");
593 for suite in changes["distribution"].keys():
594 q = projectB.query("SELECT s.version FROM source s, src_associations sa, suite su WHERE s.source = '%s' AND su.suite_name = '%s' AND sa.source = s.id AND sa.suite = su.id"
596 ql = map(lambda x: x[0], q.getresult());
597 for old_version in ql:
598 if apt_pkg.VersionCompare(new_version, old_version) != 1:
599 reject_message = reject_message + "Rejected: %s Old version `%s' >= new version `%s'.\n" % (file, old_version, new_version)
601 # Try and find all files mentioned in the .dsc. This has
602 # to work harder to cope with the multiple possible
603 # locations of an .orig.tar.gz.
604 for dsc_file in dsc_files.keys():
605 if files.has_key(dsc_file):
606 actual_md5 = files[dsc_file]["md5sum"];
607 actual_size = int(files[dsc_file]["size"]);
608 found = "%s in incoming" % (dsc_file)
609 # Check the file does not already exist in the archive
610 if not changes.has_key("stable upload"):
611 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));
613 # "It has not broken them. It has fixed a
614 # brokenness. Your crappy hack exploited a
615 # bug in the old dinstall.
617 # "(Come on! I thought it was always obvious
618 # that one just doesn't release different
619 # files with the same name and version.)"
620 # -- ajk@ on d-devel@l.d.o
622 if q.getresult() != []:
623 reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (dsc_file)
624 elif dsc_file[-12:] == ".orig.tar.gz":
626 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));
630 # Unfortunately, we make get more than one match
631 # here if, for example, the package was in potato
632 # but had a -sa upload in woody. So we need to a)
633 # choose the right one and b) mark all wrong ones
634 # as excluded from the source poolification (to
635 # avoid file overwrites).
637 x = ql[0]; # default to something sane in case we don't match any or have only one
641 old_file = i[0] + i[1];
642 actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
643 actual_size = os.stat(old_file)[stat.ST_SIZE];
644 if actual_md5 == dsc_files[dsc_file]["md5sum"] and actual_size == int(dsc_files[dsc_file]["size"]):
647 legacy_source_untouchable[i[3]] = "";
649 old_file = x[0] + x[1];
650 actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
651 actual_size = os.stat(old_file)[stat.ST_SIZE];
654 dsc_files[dsc_file]["files id"] = x[3]; # need this for updating dsc_files in install()
657 if suite_type == "legacy" or suite_type == "legacy-mixed":
658 orig_tar_location = "legacy";
660 orig_tar_location = x[4];
662 # Not there? Check in Incoming...
663 # [See comment above process_it() for explanation
664 # of why this is necessary...]
665 if os.path.exists(dsc_file):
666 files[dsc_file] = {};
667 files[dsc_file]["size"] = os.stat(dsc_file)[stat.ST_SIZE];
668 files[dsc_file]["md5sum"] = dsc_files[dsc_file]["md5sum"];
669 files[dsc_file]["section"] = files[file]["section"];
670 files[dsc_file]["priority"] = files[file]["priority"];
671 files[dsc_file]["component"] = files[file]["component"];
672 files[dsc_file]["type"] = "orig.tar.gz";
676 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);
679 reject_message = reject_message + "Rejected: %s refers to %s, but I can't find it in Incoming.\n" % (file, dsc_file);
681 if actual_md5 != dsc_files[dsc_file]["md5sum"]:
682 reject_message = reject_message + "Rejected: md5sum for %s doesn't match %s.\n" % (found, file);
683 if actual_size != int(dsc_files[dsc_file]["size"]):
684 reject_message = reject_message + "Rejected: size for %s doesn't match %s.\n" % (found, file);
686 if string.find(reject_message, "Rejected:") != -1:
691 ###############################################################################
693 # Some cunning stunt broke dpkg-source in dpkg 1.8{,.1}; detect the
694 # resulting bad source packages and reject them.
696 # Even more amusingly the fix in 1.8.1.1 didn't actually fix the
697 # problem just changed the symptoms.
700 global dsc, dsc_files, reject_message, reprocess;
702 for filename in files.keys():
703 if files[filename]["type"] == "diff.gz":
704 file = gzip.GzipFile(filename, 'r');
705 for line in file.readlines():
706 if re_bad_diff.search(line):
707 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";
710 if string.find(reject_message, "Rejected:") != -1:
715 ###############################################################################
717 def check_md5sums ():
718 global reject_message;
720 for file in files.keys():
722 file_handle = utils.open_file(file,"r");
723 except utils.cant_open_exc:
726 if apt_pkg.md5sum(file_handle) != files[file]["md5sum"]:
727 reject_message = reject_message + "Rejected: md5sum check failed for %s.\n" % (file);
729 def check_override ():
732 # Only check section & priority on sourceful uploads
733 if not changes["architecture"].has_key("source"):
737 for file in files.keys():
738 if not files[file].has_key("new") and files[file]["type"] == "deb":
739 section = files[file]["section"];
740 override_section = files[file]["override section"];
741 if section != override_section and section != "-":
742 # Ignore this; it's a common mistake and not worth whining about
743 if string.lower(section) == "non-us/main" and string.lower(override_section) == "non-us":
745 summary = summary + "%s: section is overridden from %s to %s.\n" % (file, section, override_section);
746 priority = files[file]["priority"];
747 override_priority = files[file]["override priority"];
748 if priority != override_priority and priority != "-":
749 summary = summary + "%s: priority is overridden from %s to %s.\n" % (file, priority, override_priority);
754 Subst["__SUMMARY__"] = summary;
755 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.override-disparity","r").read());
756 utils.send_mail (mail_message, "")
758 #####################################################################################################################
760 # Set up the per-package template substitution mappings
762 def update_subst (changes_filename):
765 # If katie crashed out in the right place, architecture may still be a string.
766 if not changes.has_key("architecture") or not isinstance(changes["architecture"], DictType):
767 changes["architecture"] = { "Unknown" : "" };
768 # and maintainer822 may not exist.
769 if not changes.has_key("maintainer822"):
770 changes["maintainer822"] = Cnf["Dinstall::MyEmailAddress"];
772 Subst["__ARCHITECTURE__"] = string.join(changes["architecture"].keys(), ' ' );
773 Subst["__CHANGES_FILENAME__"] = os.path.basename(changes_filename);
774 Subst["__FILE_CONTENTS__"] = changes.get("filecontents", "");
776 # For source uploads the Changed-By field wins; otherwise Maintainer wins.
777 if changes["architecture"].has_key("source") and changes["changedby822"] != "" and (changes["changedby822"] != changes["maintainer822"]):
778 Subst["__MAINTAINER_FROM__"] = changes["changedby822"];
779 Subst["__MAINTAINER_TO__"] = changes["changedby822"] + ", " + changes["maintainer822"];
780 Subst["__MAINTAINER__"] = changes.get("changed-by", "Unknown");
782 Subst["__MAINTAINER_FROM__"] = changes["maintainer822"];
783 Subst["__MAINTAINER_TO__"] = changes["maintainer822"];
784 Subst["__MAINTAINER__"] = changes.get("maintainer", "Unknown");
786 Subst["__REJECT_MESSAGE__"] = reject_message;
787 Subst["__SOURCE__"] = changes.get("source", "Unknown");
788 Subst["__VERSION__"] = changes.get("version", "Unknown");
790 #####################################################################################################################
792 def action (changes_filename):
793 byhand = confirm = suites = summary = new = "";
795 # changes["distribution"] may not exist in corner cases
796 # (e.g. unreadable changes files)
797 if not changes.has_key("distribution") or not isinstance(changes["distribution"], DictType):
798 changes["distribution"] = {};
800 for suite in changes["distribution"].keys():
801 if Cnf.has_key("Suite::%s::Confirm"):
802 confirm = confirm + suite + ", "
803 suites = suites + suite + ", "
804 confirm = confirm[:-2]
807 for file in files.keys():
808 if files[file].has_key("byhand"):
810 summary = summary + file + " byhand\n"
811 elif files[file].has_key("new"):
813 summary = summary + "(new) %s %s %s\n" % (file, files[file]["priority"], files[file]["section"])
814 if files[file].has_key("othercomponents"):
815 summary = summary + "WARNING: Already present in %s distribution.\n" % (files[file]["othercomponents"])
816 if files[file]["type"] == "deb":
817 summary = summary + apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))["Description"] + '\n';
819 files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"])
820 destination = Cnf["Dir::PoolRoot"] + files[file]["pool name"] + file
821 summary = summary + file + "\n to " + destination + "\n"
823 short_summary = summary;
825 # This is for direport's benefit...
826 f = re_fdnic.sub("\n .\n", changes.get("changes",""));
828 if confirm or byhand or new:
829 summary = summary + "Changes: " + f;
831 summary = summary + announce (short_summary, 0)
833 (prompt, answer) = ("", "XXX")
834 if Options["No-Action"] or Options["Automatic"]:
837 if string.find(reject_message, "Rejected") != -1:
839 modified_time = time.time()-os.path.getmtime(changes_filename);
840 except: # i.e. ignore errors like 'file does not exist';
842 if modified_time < 86400:
843 print "SKIP (too new)\n" + reject_message,;
844 prompt = "[S]kip, Manual reject, Quit ?";
846 print "REJECT\n" + reject_message,;
847 prompt = "[R]eject, Manual reject, Skip, Quit ?";
848 if Options["Automatic"]:
851 print "NEW to %s\n%s%s" % (suites, reject_message, summary),;
852 prompt = "[S]kip, New ack, Manual reject, Quit ?";
853 if Options["Automatic"] and Options["Ack-New"]:
856 print "BYHAND\n" + reject_message + summary,;
857 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
859 print "CONFIRM to %s\n%s%s" % (confirm, reject_message, summary),
860 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
862 print "INSTALL\n" + reject_message + summary,;
863 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
864 if Options["Automatic"]:
867 while string.find(prompt, answer) == -1:
869 answer = utils.our_raw_input()
870 m = re_default_answer.match(prompt)
873 answer = string.upper(answer[:1])
876 reject (changes_filename, "");
878 manual_reject (changes_filename);
880 install (changes_filename, summary, short_summary);
882 acknowledge_new (changes_filename, summary);
886 #####################################################################################################################
888 def install (changes_filename, summary, short_summary):
889 global install_count, install_bytes, Subst;
891 # Stable uploads are a special case
892 if changes.has_key("stable upload"):
893 stable_install (changes_filename, summary, short_summary);
898 Logger.log(["installing changes",changes_filename]);
900 archive = utils.where_am_i();
902 # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
903 projectB.query("BEGIN WORK");
905 # Add the .dsc file to the DB
906 for file in files.keys():
907 if files[file]["type"] == "dsc":
908 package = dsc["source"]
909 version = dsc["version"] # NB: not files[file]["version"], that has no epoch
910 maintainer = dsc["maintainer"]
911 maintainer = string.replace(maintainer, "'", "\\'")
912 maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
913 filename = files[file]["pool name"] + file;
914 dsc_location_id = files[file]["location id"];
915 if not files[file]["files id"]:
916 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], dsc_location_id)
917 projectB.query("INSERT INTO source (source, version, maintainer, file) VALUES ('%s', '%s', %d, %d)"
918 % (package, version, maintainer_id, files[file]["files id"]))
920 for suite in changes["distribution"].keys():
921 suite_id = db_access.get_suite_id(suite);
922 projectB.query("INSERT INTO src_associations (suite, source) VALUES (%d, currval('source_id_seq'))" % (suite_id))
924 # Add the source files to the DB (files and dsc_files)
925 projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files[file]["files id"]));
926 for dsc_file in dsc_files.keys():
927 filename = files[file]["pool name"] + dsc_file;
928 # If the .orig.tar.gz is already in the pool, it's
929 # files id is stored in dsc_files by check_dsc().
930 files_id = dsc_files[dsc_file].get("files id", None);
932 files_id = db_access.get_files_id(filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], dsc_location_id);
933 # FIXME: needs to check for -1/-2 and or handle exception
935 files_id = db_access.set_files_id (filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], dsc_location_id);
936 projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files_id));
938 # Add the .deb files to the DB
939 for file in files.keys():
940 if files[file]["type"] == "deb":
941 package = files[file]["package"]
942 version = files[file]["version"]
943 maintainer = files[file]["maintainer"]
944 maintainer = string.replace(maintainer, "'", "\\'")
945 maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
946 architecture = files[file]["architecture"]
947 architecture_id = db_access.get_architecture_id (architecture);
948 type = files[file]["dbtype"];
949 dsc_component = files[file]["component"]
950 source = files[file]["source package"]
951 source_version = files[file]["source version"];
952 filename = files[file]["pool name"] + file;
953 if not files[file]["files id"]:
954 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], files[file]["location id"])
955 source_id = db_access.get_source_id (source, source_version);
957 projectB.query("INSERT INTO binaries (package, version, maintainer, source, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, %d, '%s')"
958 % (package, version, maintainer_id, source_id, architecture_id, files[file]["files id"], type));
960 projectB.query("INSERT INTO binaries (package, version, maintainer, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, '%s')"
961 % (package, version, maintainer_id, architecture_id, files[file]["files id"], type));
962 for suite in changes["distribution"].keys():
963 suite_id = db_access.get_suite_id(suite);
964 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES (%d, currval('binaries_id_seq'))" % (suite_id));
966 # If the .orig.tar.gz is in a legacy directory we need to poolify
967 # it, so that apt-get source (and anything else that goes by the
968 # "Directory:" field in the Sources.gz file) works.
969 if orig_tar_id != None and orig_tar_location == "legacy":
970 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));
973 # Is this an old upload superseded by a newer -sa upload? (See check_dsc() for details)
974 if legacy_source_untouchable.has_key(qid["files_id"]):
976 # First move the files to the new location
977 legacy_filename = qid["path"]+qid["filename"];
978 pool_location = utils.poolify (changes["source"], files[file]["component"]);
979 pool_filename = pool_location + os.path.basename(qid["filename"]);
980 destination = Cnf["Dir::PoolDir"] + pool_location
981 utils.move(legacy_filename, destination);
982 # Then Update the DB's files table
983 q = projectB.query("UPDATE files SET filename = '%s', location = '%s' WHERE id = '%s'" % (pool_filename, dsc_location_id, qid["files_id"]));
985 # If this is a sourceful diff only upload that is moving non-legacy
986 # cross-component we need to copy the .orig.tar.gz into the new
987 # component too for the same reasons as above.
989 if changes["architecture"].has_key("source") and orig_tar_id != None and \
990 orig_tar_location != "legacy" and orig_tar_location != dsc_location_id:
991 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));
992 ql = q.getresult()[0];
993 old_filename = ql[0] + ql[1];
996 new_filename = utils.poolify (changes["source"], dsc_component) + os.path.basename(old_filename);
997 new_files_id = db_access.get_files_id(new_filename, file_size, file_md5sum, dsc_location_id);
998 if new_files_id == None:
999 utils.copy(old_filename, Cnf["Dir::PoolDir"] + new_filename);
1000 new_files_id = db_access.set_files_id(new_filename, file_size, file_md5sum, dsc_location_id);
1001 projectB.query("UPDATE dsc_files SET file = %s WHERE source = %s AND file = %s" % (new_files_id, source_id, orig_tar_id));
1003 # Install the files into the pool
1004 for file in files.keys():
1005 if files[file].has_key("byhand"):
1007 destination = Cnf["Dir::PoolDir"] + files[file]["pool name"] + file
1008 destdir = os.path.dirname(destination)
1009 utils.move (file, destination)
1010 Logger.log(["installed", file, files[file]["type"], files[file]["size"], files[file]["architecture"]]);
1011 install_bytes = install_bytes + float(files[file]["size"])
1013 # Copy the .changes file across for suite which need it.
1014 for suite in changes["distribution"].keys():
1015 if Cnf.has_key("Suite::%s::CopyChanges" % (suite)):
1016 utils.copy (changes_filename, Cnf["Dir::RootDir"] + Cnf["Suite::%s::CopyChanges" % (suite)]);
1018 projectB.query("COMMIT WORK");
1021 utils.move (changes_filename, Cnf["Dir::IncomingDir"] + 'DONE/' + os.path.basename(changes_filename))
1023 utils.warn("couldn't move changes file '%s' to DONE directory. [Got %s]" % (os.path.basename(changes_filename), sys.exc_type));
1025 install_count = install_count + 1;
1027 if not Options["No-Mail"]:
1028 Subst["__SUITE__"] = "";
1029 Subst["__SUMMARY__"] = summary;
1030 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.installed","r").read());
1031 utils.send_mail (mail_message, "")
1032 announce (short_summary, 1)
1035 #####################################################################################################################
1037 def stable_install (changes_filename, summary, short_summary):
1038 global install_count, install_bytes, Subst;
1040 print "Installing to stable."
1042 archive = utils.where_am_i();
1044 # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
1045 projectB.query("BEGIN WORK");
1047 # Add the .dsc file to the DB
1048 for file in files.keys():
1049 if files[file]["type"] == "dsc":
1050 package = dsc["source"]
1051 version = dsc["version"] # NB: not files[file]["version"], that has no epoch
1052 q = projectB.query("SELECT id FROM source WHERE source = '%s' AND version = '%s'" % (package, version))
1055 utils.fubar("[INTERNAL ERROR] couldn't find '%s' (%s) in source table." % (package, version));
1056 source_id = ql[0][0];
1057 suite_id = db_access.get_suite_id('proposed-updates');
1058 projectB.query("DELETE FROM src_associations WHERE suite = '%s' AND source = '%s'" % (suite_id, source_id));
1059 suite_id = db_access.get_suite_id('stable');
1060 projectB.query("INSERT INTO src_associations (suite, source) VALUES ('%s', '%s')" % (suite_id, source_id));
1062 # Add the .deb files to the DB
1063 for file in files.keys():
1064 if files[file]["type"] == "deb":
1065 package = files[file]["package"]
1066 version = files[file]["version"]
1067 architecture = files[file]["architecture"]
1068 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))
1071 utils.fubar("[INTERNAL ERROR] couldn't find '%s' (%s for %s architecture) in binaries table." % (package, version, architecture));
1072 binary_id = ql[0][0];
1073 suite_id = db_access.get_suite_id('proposed-updates');
1074 projectB.query("DELETE FROM bin_associations WHERE suite = '%s' AND bin = '%s'" % (suite_id, binary_id));
1075 suite_id = db_access.get_suite_id('stable');
1076 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES ('%s', '%s')" % (suite_id, binary_id));
1078 projectB.query("COMMIT WORK");
1081 utils.move (changes_filename, Cnf["Dir::Morgue"] + '/katie/' + os.path.basename(changes_filename));
1083 # Update the Stable ChangeLog file
1085 new_changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + ".ChangeLog";
1086 changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + "ChangeLog";
1087 if os.path.exists(new_changelog_filename):
1088 os.unlink (new_changelog_filename);
1090 new_changelog = utils.open_file(new_changelog_filename, 'w');
1091 for file in files.keys():
1092 if files[file]["type"] == "deb":
1093 new_changelog.write("stable/%s/binary-%s/%s\n" % (files[file]["component"], files[file]["architecture"], file));
1094 elif utils.re_issource.match(file) != None:
1095 new_changelog.write("stable/%s/source/%s\n" % (files[file]["component"], file));
1097 new_changelog.write("%s\n" % (file));
1098 chop_changes = re_fdnic.sub("\n", changes["changes"]);
1099 new_changelog.write(chop_changes + '\n\n');
1100 if os.access(changelog_filename, os.R_OK) != 0:
1101 changelog = utils.open_file(changelog_filename, 'r');
1102 new_changelog.write(changelog.read());
1103 new_changelog.close();
1104 if os.access(changelog_filename, os.R_OK) != 0:
1105 os.unlink(changelog_filename);
1106 utils.move(new_changelog_filename, changelog_filename);
1108 install_count = install_count + 1;
1110 if not Options["No-Mail"]:
1111 Subst["__SUITE__"] = " into stable";
1112 Subst["__SUMMARY__"] = summary;
1113 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.installed","r").read());
1114 utils.send_mail (mail_message, "")
1115 announce (short_summary, 1)
1117 #####################################################################################################################
1119 def reject (changes_filename, manual_reject_mail_filename):
1122 print "Rejecting.\n"
1124 base_changes_filename = os.path.basename(changes_filename);
1125 reason_filename = re_changes.sub("reason", base_changes_filename);
1126 reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename);
1128 # Move the .changes files and it's contents into REJECT/ (if we can; errors are ignored)
1130 utils.move (changes_filename, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], base_changes_filename));
1132 utils.warn("couldn't reject changes file '%s'. [Got %s]" % (base_changes_filename, sys.exc_type));
1134 for file in files.keys():
1135 if os.path.exists(file):
1137 utils.move (file, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], file));
1139 utils.warn("couldn't reject file '%s'. [Got %s]" % (file, sys.exc_type));
1142 # If this is not a manual rejection generate the .reason file and rejection mail message
1143 if manual_reject_mail_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_message);
1149 Subst["__MANUAL_REJECT_MESSAGE__"] = "";
1150 Subst["__CC__"] = "X-Katie-Rejection: automatic (moo)";
1151 reject_mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.rejected","r").read());
1152 else: # Have a manual rejection file to use
1153 reject_mail_message = ""; # avoid <undef>'s
1155 # Send the rejection mail if appropriate
1156 if not Options["No-Mail"]:
1157 utils.send_mail (reject_mail_message, manual_reject_mail_filename);
1159 Logger.log(["rejected", changes_filename]);
1161 ##################################################################
1163 def manual_reject (changes_filename):
1166 # Build up the rejection email
1167 user_email_address = utils.whoami() + " <%s@%s>" % (pwd.getpwuid(os.getuid())[0], Cnf["Dinstall::MyHost"])
1168 manual_reject_message = Cnf.get("Dinstall::Options::Manual-Reject", "")
1170 Subst["__MANUAL_REJECT_MESSAGE__"] = manual_reject_message;
1171 Subst["__CC__"] = "Cc: " + Cnf["Dinstall::MyEmailAddress"];
1172 reject_mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.rejected","r").read());
1174 # Write the rejection email out as the <foo>.reason file
1175 reason_filename = re_changes.sub("reason", os.path.basename(changes_filename));
1176 reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename)
1177 if os.path.exists(reject_filename):
1178 os.unlink(reject_filename);
1179 fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
1180 os.write(fd, reject_mail_message);
1183 # If we weren't given one, spawn an editor so the user can add one in
1184 if manual_reject_message == "":
1185 result = os.system("vi +6 %s" % (reject_filename))
1187 utils.fubar("vi invocation failed for `%s'!" % (reject_filename), result);
1189 # Then process it as if it were an automatic rejection
1190 reject (changes_filename, reject_filename)
1192 #####################################################################################################################
1194 def acknowledge_new (changes_filename, summary):
1195 global new_ack_new, Subst;
1197 changes_filename = os.path.basename(changes_filename);
1199 new_ack_new[changes_filename] = 1;
1201 if new_ack_old.has_key(changes_filename):
1202 print "Ack already sent.";
1205 print "Sending new ack.";
1206 if not Options["No-Mail"]:
1207 Subst["__SUMMARY__"] = summary;
1208 new_ack_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.new","r").read());
1209 utils.send_mail(new_ack_message,"");
1211 #####################################################################################################################
1213 def announce (short_summary, action):
1216 # Only do announcements for source uploads with a recent dpkg-dev installed
1217 if float(changes.get("format", 0)) < 1.6 or not changes["architecture"].has_key("source"):
1222 Subst["__SHORT_SUMMARY__"] = short_summary;
1224 for dist in changes["distribution"].keys():
1225 list = Cnf.Find("Suite::%s::Announce" % (dist))
1226 if list == "" or lists_done.has_key(list):
1228 lists_done[list] = 1
1229 summary = summary + "Announcing to %s\n" % (list)
1232 Subst["__ANNOUNCE_LIST_ADDRESS__"] = list;
1233 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.announce","r").read());
1234 utils.send_mail (mail_message, "")
1236 bugs = changes["closes"].keys()
1238 if not nmu.is_an_nmu(changes, dsc):
1239 summary = summary + "Closing bugs: "
1241 summary = summary + "%s " % (bug)
1243 Subst["__BUG_NUMBER__"] = bug;
1244 if changes["distribution"].has_key("stable"):
1245 Subst["__STABLE_WARNING__"] = """
1246 Note that this package is not part of the released stable Debian
1247 distribution. It may have dependencies on other unreleased software,
1248 or other instabilities. Please take care if you wish to install it.
1249 The update will eventually make its way into the next released Debian
1252 Subst["__STABLE_WARNING__"] = "";
1253 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.bug-close","r").read());
1254 utils.send_mail (mail_message, "")
1256 Logger.log(["closing bugs"]+bugs);
1258 summary = summary + "Setting bugs to severity fixed: "
1259 control_message = ""
1261 summary = summary + "%s " % (bug)
1262 control_message = control_message + "tag %s + fixed\n" % (bug)
1263 if action and control_message != "":
1264 Subst["__CONTROL_MESSAGE__"] = control_message;
1265 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.bug-nmu-fixed","r").read());
1266 utils.send_mail (mail_message, "")
1268 Logger.log(["setting bugs to fixed"]+bugs);
1269 summary = summary + "\n"
1273 ###############################################################################
1275 # reprocess is necessary for the case of foo_1.2-1 and foo_1.2-2 in
1276 # Incoming. -1 will reference the .orig.tar.gz, but -2 will not.
1277 # dsccheckdistrib() can find the .orig.tar.gz but it will not have
1278 # processed it during it's checks of -2. If -1 has been deleted or
1279 # otherwise not checked by da-install, the .orig.tar.gz will not have
1280 # been checked at all. To get round this, we force the .orig.tar.gz
1281 # into the .changes structure and reprocess the .changes file.
1283 def process_it (changes_file):
1284 global reprocess, orig_tar_id, orig_tar_location, changes, dsc, dsc_files, files, reject_message;
1286 # Reset some globals
1293 orig_tar_location = "";
1294 legacy_source_untouchable = {};
1295 reject_message = "";
1297 # Absolutize the filename to avoid the requirement of being in the
1298 # same directory as the .changes file.
1299 changes_file = os.path.abspath(changes_file);
1301 # And since handling of installs to stable munges with the CWD;
1302 # save and restore it.
1306 check_signature (changes_file);
1307 check_changes (changes_file);
1316 traceback.print_exc(file=sys.stdout);
1319 update_subst(changes_file);
1320 action(changes_file);
1325 ###############################################################################
1328 global Cnf, Options, projectB, install_bytes, new_ack_old, Subst, nmu, Logger
1332 Cnf = apt_pkg.newConfiguration();
1333 apt_pkg.ReadConfigFileISC(Cnf,utils.which_conf_file());
1335 Arguments = [('a',"automatic","Dinstall::Options::Automatic"),
1336 ('d',"debug","Dinstall::Options::Debug", "IntVal"),
1337 ('h',"help","Dinstall::Options::Help"),
1338 ('k',"ack-new","Dinstall::Options::Ack-New"),
1339 ('m',"manual-reject","Dinstall::Options::Manual-Reject", "HasArg"),
1340 ('n',"no-action","Dinstall::Options::No-Action"),
1341 ('p',"no-lock", "Dinstall::Options::No-Lock"),
1342 ('s',"no-mail", "Dinstall::Options::No-Mail"),
1343 ('u',"override-distribution", "Dinstall::Options::Override-Distribution", "HasArg"),
1344 ('v',"version","Dinstall::Options::Version")];
1346 changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
1347 Options = Cnf.SubTree("Dinstall::Options")
1352 if Options["Version"]:
1353 print "katie version 0.0000000000";
1356 # -n/--dry-run invalidates some other options which would involve things happening
1357 if Options["No-Action"]:
1358 Options["Automatic"] = "";
1359 Options["Ack-New"] = "";
1361 projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"]));
1363 db_access.init(Cnf, projectB);
1365 # Check that we aren't going to clash with the daily cron job
1367 if not Options["No-Action"] and os.path.exists("%s/Archive_Maintenance_In_Progress" % (Cnf["Dir::RootDir"])) and not Options["No-Lock"]:
1368 utils.fubar("Archive maintenance in progress. Try again later.");
1370 # Obtain lock if not in no-action mode and initialize the log
1372 if not Options["No-Action"]:
1373 lock_fd = os.open(Cnf["Dinstall::LockFile"], os.O_RDWR);
1374 fcntl.lockf(lock_fd, FCNTL.F_TLOCK);
1375 Logger = logging.Logger(Cnf, "katie");
1377 # Read in the list of already-acknowledged NEW packages
1378 new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'r');
1380 for line in new_ack_list.readlines():
1381 new_ack_old[line[:-1]] = 1;
1382 new_ack_list.close();
1384 # Initialize the substitution template mapping global
1386 Subst["__ADMIN_ADDRESS__"] = Cnf["Dinstall::MyAdminAddress"];
1387 Subst["__BUG_SERVER__"] = Cnf["Dinstall::BugServer"];
1388 bcc = "X-Katie: $Revision: 1.59 $"
1389 if Cnf.has_key("Dinstall::Bcc"):
1390 Subst["__BCC__"] = bcc + "\nBcc: %s" % (Cnf["Dinstall::Bcc"]);
1392 Subst["__BCC__"] = bcc;
1393 Subst["__DISTRO__"] = Cnf["Dinstall::MyDistribution"];
1394 Subst["__KATIE_ADDRESS__"] = Cnf["Dinstall::MyEmailAddress"];
1396 # Read in the group-maint override file
1399 # Sort the .changes files so that we process sourceful ones first
1400 changes_files.sort(utils.changes_compare);
1402 # Process the changes files
1403 for changes_file in changes_files:
1404 print "\n" + changes_file;
1405 process_it (changes_file);
1409 if install_count > 1:
1411 sys.stderr.write("Installed %d package %s, %s.\n" % (install_count, sets, utils.size_type(int(install_bytes))));
1412 Logger.log(["total",install_count,install_bytes]);
1414 # Write out the list of already-acknowledged NEW packages
1415 if Options["Ack-New"]:
1416 new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'w')
1417 for i in new_ack_new.keys():
1418 new_ack_list.write(i+'\n')
1419 new_ack_list.close()
1421 if not Options["No-Action"]:
1424 if __name__ == '__main__':