3 # Installs Debian packaes
4 # Copyright (C) 2000 James Troup <james@nocrew.org>
5 # $Id: katie,v 1.10 2000-12-13 03:18:50 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):
122 file = utils.open_file(filename, 'r');
123 for line in file.readlines():
124 line = string.strip(utils.re_comments.sub('', line))
125 override_package = re_override_package.sub(r'\1', line)
126 if override_package != "":
127 overrides[suite][component][override_package] = 1
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 # Avoid <undef> on unknown distributions
137 if db_access.get_suite_id(suite) == -1:
140 # FIXME: nasty non-US speficic hack
141 if string.lower(component[:7]) == "non-us/":
142 component = component[7:];
143 if not overrides.has_key(suite) or not overrides[suite].has_key(component):
144 if not overrides.has_key(suite):
145 overrides[suite] = {}
146 if not overrides[suite].has_key(component):
147 overrides[suite][component] = {}
148 if Cnf.has_key("Suite::%s::SingleOverrideFile" % (suite)): # legacy mixed suite (i.e. experimental)
149 override_filename = Cnf["Dir::OverrideDir"] + 'override.' + Cnf["Suite::%s::OverrideCodeName" % (suite)];
150 read_override_file (override_filename, suite, component);
152 if binary_type == "udeb":
153 override_filename = Cnf["Dir::OverrideDir"] + 'override.' + Cnf["Suite::%s::OverrideCodeName" % (suite)] + '.debian-installer.' + component;
154 read_override_file (override_filename, suite, component);
156 for src in ("", ".src"):
157 override_filename = Cnf["Dir::OverrideDir"] + 'override.' + Cnf["Suite::%s::OverrideCodeName" % (suite)] + '.' + component + src;
158 read_override_file (override_filename, suite, component);
160 return overrides[suite][component].get(package, None);
162 #####################################################################################################################
164 def check_changes(filename):
165 global reject_message, changes, files
167 # Parse the .changes field into a dictionary [FIXME - need to trap errors, pass on to reject_message etc.]
169 changes = utils.parse_changes(filename)
170 except utils.cant_open_exc:
171 reject_message = "Rejected: can't read changes file '%s'.\n" % (filename)
173 except utils.changes_parse_error_exc, line:
174 reject_message = "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line)
175 changes["maintainer822"] = Cnf["Dinstall::MyEmailAddress"];
178 # Parse the Files field from the .changes into another dictionary [FIXME need to trap errors as above]
179 files = utils.build_file_list(changes, "")
181 # Check for mandatory fields
182 for i in ("source", "binary", "architecture", "version", "distribution","maintainer", "files"):
183 if not changes.has_key(i):
184 reject_message = "Rejected: Missing field `%s' in changes file." % (i)
185 return 0 # Avoid <undef> errors during later tests
187 # Fix the Maintainer: field to be RFC822 compatible
188 (changes["maintainer822"], changes["maintainername"], changes["maintaineremail"]) = utils.fix_maintainer (changes["maintainer"])
190 # Override the Distribution: field if appropriate
191 if Cnf["Dinstall::Options::Override-Distribution"] != "":
192 reject_message = reject_message + "Warning: Distribution was overriden from %s to %s.\n" % (changes["distribution"], Cnf["Dinstall::Options::Override-Distribution"])
193 changes["distribution"] = Cnf["Dinstall::Options::Override-Distribution"]
195 # Split multi-value fields into a lower-level dictionary
196 for i in ("architecture", "distribution", "binary", "closes"):
197 o = changes.get(i, "")
201 for j in string.split(o):
204 # Ensure all the values in Closes: are numbers
205 if changes.has_key("closes"):
206 for i in changes["closes"].keys():
207 if re_isanum.match (i) == None:
208 reject_message = reject_message + "Rejected: `%s' from Closes field isn't a number.\n" % (i)
210 # Map frozen to unstable if frozen doesn't exist
211 if changes["distribution"].has_key("frozen") and not Cnf.has_key("Suite::Frozen"):
212 del changes["distribution"]["frozen"]
213 reject_message = reject_message + "Mapping frozen to unstable.\n"
215 # Ensure target distributions exist
216 for i in changes["distribution"].keys():
217 if not Cnf.has_key("Suite::%s" % (i)):
218 reject_message = reject_message + "Rejected: Unknown distribution `%s'.\n" % (i)
220 # Map unreleased arches from stable to unstable
221 if changes["distribution"].has_key("stable"):
222 for i in changes["architecture"].keys():
223 if not Cnf.has_key("Suite::Stable::Architectures::%s" % (i)):
224 reject_message = reject_message + "Mapping stable to unstable for unreleased arch `%s'.\n" % (i)
225 del changes["distribution"]["stable"]
227 # Map arches not being released from frozen to unstable
228 if changes["distribution"].has_key("frozen"):
229 for i in changes["architecture"].keys():
230 if not Cnf.has_key("Suite::Frozen::Architectures::%s" % (i)):
231 reject_message = reject_message + "Mapping frozen to unstable for non-releasing arch `%s'.\n" % (i)
232 del changes["distribution"]["frozen"]
234 # Handle uploads to stable
235 if changes["distribution"].has_key("stable"):
236 # If running from within proposed-updates; assume an install to stable
237 if string.find(os.getcwd(), 'proposed-updates') != -1:
238 # FIXME: should probably remove anything that != stable
239 for i in ("frozen", "unstable"):
240 if changes["distribution"].has_key(i):
241 reject_message = reject_message + "Removing %s from distribution list.\n"
242 del changes["distribution"][i]
243 changes["stable upload"] = 1;
244 # If we can't find a file from the .changes; assume it's a package already in the pool and move into the pool
245 file = files.keys()[0];
246 if os.access(file, os.R_OK) == 0:
247 pool_dir = Cnf["Dir::PoolDir"] + '/' + utils.poolify(changes["source"], files[file]["component"]);
249 # Otherwise (normal case) map stable to updates
251 reject_message = reject_message + "Mapping stable to updates.\n";
252 del changes["distribution"]["stable"];
253 changes["distribution"]["proposed-updates"] = 1;
255 # chopversion = no epoch; chopversion2 = no epoch and no revision (e.g. for .orig.tar.gz comparison)
256 changes["chopversion"] = utils.re_no_epoch.sub('', changes["version"])
257 changes["chopversion2"] = utils.re_no_revision.sub('', changes["chopversion"])
259 if string.find(reject_message, "Rejected:") != -1:
265 global reject_message
267 archive = utils.where_am_i();
269 for file in files.keys():
270 # Check the file is readable
271 if os.access(file,os.R_OK) == 0:
272 reject_message = reject_message + "Rejected: Can't read `%s'.\n" % (file)
273 files[file]["type"] = "unreadable";
275 # If it's byhand skip remaining checks
276 if files[file]["section"] == "byhand":
277 files[file]["byhand"] = 1;
278 files[file]["type"] = "byhand";
279 # Checks for a binary package...
280 elif re_isadeb.match(file) != None:
281 # Extract package information using dpkg-deb
282 control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))
284 # Check for mandatory fields
285 if control.Find("Package") == None:
286 reject_message = reject_message + "Rejected: %s: No package field in control.\n" % (file)
287 if control.Find("Architecture") == None:
288 reject_message = reject_message + "Rejected: %s: No architecture field in control.\n" % (file)
289 if control.Find("Version") == None:
290 reject_message = reject_message + "Rejected: %s: No version field in control.\n" % (file)
292 # Ensure the package name matches the one give in the .changes
293 if not changes["binary"].has_key(control.Find("Package", "")):
294 reject_message = reject_message + "Rejected: %s: control file lists name as `%s', which isn't in changes file.\n" % (file, control.Find("Package", ""))
296 # Validate the architecture
297 if not Cnf.has_key("Suite::Unstable::Architectures::%s" % (control.Find("Architecture", ""))):
298 reject_message = reject_message + "Rejected: Unknown architecture '%s'.\n" % (control.Find("Architecture", ""))
300 # Check the architecture matches the one given in the .changes
301 if not changes["architecture"].has_key(control.Find("Architecture", "")):
302 reject_message = reject_message + "Rejected: %s: control file lists arch as `%s', which isn't in changes file.\n" % (file, control.Find("Architecture", ""))
303 # Check the section & priority match those given in the .changes (non-fatal)
304 if control.Find("Section") != None and files[file]["section"] != "" and files[file]["section"] != control.Find("Section"):
305 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"])
306 if control.Find("Priority") != None and files[file]["priority"] != "" and files[file]["priority"] != control.Find("Priority"):
307 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"])
309 epochless_version = utils.re_no_epoch.sub('', control.Find("Version", ""))
311 files[file]["package"] = control.Find("Package");
312 files[file]["architecture"] = control.Find("Architecture");
313 files[file]["version"] = control.Find("Version");
314 files[file]["maintainer"] = control.Find("Maintainer", "");
315 if file[-5:] == ".udeb":
316 files[file]["dbtype"] = "udeb";
317 elif file[-4:] == ".deb":
318 files[file]["dbtype"] = "deb";
320 reject_message = reject_message + "Rejected: %s is neither a .deb or a .udeb.\n " % (file);
321 files[file]["type"] = "deb";
322 files[file]["fullname"] = "%s_%s_%s.deb" % (control.Find("Package", ""), epochless_version, control.Find("Architecture", ""))
323 files[file]["source"] = control.Find("Source", "");
324 if files[file]["source"] == "":
325 files[file]["source"] = files[file]["package"];
326 # Checks for a source package...
328 m = re_issource.match(file)
330 files[file]["package"] = m.group(1)
331 files[file]["version"] = m.group(2)
332 files[file]["type"] = m.group(3)
334 # Ensure the source package name matches the Source filed in the .changes
335 if changes["source"] != files[file]["package"]:
336 reject_message = reject_message + "Rejected: %s: changes file doesn't say %s for Source\n" % (file, files[file]["package"])
338 # Ensure the source version matches the version in the .changes file
339 if files[file]["type"] == "orig.tar.gz":
340 changes_version = changes["chopversion2"]
342 changes_version = changes["chopversion"]
343 if changes_version != files[file]["version"]:
344 reject_message = reject_message + "Rejected: %s: should be %s according to changes file.\n" % (file, changes_version)
346 # Ensure the .changes lists source in the Architecture field
347 if not changes["architecture"].has_key("source"):
348 reject_message = reject_message + "Rejected: %s: changes file doesn't list `source' in Architecture field.\n" % (file)
350 # Check the signature of a .dsc file
351 if files[file]["type"] == "dsc":
352 check_signature(file)
354 files[file]["fullname"] = file
356 # Not a binary or source package? Assume byhand...
358 files[file]["byhand"] = 1;
359 files[file]["type"] = "byhand";
361 files[file]["oldfiles"] = {}
362 for suite in changes["distribution"].keys():
364 if files[file].has_key("byhand"):
367 if Cnf.has_key("Suite:%s::Components" % (suite)) and not Cnf.has_key("Suite::%s::Components::%s" % (suite, files[file]["component"])):
368 reject_message = reject_message + "Rejected: unknown component `%s' for suite `%s'.\n" % (files[file]["component"], suite)
371 # See if the package is NEW
372 if not in_override_p(files[file]["package"], files[file]["component"], suite, files[file].get("dbtype","")):
373 files[file]["new"] = 1
375 # Find any old binary packages
376 if files[file]["type"] == "deb":
377 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"
378 % (files[file]["package"], suite, files[file]["architecture"]))
379 oldfiles = q.dictresult()
380 for oldfile in oldfiles:
381 files[file]["oldfiles"][suite] = oldfile
382 # Check versions [NB: per-suite only; no cross-suite checking done (yet)]
383 if apt_pkg.VersionCompare(files[file]["version"], oldfile["version"]) != 1:
384 if Cnf["Dinstall::Options::No-Version-Check"]:
385 reject_message = reject_message + "Overriden rejection"
387 reject_message = reject_message + "Rejected"
388 reject_message = reject_message + ": %s Old version `%s' >= new version `%s'.\n" % (file, oldfile["version"], files[file]["version"])
389 # Check for existing copies of the file
390 if not changes.has_key("stable upload"):
391 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"]))
392 if q.getresult() != []:
393 reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (file)
395 # Find any old .dsc files
396 elif files[file]["type"] == "dsc":
397 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"
398 % (files[file]["package"], suite))
399 oldfiles = q.dictresult()
400 if len(oldfiles) >= 1:
401 files[file]["oldfiles"][suite] = oldfiles[0]
403 # Validate the component
404 component = files[file]["component"];
405 component_id = db_access.get_component_id(component);
406 if component_id == -1:
407 reject_message = reject_message + "Rejected: file '%s' has unknown component '%s'.\n" % (file, component);
410 # Check the md5sum & size against existing files (if any)
411 location = Cnf["Dir::PoolDir"];
412 files[file]["location id"] = db_access.get_location_id (location, component, archive);
413 files_id = db_access.get_files_id(component + '/' + file, files[file]["size"], files[file]["md5sum"], files[file]["location id"]);
415 reject_message = reject_message + "Rejected: INTERNAL ERROR, get_files_id() returned multiple matches for %s.\n" % (file)
417 reject_message = reject_message + "Rejected: md5sum and/or size mismatch on existing copy of %s.\n" % (file)
418 files[file]["files id"] = files_id
420 # Check for packages that have moved from one component to another
421 if files[file]["oldfiles"].has_key(suite) and files[file]["oldfiles"][suite]["name"] != files[file]["component"]:
422 files[file]["othercomponents"] = files[file]["oldfiles"][suite]["name"];
425 if string.find(reject_message, "Rejected:") != -1:
430 ###############################################################################
433 global dsc, dsc_files, reject_message, reprocess, orig_tar_id;
435 for file in files.keys():
436 if files[file]["type"] == "dsc":
438 dsc = utils.parse_changes(file)
439 except utils.cant_open_exc:
440 reject_message = reject_message + "Rejected: can't read changes file '%s'.\n" % (filename)
442 except utils.changes_parse_error_exc, line:
443 reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line)
446 dsc_files = utils.build_file_list(dsc, 1)
447 except utils.no_files_exc:
448 reject_message = reject_message + "Rejected: no Files: field in .dsc file.\n";
451 # Try and find all files mentioned in the .dsc. This has
452 # to work harder to cope with the multiple possible
453 # locations of an .orig.tar.gz.
454 for dsc_file in dsc_files.keys():
455 if files.has_key(dsc_file):
456 actual_md5 = files[dsc_file]["md5sum"]
457 found = "%s in incoming" % (dsc_file)
458 # Check the file does not already exist in the archive
459 if not changes.has_key("stable upload"):
460 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)));
461 if q.getresult() != []:
462 reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (dsc_file)
463 elif dsc_file[-12:] == ".orig.tar.gz":
465 q = projectB.query("SELECT l.path, f.filename, l.type, f.id, l.id FROM files f, location l WHERE f.filename ~ '%s' AND l.id = f.location" % (utils.regex_safe(dsc_file)));
468 old_file = ql[0][0] + ql[0][1];
469 actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
471 suite_type = ql[0][2];
472 dsc_files[dsc_file]["location id"] = ql[0][4]; # need this for updating dsc_files in install()
474 if suite_type == "legacy" or suite_type == "legacy-mixed":
475 orig_tar_id = ql[0][3];
477 # Not there? Check in Incoming...
478 # [See comment above process_it() for explanation
479 # of why this is necessary...]
480 if os.access(dsc_file, os.R_OK) != 0:
481 files[dsc_file] = {};
482 files[dsc_file]["size"] = os.stat(dsc_file)[stat.ST_SIZE];
483 files[dsc_file]["md5sum"] = dsc_files[dsc_file]["md5sum"];
484 files[dsc_file]["section"] = files[file]["section"];
485 files[dsc_file]["priority"] = files[file]["priority"];
486 files[dsc_file]["component"] = files[file]["component"];
490 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);
493 reject_message = reject_message + "Rejected: %s refers to %s, but I can't find it in Incoming." % (file, dsc_file);
495 if actual_md5 != dsc_files[dsc_file]["md5sum"]:
496 reject_message = reject_message + "Rejected: md5sum for %s doesn't match %s.\n" % (found, file)
498 if string.find(reject_message, "Rejected:") != -1:
503 ###############################################################################
505 def check_md5sums ():
506 global reject_message;
508 for file in files.keys():
510 file_handle = utils.open_file(file,"r");
511 except utils.cant_open_exc:
514 if apt_pkg.md5sum(file_handle) != files[file]["md5sum"]:
515 reject_message = reject_message + "Rejected: md5sum check failed for %s.\n" % (file);
517 #####################################################################################################################
519 def action (changes_filename):
520 byhand = confirm = suites = summary = new = "";
522 # changes["distribution"] may not exist in corner cases
523 # (e.g. unreadable changes files)
524 if not changes.has_key("distribution"):
525 changes["distribution"] = {};
527 for suite in changes["distribution"].keys():
528 if Cnf.has_key("Suite::%s::Confirm"):
529 confirm = confirm + suite + ", "
530 suites = suites + suite + ", "
531 confirm = confirm[:-2]
534 for file in files.keys():
535 if files[file].has_key("byhand"):
537 summary = summary + file + " byhand\n"
538 elif files[file].has_key("new"):
540 summary = summary + "(new) %s %s %s\n" % (file, files[file]["priority"], files[file]["section"])
541 if files[file].has_key("othercomponents"):
542 summary = summary + "WARNING: Already present in %s distribution.\n" % (files[file]["othercomponents"])
543 if files[file]["type"] == "deb":
544 summary = summary + apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))["Description"] + '\n';
546 files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"])
547 destination = Cnf["Dir::PoolRoot"] + files[file]["pool name"] + file
548 summary = summary + file + "\n to " + destination + "\n"
550 short_summary = summary;
552 # This is for direport's benefit...
553 f = re_fdnic.sub("\n .\n", changes.get("changes",""));
555 if confirm or byhand or new:
556 summary = summary + "Changes: " + f;
558 summary = summary + announce (short_summary, 0)
560 (prompt, answer) = ("", "XXX")
561 if Cnf["Dinstall::Options::No-Action"] or Cnf["Dinstall::Options::Automatic"]:
564 if string.find(reject_message, "Rejected") != -1:
565 if time.time()-os.path.getmtime(changes_filename) < 86400:
566 print "SKIP (too new)\n" + reject_message,;
567 prompt = "[S]kip, Manual reject, Quit ?";
569 print "REJECT\n" + reject_message,;
570 prompt = "[R]eject, Manual reject, Skip, Quit ?";
571 if Cnf["Dinstall::Options::Automatic"]:
574 print "NEW to %s\n%s%s" % (suites, reject_message, summary),;
575 prompt = "[S]kip, New ack, Manual reject, Quit ?";
576 if Cnf["Dinstall::Options::Automatic"] and Cnf["Dinstall::Options::Ack-New"]:
579 print "BYHAND\n" + reject_message + summary,;
580 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
582 print "CONFIRM to %s\n%s%s" % (confirm, reject_message, summary),
583 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
585 print "INSTALL\n" + reject_message + summary,;
586 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
587 if Cnf["Dinstall::Options::Automatic"]:
590 while string.find(prompt, answer) == -1:
592 answer = utils.our_raw_input()
593 m = re_default_answer.match(prompt)
596 answer = string.upper(answer[:1])
599 reject (changes_filename, "");
601 manual_reject (changes_filename);
603 install (changes_filename, summary, short_summary);
605 acknowledge_new (changes_filename, summary);
609 #####################################################################################################################
611 def install (changes_filename, summary, short_summary):
612 global install_count, install_bytes
614 # Stable uploads are a special case
615 if changes.has_key("stable upload"):
616 stable_install (changes_filename, summary, short_summary);
621 archive = utils.where_am_i();
623 # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
624 projectB.query("BEGIN WORK");
626 # Add the .dsc file to the DB
627 for file in files.keys():
628 if files[file]["type"] == "dsc":
629 package = dsc["source"]
630 version = dsc["version"] # NB: not files[file]["version"], that has no epoch
631 maintainer = dsc["maintainer"]
632 maintainer = string.replace(maintainer, "'", "\\'")
633 maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
634 filename = files[file]["pool name"] + file;
635 dsc_location_id = files[file]["location id"];
636 if not files[file]["files id"]:
637 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], dsc_location_id)
638 projectB.query("INSERT INTO source (source, version, maintainer, file) VALUES ('%s', '%s', %d, %d)"
639 % (package, version, maintainer_id, files[file]["files id"]))
641 for suite in changes["distribution"].keys():
642 suite_id = db_access.get_suite_id(suite);
643 projectB.query("INSERT INTO src_associations (suite, source) VALUES (%d, currval('source_id_seq'))" % (suite_id))
645 # Add the source files to the DB (files and dsc_files)
646 projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files[file]["files id"]));
647 for dsc_file in dsc_files.keys():
648 filename = files[file]["pool name"] + dsc_file;
649 # use location id from dsc_files first if it exists as
650 # the .orig.tar.gz may still be in a legacy location
651 location_id = dsc_files[dsc_file].get("location id", None);
652 if location_id == None:
653 location_id = files[file]["location id"];
654 files_id = db_access.get_files_id(filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], location_id);
655 # FIXME: needs to check for -1/-2 and or handle exception
657 files_id = db_access.set_files_id (filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], location_id);
658 projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files_id));
660 # Add the .deb files to the DB
661 for file in files.keys():
662 if files[file]["type"] == "deb":
663 package = files[file]["package"]
664 version = files[file]["version"]
665 maintainer = files[file]["maintainer"]
666 maintainer = string.replace(maintainer, "'", "\\'")
667 maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
668 architecture = files[file]["architecture"]
669 architecture_id = db_access.get_architecture_id (architecture);
670 type = files[file]["dbtype"];
671 component = files[file]["component"]
672 source = files[file]["source"]
674 if string.find(source, "(") != -1:
675 m = utils.re_extract_src_version.match(source)
677 source_version = m.group(2)
678 if not source_version:
679 source_version = version
680 filename = files[file]["pool name"] + file;
681 if not files[file]["files id"]:
682 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], files[file]["location id"])
683 source_id = db_access.get_source_id (source, source_version);
685 projectB.query("INSERT INTO binaries (package, version, maintainer, source, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, %d, '%s')"
686 % (package, version, maintainer_id, source_id, architecture_id, files[file]["files id"], type));
688 projectB.query("INSERT INTO binaries (package, version, maintainer, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, '%s')"
689 % (package, version, maintainer_id, architecture_id, files[file]["files id"], type));
690 for suite in changes["distribution"].keys():
691 suite_id = db_access.get_suite_id(suite);
692 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES (%d, currval('binaries_id_seq'))" % (suite_id));
694 # If the .orig.tar.gz is in a legacy directory we need to poolify
695 # it, so that apt-get source (and anything else that goes by the
696 # "Directory:" field in the Sources.gz file) works.
697 if orig_tar_id != None:
698 q = projectB.query("SELECT l.path, f.filename, f.id as files_id, df.source, df.id as dsc_files_id, f.size, f.md5sum FROM files f, dsc_files df, location l WHERE df.source IN (SELECT source FROM dsc_files WHERE file = %s) AND f.id = df.file AND l.id = f.location" % (orig_tar_id));
701 # First move the files to the new location
702 legacy_filename = qid["path"]+qid["filename"];
703 pool_location = utils.poolify (changes["source"], files[file]["component"]);
704 pool_filename = pool_location + os.path.basename(qid["filename"]);
705 destination = Cnf["Dir::PoolDir"] + pool_location
706 utils.move(legacy_filename, destination);
707 # Update the DB: files table
708 new_files_id = db_access.set_files_id(pool_filename, qid["size"], qid["md5sum"], dsc_location_id);
709 # Update the DB: dsc_files table
710 projectB.query("INSERT INTO dsc_files (source, file) VALUES (%s, %s)" % (qid["source"], new_files_id));
711 # Update the DB: source table
712 if legacy_filename[-4:] == ".dsc":
713 projectB.query("UPDATE source SET file = %s WHERE id = %d" % (new_files_id, qid["source"]));
716 # Remove old data from the DB: dsc_files table
717 projectB.query("DELETE FROM dsc_files WHERE id = %s" % (qid["dsc_files_id"]));
718 # Remove old data from the DB: files table
719 projectB.query("DELETE FROM files WHERE id = %s" % (qid["files_id"]));
721 # Install the files into the pool
722 for file in files.keys():
723 if files[file].has_key("byhand"):
725 destination = Cnf["Dir::PoolDir"] + files[file]["pool name"] + file
726 destdir = os.path.dirname(destination)
727 utils.move (file, destination)
728 install_bytes = install_bytes + float(files[file]["size"])
730 # Copy the .changes file across for suite which need it.
731 for suite in changes["distribution"].keys():
732 if Cnf.has_key("Suite::%s::CopyChanges" % (suite)):
733 utils.copy (changes_filename, Cnf["Dir::RootDir"] + Cnf["Suite::%s::CopyChanges" % (suite)]);
735 projectB.query("COMMIT WORK");
737 utils.move (changes_filename, Cnf["Dir::IncomingDir"] + 'DONE/' + os.path.basename(changes_filename))
739 install_count = install_count + 1;
741 if not Cnf["Dinstall::Options::No-Mail"]:
742 mail_message = """Return-Path: %s
745 Bcc: troup@auric.debian.org
746 Subject: %s INSTALLED
752 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], os.path.basename(changes_filename), reject_message, summary, installed_footer)
753 utils.send_mail (mail_message, "")
754 announce (short_summary, 1)
756 #####################################################################################################################
758 def stable_install (changes_filename, summary, short_summary):
759 global install_count, install_bytes
761 print "Installing to stable."
763 archive = utils.where_am_i();
765 # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
766 projectB.query("BEGIN WORK");
768 # Add the .dsc file to the DB
769 for file in files.keys():
770 if files[file]["type"] == "dsc":
771 package = dsc["source"]
772 version = dsc["version"] # NB: not files[file]["version"], that has no epoch
773 q = projectB.query("SELECT id FROM source WHERE source = '%s' AND version = '%s'" % (package, version))
776 sys.stderr.write("INTERNAL ERROR: couldn't find '%s' (%s) in source table.\n" % (package, version));
778 source_id = ql[0][0];
779 suite_id = db_access.get_suite_id('proposed-updates');
780 projectB.query("DELETE FROM src_associations WHERE suite = '%s' AND source = '%s'" % (suite_id, source_id));
781 suite_id = db_access.get_suite_id('stable');
782 projectB.query("INSERT INTO src_associations (suite, source) VALUES ('%s', '%s')" % (suite_id, source_id));
784 # Add the .deb files to the DB
785 for file in files.keys():
786 if files[file]["type"] == "deb":
787 package = files[file]["package"]
788 version = files[file]["version"]
789 architecture = files[file]["architecture"]
790 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))
793 sys.stderr.write("INTERNAL ERROR: couldn't find '%s' (%s for %s architecture) in binaries table.\n" % (package, version, architecture));
795 binary_id = ql[0][0];
796 suite_id = db_access.get_suite_id('proposed-updates');
797 projectB.query("DELETE FROM bin_associations WHERE suite = '%s' AND bin = '%s'" % (suite_id, binary_id));
798 suite_id = db_access.get_suite_id('stable');
799 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES ('%s', '%s')" % (suite_id, binary_id));
801 projectB.query("COMMIT WORK");
803 utils.move (changes_filename, Cnf["Rhona::Morgue"] + os.path.basename(changes_filename));
805 # Update the Stable ChangeLog file
807 new_changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + ".ChangeLog";
808 changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + "ChangeLog";
809 if os.path.exists(new_changelog_filename):
810 os.unlink (new_changelog_filename);
812 new_changelog = utils.open_file(new_changelog_filename, 'w');
813 for file in files.keys():
814 if files[file]["type"] == "deb":
815 new_changelog.write("stable/%s/binary-%s/%s\n" % (files[file]["component"], files[file]["architecture"], file));
816 elif re_issource.match(file) != None:
817 new_changelog.write("stable/%s/source/%s\n" % (files[file]["component"], file));
819 new_changelog.write("%s\n" % (file));
820 chop_changes = re_fdnic.sub("\n", changes["changes"]);
821 new_changelog.write(chop_changes + '\n\n');
822 if os.access(changelog_filename, os.R_OK) != 0:
823 changelog = utils.open_file(changelog_filename, 'r');
824 new_changelog.write(changelog.read());
825 new_changelog.close();
826 if os.access(changelog_filename, os.R_OK) != 0:
827 os.unlink(changelog_filename);
828 utils.move(new_changelog_filename, changelog_filename);
830 install_count = install_count + 1;
832 if not Cnf["Dinstall::Options::No-Mail"]:
833 mail_message = """Return-Path: %s
836 Bcc: troup@auric.debian.org
837 Subject: %s INSTALLED into stable
843 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], os.path.basename(changes_filename), reject_message, summary, installed_footer)
844 utils.send_mail (mail_message, "")
845 announce (short_summary, 1)
847 #####################################################################################################################
849 def reject (changes_filename, manual_reject_mail_filename):
852 base_changes_filename = os.path.basename(changes_filename);
853 reason_filename = re_changes.sub("reason", base_changes_filename);
854 reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename);
856 # Move the .changes files and it's contents into REJECT/
857 utils.move (changes_filename, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], base_changes_filename));
858 for file in files.keys():
859 if os.access(file,os.R_OK) == 0:
860 utils.move (file, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], file));
862 # If this is not a manual rejection generate the .reason file and rejection mail message
863 if manual_reject_mail_filename == "":
864 if os.path.exists(reject_filename):
865 os.unlink(reject_filename);
866 fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
867 os.write(fd, reject_message);
869 reject_mail_message = """From: %s
871 Bcc: troup@auric.debian.org
876 %s""" % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], os.path.basename(changes_filename), reject_message, reject_footer);
877 else: # Have a manual rejection file to use
878 reject_mail_message = ""; # avoid <undef>'s
880 # Send the rejection mail if appropriate
881 if not Cnf["Dinstall::Options::No-Mail"]:
882 utils.send_mail (reject_mail_message, manual_reject_mail_filename);
884 ##################################################################
886 def manual_reject (changes_filename):
887 # Build up the rejection email
888 user_email_address = string.replace(string.split(pwd.getpwuid(os.getuid())[4],',')[0], '.', '')
889 user_email_address = user_email_address + " <%s@%s>" % (pwd.getpwuid(os.getuid())[0], Cnf["Dinstall::MyHost"])
890 manual_reject_message = Cnf.get("Dinstall::Options::Manual-Reject", "")
892 reject_mail_message = """From: %s
895 Bcc: troup@auric.debian.org
901 %s""" % (user_email_address, Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], os.path.basename(changes_filename), manual_reject_message, reject_message, reject_footer)
903 # Write the rejection email out as the <foo>.reason file
904 reason_filename = re_changes.sub("reason", os.path.basename(changes_filename));
905 reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename)
906 if os.path.exists(reject_filename):
907 os.unlink(reject_filename);
908 fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
909 os.write(fd, reject_mail_message);
912 # If we weren't given one, spawn an editor so the user can add one in
913 if manual_reject_message == "":
914 result = os.system("vi +6 %s" % (reject_file))
916 sys.stderr.write ("vi invocation failed for `%s'!" % (reject_file))
919 # Then process it as if it were an automatic rejection
920 reject (changes_filename, reject_filename)
922 #####################################################################################################################
924 def acknowledge_new (changes_filename, summary):
927 changes_filename = os.path.basename(changes_filename);
929 new_ack_new[changes_filename] = 1;
931 if new_ack_old.has_key(changes_filename):
932 print "Ack already sent.";
935 print "Sending new ack.";
936 if not Cnf["Dinstall::Options::No-Mail"]:
937 new_ack_message = """Return-Path: %s
940 Bcc: troup@auric.debian.org
944 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, summary, new_ack_footer);
945 utils.send_mail(new_ack_message,"");
947 #####################################################################################################################
949 def announce (short_summary, action):
950 # Only do announcements for source uploads with a recent dpkg-dev installed
951 if float(changes.get("format", 0)) < 1.6 or not changes["architecture"].has_key("source"):
957 for dist in changes["distribution"].keys():
958 list = Cnf.Find("Suite::%s::Announce" % (dist))
959 if list == None or lists_done.has_key(list):
962 summary = summary + "Announcing to %s\n" % (list)
965 mail_message = """Return-Path: %s
968 Bcc: troup@auric.debian.org
969 Subject: Installed %s %s (%s)
975 """ % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], list, changes["source"], changes["version"], string.join(changes["architecture"].keys(), ' ' ),
976 changes["filecontents"], short_summary)
977 utils.send_mail (mail_message, "")
979 (dsc_rfc822, dsc_name, dsc_email) = utils.fix_maintainer (dsc.get("maintainer",Cnf["Dinstall::MyEmailAddress"]));
980 bugs = changes["closes"].keys()
982 if dsc_name == changes["maintainername"]:
983 summary = summary + "Closing bugs: "
985 summary = summary + "%s " % (bug)
987 mail_message = """Return-Path: %s
989 To: %s-close@bugs.debian.org
990 Bcc: troup@auric.debian.org
991 Subject: Bug#%s: fixed in %s %s
993 We believe that the bug you reported is fixed in the latest version of
994 %s, which has been installed in the Debian FTP archive:
996 %s""" % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], bug, bug, changes["source"], changes["version"], changes["source"], short_summary)
998 if changes["distribution"].has_key("stable"):
999 mail_message = mail_message + """Note that this package is not part of the released stable Debian
1000 distribution. It may have dependencies on other unreleased software,
1001 or other instabilities. Please take care if you wish to install it.
1002 The update will eventually make its way into the next released Debian
1005 mail_message = mail_message + """A summary of the changes between this version and the previous one is
1008 Thank you for reporting the bug, which will now be closed. If you
1009 have further comments please address them to %s@bugs.debian.org,
1010 and the maintainer will reopen the bug report if appropriate.
1012 Debian distribution maintenance software
1014 %s (supplier of updated %s package)
1016 (This message was generated automatically at their request; if you
1017 believe that there is a problem with it please contact the archive
1018 administrators by mailing ftpmaster@debian.org)
1021 %s""" % (bug, changes["maintainer"], changes["source"], changes["filecontents"])
1023 utils.send_mail (mail_message, "")
1025 summary = summary + "Setting bugs to severity fixed: "
1026 control_message = ""
1028 summary = summary + "%s " % (bug)
1029 control_message = control_message + "severity %s fixed\n" % (bug)
1030 if action and control_message != "":
1031 mail_message = """Return-Path: %s
1033 To: control@bugs.debian.org
1034 Bcc: troup@auric.debian.org, %s
1035 Subject: Fixed in NMU of %s %s
1040 This message was generated automatically in response to a
1041 non-maintainer upload. The .changes file follows.
1044 """ % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes["maintainer822"], changes["source"], changes["version"], control_message, changes["filecontents"])
1045 utils.send_mail (mail_message, "")
1046 summary = summary + "\n"
1050 ###############################################################################
1052 # reprocess is necessary for the case of foo_1.2-1 and foo_1.2-2 in
1053 # Incoming. -1 will reference the .orig.tar.gz, but -2 will not.
1054 # dsccheckdistrib() can find the .orig.tar.gz but it will not have
1055 # processed it during it's checks of -2. If -1 has been deleted or
1056 # otherwise not checked by da-install, the .orig.tar.gz will not have
1057 # been checked at all. To get round this, we force the .orig.tar.gz
1058 # into the .changes structure and reprocess the .changes file.
1060 def process_it (changes_file):
1061 global reprocess, orig_tar_id;
1066 # Absolutize the filename to avoid the requirement of being in the
1067 # same directory as the .changes file.
1068 changes_file = os.path.abspath(changes_file);
1070 # And since handling of installs to stable munges with the CWD;
1071 # save and restore it.
1074 check_signature (changes_file);
1075 check_changes (changes_file);
1082 action(changes_file);
1087 ###############################################################################
1090 global Cnf, projectB, reject_message, install_bytes, new_ack_old
1094 Cnf = apt_pkg.newConfiguration();
1095 apt_pkg.ReadConfigFileISC(Cnf,utils.which_conf_file());
1097 Arguments = [('a',"automatic","Dinstall::Options::Automatic"),
1098 ('d',"debug","Dinstall::Options::Debug", "IntVal"),
1099 ('h',"help","Dinstall::Options::Help"),
1100 ('k',"ack-new","Dinstall::Options::Ack-New"),
1101 ('m',"manual-reject","Dinstall::Options::Manual-Reject", "HasArg"),
1102 ('n',"no-action","Dinstall::Options::No-Action"),
1103 ('p',"no-lock", "Dinstall::Options::No-Lock"),
1104 ('r',"no-version-check", "Dinstall::Options::No-Version-Check"),
1105 ('s',"no-mail", "Dinstall::Options::No-Mail"),
1106 ('u',"override-distribution", "Dinstall::Options::Override-Distribution", "HasArg"),
1107 ('v',"version","Dinstall::Options::Version")];
1109 changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
1111 if Cnf["Dinstall::Options::Help"]:
1114 if Cnf["Dinstall::Options::Version"]:
1115 print "katie version 0.0000000000";
1118 postgresql_user = None; # Default == Connect as user running program.
1120 # -n/--dry-run invalidates some other options which would involve things happening
1121 if Cnf["Dinstall::Options::No-Action"]:
1122 Cnf["Dinstall::Options::Automatic"] = ""
1123 Cnf["Dinstall::Options::Ack-New"] = ""
1124 postgresql_user = Cnf["DB::ROUser"];
1126 projectB = pg.connect('projectb', Cnf["DB::Host"], int(Cnf["DB::Port"]), None, None, postgresql_user);
1128 db_access.init(Cnf, projectB);
1130 # Check that we aren't going to clash with the daily cron job
1132 if os.path.exists("%s/Archive_Maintenance_In_Progress" % (Cnf["Dir::RootDir"])) and not Cnf["Dinstall::Options::No-Lock"]:
1133 sys.stderr.write("Archive maintenance in progress. Try again later.\n");
1136 # Obtain lock if not in no-action mode
1138 if not Cnf["Dinstall::Options::No-Action"]:
1139 lock_fd = os.open(Cnf["Dinstall::LockFile"], os.O_RDWR);
1140 fcntl.lockf(lock_fd, FCNTL.F_TLOCK);
1142 # Read in the list of already-acknowledged NEW packages
1143 new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'r');
1145 for line in new_ack_list.readlines():
1146 new_ack_old[line[:-1]] = 1;
1147 new_ack_list.close();
1149 # Process the changes files
1150 for changes_file in changes_files:
1152 print "\n" + changes_file;
1153 process_it (changes_file);
1156 if install_bytes > 10000:
1157 install_bytes = install_bytes / 1000;
1158 install_mag = " Kb";
1159 if install_bytes > 10000:
1160 install_bytes = install_bytes / 1000;
1161 install_mag = " Mb";
1164 if install_count > 1:
1166 sys.stderr.write("Installed %d package %s, %d%s.\n" % (install_count, sets, int(install_bytes), install_mag))
1168 # Write out the list of already-acknowledged NEW packages
1169 if Cnf["Dinstall::Options::Ack-New"]:
1170 new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'w')
1171 for i in new_ack_new.keys():
1172 new_ack_list.write(i+'\n')
1173 new_ack_list.close()
1176 if __name__ == '__main__':