3 # Installs Debian packaes
4 # Copyright (C) 2000 James Troup <james@nocrew.org>
5 # $Id: katie,v 1.14 2000-12-19 21:06:20 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 # 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, os, pg, pwd, re, shutil, stat, string, sys, tempfile, time
36 import apt_inst, apt_pkg
37 import utils, db_access
39 ###############################################################################
41 re_isanum = re.compile (r'^\d+$');
42 re_isadeb = re.compile (r'.*\.u?deb$');
43 re_issource = re.compile (r'(.+)_(.+?)\.(orig\.tar\.gz|diff\.gz|tar\.gz|dsc)');
44 re_dpackage = re.compile (r'^package:\s*(.*)', re.IGNORECASE);
45 re_darchitecture = re.compile (r'^architecture:\s*(.*)', re.IGNORECASE);
46 re_dversion = re.compile (r'^version:\s*(.*)', re.IGNORECASE);
47 re_dsection = re.compile (r'^section:\s*(.*)', re.IGNORECASE);
48 re_dpriority = re.compile (r'^priority:\s*(.*)', re.IGNORECASE);
49 re_changes = re.compile (r'changes$');
50 re_override_package = re.compile(r'(\S*)\s+.*');
51 re_default_answer = re.compile(r"\[(.*)\]");
52 re_fdnic = re.compile("\n\n");
54 ###############################################################################
57 reject_footer = """If you don't understand why your files were rejected, or if the
58 override file requires editing, reply to this email.
60 Your rejected files are in incoming/REJECT/. (Some may also be in
61 incoming/ if your .changes file was unparsable.) If only some of the
62 files need to repaired, you may move any good files back to incoming/.
63 Please remove any bad files from incoming/REJECT/."""
65 new_ack_footer = """Your package contains new components which requires manual editing of
66 the override file. It is ok otherwise, so please be patient. New
67 packages are usually added to the override file about once a week.
69 You may have gotten the distribution wrong. You'll get warnings above
70 if files already exist in other distributions."""
72 installed_footer = """If the override file requires editing, file a bug on ftp.debian.org.
74 Thank you for your contribution to Debian GNU."""
76 #########################################################################################
94 #########################################################################################
96 def usage (exit_code):
97 print """Usage: dinstall [OPTION]... [CHANGES]...
98 -a, --automatic automatic run
99 -d, --debug=VALUE debug
100 -k, --ack-new acknowledge new packages
101 -m, --manual-reject=MSG manual reject with `msg'
102 -n, --dry-run don't do anything
103 -p, --no-lock don't check lockfile !! for cron.daily only !!
104 -r, --no-version-check override version check
105 -u, --distribution=DIST override distribution to `dist'"""
108 def check_signature (filename):
109 global reject_message
111 (result, output) = commands.getstatusoutput("gpg --emulate-md-encode-bug --batch --no-options --no-default-keyring --always-trust --keyring=%s --keyring=%s < %s >/dev/null" % (Cnf["Dinstall::PGPKeyring"], Cnf["Dinstall::GPGKeyring"], filename))
113 reject_message = "Rejected: GPG signature check failed on `%s'.\n%s\n" % (filename, output)
117 #####################################################################################################################
119 def read_override_file (filename, suite, component, binary_type):
122 file = utils.open_file(filename, 'r');
123 for line in file.readlines():
124 line = string.strip(utils.re_comments.sub('', line))
125 override_package = re_override_package.sub(r'\1', line)
126 if override_package != "":
127 overrides[suite][component][binary_type][override_package] = 1
131 # See if a given package is in the override file. Caches and only loads override files on demand.
133 def in_override_p (package, component, suite, binary_type):
136 if binary_type == "" or binary_type == "deb":
139 # Avoid <undef> on unknown distributions
140 if db_access.get_suite_id(suite) == -1:
143 # FIXME: nasty non-US speficic hack
144 if string.lower(component[:7]) == "non-us/":
145 component = component[7:];
146 if not overrides.has_key(suite) or not overrides[suite].has_key(component) or not overrides[suite][component].has_key(binary_type):
147 if not overrides.has_key(suite):
148 overrides[suite] = {}
149 if not overrides[suite].has_key(component):
150 overrides[suite][component] = {}
151 if not overrides[suite][component].has_key(binary_type):
152 overrides[suite][component][binary_type] = {}
153 if Cnf.has_key("Suite::%s::SingleOverrideFile" % (suite)): # legacy mixed suite (i.e. experimental)
154 override_filename = Cnf["Dir::OverrideDir"] + 'override.' + Cnf["Suite::%s::OverrideCodeName" % (suite)];
155 read_override_file (override_filename, suite, component, binary_type);
157 if binary_type == "udeb":
158 override_filename = Cnf["Dir::OverrideDir"] + 'override.' + Cnf["Suite::%s::OverrideCodeName" % (suite)] + '.debian-installer.' + component;
159 read_override_file (override_filename, suite, component, binary_type);
161 for src in ("", ".src"):
162 override_filename = Cnf["Dir::OverrideDir"] + 'override.' + Cnf["Suite::%s::OverrideCodeName" % (suite)] + '.' + component + src;
163 read_override_file (override_filename, suite, component, binary_type);
165 return overrides[suite][component][binary_type].get(package, None);
167 #####################################################################################################################
169 def check_changes(filename):
170 global reject_message, changes, files
172 # Parse the .changes field into a dictionary [FIXME - need to trap errors, pass on to reject_message etc.]
174 changes = utils.parse_changes(filename)
175 except utils.cant_open_exc:
176 reject_message = "Rejected: can't read changes file '%s'.\n" % (filename)
178 except utils.changes_parse_error_exc, line:
179 reject_message = "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line)
180 changes["maintainer822"] = Cnf["Dinstall::MyEmailAddress"];
183 # Parse the Files field from the .changes into another dictionary [FIXME need to trap errors as above]
184 files = utils.build_file_list(changes, "")
186 # Check for mandatory fields
187 for i in ("source", "binary", "architecture", "version", "distribution","maintainer", "files"):
188 if not changes.has_key(i):
189 reject_message = "Rejected: Missing field `%s' in changes file." % (i)
190 return 0 # Avoid <undef> errors during later tests
192 # Fix the Maintainer: field to be RFC822 compatible
193 (changes["maintainer822"], changes["maintainername"], changes["maintaineremail"]) = utils.fix_maintainer (changes["maintainer"])
195 # Override the Distribution: field if appropriate
196 if Cnf["Dinstall::Options::Override-Distribution"] != "":
197 reject_message = reject_message + "Warning: Distribution was overriden from %s to %s.\n" % (changes["distribution"], Cnf["Dinstall::Options::Override-Distribution"])
198 changes["distribution"] = Cnf["Dinstall::Options::Override-Distribution"]
200 # Split multi-value fields into a lower-level dictionary
201 for i in ("architecture", "distribution", "binary", "closes"):
202 o = changes.get(i, "")
206 for j in string.split(o):
209 # Ensure all the values in Closes: are numbers
210 if changes.has_key("closes"):
211 for i in changes["closes"].keys():
212 if re_isanum.match (i) == None:
213 reject_message = reject_message + "Rejected: `%s' from Closes field isn't a number.\n" % (i)
215 # Map frozen to unstable if frozen doesn't exist
216 if changes["distribution"].has_key("frozen") and not Cnf.has_key("Suite::Frozen"):
217 del changes["distribution"]["frozen"]
218 reject_message = reject_message + "Mapping frozen to unstable.\n"
220 # Ensure target distributions exist
221 for i in changes["distribution"].keys():
222 if not Cnf.has_key("Suite::%s" % (i)):
223 reject_message = reject_message + "Rejected: Unknown distribution `%s'.\n" % (i)
225 # Map unreleased arches from stable to unstable
226 if changes["distribution"].has_key("stable"):
227 for i in changes["architecture"].keys():
228 if not Cnf.has_key("Suite::Stable::Architectures::%s" % (i)):
229 reject_message = reject_message + "Mapping stable to unstable for unreleased arch `%s'.\n" % (i)
230 del changes["distribution"]["stable"]
232 # Map arches not being released from frozen to unstable
233 if changes["distribution"].has_key("frozen"):
234 for i in changes["architecture"].keys():
235 if not Cnf.has_key("Suite::Frozen::Architectures::%s" % (i)):
236 reject_message = reject_message + "Mapping frozen to unstable for non-releasing arch `%s'.\n" % (i)
237 del changes["distribution"]["frozen"]
239 # Handle uploads to stable
240 if changes["distribution"].has_key("stable"):
241 # If running from within proposed-updates; assume an install to stable
242 if string.find(os.getcwd(), 'proposed-updates') != -1:
243 # FIXME: should probably remove anything that != stable
244 for i in ("frozen", "unstable"):
245 if changes["distribution"].has_key(i):
246 reject_message = reject_message + "Removing %s from distribution list.\n"
247 del changes["distribution"][i]
248 changes["stable upload"] = 1;
249 # If we can't find a file from the .changes; assume it's a package already in the pool and move into the pool
250 file = files.keys()[0];
251 if os.access(file, os.R_OK) == 0:
252 pool_dir = Cnf["Dir::PoolDir"] + '/' + utils.poolify(changes["source"], files[file]["component"]);
254 # Otherwise (normal case) map stable to updates
256 reject_message = reject_message + "Mapping stable to updates.\n";
257 del changes["distribution"]["stable"];
258 changes["distribution"]["proposed-updates"] = 1;
260 # chopversion = no epoch; chopversion2 = no epoch and no revision (e.g. for .orig.tar.gz comparison)
261 changes["chopversion"] = utils.re_no_epoch.sub('', changes["version"])
262 changes["chopversion2"] = utils.re_no_revision.sub('', changes["chopversion"])
264 if string.find(reject_message, "Rejected:") != -1:
270 global reject_message
272 archive = utils.where_am_i();
274 for file in files.keys():
275 # Check the file is readable
276 if os.access(file,os.R_OK) == 0:
277 reject_message = reject_message + "Rejected: Can't read `%s'.\n" % (file)
278 files[file]["type"] = "unreadable";
280 # If it's byhand skip remaining checks
281 if files[file]["section"] == "byhand":
282 files[file]["byhand"] = 1;
283 files[file]["type"] = "byhand";
284 # Checks for a binary package...
285 elif re_isadeb.match(file) != None:
286 # Extract package information using dpkg-deb
287 control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))
289 # Check for mandatory fields
290 if control.Find("Package") == None:
291 reject_message = reject_message + "Rejected: %s: No package field in control.\n" % (file)
292 if control.Find("Architecture") == None:
293 reject_message = reject_message + "Rejected: %s: No architecture field in control.\n" % (file)
294 if control.Find("Version") == None:
295 reject_message = reject_message + "Rejected: %s: No version field in control.\n" % (file)
297 # Ensure the package name matches the one give in the .changes
298 if not changes["binary"].has_key(control.Find("Package", "")):
299 reject_message = reject_message + "Rejected: %s: control file lists name as `%s', which isn't in changes file.\n" % (file, control.Find("Package", ""))
301 # Validate the architecture
302 if not Cnf.has_key("Suite::Unstable::Architectures::%s" % (control.Find("Architecture", ""))):
303 reject_message = reject_message + "Rejected: Unknown architecture '%s'.\n" % (control.Find("Architecture", ""))
305 # Check the architecture matches the one given in the .changes
306 if not changes["architecture"].has_key(control.Find("Architecture", "")):
307 reject_message = reject_message + "Rejected: %s: control file lists arch as `%s', which isn't in changes file.\n" % (file, control.Find("Architecture", ""))
308 # Check the section & priority match those given in the .changes (non-fatal)
309 if control.Find("Section") != None and files[file]["section"] != "" and files[file]["section"] != control.Find("Section"):
310 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"])
311 if control.Find("Priority") != None and files[file]["priority"] != "" and files[file]["priority"] != control.Find("Priority"):
312 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"])
314 epochless_version = utils.re_no_epoch.sub('', control.Find("Version", ""))
316 files[file]["package"] = control.Find("Package");
317 files[file]["architecture"] = control.Find("Architecture");
318 files[file]["version"] = control.Find("Version");
319 files[file]["maintainer"] = control.Find("Maintainer", "");
320 if file[-5:] == ".udeb":
321 files[file]["dbtype"] = "udeb";
322 elif file[-4:] == ".deb":
323 files[file]["dbtype"] = "deb";
325 reject_message = reject_message + "Rejected: %s is neither a .deb or a .udeb.\n " % (file);
326 files[file]["type"] = "deb";
327 files[file]["fullname"] = "%s_%s_%s.deb" % (control.Find("Package", ""), epochless_version, control.Find("Architecture", ""))
328 files[file]["source"] = control.Find("Source", "");
329 if files[file]["source"] == "":
330 files[file]["source"] = files[file]["package"];
331 # Checks for a source package...
333 m = re_issource.match(file)
335 files[file]["package"] = m.group(1)
336 files[file]["version"] = m.group(2)
337 files[file]["type"] = m.group(3)
339 # Ensure the source package name matches the Source filed in the .changes
340 if changes["source"] != files[file]["package"]:
341 reject_message = reject_message + "Rejected: %s: changes file doesn't say %s for Source\n" % (file, files[file]["package"])
343 # Ensure the source version matches the version in the .changes file
344 if files[file]["type"] == "orig.tar.gz":
345 changes_version = changes["chopversion2"]
347 changes_version = changes["chopversion"]
348 if changes_version != files[file]["version"]:
349 reject_message = reject_message + "Rejected: %s: should be %s according to changes file.\n" % (file, changes_version)
351 # Ensure the .changes lists source in the Architecture field
352 if not changes["architecture"].has_key("source"):
353 reject_message = reject_message + "Rejected: %s: changes file doesn't list `source' in Architecture field.\n" % (file)
355 # Check the signature of a .dsc file
356 if files[file]["type"] == "dsc":
357 check_signature(file)
359 files[file]["fullname"] = file
361 # Not a binary or source package? Assume byhand...
363 files[file]["byhand"] = 1;
364 files[file]["type"] = "byhand";
366 files[file]["oldfiles"] = {}
367 for suite in changes["distribution"].keys():
369 if files[file].has_key("byhand"):
372 if Cnf.has_key("Suite:%s::Components" % (suite)) and not Cnf.has_key("Suite::%s::Components::%s" % (suite, files[file]["component"])):
373 reject_message = reject_message + "Rejected: unknown component `%s' for suite `%s'.\n" % (files[file]["component"], suite)
376 # See if the package is NEW
377 if not in_override_p(files[file]["package"], files[file]["component"], suite, files[file].get("dbtype","")):
378 files[file]["new"] = 1
380 # Find any old binary packages
381 if files[file]["type"] == "deb":
382 q = projectB.query("SELECT b.id, b.version, f.filename, l.path, c.name FROM binaries b, bin_associations ba, suite s, location l, component c, architecture a, files f WHERE b.package = '%s' AND s.suite_name = '%s' AND a.arch_string = '%s' AND ba.bin = b.id AND ba.suite = s.id AND b.architecture = a.id AND f.location = l.id AND l.component = c.id AND b.file = f.id"
383 % (files[file]["package"], suite, files[file]["architecture"]))
384 oldfiles = q.dictresult()
385 for oldfile in oldfiles:
386 files[file]["oldfiles"][suite] = oldfile
387 # Check versions [NB: per-suite only; no cross-suite checking done (yet)]
388 if apt_pkg.VersionCompare(files[file]["version"], oldfile["version"]) != 1:
389 if Cnf["Dinstall::Options::No-Version-Check"]:
390 reject_message = reject_message + "Overriden rejection"
392 reject_message = reject_message + "Rejected"
393 reject_message = reject_message + ": %s Old version `%s' >= new version `%s'.\n" % (file, oldfile["version"], files[file]["version"])
394 # Check for existing copies of the file
395 if not changes.has_key("stable upload"):
396 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"]))
397 if q.getresult() != []:
398 reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (file)
400 # Find any old .dsc files
401 elif files[file]["type"] == "dsc":
402 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"
403 % (files[file]["package"], suite))
404 oldfiles = q.dictresult()
405 if len(oldfiles) >= 1:
406 files[file]["oldfiles"][suite] = oldfiles[0]
408 # Validate the component
409 component = files[file]["component"];
410 component_id = db_access.get_component_id(component);
411 if component_id == -1:
412 reject_message = reject_message + "Rejected: file '%s' has unknown component '%s'.\n" % (file, component);
415 # Check the md5sum & size against existing files (if any)
416 location = Cnf["Dir::PoolDir"];
417 files[file]["location id"] = db_access.get_location_id (location, component, archive);
419 files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"]);
420 files_id = db_access.get_files_id(files[file]["pool name"] + file, files[file]["size"], files[file]["md5sum"], files[file]["location id"]);
422 reject_message = reject_message + "Rejected: INTERNAL ERROR, get_files_id() returned multiple matches for %s.\n" % (file)
424 reject_message = reject_message + "Rejected: md5sum and/or size mismatch on existing copy of %s.\n" % (file)
425 files[file]["files id"] = files_id
427 # Check for packages that have moved from one component to another
428 if files[file]["oldfiles"].has_key(suite) and files[file]["oldfiles"][suite]["name"] != files[file]["component"]:
429 files[file]["othercomponents"] = files[file]["oldfiles"][suite]["name"];
432 if string.find(reject_message, "Rejected:") != -1:
437 ###############################################################################
440 global dsc, dsc_files, reject_message, reprocess, orig_tar_id;
442 for file in files.keys():
443 if files[file]["type"] == "dsc":
445 dsc = utils.parse_changes(file)
446 except utils.cant_open_exc:
447 reject_message = reject_message + "Rejected: can't read changes file '%s'.\n" % (filename)
449 except utils.changes_parse_error_exc, line:
450 reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line)
453 dsc_files = utils.build_file_list(dsc, 1)
454 except utils.no_files_exc:
455 reject_message = reject_message + "Rejected: no Files: field in .dsc file.\n";
458 # Try and find all files mentioned in the .dsc. This has
459 # to work harder to cope with the multiple possible
460 # locations of an .orig.tar.gz.
461 for dsc_file in dsc_files.keys():
462 if files.has_key(dsc_file):
463 actual_md5 = files[dsc_file]["md5sum"]
464 found = "%s in incoming" % (dsc_file)
465 # Check the file does not already exist in the archive
466 if not changes.has_key("stable upload"):
467 q = projectB.query("SELECT f.id FROM files f, location l WHERE f.filename ~ '/%s' AND l.id = f.location" % (utils.regex_safe(dsc_file)));
468 if q.getresult() != []:
469 reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (dsc_file)
470 elif dsc_file[-12:] == ".orig.tar.gz":
472 q = projectB.query("SELECT l.path, f.filename, l.type, f.id FROM files f, location l WHERE f.filename ~ '/%s' AND l.id = f.location" % (utils.regex_safe(dsc_file)));
475 old_file = ql[0][0] + ql[0][1];
476 actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
478 suite_type = ql[0][2];
479 dsc_files[dsc_file]["files id"] = ql[0][3]; # need this for updating dsc_files in install()
481 if suite_type == "legacy" or suite_type == "legacy-mixed":
482 orig_tar_id = ql[0][3];
484 # Not there? Check in Incoming...
485 # [See comment above process_it() for explanation
486 # of why this is necessary...]
487 if os.access(dsc_file, os.R_OK) != 0:
488 files[dsc_file] = {};
489 files[dsc_file]["size"] = os.stat(dsc_file)[stat.ST_SIZE];
490 files[dsc_file]["md5sum"] = dsc_files[dsc_file]["md5sum"];
491 files[dsc_file]["section"] = files[file]["section"];
492 files[dsc_file]["priority"] = files[file]["priority"];
493 files[dsc_file]["component"] = files[file]["component"];
497 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);
500 reject_message = reject_message + "Rejected: %s refers to %s, but I can't find it in Incoming." % (file, dsc_file);
502 if actual_md5 != dsc_files[dsc_file]["md5sum"]:
503 reject_message = reject_message + "Rejected: md5sum for %s doesn't match %s.\n" % (found, file)
505 if string.find(reject_message, "Rejected:") != -1:
510 ###############################################################################
512 def check_md5sums ():
513 global reject_message;
515 for file in files.keys():
517 file_handle = utils.open_file(file,"r");
518 except utils.cant_open_exc:
521 if apt_pkg.md5sum(file_handle) != files[file]["md5sum"]:
522 reject_message = reject_message + "Rejected: md5sum check failed for %s.\n" % (file);
524 #####################################################################################################################
526 def action (changes_filename):
527 byhand = confirm = suites = summary = new = "";
529 # changes["distribution"] may not exist in corner cases
530 # (e.g. unreadable changes files)
531 if not changes.has_key("distribution"):
532 changes["distribution"] = {};
534 for suite in changes["distribution"].keys():
535 if Cnf.has_key("Suite::%s::Confirm"):
536 confirm = confirm + suite + ", "
537 suites = suites + suite + ", "
538 confirm = confirm[:-2]
541 for file in files.keys():
542 if files[file].has_key("byhand"):
544 summary = summary + file + " byhand\n"
545 elif files[file].has_key("new"):
547 summary = summary + "(new) %s %s %s\n" % (file, files[file]["priority"], files[file]["section"])
548 if files[file].has_key("othercomponents"):
549 summary = summary + "WARNING: Already present in %s distribution.\n" % (files[file]["othercomponents"])
550 if files[file]["type"] == "deb":
551 summary = summary + apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))["Description"] + '\n';
553 files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"])
554 destination = Cnf["Dir::PoolRoot"] + files[file]["pool name"] + file
555 summary = summary + file + "\n to " + destination + "\n"
557 short_summary = summary;
559 # This is for direport's benefit...
560 f = re_fdnic.sub("\n .\n", changes.get("changes",""));
562 if confirm or byhand or new:
563 summary = summary + "Changes: " + f;
565 summary = summary + announce (short_summary, 0)
567 (prompt, answer) = ("", "XXX")
568 if Cnf["Dinstall::Options::No-Action"] or Cnf["Dinstall::Options::Automatic"]:
571 if string.find(reject_message, "Rejected") != -1:
572 if time.time()-os.path.getmtime(changes_filename) < 86400:
573 print "SKIP (too new)\n" + reject_message,;
574 prompt = "[S]kip, Manual reject, Quit ?";
576 print "REJECT\n" + reject_message,;
577 prompt = "[R]eject, Manual reject, Skip, Quit ?";
578 if Cnf["Dinstall::Options::Automatic"]:
581 print "NEW to %s\n%s%s" % (suites, reject_message, summary),;
582 prompt = "[S]kip, New ack, Manual reject, Quit ?";
583 if Cnf["Dinstall::Options::Automatic"] and Cnf["Dinstall::Options::Ack-New"]:
586 print "BYHAND\n" + reject_message + summary,;
587 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
589 print "CONFIRM to %s\n%s%s" % (confirm, reject_message, summary),
590 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
592 print "INSTALL\n" + reject_message + summary,;
593 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
594 if Cnf["Dinstall::Options::Automatic"]:
597 while string.find(prompt, answer) == -1:
599 answer = utils.our_raw_input()
600 m = re_default_answer.match(prompt)
603 answer = string.upper(answer[:1])
606 reject (changes_filename, "");
608 manual_reject (changes_filename);
610 install (changes_filename, summary, short_summary);
612 acknowledge_new (changes_filename, summary);
616 #####################################################################################################################
618 def install (changes_filename, summary, short_summary):
619 global install_count, install_bytes
621 # Stable uploads are a special case
622 if changes.has_key("stable upload"):
623 stable_install (changes_filename, summary, short_summary);
628 archive = utils.where_am_i();
630 # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
631 projectB.query("BEGIN WORK");
633 # Add the .dsc file to the DB
634 for file in files.keys():
635 if files[file]["type"] == "dsc":
636 package = dsc["source"]
637 version = dsc["version"] # NB: not files[file]["version"], that has no epoch
638 maintainer = dsc["maintainer"]
639 maintainer = string.replace(maintainer, "'", "\\'")
640 maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
641 filename = files[file]["pool name"] + file;
642 dsc_location_id = files[file]["location id"];
643 if not files[file]["files id"]:
644 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], dsc_location_id)
645 projectB.query("INSERT INTO source (source, version, maintainer, file) VALUES ('%s', '%s', %d, %d)"
646 % (package, version, maintainer_id, files[file]["files id"]))
648 for suite in changes["distribution"].keys():
649 suite_id = db_access.get_suite_id(suite);
650 projectB.query("INSERT INTO src_associations (suite, source) VALUES (%d, currval('source_id_seq'))" % (suite_id))
652 # Add the source files to the DB (files and dsc_files)
653 projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files[file]["files id"]));
654 for dsc_file in dsc_files.keys():
655 filename = files[file]["pool name"] + dsc_file;
656 # If the .orig.tar.gz is already in the pool, it's
657 # files id is stored in dsc_files by check_dsc().
658 files_id = dsc_files[dsc_file].get("files id", None);
660 files_id = db_access.get_files_id(filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], dsc_location_id);
661 # FIXME: needs to check for -1/-2 and or handle exception
663 files_id = db_access.set_files_id (filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], dsc_location_id);
664 projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files_id));
666 # Add the .deb files to the DB
667 for file in files.keys():
668 if files[file]["type"] == "deb":
669 package = files[file]["package"]
670 version = files[file]["version"]
671 maintainer = files[file]["maintainer"]
672 maintainer = string.replace(maintainer, "'", "\\'")
673 maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
674 architecture = files[file]["architecture"]
675 architecture_id = db_access.get_architecture_id (architecture);
676 type = files[file]["dbtype"];
677 component = files[file]["component"]
678 source = files[file]["source"]
680 if string.find(source, "(") != -1:
681 m = utils.re_extract_src_version.match(source)
683 source_version = m.group(2)
684 if not source_version:
685 source_version = version
686 filename = files[file]["pool name"] + file;
687 if not files[file]["files id"]:
688 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], files[file]["location id"])
689 source_id = db_access.get_source_id (source, source_version);
691 projectB.query("INSERT INTO binaries (package, version, maintainer, source, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, %d, '%s')"
692 % (package, version, maintainer_id, source_id, architecture_id, files[file]["files id"], type));
694 projectB.query("INSERT INTO binaries (package, version, maintainer, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, '%s')"
695 % (package, version, maintainer_id, architecture_id, files[file]["files id"], type));
696 for suite in changes["distribution"].keys():
697 suite_id = db_access.get_suite_id(suite);
698 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES (%d, currval('binaries_id_seq'))" % (suite_id));
700 # If the .orig.tar.gz is in a legacy directory we need to poolify
701 # it, so that apt-get source (and anything else that goes by the
702 # "Directory:" field in the Sources.gz file) works.
703 if orig_tar_id != None:
704 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));
707 # First move the files to the new location
708 legacy_filename = qid["path"]+qid["filename"];
709 pool_location = utils.poolify (changes["source"], files[file]["component"]);
710 pool_filename = pool_location + os.path.basename(qid["filename"]);
711 destination = Cnf["Dir::PoolDir"] + pool_location
712 utils.move(legacy_filename, destination);
713 # Then Update the DB's files table
714 q = projectB.query("UPDATE files SET filename = '%s', location = '%s' WHERE id = '%s'" % (pool_filename, dsc_location_id, qid["files_id"]));
716 # Install the files into the pool
717 for file in files.keys():
718 if files[file].has_key("byhand"):
720 destination = Cnf["Dir::PoolDir"] + files[file]["pool name"] + file
721 destdir = os.path.dirname(destination)
722 utils.move (file, destination)
723 install_bytes = install_bytes + float(files[file]["size"])
725 # Copy the .changes file across for suite which need it.
726 for suite in changes["distribution"].keys():
727 if Cnf.has_key("Suite::%s::CopyChanges" % (suite)):
728 utils.copy (changes_filename, Cnf["Dir::RootDir"] + Cnf["Suite::%s::CopyChanges" % (suite)]);
730 projectB.query("COMMIT WORK");
732 utils.move (changes_filename, Cnf["Dir::IncomingDir"] + 'DONE/' + os.path.basename(changes_filename))
734 install_count = install_count + 1;
736 if not Cnf["Dinstall::Options::No-Mail"]:
737 mail_message = """Return-Path: %s
740 Bcc: troup@auric.debian.org
741 Subject: %s INSTALLED
747 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], os.path.basename(changes_filename), reject_message, summary, installed_footer)
748 utils.send_mail (mail_message, "")
749 announce (short_summary, 1)
751 #####################################################################################################################
753 def stable_install (changes_filename, summary, short_summary):
754 global install_count, install_bytes
756 print "Installing to stable."
758 archive = utils.where_am_i();
760 # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
761 projectB.query("BEGIN WORK");
763 # Add the .dsc file to the DB
764 for file in files.keys():
765 if files[file]["type"] == "dsc":
766 package = dsc["source"]
767 version = dsc["version"] # NB: not files[file]["version"], that has no epoch
768 q = projectB.query("SELECT id FROM source WHERE source = '%s' AND version = '%s'" % (package, version))
771 sys.stderr.write("INTERNAL ERROR: couldn't find '%s' (%s) in source table.\n" % (package, version));
773 source_id = ql[0][0];
774 suite_id = db_access.get_suite_id('proposed-updates');
775 projectB.query("DELETE FROM src_associations WHERE suite = '%s' AND source = '%s'" % (suite_id, source_id));
776 suite_id = db_access.get_suite_id('stable');
777 projectB.query("INSERT INTO src_associations (suite, source) VALUES ('%s', '%s')" % (suite_id, source_id));
779 # Add the .deb files to the DB
780 for file in files.keys():
781 if files[file]["type"] == "deb":
782 package = files[file]["package"]
783 version = files[file]["version"]
784 architecture = files[file]["architecture"]
785 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))
788 sys.stderr.write("INTERNAL ERROR: couldn't find '%s' (%s for %s architecture) in binaries table.\n" % (package, version, architecture));
790 binary_id = ql[0][0];
791 suite_id = db_access.get_suite_id('proposed-updates');
792 projectB.query("DELETE FROM bin_associations WHERE suite = '%s' AND bin = '%s'" % (suite_id, binary_id));
793 suite_id = db_access.get_suite_id('stable');
794 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES ('%s', '%s')" % (suite_id, binary_id));
796 projectB.query("COMMIT WORK");
798 utils.move (changes_filename, Cnf["Rhona::Morgue"] + os.path.basename(changes_filename));
800 # Update the Stable ChangeLog file
802 new_changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + ".ChangeLog";
803 changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + "ChangeLog";
804 if os.path.exists(new_changelog_filename):
805 os.unlink (new_changelog_filename);
807 new_changelog = utils.open_file(new_changelog_filename, 'w');
808 for file in files.keys():
809 if files[file]["type"] == "deb":
810 new_changelog.write("stable/%s/binary-%s/%s\n" % (files[file]["component"], files[file]["architecture"], file));
811 elif re_issource.match(file) != None:
812 new_changelog.write("stable/%s/source/%s\n" % (files[file]["component"], file));
814 new_changelog.write("%s\n" % (file));
815 chop_changes = re_fdnic.sub("\n", changes["changes"]);
816 new_changelog.write(chop_changes + '\n\n');
817 if os.access(changelog_filename, os.R_OK) != 0:
818 changelog = utils.open_file(changelog_filename, 'r');
819 new_changelog.write(changelog.read());
820 new_changelog.close();
821 if os.access(changelog_filename, os.R_OK) != 0:
822 os.unlink(changelog_filename);
823 utils.move(new_changelog_filename, changelog_filename);
825 install_count = install_count + 1;
827 if not Cnf["Dinstall::Options::No-Mail"]:
828 mail_message = """Return-Path: %s
831 Bcc: troup@auric.debian.org
832 Subject: %s INSTALLED into stable
838 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], os.path.basename(changes_filename), reject_message, summary, installed_footer)
839 utils.send_mail (mail_message, "")
840 announce (short_summary, 1)
842 #####################################################################################################################
844 def reject (changes_filename, manual_reject_mail_filename):
847 base_changes_filename = os.path.basename(changes_filename);
848 reason_filename = re_changes.sub("reason", base_changes_filename);
849 reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename);
851 # Move the .changes files and it's contents into REJECT/ (if we can; errors are ignored)
853 utils.move (changes_filename, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], base_changes_filename));
854 except utils.cant_overwrite_exc:
855 sys.stderr.write("W: couldn't overwrite existing file '%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], base_changes_filename));
857 for file in files.keys():
858 if os.path.exists(file):
860 utils.move (file, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], file));
861 except utils.cant_overwrite_exc:
862 sys.stderr.write("W: couldn't overwrite existing file '%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], file));
865 # If this is not a manual rejection generate the .reason file and rejection mail message
866 if manual_reject_mail_filename == "":
867 if os.path.exists(reject_filename):
868 os.unlink(reject_filename);
869 fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
870 os.write(fd, reject_message);
872 reject_mail_message = """From: %s
874 Bcc: troup@auric.debian.org
879 %s""" % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], os.path.basename(changes_filename), reject_message, reject_footer);
880 else: # Have a manual rejection file to use
881 reject_mail_message = ""; # avoid <undef>'s
883 # Send the rejection mail if appropriate
884 if not Cnf["Dinstall::Options::No-Mail"]:
885 utils.send_mail (reject_mail_message, manual_reject_mail_filename);
887 ##################################################################
889 def manual_reject (changes_filename):
890 # Build up the rejection email
891 user_email_address = string.replace(string.split(pwd.getpwuid(os.getuid())[4],',')[0], '.', '')
892 user_email_address = user_email_address + " <%s@%s>" % (pwd.getpwuid(os.getuid())[0], Cnf["Dinstall::MyHost"])
893 manual_reject_message = Cnf.get("Dinstall::Options::Manual-Reject", "")
895 reject_mail_message = """From: %s
898 Bcc: troup@auric.debian.org
904 %s""" % (user_email_address, Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], os.path.basename(changes_filename), manual_reject_message, reject_message, reject_footer)
906 # Write the rejection email out as the <foo>.reason file
907 reason_filename = re_changes.sub("reason", os.path.basename(changes_filename));
908 reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename)
909 if os.path.exists(reject_filename):
910 os.unlink(reject_filename);
911 fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
912 os.write(fd, reject_mail_message);
915 # If we weren't given one, spawn an editor so the user can add one in
916 if manual_reject_message == "":
917 result = os.system("vi +6 %s" % (reject_file))
919 sys.stderr.write ("vi invocation failed for `%s'!" % (reject_file))
922 # Then process it as if it were an automatic rejection
923 reject (changes_filename, reject_filename)
925 #####################################################################################################################
927 def acknowledge_new (changes_filename, summary):
930 changes_filename = os.path.basename(changes_filename);
932 new_ack_new[changes_filename] = 1;
934 if new_ack_old.has_key(changes_filename):
935 print "Ack already sent.";
938 print "Sending new ack.";
939 if not Cnf["Dinstall::Options::No-Mail"]:
940 new_ack_message = """Return-Path: %s
943 Bcc: troup@auric.debian.org
947 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, summary, new_ack_footer);
948 utils.send_mail(new_ack_message,"");
950 #####################################################################################################################
952 def announce (short_summary, action):
953 # Only do announcements for source uploads with a recent dpkg-dev installed
954 if float(changes.get("format", 0)) < 1.6 or not changes["architecture"].has_key("source"):
960 for dist in changes["distribution"].keys():
961 list = Cnf.Find("Suite::%s::Announce" % (dist))
962 if list == None or lists_done.has_key(list):
965 summary = summary + "Announcing to %s\n" % (list)
968 mail_message = """Return-Path: %s
971 Bcc: troup@auric.debian.org
972 Subject: Installed %s %s (%s)
978 """ % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], list, changes["source"], changes["version"], string.join(changes["architecture"].keys(), ' ' ),
979 changes["filecontents"], short_summary)
980 utils.send_mail (mail_message, "")
982 (dsc_rfc822, dsc_name, dsc_email) = utils.fix_maintainer (dsc.get("maintainer",Cnf["Dinstall::MyEmailAddress"]));
983 bugs = changes["closes"].keys()
985 if dsc_name == changes["maintainername"]:
986 summary = summary + "Closing bugs: "
988 summary = summary + "%s " % (bug)
990 mail_message = """Return-Path: %s
992 To: %s-close@bugs.debian.org
993 Bcc: troup@auric.debian.org
994 Subject: Bug#%s: fixed in %s %s
996 We believe that the bug you reported is fixed in the latest version of
997 %s, which has been installed in the Debian FTP archive:
999 %s""" % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], bug, bug, changes["source"], changes["version"], changes["source"], short_summary)
1001 if changes["distribution"].has_key("stable"):
1002 mail_message = mail_message + """Note that this package is not part of the released stable Debian
1003 distribution. It may have dependencies on other unreleased software,
1004 or other instabilities. Please take care if you wish to install it.
1005 The update will eventually make its way into the next released Debian
1008 mail_message = mail_message + """A summary of the changes between this version and the previous one is
1011 Thank you for reporting the bug, which will now be closed. If you
1012 have further comments please address them to %s@bugs.debian.org,
1013 and the maintainer will reopen the bug report if appropriate.
1015 Debian distribution maintenance software
1017 %s (supplier of updated %s package)
1019 (This message was generated automatically at their request; if you
1020 believe that there is a problem with it please contact the archive
1021 administrators by mailing ftpmaster@debian.org)
1024 %s""" % (bug, changes["maintainer"], changes["source"], changes["filecontents"])
1026 utils.send_mail (mail_message, "")
1028 summary = summary + "Setting bugs to severity fixed: "
1029 control_message = ""
1031 summary = summary + "%s " % (bug)
1032 control_message = control_message + "severity %s fixed\n" % (bug)
1033 if action and control_message != "":
1034 mail_message = """Return-Path: %s
1036 To: control@bugs.debian.org
1037 Bcc: troup@auric.debian.org, %s
1038 Subject: Fixed in NMU of %s %s
1043 This message was generated automatically in response to a
1044 non-maintainer upload. The .changes file follows.
1047 """ % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes["maintainer822"], changes["source"], changes["version"], control_message, changes["filecontents"])
1048 utils.send_mail (mail_message, "")
1049 summary = summary + "\n"
1053 ###############################################################################
1055 # reprocess is necessary for the case of foo_1.2-1 and foo_1.2-2 in
1056 # Incoming. -1 will reference the .orig.tar.gz, but -2 will not.
1057 # dsccheckdistrib() can find the .orig.tar.gz but it will not have
1058 # processed it during it's checks of -2. If -1 has been deleted or
1059 # otherwise not checked by da-install, the .orig.tar.gz will not have
1060 # been checked at all. To get round this, we force the .orig.tar.gz
1061 # into the .changes structure and reprocess the .changes file.
1063 def process_it (changes_file):
1064 global reprocess, orig_tar_id, changes, dsc, dsc_files, files;
1068 # Reset some globals
1075 # Absolutize the filename to avoid the requirement of being in the
1076 # same directory as the .changes file.
1077 changes_file = os.path.abspath(changes_file);
1079 # And since handling of installs to stable munges with the CWD;
1080 # save and restore it.
1083 check_signature (changes_file);
1084 check_changes (changes_file);
1091 action(changes_file);
1096 ###############################################################################
1099 global Cnf, projectB, reject_message, install_bytes, new_ack_old
1103 Cnf = apt_pkg.newConfiguration();
1104 apt_pkg.ReadConfigFileISC(Cnf,utils.which_conf_file());
1106 Arguments = [('a',"automatic","Dinstall::Options::Automatic"),
1107 ('d',"debug","Dinstall::Options::Debug", "IntVal"),
1108 ('h',"help","Dinstall::Options::Help"),
1109 ('k',"ack-new","Dinstall::Options::Ack-New"),
1110 ('m',"manual-reject","Dinstall::Options::Manual-Reject", "HasArg"),
1111 ('n',"no-action","Dinstall::Options::No-Action"),
1112 ('p',"no-lock", "Dinstall::Options::No-Lock"),
1113 ('r',"no-version-check", "Dinstall::Options::No-Version-Check"),
1114 ('s',"no-mail", "Dinstall::Options::No-Mail"),
1115 ('u',"override-distribution", "Dinstall::Options::Override-Distribution", "HasArg"),
1116 ('v',"version","Dinstall::Options::Version")];
1118 changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
1120 if Cnf["Dinstall::Options::Help"]:
1123 if Cnf["Dinstall::Options::Version"]:
1124 print "katie version 0.0000000000";
1127 postgresql_user = None; # Default == Connect as user running program.
1129 # -n/--dry-run invalidates some other options which would involve things happening
1130 if Cnf["Dinstall::Options::No-Action"]:
1131 Cnf["Dinstall::Options::Automatic"] = ""
1132 Cnf["Dinstall::Options::Ack-New"] = ""
1133 postgresql_user = Cnf["DB::ROUser"];
1135 projectB = pg.connect('projectb', Cnf["DB::Host"], int(Cnf["DB::Port"]), None, None, postgresql_user);
1137 db_access.init(Cnf, projectB);
1139 # Check that we aren't going to clash with the daily cron job
1141 if os.path.exists("%s/Archive_Maintenance_In_Progress" % (Cnf["Dir::RootDir"])) and not Cnf["Dinstall::Options::No-Lock"]:
1142 sys.stderr.write("Archive maintenance in progress. Try again later.\n");
1145 # Obtain lock if not in no-action mode
1147 if not Cnf["Dinstall::Options::No-Action"]:
1148 lock_fd = os.open(Cnf["Dinstall::LockFile"], os.O_RDWR);
1149 fcntl.lockf(lock_fd, FCNTL.F_TLOCK);
1151 # Read in the list of already-acknowledged NEW packages
1152 new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'r');
1154 for line in new_ack_list.readlines():
1155 new_ack_old[line[:-1]] = 1;
1156 new_ack_list.close();
1158 # Process the changes files
1159 for changes_file in changes_files:
1161 print "\n" + changes_file;
1162 process_it (changes_file);
1166 if install_count > 1:
1168 sys.stderr.write("Installed %d package %s, %s.\n" % (install_count, sets, utils.size_type(int(install_bytes))));
1170 # Write out the list of already-acknowledged NEW packages
1171 if Cnf["Dinstall::Options::Ack-New"]:
1172 new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'w')
1173 for i in new_ack_new.keys():
1174 new_ack_list.write(i+'\n')
1175 new_ack_list.close()
1178 if __name__ == '__main__':