3 # Installs Debian packaes
4 # Copyright (C) 2000, 2001 James Troup <james@nocrew.org>
5 # $Id: katie,v 1.60 2001-09-27 14:39:06 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 = {};
73 katie_version = "$Revision: 1.60 $";
75 ###############################################################################
82 Cnf = apt_pkg.newConfiguration();
83 apt_pkg.ReadConfigFileISC(Cnf,utils.which_conf_file());
85 Arguments = [('a',"automatic","Dinstall::Options::Automatic"),
86 ('h',"help","Dinstall::Options::Help"),
87 ('k',"ack-new","Dinstall::Options::Ack-New"),
88 ('m',"manual-reject","Dinstall::Options::Manual-Reject", "HasArg"),
89 ('n',"no-action","Dinstall::Options::No-Action"),
90 ('p',"no-lock", "Dinstall::Options::No-Lock"),
91 ('s',"no-mail", "Dinstall::Options::No-Mail"),
92 ('u',"override-distribution", "Dinstall::Options::Override-Distribution", "HasArg"),
93 ('V',"version","Dinstall::Options::Version")];
95 for i in ["automatic", "help", "ack-new", "manual-reject", "no-action",
96 "no-lock", "no-mail", "override-distribution", "version"]:
97 Cnf["Dinstall::Options::%s" % (i)] = "";
99 changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
100 Options = Cnf.SubTree("Dinstall::Options")
102 return changes_files;
104 #########################################################################################
106 def usage (exit_code=0):
107 print """Usage: dinstall [OPTION]... [CHANGES]...
108 -a, --automatic automatic run
109 -h, --help show this help and exit.
110 -k, --ack-new acknowledge new packages !! for cron.daily only !!
111 -m, --manual-reject=MSG manual reject with `msg'
112 -n, --no-action don't do anything
113 -p, --no-lock don't check lockfile !! for cron.daily only !!
114 -s, --no-mail don't send any mail
115 -u, --distribution=DIST override distribution to `dist'
116 -V, --version display the version number and exit"""
119 #########################################################################################
121 def check_signature (filename):
122 global reject_message
124 (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))
126 reject_message = reject_message + "Rejected: GPG signature check failed on `%s'.\n%s\n" % (os.path.basename(filename), output)
130 ######################################################################################################
133 # Read in the group maintainer override file
135 self.group_maint = {};
136 if Cnf.get("Dinstall::GroupOverrideFilename"):
137 filename = Cnf["Dir::OverrideDir"] + Cnf["Dinstall::GroupOverrideFilename"];
138 file = utils.open_file(filename, 'r');
139 for line in file.readlines():
140 line = string.strip(utils.re_comments.sub('', line));
142 self.group_maint[line] = 1;
145 def is_an_nmu (self, changes, dsc):
146 (dsc_rfc822, dsc_name, dsc_email) = utils.fix_maintainer (dsc.get("maintainer",Cnf["Dinstall::MyEmailAddress"]));
147 # changes["changedbyname"] == dsc_name is probably never true, but better safe than sorry
148 if dsc_name == changes["maintainername"] and (changes["changedby822"] == "" or changes["changedbyname"] == dsc_name):
151 if dsc.has_key("uploaders"):
152 uploaders = string.split(dsc["uploaders"], ",");
155 (rfc822, name, email) = utils.fix_maintainer (string.strip(i));
156 uploadernames[name] = "";
157 if uploadernames.has_key(changes["changedbyname"]):
160 # Some group maintained packages (e.g. Debian QA) are never NMU's
161 if self.group_maint.has_key(changes["maintaineremail"]):
166 ######################################################################################################
168 # Ensure that source exists somewhere in the archive for the binary
169 # upload being processed.
171 # (1) exact match => 1.0-3
172 # (2) Bin-only NMU of an MU => 1.0-3.0.1
173 # (3) Bin-only NMU of a sourceful-NMU => 1.0-3.1.1
175 def source_exists (package, source_version):
176 q = projectB.query("SELECT s.version FROM source s WHERE s.source = '%s'" % (package));
178 # Reduce the query results to a list of version numbers
179 ql = map(lambda x: x[0], q.getresult());
182 if ql.count(source_version):
186 orig_source_version = re_bin_only_nmu_of_mu.sub('', source_version);
187 if ql.count(orig_source_version):
191 orig_source_version = re_bin_only_nmu_of_nmu.sub('', source_version);
192 if ql.count(orig_source_version):
198 ######################################################################################################
200 # See if a given package is in the override table
202 def in_override_p (package, component, suite, binary_type, file):
205 if binary_type == "": # must be source
210 # Override suite name; used for example with proposed-updates
211 if Cnf.Find("Suite::%s::OverrideSuite" % (suite)) != "":
212 suite = Cnf["Suite::%s::OverrideSuite" % (suite)];
214 # Avoid <undef> on unknown distributions
215 suite_id = db_access.get_suite_id(suite);
218 component_id = db_access.get_component_id(component);
219 type_id = db_access.get_override_type_id(type);
221 # FIXME: nasty non-US speficic hack
222 if string.lower(component[:7]) == "non-us/":
223 component = component[7:];
225 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"
226 % (package, suite_id, component_id, type_id));
227 result = q.getresult();
228 # If checking for a source package fall back on the binary override type
229 if type == "dsc" and not result:
230 type_id = db_access.get_override_type_id("deb");
231 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"
232 % (package, suite_id, component_id, type_id));
233 result = q.getresult();
235 # Remember the section and priority so we can check them later if appropriate
237 files[file]["override section"] = result[0][0];
238 files[file]["override priority"] = result[0][1];
242 #####################################################################################################################
244 def check_changes(filename):
245 global reject_message, changes, files
247 # Default in case we bail out
248 changes["maintainer822"] = Cnf["Dinstall::MyEmailAddress"];
249 changes["changedby822"] = Cnf["Dinstall::MyEmailAddress"];
250 changes["architecture"] = {};
252 # Parse the .changes field into a dictionary
254 changes = utils.parse_changes(filename, 0)
255 except utils.cant_open_exc:
256 reject_message = reject_message + "Rejected: can't read changes file '%s'.\n" % (filename)
258 except utils.changes_parse_error_exc, line:
259 reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line)
262 # Parse the Files field from the .changes into another dictionary
264 files = utils.build_file_list(changes, "");
265 except utils.changes_parse_error_exc, line:
266 reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line);
267 except utils.nk_format_exc, format:
268 reject_message = reject_message + "Rejected: unknown format '%s' of changes file '%s'.\n" % (format, filename);
271 # Check for mandatory fields
272 for i in ("source", "binary", "architecture", "version", "distribution", "maintainer", "files"):
273 if not changes.has_key(i):
274 reject_message = reject_message + "Rejected: Missing field `%s' in changes file.\n" % (i)
275 return 0 # Avoid <undef> errors during later tests
277 # Override the Distribution: field if appropriate
278 if Options["Override-Distribution"] != "":
279 reject_message = reject_message + "Warning: Distribution was overriden from %s to %s.\n" % (changes["distribution"], Options["Override-Distribution"])
280 changes["distribution"] = Options["Override-Distribution"]
282 # Split multi-value fields into a lower-level dictionary
283 for i in ("architecture", "distribution", "binary", "closes"):
284 o = changes.get(i, "")
288 for j in string.split(o):
291 # Fix the Maintainer: field to be RFC822 compatible
292 (changes["maintainer822"], changes["maintainername"], changes["maintaineremail"]) = utils.fix_maintainer (changes["maintainer"])
294 # Fix the Changed-By: field to be RFC822 compatible; if it exists.
295 (changes["changedby822"], changes["changedbyname"], changes["changedbyemail"]) = utils.fix_maintainer(changes.get("changed-by",""));
297 # Ensure all the values in Closes: are numbers
298 if changes.has_key("closes"):
299 for i in changes["closes"].keys():
300 if re_isanum.match (i) == None:
301 reject_message = reject_message + "Rejected: `%s' from Closes field isn't a number.\n" % (i)
303 # Ensure there _is_ a target distribution
304 if changes["distribution"].keys() == []:
305 reject_message = reject_message + "Rejected: huh? Distribution field is empty in changes file.\n";
307 # Map frozen to unstable if frozen doesn't exist
308 if changes["distribution"].has_key("frozen") and not Cnf.has_key("Suite::Frozen"):
309 del changes["distribution"]["frozen"]
310 changes["distribution"]["unstable"] = 1;
311 reject_message = reject_message + "Mapping frozen to unstable.\n"
313 # Map testing to unstable
314 if changes["distribution"].has_key("testing"):
315 if len(changes["distribution"].keys()) > 1:
316 del changes["distribution"]["testing"];
317 reject_message = reject_message + "Warning: Ignoring testing as a target suite.\n";
319 reject_message = reject_message + "Rejected: invalid distribution 'testing'.\n";
321 # Ensure target distributions exist
322 for i in changes["distribution"].keys():
323 if not Cnf.has_key("Suite::%s" % (i)):
324 reject_message = reject_message + "Rejected: Unknown distribution `%s'.\n" % (i)
326 # Map unreleased arches from stable to unstable
327 if changes["distribution"].has_key("stable"):
328 for i in changes["architecture"].keys():
329 if not Cnf.has_key("Suite::Stable::Architectures::%s" % (i)):
330 reject_message = reject_message + "Mapping stable to unstable for unreleased arch `%s'.\n" % (i)
331 del changes["distribution"]["stable"]
332 changes["distribution"]["unstable"] = 1;
334 # Map arches not being released from frozen to unstable
335 if changes["distribution"].has_key("frozen"):
336 for i in changes["architecture"].keys():
337 if not Cnf.has_key("Suite::Frozen::Architectures::%s" % (i)):
338 reject_message = reject_message + "Mapping frozen to unstable for non-releasing arch `%s'.\n" % (i)
339 del changes["distribution"]["frozen"]
340 changes["distribution"]["unstable"] = 1;
342 # Handle uploads to stable
343 if changes["distribution"].has_key("stable"):
344 # If running from within proposed-updates; assume an install to stable
345 if string.find(os.getcwd(), 'proposed-updates') != -1:
346 # FIXME: should probably remove anything that != stable
347 for i in ("frozen", "unstable"):
348 if changes["distribution"].has_key(i):
349 reject_message = reject_message + "Removing %s from distribution list.\n" % (i)
350 del changes["distribution"][i]
351 changes["stable upload"] = 1;
352 # If we can't find a file from the .changes; assume it's a package already in the pool and move into the pool
353 file = files.keys()[0];
354 if os.access(file, os.R_OK) == 0:
355 pool_dir = Cnf["Dir::PoolDir"] + '/' + utils.poolify(changes["source"], files[file]["component"]);
357 # Otherwise (normal case) map stable to updates
359 reject_message = reject_message + "Mapping stable to updates.\n";
360 del changes["distribution"]["stable"];
361 changes["distribution"]["proposed-updates"] = 1;
363 # chopversion = no epoch; chopversion2 = no epoch and no revision (e.g. for .orig.tar.gz comparison)
364 changes["chopversion"] = utils.re_no_epoch.sub('', changes["version"])
365 changes["chopversion2"] = utils.re_no_revision.sub('', changes["chopversion"])
367 if string.find(reject_message, "Rejected:") != -1:
373 global reject_message
375 archive = utils.where_am_i();
377 for file in files.keys():
378 # Check the file is readable
379 if os.access(file,os.R_OK) == 0:
380 if os.path.exists(file):
381 reject_message = reject_message + "Rejected: Can't read `%s'. [permission denied]\n" % (file)
383 reject_message = reject_message + "Rejected: Can't read `%s'. [file not found]\n" % (file)
385 files[file]["type"] = "unreadable";
387 # If it's byhand skip remaining checks
388 if files[file]["section"] == "byhand":
389 files[file]["byhand"] = 1;
390 files[file]["type"] = "byhand";
391 # Checks for a binary package...
392 elif utils.re_isadeb.match(file) != None:
393 files[file]["type"] = "deb";
395 # Extract package information using dpkg-deb
397 control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))
399 reject_message = reject_message + "Rejected: %s: debExtractControl() raised %s.\n" % (file, sys.exc_type);
400 # Can't continue, none of the checks on control would work.
403 # Check for mandatory fields
404 if control.Find("Package") == None:
405 reject_message = reject_message + "Rejected: %s: No package field in control.\n" % (file)
406 if control.Find("Architecture") == None:
407 reject_message = reject_message + "Rejected: %s: No architecture field in control.\n" % (file)
408 if control.Find("Version") == None:
409 reject_message = reject_message + "Rejected: %s: No version field in control.\n" % (file)
411 # Ensure the package name matches the one give in the .changes
412 if not changes["binary"].has_key(control.Find("Package", "")):
413 reject_message = reject_message + "Rejected: %s: control file lists name as `%s', which isn't in changes file.\n" % (file, control.Find("Package", ""))
415 # Validate the architecture
416 if not Cnf.has_key("Suite::Unstable::Architectures::%s" % (control.Find("Architecture", ""))):
417 reject_message = reject_message + "Rejected: Unknown architecture '%s'.\n" % (control.Find("Architecture", ""))
419 # Check the architecture matches the one given in the .changes
420 if not changes["architecture"].has_key(control.Find("Architecture", "")):
421 reject_message = reject_message + "Rejected: %s: control file lists arch as `%s', which isn't in changes file.\n" % (file, control.Find("Architecture", ""))
422 # Check the section & priority match those given in the .changes (non-fatal)
423 if control.Find("Section") != None and files[file]["section"] != "" and files[file]["section"] != control.Find("Section"):
424 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"])
425 if control.Find("Priority") != None and files[file]["priority"] != "" and files[file]["priority"] != control.Find("Priority"):
426 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"])
428 epochless_version = utils.re_no_epoch.sub('', control.Find("Version", ""))
430 files[file]["package"] = control.Find("Package");
431 files[file]["architecture"] = control.Find("Architecture");
432 files[file]["version"] = control.Find("Version");
433 files[file]["maintainer"] = control.Find("Maintainer", "");
434 if file[-5:] == ".udeb":
435 files[file]["dbtype"] = "udeb";
436 elif file[-4:] == ".deb":
437 files[file]["dbtype"] = "deb";
439 reject_message = reject_message + "Rejected: %s is neither a .deb or a .udeb.\n " % (file);
440 files[file]["fullname"] = "%s_%s_%s.deb" % (control.Find("Package", ""), epochless_version, control.Find("Architecture", ""))
441 files[file]["source"] = control.Find("Source", "");
442 if files[file]["source"] == "":
443 files[file]["source"] = files[file]["package"];
444 # Get the source version
445 source = files[file]["source"];
447 if string.find(source, "(") != -1:
448 m = utils.re_extract_src_version.match(source)
450 source_version = m.group(2)
451 if not source_version:
452 source_version = files[file]["version"];
453 files[file]["source package"] = source;
454 files[file]["source version"] = source_version;
456 # Checks for a source package...
458 m = utils.re_issource.match(file)
460 files[file]["package"] = m.group(1)
461 files[file]["version"] = m.group(2)
462 files[file]["type"] = m.group(3)
464 # Ensure the source package name matches the Source filed in the .changes
465 if changes["source"] != files[file]["package"]:
466 reject_message = reject_message + "Rejected: %s: changes file doesn't say %s for Source\n" % (file, files[file]["package"])
468 # Ensure the source version matches the version in the .changes file
469 if files[file]["type"] == "orig.tar.gz":
470 changes_version = changes["chopversion2"]
472 changes_version = changes["chopversion"]
473 if changes_version != files[file]["version"]:
474 reject_message = reject_message + "Rejected: %s: should be %s according to changes file.\n" % (file, changes_version)
476 # Ensure the .changes lists source in the Architecture field
477 if not changes["architecture"].has_key("source"):
478 reject_message = reject_message + "Rejected: %s: changes file doesn't list `source' in Architecture field.\n" % (file)
480 # Check the signature of a .dsc file
481 if files[file]["type"] == "dsc":
482 check_signature(file)
484 files[file]["fullname"] = file
485 files[file]["architecture"] = "source";
487 # Not a binary or source package? Assume byhand...
489 files[file]["byhand"] = 1;
490 files[file]["type"] = "byhand";
492 files[file]["oldfiles"] = {}
493 for suite in changes["distribution"].keys():
495 if files[file].has_key("byhand"):
498 if Cnf.has_key("Suite:%s::Components" % (suite)) and not Cnf.has_key("Suite::%s::Components::%s" % (suite, files[file]["component"])):
499 reject_message = reject_message + "Rejected: unknown component `%s' for suite `%s'.\n" % (files[file]["component"], suite)
502 # See if the package is NEW
503 if not in_override_p(files[file]["package"], files[file]["component"], suite, files[file].get("dbtype",""), file):
504 files[file]["new"] = 1
506 if files[file]["type"] == "deb":
507 # Find any old binary packages
508 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"
509 % (files[file]["package"], suite, files[file]["architecture"]))
510 oldfiles = q.dictresult()
511 for oldfile in oldfiles:
512 files[file]["oldfiles"][suite] = oldfile
513 # Check versions [NB: per-suite only; no cross-suite checking done (yet)]
514 if apt_pkg.VersionCompare(files[file]["version"], oldfile["version"]) != 1:
515 reject_message = reject_message + "Rejected: %s Old version `%s' >= new version `%s'.\n" % (file, oldfile["version"], files[file]["version"])
516 # Check for existing copies of the file
517 if not changes.has_key("stable upload"):
518 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"]))
519 if q.getresult() != []:
520 reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (file)
522 # Check for existent source
523 # FIXME: this is no longer per suite
524 if changes["architecture"].has_key("source"):
525 source_version = files[file]["source version"];
526 if source_version != changes["version"]:
527 reject_message = reject_message + "Rejected: source version (%s) for %s doesn't match changes version %s.\n" % (files[file]["source version"], file, changes["version"]);
529 if not source_exists (files[file]["source package"], source_version):
530 reject_message = reject_message + "Rejected: no source found for %s %s (%s).\n" % (files[file]["source package"], source_version, file);
532 # Find any old .dsc files
533 elif files[file]["type"] == "dsc":
534 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"
535 % (files[file]["package"], suite))
536 oldfiles = q.dictresult()
537 if len(oldfiles) >= 1:
538 files[file]["oldfiles"][suite] = oldfiles[0]
540 # Validate the component
541 component = files[file]["component"];
542 component_id = db_access.get_component_id(component);
543 if component_id == -1:
544 reject_message = reject_message + "Rejected: file '%s' has unknown component '%s'.\n" % (file, component);
547 # Validate the priority
548 if string.find(files[file]["priority"],'/') != -1:
549 reject_message = reject_message + "Rejected: file '%s' has invalid priority '%s' [contains '/'].\n" % (file, files[file]["priority"]);
551 # Check the md5sum & size against existing files (if any)
552 location = Cnf["Dir::PoolDir"];
553 files[file]["location id"] = db_access.get_location_id (location, component, archive);
555 files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"]);
556 files_id = db_access.get_files_id(files[file]["pool name"] + file, files[file]["size"], files[file]["md5sum"], files[file]["location id"]);
558 reject_message = reject_message + "Rejected: INTERNAL ERROR, get_files_id() returned multiple matches for %s.\n" % (file)
560 reject_message = reject_message + "Rejected: md5sum and/or size mismatch on existing copy of %s.\n" % (file)
561 files[file]["files id"] = files_id
563 # Check for packages that have moved from one component to another
564 if files[file]["oldfiles"].has_key(suite) and files[file]["oldfiles"][suite]["name"] != files[file]["component"]:
565 files[file]["othercomponents"] = files[file]["oldfiles"][suite]["name"];
568 if string.find(reject_message, "Rejected:") != -1:
573 ###############################################################################
576 global dsc, dsc_files, reject_message, reprocess, orig_tar_id, orig_tar_location, legacy_source_untouchable;
578 for file in files.keys():
579 if files[file]["type"] == "dsc":
581 dsc = utils.parse_changes(file, 1)
582 except utils.cant_open_exc:
583 reject_message = reject_message + "Rejected: can't read changes file '%s'.\n" % (file)
585 except utils.changes_parse_error_exc, line:
586 reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (file, line)
588 except utils.invalid_dsc_format_exc, line:
589 reject_message = reject_message + "Rejected: syntax error in .dsc file '%s', line %s.\n" % (file, line)
592 dsc_files = utils.build_file_list(dsc, 1)
593 except utils.no_files_exc:
594 reject_message = reject_message + "Rejected: no Files: field in .dsc file.\n";
596 except utils.changes_parse_error_exc, line:
597 reject_message = reject_message + "Rejected: error parsing .dsc file '%s', can't grok: %s.\n" % (file, line);
600 # Enforce mandatory fields
601 for i in ("format", "source", "version", "binary", "maintainer", "architecture", "files"):
602 if not dsc.has_key(i):
603 reject_message = reject_message + "Rejected: Missing field `%s' in dsc file.\n" % (i)
605 # The dpkg maintainer from hell strikes again! Bumping the
606 # version number of the .dsc breaks extraction by stable's
608 if dsc["format"] != "1.0":
609 reject_message = reject_message + """Rejected: [dpkg-sucks] source package was produced by a broken version
610 of dpkg-dev 1.9.1{3,4}; please rebuild with >= 1.9.15 version
614 # Ensure the version number in the .dsc matches the version number in the .changes
615 epochless_dsc_version = utils.re_no_epoch.sub('', dsc.get("version"));
616 changes_version = files[file]["version"];
617 if epochless_dsc_version != files[file]["version"]:
618 reject_message = reject_message + "Rejected: version ('%s') in .dsc does not match version ('%s') in .changes\n" % (epochless_dsc_version, changes_version);
620 # Ensure source is newer than existing source in target suites
621 package = dsc.get("source");
622 new_version = dsc.get("version");
623 for suite in changes["distribution"].keys():
624 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"
626 ql = map(lambda x: x[0], q.getresult());
627 for old_version in ql:
628 if apt_pkg.VersionCompare(new_version, old_version) != 1:
629 reject_message = reject_message + "Rejected: %s Old version `%s' >= new version `%s'.\n" % (file, old_version, new_version)
631 # Try and find all files mentioned in the .dsc. This has
632 # to work harder to cope with the multiple possible
633 # locations of an .orig.tar.gz.
634 for dsc_file in dsc_files.keys():
635 if files.has_key(dsc_file):
636 actual_md5 = files[dsc_file]["md5sum"];
637 actual_size = int(files[dsc_file]["size"]);
638 found = "%s in incoming" % (dsc_file)
639 # Check the file does not already exist in the archive
640 if not changes.has_key("stable upload"):
641 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));
643 # "It has not broken them. It has fixed a
644 # brokenness. Your crappy hack exploited a
645 # bug in the old dinstall.
647 # "(Come on! I thought it was always obvious
648 # that one just doesn't release different
649 # files with the same name and version.)"
650 # -- ajk@ on d-devel@l.d.o
652 if q.getresult() != []:
653 reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (dsc_file)
654 elif dsc_file[-12:] == ".orig.tar.gz":
656 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));
660 # Unfortunately, we make get more than one match
661 # here if, for example, the package was in potato
662 # but had a -sa upload in woody. So we need to a)
663 # choose the right one and b) mark all wrong ones
664 # as excluded from the source poolification (to
665 # avoid file overwrites).
667 x = ql[0]; # default to something sane in case we don't match any or have only one
671 old_file = i[0] + i[1];
672 actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
673 actual_size = os.stat(old_file)[stat.ST_SIZE];
674 if actual_md5 == dsc_files[dsc_file]["md5sum"] and actual_size == int(dsc_files[dsc_file]["size"]):
677 legacy_source_untouchable[i[3]] = "";
679 old_file = x[0] + x[1];
680 actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
681 actual_size = os.stat(old_file)[stat.ST_SIZE];
684 dsc_files[dsc_file]["files id"] = x[3]; # need this for updating dsc_files in install()
687 if suite_type == "legacy" or suite_type == "legacy-mixed":
688 orig_tar_location = "legacy";
690 orig_tar_location = x[4];
692 # Not there? Check in Incoming...
693 # [See comment above process_it() for explanation
694 # of why this is necessary...]
695 if os.path.exists(dsc_file):
696 files[dsc_file] = {};
697 files[dsc_file]["size"] = os.stat(dsc_file)[stat.ST_SIZE];
698 files[dsc_file]["md5sum"] = dsc_files[dsc_file]["md5sum"];
699 files[dsc_file]["section"] = files[file]["section"];
700 files[dsc_file]["priority"] = files[file]["priority"];
701 files[dsc_file]["component"] = files[file]["component"];
702 files[dsc_file]["type"] = "orig.tar.gz";
706 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);
709 reject_message = reject_message + "Rejected: %s refers to %s, but I can't find it in Incoming.\n" % (file, dsc_file);
711 if actual_md5 != dsc_files[dsc_file]["md5sum"]:
712 reject_message = reject_message + "Rejected: md5sum for %s doesn't match %s.\n" % (found, file);
713 if actual_size != int(dsc_files[dsc_file]["size"]):
714 reject_message = reject_message + "Rejected: size for %s doesn't match %s.\n" % (found, file);
716 if string.find(reject_message, "Rejected:") != -1:
721 ###############################################################################
723 # Some cunning stunt broke dpkg-source in dpkg 1.8{,.1}; detect the
724 # resulting bad source packages and reject them.
726 # Even more amusingly the fix in 1.8.1.1 didn't actually fix the
727 # problem just changed the symptoms.
730 global dsc, dsc_files, reject_message, reprocess;
732 for filename in files.keys():
733 if files[filename]["type"] == "diff.gz":
734 file = gzip.GzipFile(filename, 'r');
735 for line in file.readlines():
736 if re_bad_diff.search(line):
737 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";
740 if string.find(reject_message, "Rejected:") != -1:
745 ###############################################################################
747 def check_md5sums ():
748 global reject_message;
750 for file in files.keys():
752 file_handle = utils.open_file(file,"r");
753 except utils.cant_open_exc:
756 if apt_pkg.md5sum(file_handle) != files[file]["md5sum"]:
757 reject_message = reject_message + "Rejected: md5sum check failed for %s.\n" % (file);
759 def check_override ():
762 # Only check section & priority on sourceful uploads
763 if not changes["architecture"].has_key("source"):
767 for file in files.keys():
768 if not files[file].has_key("new") and files[file]["type"] == "deb":
769 section = files[file]["section"];
770 override_section = files[file]["override section"];
771 if section != override_section and section != "-":
772 # Ignore this; it's a common mistake and not worth whining about
773 if string.lower(section) == "non-us/main" and string.lower(override_section) == "non-us":
775 summary = summary + "%s: section is overridden from %s to %s.\n" % (file, section, override_section);
776 priority = files[file]["priority"];
777 override_priority = files[file]["override priority"];
778 if priority != override_priority and priority != "-":
779 summary = summary + "%s: priority is overridden from %s to %s.\n" % (file, priority, override_priority);
784 Subst["__SUMMARY__"] = summary;
785 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.override-disparity","r").read());
786 utils.send_mail (mail_message, "")
788 #####################################################################################################################
790 # Set up the per-package template substitution mappings
792 def update_subst (changes_filename):
795 # If katie crashed out in the right place, architecture may still be a string.
796 if not changes.has_key("architecture") or not isinstance(changes["architecture"], DictType):
797 changes["architecture"] = { "Unknown" : "" };
798 # and maintainer822 may not exist.
799 if not changes.has_key("maintainer822"):
800 changes["maintainer822"] = Cnf["Dinstall::MyEmailAddress"];
802 Subst["__ARCHITECTURE__"] = string.join(changes["architecture"].keys(), ' ' );
803 Subst["__CHANGES_FILENAME__"] = os.path.basename(changes_filename);
804 Subst["__FILE_CONTENTS__"] = changes.get("filecontents", "");
806 # For source uploads the Changed-By field wins; otherwise Maintainer wins.
807 if changes["architecture"].has_key("source") and changes["changedby822"] != "" and (changes["changedby822"] != changes["maintainer822"]):
808 Subst["__MAINTAINER_FROM__"] = changes["changedby822"];
809 Subst["__MAINTAINER_TO__"] = changes["changedby822"] + ", " + changes["maintainer822"];
810 Subst["__MAINTAINER__"] = changes.get("changed-by", "Unknown");
812 Subst["__MAINTAINER_FROM__"] = changes["maintainer822"];
813 Subst["__MAINTAINER_TO__"] = changes["maintainer822"];
814 Subst["__MAINTAINER__"] = changes.get("maintainer", "Unknown");
816 Subst["__REJECT_MESSAGE__"] = reject_message;
817 Subst["__SOURCE__"] = changes.get("source", "Unknown");
818 Subst["__VERSION__"] = changes.get("version", "Unknown");
820 #####################################################################################################################
822 def action (changes_filename):
823 byhand = confirm = suites = summary = new = "";
825 # changes["distribution"] may not exist in corner cases
826 # (e.g. unreadable changes files)
827 if not changes.has_key("distribution") or not isinstance(changes["distribution"], DictType):
828 changes["distribution"] = {};
830 for suite in changes["distribution"].keys():
831 if Cnf.has_key("Suite::%s::Confirm"):
832 confirm = confirm + suite + ", "
833 suites = suites + suite + ", "
834 confirm = confirm[:-2]
837 for file in files.keys():
838 if files[file].has_key("byhand"):
840 summary = summary + file + " byhand\n"
841 elif files[file].has_key("new"):
843 summary = summary + "(new) %s %s %s\n" % (file, files[file]["priority"], files[file]["section"])
844 if files[file].has_key("othercomponents"):
845 summary = summary + "WARNING: Already present in %s distribution.\n" % (files[file]["othercomponents"])
846 if files[file]["type"] == "deb":
847 summary = summary + apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))["Description"] + '\n';
849 files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"])
850 destination = Cnf["Dir::PoolRoot"] + files[file]["pool name"] + file
851 summary = summary + file + "\n to " + destination + "\n"
853 short_summary = summary;
855 # This is for direport's benefit...
856 f = re_fdnic.sub("\n .\n", changes.get("changes",""));
858 if confirm or byhand or new:
859 summary = summary + "Changes: " + f;
861 summary = summary + announce (short_summary, 0)
863 (prompt, answer) = ("", "XXX")
864 if Options["No-Action"] or Options["Automatic"]:
867 if string.find(reject_message, "Rejected") != -1:
869 modified_time = time.time()-os.path.getmtime(changes_filename);
870 except: # i.e. ignore errors like 'file does not exist';
872 if modified_time < 86400:
873 print "SKIP (too new)\n" + reject_message,;
874 prompt = "[S]kip, Manual reject, Quit ?";
876 print "REJECT\n" + reject_message,;
877 prompt = "[R]eject, Manual reject, Skip, Quit ?";
878 if Options["Automatic"]:
881 print "NEW to %s\n%s%s" % (suites, reject_message, summary),;
882 prompt = "[S]kip, New ack, Manual reject, Quit ?";
883 if Options["Automatic"] and Options["Ack-New"]:
886 print "BYHAND\n" + reject_message + summary,;
887 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
889 print "CONFIRM to %s\n%s%s" % (confirm, reject_message, summary),
890 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
892 print "INSTALL\n" + reject_message + summary,;
893 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
894 if Options["Automatic"]:
897 while string.find(prompt, answer) == -1:
899 answer = utils.our_raw_input()
900 m = re_default_answer.match(prompt)
903 answer = string.upper(answer[:1])
906 reject (changes_filename, "");
908 manual_reject (changes_filename);
910 install (changes_filename, summary, short_summary);
912 acknowledge_new (changes_filename, summary);
916 #####################################################################################################################
918 def install (changes_filename, summary, short_summary):
919 global install_count, install_bytes, Subst;
921 # Stable uploads are a special case
922 if changes.has_key("stable upload"):
923 stable_install (changes_filename, summary, short_summary);
928 Logger.log(["installing changes",changes_filename]);
930 archive = utils.where_am_i();
932 # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
933 projectB.query("BEGIN WORK");
935 # Add the .dsc file to the DB
936 for file in files.keys():
937 if files[file]["type"] == "dsc":
938 package = dsc["source"]
939 version = dsc["version"] # NB: not files[file]["version"], that has no epoch
940 maintainer = dsc["maintainer"]
941 maintainer = string.replace(maintainer, "'", "\\'")
942 maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
943 filename = files[file]["pool name"] + file;
944 dsc_location_id = files[file]["location id"];
945 if not files[file]["files id"]:
946 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], dsc_location_id)
947 projectB.query("INSERT INTO source (source, version, maintainer, file) VALUES ('%s', '%s', %d, %d)"
948 % (package, version, maintainer_id, files[file]["files id"]))
950 for suite in changes["distribution"].keys():
951 suite_id = db_access.get_suite_id(suite);
952 projectB.query("INSERT INTO src_associations (suite, source) VALUES (%d, currval('source_id_seq'))" % (suite_id))
954 # Add the source files to the DB (files and dsc_files)
955 projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files[file]["files id"]));
956 for dsc_file in dsc_files.keys():
957 filename = files[file]["pool name"] + dsc_file;
958 # If the .orig.tar.gz is already in the pool, it's
959 # files id is stored in dsc_files by check_dsc().
960 files_id = dsc_files[dsc_file].get("files id", None);
962 files_id = db_access.get_files_id(filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], dsc_location_id);
963 # FIXME: needs to check for -1/-2 and or handle exception
965 files_id = db_access.set_files_id (filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], dsc_location_id);
966 projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files_id));
968 # Add the .deb files to the DB
969 for file in files.keys():
970 if files[file]["type"] == "deb":
971 package = files[file]["package"]
972 version = files[file]["version"]
973 maintainer = files[file]["maintainer"]
974 maintainer = string.replace(maintainer, "'", "\\'")
975 maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
976 architecture = files[file]["architecture"]
977 architecture_id = db_access.get_architecture_id (architecture);
978 type = files[file]["dbtype"];
979 dsc_component = files[file]["component"]
980 source = files[file]["source package"]
981 source_version = files[file]["source version"];
982 filename = files[file]["pool name"] + file;
983 if not files[file]["files id"]:
984 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], files[file]["location id"])
985 source_id = db_access.get_source_id (source, source_version);
987 projectB.query("INSERT INTO binaries (package, version, maintainer, source, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, %d, '%s')"
988 % (package, version, maintainer_id, source_id, architecture_id, files[file]["files id"], type));
990 projectB.query("INSERT INTO binaries (package, version, maintainer, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, '%s')"
991 % (package, version, maintainer_id, architecture_id, files[file]["files id"], type));
992 for suite in changes["distribution"].keys():
993 suite_id = db_access.get_suite_id(suite);
994 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES (%d, currval('binaries_id_seq'))" % (suite_id));
996 # If the .orig.tar.gz is in a legacy directory we need to poolify
997 # it, so that apt-get source (and anything else that goes by the
998 # "Directory:" field in the Sources.gz file) works.
999 if orig_tar_id != None and orig_tar_location == "legacy":
1000 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));
1001 qd = q.dictresult();
1003 # Is this an old upload superseded by a newer -sa upload? (See check_dsc() for details)
1004 if legacy_source_untouchable.has_key(qid["files_id"]):
1006 # First move the files to the new location
1007 legacy_filename = qid["path"]+qid["filename"];
1008 pool_location = utils.poolify (changes["source"], files[file]["component"]);
1009 pool_filename = pool_location + os.path.basename(qid["filename"]);
1010 destination = Cnf["Dir::PoolDir"] + pool_location
1011 utils.move(legacy_filename, destination);
1012 # Then Update the DB's files table
1013 q = projectB.query("UPDATE files SET filename = '%s', location = '%s' WHERE id = '%s'" % (pool_filename, dsc_location_id, qid["files_id"]));
1015 # If this is a sourceful diff only upload that is moving non-legacy
1016 # cross-component we need to copy the .orig.tar.gz into the new
1017 # component too for the same reasons as above.
1019 if changes["architecture"].has_key("source") and orig_tar_id != None and \
1020 orig_tar_location != "legacy" and orig_tar_location != dsc_location_id:
1021 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));
1022 ql = q.getresult()[0];
1023 old_filename = ql[0] + ql[1];
1025 file_md5sum = ql[3];
1026 new_filename = utils.poolify (changes["source"], dsc_component) + os.path.basename(old_filename);
1027 new_files_id = db_access.get_files_id(new_filename, file_size, file_md5sum, dsc_location_id);
1028 if new_files_id == None:
1029 utils.copy(old_filename, Cnf["Dir::PoolDir"] + new_filename);
1030 new_files_id = db_access.set_files_id(new_filename, file_size, file_md5sum, dsc_location_id);
1031 projectB.query("UPDATE dsc_files SET file = %s WHERE source = %s AND file = %s" % (new_files_id, source_id, orig_tar_id));
1033 # Install the files into the pool
1034 for file in files.keys():
1035 if files[file].has_key("byhand"):
1037 destination = Cnf["Dir::PoolDir"] + files[file]["pool name"] + file
1038 destdir = os.path.dirname(destination)
1039 utils.move (file, destination)
1040 Logger.log(["installed", file, files[file]["type"], files[file]["size"], files[file]["architecture"]]);
1041 install_bytes = install_bytes + float(files[file]["size"])
1043 # Copy the .changes file across for suite which need it.
1044 for suite in changes["distribution"].keys():
1045 if Cnf.has_key("Suite::%s::CopyChanges" % (suite)):
1046 utils.copy (changes_filename, Cnf["Dir::RootDir"] + Cnf["Suite::%s::CopyChanges" % (suite)]);
1048 projectB.query("COMMIT WORK");
1051 utils.move (changes_filename, Cnf["Dir::IncomingDir"] + 'DONE/' + os.path.basename(changes_filename))
1053 utils.warn("couldn't move changes file '%s' to DONE directory. [Got %s]" % (os.path.basename(changes_filename), sys.exc_type));
1055 install_count = install_count + 1;
1057 if not Options["No-Mail"]:
1058 Subst["__SUITE__"] = "";
1059 Subst["__SUMMARY__"] = summary;
1060 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.installed","r").read());
1061 utils.send_mail (mail_message, "")
1062 announce (short_summary, 1)
1065 #####################################################################################################################
1067 def stable_install (changes_filename, summary, short_summary):
1068 global install_count, install_bytes, Subst;
1070 print "Installing to stable."
1072 archive = utils.where_am_i();
1074 # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
1075 projectB.query("BEGIN WORK");
1077 # Add the .dsc file to the DB
1078 for file in files.keys():
1079 if files[file]["type"] == "dsc":
1080 package = dsc["source"]
1081 version = dsc["version"] # NB: not files[file]["version"], that has no epoch
1082 q = projectB.query("SELECT id FROM source WHERE source = '%s' AND version = '%s'" % (package, version))
1085 utils.fubar("[INTERNAL ERROR] couldn't find '%s' (%s) in source table." % (package, version));
1086 source_id = ql[0][0];
1087 suite_id = db_access.get_suite_id('proposed-updates');
1088 projectB.query("DELETE FROM src_associations WHERE suite = '%s' AND source = '%s'" % (suite_id, source_id));
1089 suite_id = db_access.get_suite_id('stable');
1090 projectB.query("INSERT INTO src_associations (suite, source) VALUES ('%s', '%s')" % (suite_id, source_id));
1092 # Add the .deb files to the DB
1093 for file in files.keys():
1094 if files[file]["type"] == "deb":
1095 package = files[file]["package"]
1096 version = files[file]["version"]
1097 architecture = files[file]["architecture"]
1098 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))
1101 utils.fubar("[INTERNAL ERROR] couldn't find '%s' (%s for %s architecture) in binaries table." % (package, version, architecture));
1102 binary_id = ql[0][0];
1103 suite_id = db_access.get_suite_id('proposed-updates');
1104 projectB.query("DELETE FROM bin_associations WHERE suite = '%s' AND bin = '%s'" % (suite_id, binary_id));
1105 suite_id = db_access.get_suite_id('stable');
1106 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES ('%s', '%s')" % (suite_id, binary_id));
1108 projectB.query("COMMIT WORK");
1111 utils.move (changes_filename, Cnf["Dir::Morgue"] + '/katie/' + os.path.basename(changes_filename));
1113 # Update the Stable ChangeLog file
1115 new_changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + ".ChangeLog";
1116 changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + "ChangeLog";
1117 if os.path.exists(new_changelog_filename):
1118 os.unlink (new_changelog_filename);
1120 new_changelog = utils.open_file(new_changelog_filename, 'w');
1121 for file in files.keys():
1122 if files[file]["type"] == "deb":
1123 new_changelog.write("stable/%s/binary-%s/%s\n" % (files[file]["component"], files[file]["architecture"], file));
1124 elif utils.re_issource.match(file) != None:
1125 new_changelog.write("stable/%s/source/%s\n" % (files[file]["component"], file));
1127 new_changelog.write("%s\n" % (file));
1128 chop_changes = re_fdnic.sub("\n", changes["changes"]);
1129 new_changelog.write(chop_changes + '\n\n');
1130 if os.access(changelog_filename, os.R_OK) != 0:
1131 changelog = utils.open_file(changelog_filename, 'r');
1132 new_changelog.write(changelog.read());
1133 new_changelog.close();
1134 if os.access(changelog_filename, os.R_OK) != 0:
1135 os.unlink(changelog_filename);
1136 utils.move(new_changelog_filename, changelog_filename);
1138 install_count = install_count + 1;
1140 if not Options["No-Mail"]:
1141 Subst["__SUITE__"] = " into stable";
1142 Subst["__SUMMARY__"] = summary;
1143 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.installed","r").read());
1144 utils.send_mail (mail_message, "")
1145 announce (short_summary, 1)
1147 #####################################################################################################################
1149 def reject (changes_filename, manual_reject_mail_filename):
1152 print "Rejecting.\n"
1154 base_changes_filename = os.path.basename(changes_filename);
1155 reason_filename = re_changes.sub("reason", base_changes_filename);
1156 reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename);
1158 # Move the .changes files and it's contents into REJECT/ (if we can; errors are ignored)
1160 utils.move (changes_filename, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], base_changes_filename));
1162 utils.warn("couldn't reject changes file '%s'. [Got %s]" % (base_changes_filename, sys.exc_type));
1164 for file in files.keys():
1165 if os.path.exists(file):
1167 utils.move (file, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], file));
1169 utils.warn("couldn't reject file '%s'. [Got %s]" % (file, sys.exc_type));
1172 # If this is not a manual rejection generate the .reason file and rejection mail message
1173 if manual_reject_mail_filename == "":
1174 if os.path.exists(reject_filename):
1175 os.unlink(reject_filename);
1176 fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
1177 os.write(fd, reject_message);
1179 Subst["__MANUAL_REJECT_MESSAGE__"] = "";
1180 Subst["__CC__"] = "X-Katie-Rejection: automatic (moo)";
1181 reject_mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.rejected","r").read());
1182 else: # Have a manual rejection file to use
1183 reject_mail_message = ""; # avoid <undef>'s
1185 # Send the rejection mail if appropriate
1186 if not Options["No-Mail"]:
1187 utils.send_mail (reject_mail_message, manual_reject_mail_filename);
1189 Logger.log(["rejected", changes_filename]);
1191 ##################################################################
1193 def manual_reject (changes_filename):
1196 # Build up the rejection email
1197 user_email_address = utils.whoami() + " <%s@%s>" % (pwd.getpwuid(os.getuid())[0], Cnf["Dinstall::MyHost"])
1198 manual_reject_message = Cnf.get("Dinstall::Options::Manual-Reject", "")
1200 Subst["__MANUAL_REJECT_MESSAGE__"] = manual_reject_message;
1201 Subst["__CC__"] = "Cc: " + Cnf["Dinstall::MyEmailAddress"];
1202 reject_mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.rejected","r").read());
1204 # Write the rejection email out as the <foo>.reason file
1205 reason_filename = re_changes.sub("reason", os.path.basename(changes_filename));
1206 reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename)
1207 if os.path.exists(reject_filename):
1208 os.unlink(reject_filename);
1209 fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
1210 os.write(fd, reject_mail_message);
1213 # If we weren't given one, spawn an editor so the user can add one in
1214 if manual_reject_message == "":
1215 result = os.system("vi +6 %s" % (reject_filename))
1217 utils.fubar("vi invocation failed for `%s'!" % (reject_filename), result);
1219 # Then process it as if it were an automatic rejection
1220 reject (changes_filename, reject_filename)
1222 #####################################################################################################################
1224 def acknowledge_new (changes_filename, summary):
1225 global new_ack_new, Subst;
1227 changes_filename = os.path.basename(changes_filename);
1229 new_ack_new[changes_filename] = 1;
1231 if new_ack_old.has_key(changes_filename):
1232 print "Ack already sent.";
1235 print "Sending new ack.";
1236 if not Options["No-Mail"]:
1237 Subst["__SUMMARY__"] = summary;
1238 new_ack_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.new","r").read());
1239 utils.send_mail(new_ack_message,"");
1241 #####################################################################################################################
1243 def announce (short_summary, action):
1246 # Only do announcements for source uploads with a recent dpkg-dev installed
1247 if float(changes.get("format", 0)) < 1.6 or not changes["architecture"].has_key("source"):
1252 Subst["__SHORT_SUMMARY__"] = short_summary;
1254 for dist in changes["distribution"].keys():
1255 list = Cnf.Find("Suite::%s::Announce" % (dist))
1256 if list == "" or lists_done.has_key(list):
1258 lists_done[list] = 1
1259 summary = summary + "Announcing to %s\n" % (list)
1262 Subst["__ANNOUNCE_LIST_ADDRESS__"] = list;
1263 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.announce","r").read());
1264 utils.send_mail (mail_message, "")
1266 bugs = changes["closes"].keys()
1268 if not nmu.is_an_nmu(changes, dsc):
1269 summary = summary + "Closing bugs: "
1271 summary = summary + "%s " % (bug)
1273 Subst["__BUG_NUMBER__"] = bug;
1274 if changes["distribution"].has_key("stable"):
1275 Subst["__STABLE_WARNING__"] = """
1276 Note that this package is not part of the released stable Debian
1277 distribution. It may have dependencies on other unreleased software,
1278 or other instabilities. Please take care if you wish to install it.
1279 The update will eventually make its way into the next released Debian
1282 Subst["__STABLE_WARNING__"] = "";
1283 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.bug-close","r").read());
1284 utils.send_mail (mail_message, "")
1286 Logger.log(["closing bugs"]+bugs);
1288 summary = summary + "Setting bugs to severity fixed: "
1289 control_message = ""
1291 summary = summary + "%s " % (bug)
1292 control_message = control_message + "tag %s + fixed\n" % (bug)
1293 if action and control_message != "":
1294 Subst["__CONTROL_MESSAGE__"] = control_message;
1295 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.bug-nmu-fixed","r").read());
1296 utils.send_mail (mail_message, "")
1298 Logger.log(["setting bugs to fixed"]+bugs);
1299 summary = summary + "\n"
1303 ###############################################################################
1305 # reprocess is necessary for the case of foo_1.2-1 and foo_1.2-2 in
1306 # Incoming. -1 will reference the .orig.tar.gz, but -2 will not.
1307 # dsccheckdistrib() can find the .orig.tar.gz but it will not have
1308 # processed it during it's checks of -2. If -1 has been deleted or
1309 # otherwise not checked by da-install, the .orig.tar.gz will not have
1310 # been checked at all. To get round this, we force the .orig.tar.gz
1311 # into the .changes structure and reprocess the .changes file.
1313 def process_it (changes_file):
1314 global reprocess, orig_tar_id, orig_tar_location, changes, dsc, dsc_files, files, reject_message;
1316 # Reset some globals
1323 orig_tar_location = "";
1324 legacy_source_untouchable = {};
1325 reject_message = "";
1327 # Absolutize the filename to avoid the requirement of being in the
1328 # same directory as the .changes file.
1329 changes_file = os.path.abspath(changes_file);
1331 # And since handling of installs to stable munges with the CWD;
1332 # save and restore it.
1336 check_signature (changes_file);
1337 check_changes (changes_file);
1346 traceback.print_exc(file=sys.stdout);
1349 update_subst(changes_file);
1350 action(changes_file);
1355 ###############################################################################
1358 global Cnf, Options, projectB, install_bytes, new_ack_old, Subst, nmu, Logger
1360 changes_files = init();
1365 if Options["Version"]:
1366 print "katie %s" % (katie_version);
1369 # -n/--dry-run invalidates some other options which would involve things happening
1370 if Options["No-Action"]:
1371 Options["Automatic"] = "";
1372 Options["Ack-New"] = "";
1374 projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"]));
1376 db_access.init(Cnf, projectB);
1378 # Check that we aren't going to clash with the daily cron job
1380 if not Options["No-Action"] and os.path.exists("%s/Archive_Maintenance_In_Progress" % (Cnf["Dir::RootDir"])) and not Options["No-Lock"]:
1381 utils.fubar("Archive maintenance in progress. Try again later.");
1383 # Obtain lock if not in no-action mode and initialize the log
1385 if not Options["No-Action"]:
1386 lock_fd = os.open(Cnf["Dinstall::LockFile"], os.O_RDWR | os.O_CREAT);
1387 fcntl.lockf(lock_fd, FCNTL.F_TLOCK);
1388 Logger = logging.Logger(Cnf, "katie");
1390 if Options["Ack-New"]:
1391 # Read in the list of already-acknowledged NEW packages
1392 if not os.path.exists(Cnf["Dinstall::NewAckList"]):
1393 utils.touch_file(Cnf["Dinstall::NewAckList"]);
1394 new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'r');
1396 for line in new_ack_list.readlines():
1397 new_ack_old[line[:-1]] = 1;
1398 new_ack_list.close();
1400 # Initialize the substitution template mapping global
1402 Subst["__ADMIN_ADDRESS__"] = Cnf["Dinstall::MyAdminAddress"];
1403 Subst["__BUG_SERVER__"] = Cnf["Dinstall::BugServer"];
1404 bcc = "X-Katie: %s" % (katie_version);
1405 if Cnf.has_key("Dinstall::Bcc"):
1406 Subst["__BCC__"] = bcc + "\nBcc: %s" % (Cnf["Dinstall::Bcc"]);
1408 Subst["__BCC__"] = bcc;
1409 Subst["__DISTRO__"] = Cnf["Dinstall::MyDistribution"];
1410 Subst["__KATIE_ADDRESS__"] = Cnf["Dinstall::MyEmailAddress"];
1412 # Read in the group-maint override file
1415 # Sort the .changes files so that we process sourceful ones first
1416 changes_files.sort(utils.changes_compare);
1418 # Process the changes files
1419 for changes_file in changes_files:
1420 print "\n" + changes_file;
1421 process_it (changes_file);
1425 if install_count > 1:
1427 sys.stderr.write("Installed %d package %s, %s.\n" % (install_count, sets, utils.size_type(int(install_bytes))));
1428 Logger.log(["total",install_count,install_bytes]);
1430 # Write out the list of already-acknowledged NEW packages
1431 if Options["Ack-New"]:
1432 new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'w')
1433 for i in new_ack_new.keys():
1434 new_ack_list.write(i+'\n')
1435 new_ack_list.close()
1437 if not Options["No-Action"]:
1440 if __name__ == '__main__':