3 # Installs Debian packaes
4 # Copyright (C) 2000 James Troup <james@nocrew.org>
5 # $Id: katie,v 1.5 2000-11-30 04:19:30 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):
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 for src in ("", ".src"):
153 override_filename = Cnf["Dir::OverrideDir"] + 'override.' + Cnf["Suite::%s::OverrideCodeName" % (suite)] + '.' + component + src;
154 read_override_file (override_filename, suite, component);
156 return overrides[suite][component].get(package, None);
158 #####################################################################################################################
160 def check_changes(filename):
161 global reject_message, changes, files
163 # Parse the .changes field into a dictionary [FIXME - need to trap errors, pass on to reject_message etc.]
165 changes = utils.parse_changes(filename)
166 except utils.cant_open_exc:
167 reject_message = "Rejected: can't read changes file '%s'.\n" % (filename)
169 except utils.changes_parse_error_exc, line:
170 reject_message = "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line)
171 changes["maintainer822"] = Cnf["Dinstall::MyEmailAddress"];
174 # Parse the Files field from the .changes into another dictionary [FIXME need to trap errors as above]
175 files = utils.build_file_list(changes, "")
177 # Check for mandatory fields
178 for i in ("source", "binary", "architecture", "version", "distribution","maintainer", "files"):
179 if not changes.has_key(i):
180 reject_message = "Rejected: Missing field `%s' in changes file." % (i)
181 return 0 # Avoid <undef> errors during later tests
183 # Fix the Maintainer: field to be RFC822 compatible
184 (changes["maintainer822"], changes["maintainername"], changes["maintaineremail"]) = utils.fix_maintainer (changes["maintainer"])
186 # Override the Distribution: field if appropriate
187 if Cnf["Dinstall::Options::Override-Distribution"] != "":
188 reject_message = reject_message + "Warning: Distribution was overriden from %s to %s.\n" % (changes["distribution"], Cnf["Dinstall::Options::Override-Distribution"])
189 changes["distribution"] = Cnf["Dinstall::Options::Override-Distribution"]
191 # Split multi-value fields into a lower-level dictionary
192 for i in ("architecture", "distribution", "binary", "closes"):
193 o = changes.get(i, "")
197 for j in string.split(o):
200 # Ensure all the values in Closes: are numbers
201 if changes.has_key("closes"):
202 for i in changes["closes"].keys():
203 if re_isanum.match (i) == None:
204 reject_message = reject_message + "Rejected: `%s' from Closes field isn't a number.\n" % (i)
206 # Map frozen to unstable if frozen doesn't exist
207 if changes["distribution"].has_key("frozen") and not Cnf.has_key("Suite::Frozen"):
208 del changes["distribution"]["frozen"]
209 reject_message = reject_message + "Mapping frozen to unstable.\n"
211 # Ensure target distributions exist
212 for i in changes["distribution"].keys():
213 if not Cnf.has_key("Suite::%s" % (i)):
214 reject_message = reject_message + "Rejected: Unknown distribution `%s'.\n" % (i)
216 # Map unreleased arches from stable to unstable
217 if changes["distribution"].has_key("stable"):
218 for i in changes["architecture"].keys():
219 if not Cnf.has_key("Suite::Stable::Architectures::%s" % (i)):
220 reject_message = reject_message + "Mapping stable to unstable for unreleased arch `%s'.\n" % (i)
221 del changes["distribution"]["stable"]
223 # Map arches not being released from frozen to unstable
224 if changes["distribution"].has_key("frozen"):
225 for i in changes["architecture"].keys():
226 if not Cnf.has_key("Suite::Frozen::Architectures::%s" % (i)):
227 reject_message = reject_message + "Mapping frozen to unstable for non-releasing arch `%s'.\n" % (i)
228 del changes["distribution"]["frozen"]
230 # Handle uploads to stable
231 if changes["distribution"].has_key("stable"):
232 # If running from within proposed-updates kill non-stable distributions
233 if string.find(os.getcwd(), 'proposed-updates') != -1:
234 for i in ("frozen", "unstable"):
235 if changes["distributions"].has_key(i):
236 reject_message = reject_message + "Removing %s from distribution list.\n"
237 del changes["distribution"][i]
238 # Otherwise (normal case) map stable to updates
240 reject_message = reject_message + "Mapping stable to updates.\n";
241 del changes["distribution"]["stable"];
242 changes["distribution"]["proposed-updates"] = 1;
244 # chopversion = no epoch; chopversion2 = no epoch and no revision (e.g. for .orig.tar.gz comparison)
245 changes["chopversion"] = utils.re_no_epoch.sub('', changes["version"])
246 changes["chopversion2"] = utils.re_no_revision.sub('', changes["chopversion"])
248 if string.find(reject_message, "Rejected:") != -1:
254 global reject_message
256 archive = utils.where_am_i();
258 for file in files.keys():
259 # Check the file is readable
260 if os.access(file,os.R_OK) == 0:
261 reject_message = reject_message + "Rejected: Can't read `%s'.\n" % (file)
262 files[file]["type"] = "unreadable";
264 # If it's byhand skip remaining checks
265 if files[file]["section"] == "byhand":
266 files[file]["byhand"] = 1;
267 files[file]["type"] = "byhand";
268 # Checks for a binary package...
269 elif re_isadeb.match(file) != None:
270 # Extract package information using dpkg-deb
271 control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))
273 # Check for mandatory fields
274 if control.Find("Package") == None:
275 reject_message = reject_message + "Rejected: %s: No package field in control.\n" % (file)
276 if control.Find("Architecture") == None:
277 reject_message = reject_message + "Rejected: %s: No architecture field in control.\n" % (file)
278 if control.Find("Version") == None:
279 reject_message = reject_message + "Rejected: %s: No version field in control.\n" % (file)
281 # Ensure the package name matches the one give in the .changes
282 if not changes["binary"].has_key(control.Find("Package", "")):
283 reject_message = reject_message + "Rejected: %s: control file lists name as `%s', which isn't in changes file.\n" % (file, control.Find("Package", ""))
285 # Validate the architecture
286 if not Cnf.has_key("Suite::Unstable::Architectures::%s" % (control.Find("Architecture", ""))):
287 reject_message = reject_message + "Rejected: Unknown architecture '%s'.\n" % (control.Find("Architecture", ""))
289 # Check the architecture matches the one given in the .changes
290 if not changes["architecture"].has_key(control.Find("Architecture", "")):
291 reject_message = reject_message + "Rejected: %s: control file lists arch as `%s', which isn't in changes file.\n" % (file, control.Find("Architecture", ""))
292 # Check the section & priority match those given in the .changes (non-fatal)
293 if control.Find("Section") != None and files[file]["section"] != "" and files[file]["section"] != control.Find("Section"):
294 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"])
295 if control.Find("Priority") != None and files[file]["priority"] != "" and files[file]["priority"] != control.Find("Priority"):
296 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"])
298 epochless_version = utils.re_no_epoch.sub('', control.Find("Version", ""))
300 files[file]["package"] = control.Find("Package");
301 files[file]["architecture"] = control.Find("Architecture");
302 files[file]["version"] = control.Find("Version");
303 files[file]["maintainer"] = control.Find("Maintainer", "");
304 if file[-5:] == ".udeb":
305 files[file]["dbtype"] = "udeb";
306 elif file[-4:] == ".deb":
307 files[file]["dbtype"] = "deb";
309 reject_message = reject_message + "Rejected: %s is neither a .deb or a .udeb.\n " % (file);
310 files[file]["type"] = "deb";
311 files[file]["fullname"] = "%s_%s_%s.deb" % (control.Find("Package", ""), epochless_version, control.Find("Architecture", ""))
312 files[file]["source"] = control.Find("Source", "");
313 if files[file]["source"] == "":
314 files[file]["source"] = files[file]["package"];
315 # Checks for a source package...
317 m = re_issource.match(file)
319 files[file]["package"] = m.group(1)
320 files[file]["version"] = m.group(2)
321 files[file]["type"] = m.group(3)
323 # Ensure the source package name matches the Source filed in the .changes
324 if changes["source"] != files[file]["package"]:
325 reject_message = reject_message + "Rejected: %s: changes file doesn't say %s for Source\n" % (file, files[file]["package"])
327 # Ensure the source version matches the version in the .changes file
328 if files[file]["type"] == "orig.tar.gz":
329 changes_version = changes["chopversion2"]
331 changes_version = changes["chopversion"]
332 if changes_version != files[file]["version"]:
333 reject_message = reject_message + "Rejected: %s: should be %s according to changes file.\n" % (file, changes_version)
335 # Ensure the .changes lists source in the Architecture field
336 if not changes["architecture"].has_key("source"):
337 reject_message = reject_message + "Rejected: %s: changes file doesn't list `source' in Architecture field.\n" % (file)
339 # Check the signature of a .dsc file
340 if files[file]["type"] == "dsc":
341 check_signature(file)
343 files[file]["fullname"] = file
345 # Not a binary or source package? Assume byhand...
347 files[file]["byhand"] = 1;
348 files[file]["type"] = "byhand";
350 files[file]["oldfiles"] = {}
351 for suite in changes["distribution"].keys():
353 if files[file].has_key("byhand"):
356 if Cnf.has_key("Suite:%s::Components" % (suite)) and not Cnf.has_key("Suite::%s::Components::%s" % (suite, files[file]["component"])):
357 reject_message = reject_message + "Rejected: unknown component `%s' for suite `%s'.\n" % (files[file]["component"], suite)
360 # See if the package is NEW
361 if not in_override_p(files[file]["package"], files[file]["component"], suite):
362 files[file]["new"] = 1
364 # Find any old binary packages
365 if files[file]["type"] == "deb":
366 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"
367 % (files[file]["package"], suite, files[file]["architecture"]))
368 oldfiles = q.dictresult()
369 for oldfile in oldfiles:
370 files[file]["oldfiles"][suite] = oldfile
371 # Check versions [NB: per-suite only; no cross-suite checking done (yet)]
372 if apt_pkg.VersionCompare(files[file]["version"], oldfile["version"]) != 1:
373 if Cnf["Dinstall::Options::No-Version-Check"]:
374 reject_message = reject_message + "Overriden rejection"
376 reject_message = reject_message + "Rejected"
377 reject_message = reject_message + ": %s Old version `%s' >= new version `%s'.\n" % (file, oldfile["version"], files[file]["version"])
378 # Check for existing copies of the file
379 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"]))
380 if q.getresult() != []:
381 reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (file)
383 # Find any old .dsc files
384 elif files[file]["type"] == "dsc":
385 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"
386 % (files[file]["package"], suite))
387 oldfiles = q.dictresult()
388 if len(oldfiles) >= 1:
389 files[file]["oldfiles"][suite] = oldfiles[0]
391 # Validate the component
392 component = files[file]["component"];
393 component_id = db_access.get_component_id(component);
394 if component_id == -1:
395 reject_message = reject_message + "Rejected: file '%s' has unknown component '%s'.\n" % (file, component);
398 # Check the md5sum & size against existing files (if any)
399 location = Cnf["Dir::PoolDir"];
400 files[file]["location id"] = db_access.get_location_id (location, component, archive);
401 files_id = db_access.get_files_id(component + '/' + file, files[file]["size"], files[file]["md5sum"], files[file]["location id"]);
403 reject_message = reject_message + "Rejected: INTERNAL ERROR, get_files_id() returned multiple matches for %s.\n" % (file)
405 reject_message = reject_message + "Rejected: md5sum and/or size mismatch on existing copy of %s.\n" % (file)
406 files[file]["files id"] = files_id
408 # Check for packages that have moved from one component to another
409 if files[file]["oldfiles"].has_key(suite) and files[file]["oldfiles"][suite]["name"] != files[file]["component"]:
410 files[file]["othercomponents"] = files[file]["oldfiles"][suite]["name"];
413 if string.find(reject_message, "Rejected:") != -1:
418 ###############################################################################
421 global dsc, dsc_files, reject_message, reprocess, orig_tar_id;
423 for file in files.keys():
424 if files[file]["type"] == "dsc":
426 dsc = utils.parse_changes(file)
427 except utils.cant_open_exc:
428 reject_message = reject_message + "Rejected: can't read changes file '%s'.\n" % (filename)
430 except utils.changes_parse_error_exc, line:
431 reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line)
434 dsc_files = utils.build_file_list(dsc, 1)
435 except utils.no_files_exc:
436 reject_message = reject_message + "Rejected: no Files: field in .dsc file.\n";
439 # Try and find all files mentioned in the .dsc. This has
440 # to work harder to cope with the multiple possible
441 # locations of an .orig.tar.gz.
442 for dsc_file in dsc_files.keys():
443 if files.has_key(dsc_file):
444 actual_md5 = files[dsc_file]["md5sum"]
445 found = "%s in incoming" % (dsc_file)
446 # Check the file does not already exist in the archive
447 q = projectB.query("SELECT f.id FROM files f, location l WHERE f.filename ~ '/%s' AND l.id = f.location" % (dsc_file));
448 if q.getresult() != []:
449 reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (dsc_file)
450 elif dsc_file[-12:] == ".orig.tar.gz":
452 # See comment above process_it() for explanation...
453 if os.access(dsc_file, os.R_OK) != 0:
454 files[dsc_file] = {};
455 files[dsc_file]["size"] = os.stat(dsc_file)[stat.ST_SIZE];
456 files[dsc_file]["md5sum"] = dsc_files[dsc_file]["md5sum"];
457 files[dsc_file]["section"] = files[file]["section"];
458 files[dsc_file]["priority"] = files[file]["priority"];
459 files[dsc_file]["component"] = files[file]["component"];
463 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" % (dsc_file));
466 old_file = ql[0][0] + ql[0][1];
467 actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
469 suite_type = ql[0][2];
471 if suite_type == "legacy" or suite_type == "legacy-mixed":
472 orig_tar_id = ql[0][3];
474 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);
477 reject_message = reject_message + "Rejected: %s refers to %s, but I can't find it in Incoming." % (file, dsc_file);
479 if actual_md5 != dsc_files[dsc_file]["md5sum"]:
480 reject_message = reject_message + "Rejected: md5sum for %s doesn't match %s.\n" % (found, file)
482 if string.find(reject_message, "Rejected:") != -1:
487 ###############################################################################
489 def check_md5sums ():
490 global reject_message;
492 for file in files.keys():
494 file_handle = utils.open_file(file,"r");
495 except utils.cant_open_exc:
498 if apt_pkg.md5sum(file_handle) != files[file]["md5sum"]:
499 reject_message = reject_message + "Rejected: md5sum check failed for %s.\n" % (file);
501 #####################################################################################################################
503 def action (changes_filename):
504 byhand = confirm = suites = summary = new = "";
506 # changes["distribution"] may not exist in corner cases
507 # (e.g. unreadable changes files)
508 if not changes.has_key("distribution"):
509 changes["distribution"] = {};
511 for suite in changes["distribution"].keys():
512 if Cnf.has_key("Suite::%s::Confirm"):
513 confirm = confirm + suite + ", "
514 suites = suites + suite + ", "
515 confirm = confirm[:-2]
518 for file in files.keys():
519 if files[file].has_key("byhand"):
521 summary = summary + file + " byhand\n"
522 elif files[file].has_key("new"):
524 summary = summary + "(new) %s %s %s\n" % (file, files[file]["priority"], files[file]["section"])
525 if files[file].has_key("othercomponents"):
526 summary = summary + "WARNING: Already present in %s distribution.\n" % (files[file]["othercomponents"])
527 if files[file]["type"] == "deb":
528 summary = summary + apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))["Description"] + '\n';
530 files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"])
531 destination = Cnf["Dir::PoolRoot"] + files[file]["pool name"] + file
532 summary = summary + file + "\n to " + destination + "\n"
534 short_summary = summary;
536 # This is for direport's benefit...
537 f = re_fdnic.sub("\n .\n", changes.get("changes",""));
539 if confirm or byhand or new:
540 summary = summary + "Changes: " + f;
542 summary = summary + announce (short_summary, 0)
544 (prompt, answer) = ("", "XXX")
545 if Cnf["Dinstall::Options::No-Action"] or Cnf["Dinstall::Options::Automatic"]:
548 if string.find(reject_message, "Rejected") != -1:
549 if time.time()-os.path.getmtime(changes_filename) < 86400:
550 print "SKIP (too new)\n" + reject_message,;
551 prompt = "[S]kip, Manual reject, Quit ?";
553 print "REJECT\n" + reject_message,;
554 prompt = "[R]eject, Manual reject, Skip, Quit ?";
555 if Cnf["Dinstall::Options::Automatic"]:
558 print "NEW to %s\n%s%s" % (suites, reject_message, summary),;
559 prompt = "[S]kip, New ack, Manual reject, Quit ?";
560 if Cnf["Dinstall::Options::Automatic"] and Cnf["Dinstall::Options::Ack-New"]:
563 print "BYHAND\n" + reject_message + summary,;
564 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
566 print "CONFIRM to %s\n%s%s" % (confirm, reject_message, summary),
567 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
569 print "INSTALL\n" + reject_message + summary,;
570 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
571 if Cnf["Dinstall::Options::Automatic"]:
574 while string.find(prompt, answer) == -1:
576 answer = utils.our_raw_input()
577 m = re_default_answer.match(prompt)
580 answer = string.upper(answer[:1])
583 reject (changes_filename, "");
585 manual_reject (changes_filename);
587 install (changes_filename, summary, short_summary);
589 acknowledge_new (changes_filename, summary);
593 #####################################################################################################################
595 def install (changes_filename, summary, short_summary):
596 global install_count, install_bytes
600 archive = utils.where_am_i();
602 # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
603 projectB.query("BEGIN WORK");
605 # Add the .dsc file to the DB
606 for file in files.keys():
607 if files[file]["type"] == "dsc":
608 package = dsc["source"]
609 version = dsc["version"] # NB: not files[file]["version"], that has no epoch
610 maintainer = dsc["maintainer"]
611 maintainer = string.replace(maintainer, "'", "\\'")
612 maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
613 filename = files[file]["pool name"] + file;
614 dsc_location_id = files[file]["location id"];
615 if not files[file]["files id"]:
616 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], dsc_location_id)
617 dsc_file_id = files[file]["files id"]
618 projectB.query("INSERT INTO source (source, version, maintainer, file) VALUES ('%s', '%s', %d, %d)"
619 % (package, version, maintainer_id, files[file]["files id"]))
621 for suite in changes["distribution"].keys():
622 suite_id = db_access.get_suite_id(suite);
623 projectB.query("INSERT INTO src_associations (suite, source) VALUES (%d, currval('source_id_seq'))" % (suite_id))
626 # Add the .diff.gz and {.orig,}.tar.gz files to the DB (files and dsc_files)
627 for file in files.keys():
628 if files[file]["type"] == "diff.gz" or files[file]["type"] == "orig.tar.gz" or files[file]["type"] == "tar.gz":
629 if not files[file]["files id"]:
630 filename = files[file]["pool name"] + file;
631 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], files[file]["location id"])
632 projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files[file]["files id"]));
634 # Add the .deb files to the DB
635 for file in files.keys():
636 if files[file]["type"] == "deb":
637 package = files[file]["package"]
638 version = files[file]["version"]
639 maintainer = files[file]["maintainer"]
640 maintainer = string.replace(maintainer, "'", "\\'")
641 maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
642 architecture = files[file]["architecture"]
643 architecture_id = db_access.get_architecture_id (architecture);
644 type = files[file]["dbtype"];
645 component = files[file]["component"]
646 source = files[file]["source"]
648 if string.find(source, "(") != -1:
649 m = utils.re_extract_src_version.match(source)
651 source_version = m.group(2)
652 if not source_version:
653 source_version = version
654 filename = files[file]["pool name"] + file;
655 if not files[file]["files id"]:
656 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], files[file]["location id"])
657 source_id = db_access.get_source_id (source, source_version);
659 projectB.query("INSERT INTO binaries (package, version, maintainer, source, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, %d, '%s')"
660 % (package, version, maintainer_id, source_id, architecture_id, files[file]["files id"], type));
662 projectB.query("INSERT INTO binaries (package, version, maintainer, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, '%s')"
663 % (package, version, maintainer_id, architecture_id, files[file]["files id"], type));
664 for suite in changes["distribution"].keys():
665 suite_id = db_access.get_suite_id(suite);
666 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES (%d, currval('binaries_id_seq'))" % (suite_id));
668 # Install the files into the pool
669 for file in files.keys():
670 if files[file].has_key("byhand"):
672 destination = Cnf["Dir::PoolDir"] + files[file]["pool name"] + file
673 destdir = os.path.dirname(destination)
674 utils.move (file, destination)
675 install_bytes = install_bytes + float(files[file]["size"])
677 # Copy the .changes file across for suite which need it.
678 for suite in changes["distribution"].keys():
679 if Cnf.has_key("Suite::%s::CopyChanges" % (suite)):
680 utils.copy (changes_filename, Cnf["Dir::RootDir"] + Cnf["Suite::%s::CopyChanges" % (suite)]);
682 # If the .orig.tar.gz is in a legacy directory we need to poolify
683 # it, so that apt-get source (and anything else that goes by the
684 # "Directory:" field in the Sources.gz file) works.
685 if orig_tar_id != None:
686 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));
689 # First move the files to the new location
690 legacy_filename = qid["path"]+qid["filename"];
691 pool_location = utils.poolify (files[file]["package"], files[file]["component"]);
692 pool_filename = pool_location + os.path.basename(qid["filename"]);
693 destination = Cnf["Dir::PoolDir"] + pool_location
694 utils.move(legacy_filename, destination);
695 # Update the DB: files table
696 new_files_id = db_access.set_files_id(pool_filename, qid["size"], qid["md5sum"], dsc_location_id);
697 # Update the DB: dsc_files table
698 projectB.query("INSERT INTO dsc_files (source, file) VALUES (%s, %s)" % (qid["source"], new_files_id));
699 # Update the DB: source table
700 if legacy_filename[-4:] == ".dsc":
701 projectB.query("UPDATE source SET file = %s WHERE id = %d" % (new_files_id, qid["source"]));
704 # Remove old data from the DB: dsc_files table
705 projectB.query("DELETE FROM dsc_files WHERE id = %s" % (qid["dsc_files_id"]));
706 # Remove old data from the DB: files table
707 projectB.query("DELETE FROM files WHERE id = %s" % (qid["files_id"]));
709 utils.move (changes_filename, Cnf["Dir::IncomingDir"] + 'DONE/' + os.path.basename(changes_filename))
711 projectB.query("COMMIT WORK");
713 install_count = install_count + 1;
715 if not Cnf["Dinstall::Options::No-Mail"]:
716 mail_message = """Return-Path: %s
719 Bcc: troup@auric.debian.org
720 Subject: %s INSTALLED
726 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, reject_message, summary, installed_footer)
727 utils.send_mail (mail_message, "")
728 announce (short_summary, 1)
730 #####################################################################################################################
732 def reject (changes_filename, manual_reject_mail_filename):
735 base_changes_filename = os.path.basename(changes_filename);
736 reason_filename = re_changes.sub("reason", base_changes_filename);
737 reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename);
739 # Move the .changes files and it's contents into REJECT/
740 utils.move (changes_filename, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], base_changes_filename));
741 for file in files.keys():
742 if os.access(file,os.R_OK) == 0:
743 utils.move (file, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], file));
745 # If this is not a manual rejection generate the .reason file and rejection mail message
746 if manual_reject_mail_filename == "":
747 if os.path.exists(reject_filename):
748 os.unlink(reject_filename);
749 fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
750 os.write(fd, reject_message);
752 reject_mail_message = """From: %s
754 Bcc: troup@auric.debian.org
759 %s""" % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, reject_message, reject_footer);
760 else: # Have a manual rejection file to use
761 reject_mail_message = ""; # avoid <undef>'s
763 # Send the rejection mail if appropriate
764 if not Cnf["Dinstall::Options::No-Mail"]:
765 utils.send_mail (reject_mail_message, manual_reject_mail_filename);
767 ##################################################################
769 def manual_reject (changes_filename):
770 # Build up the rejection email
771 user_email_address = string.replace(string.split(pwd.getpwuid(os.getuid())[4],',')[0], '.', '')
772 user_email_address = user_email_address + " <%s@%s>" % (pwd.getpwuid(os.getuid())[0], Cnf["Dinstall::MyHost"])
773 manual_reject_message = Cnf.get("Dinstall::Options::Manual-Reject", "")
775 reject_mail_message = """From: %s
778 Bcc: troup@auric.debian.org
784 %s""" % (user_email_address, Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, manual_reject_message, reject_message, reject_footer)
786 # Write the rejection email out as the <foo>.reason file
787 reason_filename = re_changes.sub("reason", os.path.basename(changes_filename));
788 reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename)
789 if os.path.exists(reject_filename):
790 os.unlink(reject_filename);
791 fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
792 os.write(fd, reject_mail_message);
795 # If we weren't given one, spawn an editor so the user can add one in
796 if manual_reject_message == "":
797 result = os.system("vi +6 %s" % (reject_file))
799 sys.stderr.write ("vi invocation failed for `%s'!" % (reject_file))
802 # Then process it as if it were an automatic rejection
803 reject (changes_filename, reject_filename)
805 #####################################################################################################################
807 def acknowledge_new (changes_filename, summary):
810 new_ack_new[changes_filename] = 1;
812 if new_ack_old.has_key(changes_filename):
813 print "Ack already sent.";
816 print "Sending new ack.";
817 if not Cnf["Dinstall::Options::No-Mail"]:
818 new_ack_message = """Return-Path: %s
821 Bcc: troup@auric.debian.org
825 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, summary, new_ack_footer);
826 utils.send_mail(new_ack_message,"");
828 #####################################################################################################################
830 def announce (short_summary, action):
831 # Only do announcements for source uploads with a recent dpkg-dev installed
832 if float(changes.get("format", 0)) < 1.6 or not changes["architecture"].has_key("source"):
838 for dist in changes["distribution"].keys():
839 list = Cnf.Find("Suite::%s::Announce" % (dist))
840 if list == None or lists_done.has_key(list):
843 summary = summary + "Announcing to %s\n" % (list)
846 mail_message = """Return-Path: %s
849 Bcc: troup@auric.debian.org
850 Subject: Installed %s %s (%s)
856 """ % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], list, changes["source"], changes["version"], string.join(changes["architecture"].keys(), ' ' ),
857 changes["filecontents"], short_summary)
858 utils.send_mail (mail_message, "")
860 (dsc_rfc822, dsc_name, dsc_email) = utils.fix_maintainer (dsc.get("maintainer",Cnf["Dinstall::MyEmailAddress"]));
861 bugs = changes["closes"].keys()
863 if dsc_name == changes["maintainername"]:
864 summary = summary + "Closing bugs: "
866 summary = summary + "%s " % (bug)
868 mail_message = """Return-Path: %s
870 To: %s-close@bugs.debian.org
871 Bcc: troup@auric.debian.org
872 Subject: Bug#%s: fixed in %s %s
874 We believe that the bug you reported is fixed in the latest version of
875 %s, which has been installed in the Debian FTP archive:
877 %s""" % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], bug, bug, changes["source"], changes["version"], changes["source"], short_summary)
879 if changes["distribution"].has_key("stable"):
880 mail_message = mail_message + """Note that this package is not part of the released stable Debian
881 distribution. It may have dependencies on other unreleased software,
882 or other instabilities. Please take care if you wish to install it.
883 The update will eventually make its way into the next released Debian
886 mail_message = mail_message + """A summary of the changes between this version and the previous one is
889 Thank you for reporting the bug, which will now be closed. If you
890 have further comments please address them to %s@bugs.debian.org,
891 and the maintainer will reopen the bug report if appropriate.
893 Debian distribution maintenance software
895 %s (supplier of updated %s package)
897 (This message was generated automatically at their request; if you
898 believe that there is a problem with it please contact the archive
899 administrators by mailing ftpmaster@debian.org)
902 %s""" % (bug, changes["maintainer"], changes["source"], changes["filecontents"])
904 utils.send_mail (mail_message, "")
906 summary = summary + "Setting bugs to severity fixed: "
909 summary = summary + "%s " % (bug)
910 control_message = control_message + "severity %s fixed\n" % (bug)
911 if action and control_message != "":
912 mail_message = """Return-Path: %s
914 To: control@bugs.debian.org
915 Bcc: troup@auric.debian.org, %s
916 Subject: Fixed in NMU of %s %s
921 This message was generated automatically in response to a
922 non-maintainer upload. The .changes file follows.
925 """ % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes["maintainer822"], changes["source"], changes["version"], control_message, changes["filecontents"])
926 utils.send_mail (mail_message, "")
927 summary = summary + "\n"
931 ###############################################################################
933 # reprocess is necessary for the case of foo_1.2-1 and foo_1.2-2 in
934 # Incoming. -1 will reference the .orig.tar.gz, but -2 will not.
935 # dsccheckdistrib() can find the .orig.tar.gz but it will not have
936 # processed it during it's checks of -2. If -1 has been deleted or
937 # otherwise not checked by da-install, the .orig.tar.gz will not have
938 # been checked at all. To get round this, we force the .orig.tar.gz
939 # into the .changes structure and reprocess the .changes file.
941 def process_it (changes_file):
942 global reprocess, orig_tar_id;
947 check_signature (changes_file);
948 check_changes (changes_file);
955 action(changes_file);
957 ###############################################################################
960 global Cnf, projectB, reject_message, install_bytes, new_ack_old
964 Cnf = apt_pkg.newConfiguration();
965 apt_pkg.ReadConfigFileISC(Cnf,utils.which_conf_file());
967 Arguments = [('a',"automatic","Dinstall::Options::Automatic"),
968 ('d',"debug","Dinstall::Options::Debug", "IntVal"),
969 ('h',"help","Dinstall::Options::Help"),
970 ('k',"ack-new","Dinstall::Options::Ack-New"),
971 ('m',"manual-reject","Dinstall::Options::Manual-Reject", "HasArg"),
972 ('n',"no-action","Dinstall::Options::No-Action"),
973 ('p',"no-lock", "Dinstall::Options::No-Lock"),
974 ('r',"no-version-check", "Dinstall::Options::No-Version-Check"),
975 ('s',"no-mail", "Dinstall::Options::No-Mail"),
976 ('u',"override-distribution", "Dinstall::Options::Override-Distribution", "HasArg"),
977 ('v',"version","Dinstall::Options::Version")];
979 changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
981 if Cnf["Dinstall::Options::Help"]:
984 if Cnf["Dinstall::Options::Version"]:
985 print "katie version 0.0000000000";
988 postgresql_user = None; # Default == Connect as user running program.
990 # -n/--dry-run invalidates some other options which would involve things happening
991 if Cnf["Dinstall::Options::No-Action"]:
992 Cnf["Dinstall::Options::Automatic"] = ""
993 Cnf["Dinstall::Options::Ack-New"] = ""
994 postgresql_user = Cnf["DB::ROUser"];
996 projectB = pg.connect('projectb', Cnf["DB::Host"], int(Cnf["DB::Port"]), None, None, postgresql_user);
998 db_access.init(Cnf, projectB);
1000 # Check that we aren't going to clash with the daily cron job
1002 if os.path.exists("%s/Archive_Maintenance_In_Progress" % (Cnf["Dir::RootDir"])) and not Cnf["Dinstall::Options::No-Lock"]:
1003 sys.stderr.write("Archive maintenance in progress. Try again later.\n");
1006 # Obtain lock if not in no-action mode
1008 if not Cnf["Dinstall::Options::No-Action"]:
1009 lock_fd = os.open(Cnf["Dinstall::LockFile"], os.O_RDWR);
1010 fcntl.lockf(lock_fd, FCNTL.F_TLOCK);
1012 # Read in the list of already-acknowledged NEW packages
1013 new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'r');
1015 for line in new_ack_list.readlines():
1016 new_ack_old[line[:-1]] = 1;
1017 new_ack_list.close();
1019 # Process the changes files
1020 for changes_file in changes_files:
1022 print "\n" + changes_file;
1023 process_it (changes_file);
1026 if install_bytes > 10000:
1027 install_bytes = install_bytes / 1000;
1028 install_mag = " Kb";
1029 if install_bytes > 10000:
1030 install_bytes = install_bytes / 1000;
1031 install_mag = " Mb";
1034 if install_count > 1:
1036 sys.stderr.write("Installed %d package %s, %d%s.\n" % (install_count, sets, int(install_bytes), install_mag))
1038 # Write out the list of already-acknowledged NEW packages
1039 if Cnf["Dinstall::Options::Ack-New"]:
1040 new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'w')
1041 for i in new_ack_new.keys():
1042 new_ack_list.write(i+'\n')
1043 new_ack_list.close()
1046 if __name__ == '__main__':