3 # Installs Debian packaes
4 # Copyright (C) 2000 James Troup <james@nocrew.org>
5 # $Id: katie,v 1.9 2000-12-05 04:27:48 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; assume an install to stable
233 if string.find(os.getcwd(), 'proposed-updates') != -1:
234 # FIXME: should probably remove anything that != stable
235 for i in ("frozen", "unstable"):
236 if changes["distribution"].has_key(i):
237 reject_message = reject_message + "Removing %s from distribution list.\n"
238 del changes["distribution"][i]
239 changes["stable upload"] = 1;
240 # If we can't find a file from the .changes; assume it's a package already in the pool and move into the pool
241 file = files.keys()[0];
242 if os.access(file, os.R_OK) == 0:
243 pool_dir = Cnf["Dir::PoolDir"] + '/' + utils.poolify(changes["source"], files[file]["component"]);
245 # Otherwise (normal case) map stable to updates
247 reject_message = reject_message + "Mapping stable to updates.\n";
248 del changes["distribution"]["stable"];
249 changes["distribution"]["proposed-updates"] = 1;
251 # chopversion = no epoch; chopversion2 = no epoch and no revision (e.g. for .orig.tar.gz comparison)
252 changes["chopversion"] = utils.re_no_epoch.sub('', changes["version"])
253 changes["chopversion2"] = utils.re_no_revision.sub('', changes["chopversion"])
255 if string.find(reject_message, "Rejected:") != -1:
261 global reject_message
263 archive = utils.where_am_i();
265 for file in files.keys():
266 # Check the file is readable
267 if os.access(file,os.R_OK) == 0:
268 reject_message = reject_message + "Rejected: Can't read `%s'.\n" % (file)
269 files[file]["type"] = "unreadable";
271 # If it's byhand skip remaining checks
272 if files[file]["section"] == "byhand":
273 files[file]["byhand"] = 1;
274 files[file]["type"] = "byhand";
275 # Checks for a binary package...
276 elif re_isadeb.match(file) != None:
277 # Extract package information using dpkg-deb
278 control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))
280 # Check for mandatory fields
281 if control.Find("Package") == None:
282 reject_message = reject_message + "Rejected: %s: No package field in control.\n" % (file)
283 if control.Find("Architecture") == None:
284 reject_message = reject_message + "Rejected: %s: No architecture field in control.\n" % (file)
285 if control.Find("Version") == None:
286 reject_message = reject_message + "Rejected: %s: No version field in control.\n" % (file)
288 # Ensure the package name matches the one give in the .changes
289 if not changes["binary"].has_key(control.Find("Package", "")):
290 reject_message = reject_message + "Rejected: %s: control file lists name as `%s', which isn't in changes file.\n" % (file, control.Find("Package", ""))
292 # Validate the architecture
293 if not Cnf.has_key("Suite::Unstable::Architectures::%s" % (control.Find("Architecture", ""))):
294 reject_message = reject_message + "Rejected: Unknown architecture '%s'.\n" % (control.Find("Architecture", ""))
296 # Check the architecture matches the one given in the .changes
297 if not changes["architecture"].has_key(control.Find("Architecture", "")):
298 reject_message = reject_message + "Rejected: %s: control file lists arch as `%s', which isn't in changes file.\n" % (file, control.Find("Architecture", ""))
299 # Check the section & priority match those given in the .changes (non-fatal)
300 if control.Find("Section") != None and files[file]["section"] != "" and files[file]["section"] != control.Find("Section"):
301 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"])
302 if control.Find("Priority") != None and files[file]["priority"] != "" and files[file]["priority"] != control.Find("Priority"):
303 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"])
305 epochless_version = utils.re_no_epoch.sub('', control.Find("Version", ""))
307 files[file]["package"] = control.Find("Package");
308 files[file]["architecture"] = control.Find("Architecture");
309 files[file]["version"] = control.Find("Version");
310 files[file]["maintainer"] = control.Find("Maintainer", "");
311 if file[-5:] == ".udeb":
312 files[file]["dbtype"] = "udeb";
313 elif file[-4:] == ".deb":
314 files[file]["dbtype"] = "deb";
316 reject_message = reject_message + "Rejected: %s is neither a .deb or a .udeb.\n " % (file);
317 files[file]["type"] = "deb";
318 files[file]["fullname"] = "%s_%s_%s.deb" % (control.Find("Package", ""), epochless_version, control.Find("Architecture", ""))
319 files[file]["source"] = control.Find("Source", "");
320 if files[file]["source"] == "":
321 files[file]["source"] = files[file]["package"];
322 # Checks for a source package...
324 m = re_issource.match(file)
326 files[file]["package"] = m.group(1)
327 files[file]["version"] = m.group(2)
328 files[file]["type"] = m.group(3)
330 # Ensure the source package name matches the Source filed in the .changes
331 if changes["source"] != files[file]["package"]:
332 reject_message = reject_message + "Rejected: %s: changes file doesn't say %s for Source\n" % (file, files[file]["package"])
334 # Ensure the source version matches the version in the .changes file
335 if files[file]["type"] == "orig.tar.gz":
336 changes_version = changes["chopversion2"]
338 changes_version = changes["chopversion"]
339 if changes_version != files[file]["version"]:
340 reject_message = reject_message + "Rejected: %s: should be %s according to changes file.\n" % (file, changes_version)
342 # Ensure the .changes lists source in the Architecture field
343 if not changes["architecture"].has_key("source"):
344 reject_message = reject_message + "Rejected: %s: changes file doesn't list `source' in Architecture field.\n" % (file)
346 # Check the signature of a .dsc file
347 if files[file]["type"] == "dsc":
348 check_signature(file)
350 files[file]["fullname"] = file
352 # Not a binary or source package? Assume byhand...
354 files[file]["byhand"] = 1;
355 files[file]["type"] = "byhand";
357 files[file]["oldfiles"] = {}
358 for suite in changes["distribution"].keys():
360 if files[file].has_key("byhand"):
363 if Cnf.has_key("Suite:%s::Components" % (suite)) and not Cnf.has_key("Suite::%s::Components::%s" % (suite, files[file]["component"])):
364 reject_message = reject_message + "Rejected: unknown component `%s' for suite `%s'.\n" % (files[file]["component"], suite)
367 # See if the package is NEW
368 if not in_override_p(files[file]["package"], files[file]["component"], suite):
369 files[file]["new"] = 1
371 # Find any old binary packages
372 if files[file]["type"] == "deb":
373 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"
374 % (files[file]["package"], suite, files[file]["architecture"]))
375 oldfiles = q.dictresult()
376 for oldfile in oldfiles:
377 files[file]["oldfiles"][suite] = oldfile
378 # Check versions [NB: per-suite only; no cross-suite checking done (yet)]
379 if apt_pkg.VersionCompare(files[file]["version"], oldfile["version"]) != 1:
380 if Cnf["Dinstall::Options::No-Version-Check"]:
381 reject_message = reject_message + "Overriden rejection"
383 reject_message = reject_message + "Rejected"
384 reject_message = reject_message + ": %s Old version `%s' >= new version `%s'.\n" % (file, oldfile["version"], files[file]["version"])
385 # Check for existing copies of the file
386 if not changes.has_key("stable upload"):
387 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"]))
388 if q.getresult() != []:
389 reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (file)
391 # Find any old .dsc files
392 elif files[file]["type"] == "dsc":
393 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"
394 % (files[file]["package"], suite))
395 oldfiles = q.dictresult()
396 if len(oldfiles) >= 1:
397 files[file]["oldfiles"][suite] = oldfiles[0]
399 # Validate the component
400 component = files[file]["component"];
401 component_id = db_access.get_component_id(component);
402 if component_id == -1:
403 reject_message = reject_message + "Rejected: file '%s' has unknown component '%s'.\n" % (file, component);
406 # Check the md5sum & size against existing files (if any)
407 location = Cnf["Dir::PoolDir"];
408 files[file]["location id"] = db_access.get_location_id (location, component, archive);
409 files_id = db_access.get_files_id(component + '/' + file, files[file]["size"], files[file]["md5sum"], files[file]["location id"]);
411 reject_message = reject_message + "Rejected: INTERNAL ERROR, get_files_id() returned multiple matches for %s.\n" % (file)
413 reject_message = reject_message + "Rejected: md5sum and/or size mismatch on existing copy of %s.\n" % (file)
414 files[file]["files id"] = files_id
416 # Check for packages that have moved from one component to another
417 if files[file]["oldfiles"].has_key(suite) and files[file]["oldfiles"][suite]["name"] != files[file]["component"]:
418 files[file]["othercomponents"] = files[file]["oldfiles"][suite]["name"];
421 if string.find(reject_message, "Rejected:") != -1:
426 ###############################################################################
429 global dsc, dsc_files, reject_message, reprocess, orig_tar_id;
431 for file in files.keys():
432 if files[file]["type"] == "dsc":
434 dsc = utils.parse_changes(file)
435 except utils.cant_open_exc:
436 reject_message = reject_message + "Rejected: can't read changes file '%s'.\n" % (filename)
438 except utils.changes_parse_error_exc, line:
439 reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line)
442 dsc_files = utils.build_file_list(dsc, 1)
443 except utils.no_files_exc:
444 reject_message = reject_message + "Rejected: no Files: field in .dsc file.\n";
447 # Try and find all files mentioned in the .dsc. This has
448 # to work harder to cope with the multiple possible
449 # locations of an .orig.tar.gz.
450 for dsc_file in dsc_files.keys():
451 if files.has_key(dsc_file):
452 actual_md5 = files[dsc_file]["md5sum"]
453 found = "%s in incoming" % (dsc_file)
454 # Check the file does not already exist in the archive
455 if not changes.has_key("stable upload"):
456 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)));
457 if q.getresult() != []:
458 reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (dsc_file)
459 elif dsc_file[-12:] == ".orig.tar.gz":
461 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)));
464 old_file = ql[0][0] + ql[0][1];
465 actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
467 suite_type = ql[0][2];
469 if suite_type == "legacy" or suite_type == "legacy-mixed":
470 orig_tar_id = ql[0][3];
472 # Not there? Check in Incoming...
473 # [See comment above process_it() for explanation
474 # of why this is necessary...]
475 if os.access(dsc_file, os.R_OK) != 0:
476 files[dsc_file] = {};
477 files[dsc_file]["size"] = os.stat(dsc_file)[stat.ST_SIZE];
478 files[dsc_file]["md5sum"] = dsc_files[dsc_file]["md5sum"];
479 files[dsc_file]["section"] = files[file]["section"];
480 files[dsc_file]["priority"] = files[file]["priority"];
481 files[dsc_file]["component"] = files[file]["component"];
485 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);
488 reject_message = reject_message + "Rejected: %s refers to %s, but I can't find it in Incoming." % (file, dsc_file);
490 if actual_md5 != dsc_files[dsc_file]["md5sum"]:
491 reject_message = reject_message + "Rejected: md5sum for %s doesn't match %s.\n" % (found, file)
493 if string.find(reject_message, "Rejected:") != -1:
498 ###############################################################################
500 def check_md5sums ():
501 global reject_message;
503 for file in files.keys():
505 file_handle = utils.open_file(file,"r");
506 except utils.cant_open_exc:
509 if apt_pkg.md5sum(file_handle) != files[file]["md5sum"]:
510 reject_message = reject_message + "Rejected: md5sum check failed for %s.\n" % (file);
512 #####################################################################################################################
514 def action (changes_filename):
515 byhand = confirm = suites = summary = new = "";
517 # changes["distribution"] may not exist in corner cases
518 # (e.g. unreadable changes files)
519 if not changes.has_key("distribution"):
520 changes["distribution"] = {};
522 for suite in changes["distribution"].keys():
523 if Cnf.has_key("Suite::%s::Confirm"):
524 confirm = confirm + suite + ", "
525 suites = suites + suite + ", "
526 confirm = confirm[:-2]
529 for file in files.keys():
530 if files[file].has_key("byhand"):
532 summary = summary + file + " byhand\n"
533 elif files[file].has_key("new"):
535 summary = summary + "(new) %s %s %s\n" % (file, files[file]["priority"], files[file]["section"])
536 if files[file].has_key("othercomponents"):
537 summary = summary + "WARNING: Already present in %s distribution.\n" % (files[file]["othercomponents"])
538 if files[file]["type"] == "deb":
539 summary = summary + apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))["Description"] + '\n';
541 files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"])
542 destination = Cnf["Dir::PoolRoot"] + files[file]["pool name"] + file
543 summary = summary + file + "\n to " + destination + "\n"
545 short_summary = summary;
547 # This is for direport's benefit...
548 f = re_fdnic.sub("\n .\n", changes.get("changes",""));
550 if confirm or byhand or new:
551 summary = summary + "Changes: " + f;
553 summary = summary + announce (short_summary, 0)
555 (prompt, answer) = ("", "XXX")
556 if Cnf["Dinstall::Options::No-Action"] or Cnf["Dinstall::Options::Automatic"]:
559 if string.find(reject_message, "Rejected") != -1:
560 if time.time()-os.path.getmtime(changes_filename) < 86400:
561 print "SKIP (too new)\n" + reject_message,;
562 prompt = "[S]kip, Manual reject, Quit ?";
564 print "REJECT\n" + reject_message,;
565 prompt = "[R]eject, Manual reject, Skip, Quit ?";
566 if Cnf["Dinstall::Options::Automatic"]:
569 print "NEW to %s\n%s%s" % (suites, reject_message, summary),;
570 prompt = "[S]kip, New ack, Manual reject, Quit ?";
571 if Cnf["Dinstall::Options::Automatic"] and Cnf["Dinstall::Options::Ack-New"]:
574 print "BYHAND\n" + reject_message + summary,;
575 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
577 print "CONFIRM to %s\n%s%s" % (confirm, reject_message, summary),
578 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
580 print "INSTALL\n" + reject_message + summary,;
581 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
582 if Cnf["Dinstall::Options::Automatic"]:
585 while string.find(prompt, answer) == -1:
587 answer = utils.our_raw_input()
588 m = re_default_answer.match(prompt)
591 answer = string.upper(answer[:1])
594 reject (changes_filename, "");
596 manual_reject (changes_filename);
598 install (changes_filename, summary, short_summary);
600 acknowledge_new (changes_filename, summary);
604 #####################################################################################################################
606 def install (changes_filename, summary, short_summary):
607 global install_count, install_bytes
609 # Stable uploads are a special case
610 if changes.has_key("stable upload"):
611 stable_install (changes_filename, summary, short_summary);
616 archive = utils.where_am_i();
618 # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
619 projectB.query("BEGIN WORK");
621 # Add the .dsc file to the DB
622 for file in files.keys():
623 if files[file]["type"] == "dsc":
624 package = dsc["source"]
625 version = dsc["version"] # NB: not files[file]["version"], that has no epoch
626 maintainer = dsc["maintainer"]
627 maintainer = string.replace(maintainer, "'", "\\'")
628 maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
629 filename = files[file]["pool name"] + file;
630 dsc_location_id = files[file]["location id"];
631 if not files[file]["files id"]:
632 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], dsc_location_id)
633 projectB.query("INSERT INTO source (source, version, maintainer, file) VALUES ('%s', '%s', %d, %d)"
634 % (package, version, maintainer_id, files[file]["files id"]))
636 for suite in changes["distribution"].keys():
637 suite_id = db_access.get_suite_id(suite);
638 projectB.query("INSERT INTO src_associations (suite, source) VALUES (%d, currval('source_id_seq'))" % (suite_id))
640 # Add the source files to the DB (files and dsc_files)
641 projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files[file]["files id"]));
642 for dsc_file in dsc_files.keys():
643 filename = files[file]["pool name"] + dsc_file;
644 files_id = db_access.get_files_id(filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], files[file]["location id"]);
645 # FIXME: needs to check for -1/-2 and or handle exception
647 files_id = db_access.set_files_id (filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], files[file]["location id"]);
648 projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files_id));
651 # Add the .deb files to the DB
652 for file in files.keys():
653 if files[file]["type"] == "deb":
654 package = files[file]["package"]
655 version = files[file]["version"]
656 maintainer = files[file]["maintainer"]
657 maintainer = string.replace(maintainer, "'", "\\'")
658 maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
659 architecture = files[file]["architecture"]
660 architecture_id = db_access.get_architecture_id (architecture);
661 type = files[file]["dbtype"];
662 component = files[file]["component"]
663 source = files[file]["source"]
665 if string.find(source, "(") != -1:
666 m = utils.re_extract_src_version.match(source)
668 source_version = m.group(2)
669 if not source_version:
670 source_version = version
671 filename = files[file]["pool name"] + file;
672 if not files[file]["files id"]:
673 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], files[file]["location id"])
674 source_id = db_access.get_source_id (source, source_version);
676 projectB.query("INSERT INTO binaries (package, version, maintainer, source, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, %d, '%s')"
677 % (package, version, maintainer_id, source_id, architecture_id, files[file]["files id"], type));
679 projectB.query("INSERT INTO binaries (package, version, maintainer, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, '%s')"
680 % (package, version, maintainer_id, architecture_id, files[file]["files id"], type));
681 for suite in changes["distribution"].keys():
682 suite_id = db_access.get_suite_id(suite);
683 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES (%d, currval('binaries_id_seq'))" % (suite_id));
685 # Install the files into the pool
686 for file in files.keys():
687 if files[file].has_key("byhand"):
689 destination = Cnf["Dir::PoolDir"] + files[file]["pool name"] + file
690 destdir = os.path.dirname(destination)
691 utils.move (file, destination)
692 install_bytes = install_bytes + float(files[file]["size"])
694 # Copy the .changes file across for suite which need it.
695 for suite in changes["distribution"].keys():
696 if Cnf.has_key("Suite::%s::CopyChanges" % (suite)):
697 utils.copy (changes_filename, Cnf["Dir::RootDir"] + Cnf["Suite::%s::CopyChanges" % (suite)]);
699 # If the .orig.tar.gz is in a legacy directory we need to poolify
700 # it, so that apt-get source (and anything else that goes by the
701 # "Directory:" field in the Sources.gz file) works.
702 if orig_tar_id != None:
703 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));
706 # First move the files to the new location
707 legacy_filename = qid["path"]+qid["filename"];
708 pool_location = utils.poolify (changes["source"], files[file]["component"]);
709 pool_filename = pool_location + os.path.basename(qid["filename"]);
710 destination = Cnf["Dir::PoolDir"] + pool_location
711 utils.move(legacy_filename, destination);
712 # Update the DB: files table
713 new_files_id = db_access.set_files_id(pool_filename, qid["size"], qid["md5sum"], dsc_location_id);
714 # Update the DB: dsc_files table
715 projectB.query("INSERT INTO dsc_files (source, file) VALUES (%s, %s)" % (qid["source"], new_files_id));
716 # Update the DB: source table
717 if legacy_filename[-4:] == ".dsc":
718 projectB.query("UPDATE source SET file = %s WHERE id = %d" % (new_files_id, qid["source"]));
721 # Remove old data from the DB: dsc_files table
722 projectB.query("DELETE FROM dsc_files WHERE id = %s" % (qid["dsc_files_id"]));
723 # Remove old data from the DB: files table
724 projectB.query("DELETE FROM files WHERE id = %s" % (qid["files_id"]));
726 projectB.query("COMMIT WORK");
728 utils.move (changes_filename, Cnf["Dir::IncomingDir"] + 'DONE/' + os.path.basename(changes_filename))
730 install_count = install_count + 1;
732 if not Cnf["Dinstall::Options::No-Mail"]:
733 mail_message = """Return-Path: %s
736 Bcc: troup@auric.debian.org
737 Subject: %s INSTALLED
743 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], os.path.basename(changes_filename), reject_message, summary, installed_footer)
744 utils.send_mail (mail_message, "")
745 announce (short_summary, 1)
747 #####################################################################################################################
749 def stable_install (changes_filename, summary, short_summary):
750 global install_count, install_bytes
752 print "Installing to stable."
754 archive = utils.where_am_i();
756 # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
757 projectB.query("BEGIN WORK");
759 # Add the .dsc file to the DB
760 for file in files.keys():
761 if files[file]["type"] == "dsc":
762 package = dsc["source"]
763 version = dsc["version"] # NB: not files[file]["version"], that has no epoch
764 q = projectB.query("SELECT id FROM source WHERE source = '%s' AND version = '%s'" % (package, version))
767 sys.stderr.write("INTERNAL ERROR: couldn't find '%s' (%s) in source table.\n" % (package, version));
769 source_id = ql[0][0];
770 suite_id = db_access.get_suite_id('proposed-updates');
771 projectB.query("DELETE FROM src_associations WHERE suite = '%s' AND source = '%s'" % (suite_id, source_id));
772 suite_id = db_access.get_suite_id('stable');
773 projectB.query("INSERT INTO src_associations (suite, source) VALUES ('%s', '%s')" % (suite_id, source_id));
775 # Add the .deb files to the DB
776 for file in files.keys():
777 if files[file]["type"] == "deb":
778 package = files[file]["package"]
779 version = files[file]["version"]
780 architecture = files[file]["architecture"]
781 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))
784 sys.stderr.write("INTERNAL ERROR: couldn't find '%s' (%s for %s architecture) in binaries table.\n" % (package, version, architecture));
786 binary_id = ql[0][0];
787 suite_id = db_access.get_suite_id('proposed-updates');
788 projectB.query("DELETE FROM bin_associations WHERE suite = '%s' AND bin = '%s'" % (suite_id, binary_id));
789 suite_id = db_access.get_suite_id('stable');
790 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES ('%s', '%s')" % (suite_id, binary_id));
792 projectB.query("COMMIT WORK");
794 utils.move (changes_filename, Cnf["Rhona::Morgue"] + os.path.basename(changes_filename));
796 # Update the Stable ChangeLog file
798 new_changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + ".ChangeLog";
799 changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + "ChangeLog";
800 if os.path.exists(new_changelog_filename):
801 os.unlink (new_changelog_filename);
803 new_changelog = utils.open_file(new_changelog_filename, 'w');
804 for file in files.keys():
805 if files[file]["type"] == "deb":
806 new_changelog.write("stable/%s/binary-%s/%s\n" % (files[file]["component"], files[file]["architecture"], file));
807 elif re_issource.match(file) != None:
808 new_changelog.write("stable/%s/source/%s\n" % (files[file]["component"], file));
810 new_changelog.write("%s\n" % (file));
811 chop_changes = re_fdnic.sub("\n", changes["changes"]);
812 new_changelog.write(chop_changes + '\n\n');
813 if os.access(changelog_filename, os.R_OK) != 0:
814 changelog = utils.open_file(changelog_filename, 'r');
815 new_changelog.write(changelog.read());
816 new_changelog.close();
817 if os.access(changelog_filename, os.R_OK) != 0:
818 os.unlink(changelog_filename);
819 utils.move(new_changelog_filename, changelog_filename);
821 install_count = install_count + 1;
823 if not Cnf["Dinstall::Options::No-Mail"]:
824 mail_message = """Return-Path: %s
827 Bcc: troup@auric.debian.org
828 Subject: %s INSTALLED into stable
834 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], os.path.basename(changes_filename), reject_message, summary, installed_footer)
835 utils.send_mail (mail_message, "")
836 announce (short_summary, 1)
838 #####################################################################################################################
840 def reject (changes_filename, manual_reject_mail_filename):
843 base_changes_filename = os.path.basename(changes_filename);
844 reason_filename = re_changes.sub("reason", base_changes_filename);
845 reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename);
847 # Move the .changes files and it's contents into REJECT/
848 utils.move (changes_filename, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], base_changes_filename));
849 for file in files.keys():
850 if os.access(file,os.R_OK) == 0:
851 utils.move (file, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], file));
853 # If this is not a manual rejection generate the .reason file and rejection mail message
854 if manual_reject_mail_filename == "":
855 if os.path.exists(reject_filename):
856 os.unlink(reject_filename);
857 fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
858 os.write(fd, reject_message);
860 reject_mail_message = """From: %s
862 Bcc: troup@auric.debian.org
867 %s""" % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, reject_message, reject_footer);
868 else: # Have a manual rejection file to use
869 reject_mail_message = ""; # avoid <undef>'s
871 # Send the rejection mail if appropriate
872 if not Cnf["Dinstall::Options::No-Mail"]:
873 utils.send_mail (reject_mail_message, manual_reject_mail_filename);
875 ##################################################################
877 def manual_reject (changes_filename):
878 # Build up the rejection email
879 user_email_address = string.replace(string.split(pwd.getpwuid(os.getuid())[4],',')[0], '.', '')
880 user_email_address = user_email_address + " <%s@%s>" % (pwd.getpwuid(os.getuid())[0], Cnf["Dinstall::MyHost"])
881 manual_reject_message = Cnf.get("Dinstall::Options::Manual-Reject", "")
883 reject_mail_message = """From: %s
886 Bcc: troup@auric.debian.org
892 %s""" % (user_email_address, Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, manual_reject_message, reject_message, reject_footer)
894 # Write the rejection email out as the <foo>.reason file
895 reason_filename = re_changes.sub("reason", os.path.basename(changes_filename));
896 reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename)
897 if os.path.exists(reject_filename):
898 os.unlink(reject_filename);
899 fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
900 os.write(fd, reject_mail_message);
903 # If we weren't given one, spawn an editor so the user can add one in
904 if manual_reject_message == "":
905 result = os.system("vi +6 %s" % (reject_file))
907 sys.stderr.write ("vi invocation failed for `%s'!" % (reject_file))
910 # Then process it as if it were an automatic rejection
911 reject (changes_filename, reject_filename)
913 #####################################################################################################################
915 def acknowledge_new (changes_filename, summary):
918 changes_filename = os.path.basename(changes_filename);
920 new_ack_new[changes_filename] = 1;
922 if new_ack_old.has_key(changes_filename):
923 print "Ack already sent.";
926 print "Sending new ack.";
927 if not Cnf["Dinstall::Options::No-Mail"]:
928 new_ack_message = """Return-Path: %s
931 Bcc: troup@auric.debian.org
935 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, summary, new_ack_footer);
936 utils.send_mail(new_ack_message,"");
938 #####################################################################################################################
940 def announce (short_summary, action):
941 # Only do announcements for source uploads with a recent dpkg-dev installed
942 if float(changes.get("format", 0)) < 1.6 or not changes["architecture"].has_key("source"):
948 for dist in changes["distribution"].keys():
949 list = Cnf.Find("Suite::%s::Announce" % (dist))
950 if list == None or lists_done.has_key(list):
953 summary = summary + "Announcing to %s\n" % (list)
956 mail_message = """Return-Path: %s
959 Bcc: troup@auric.debian.org
960 Subject: Installed %s %s (%s)
966 """ % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], list, changes["source"], changes["version"], string.join(changes["architecture"].keys(), ' ' ),
967 changes["filecontents"], short_summary)
968 utils.send_mail (mail_message, "")
970 (dsc_rfc822, dsc_name, dsc_email) = utils.fix_maintainer (dsc.get("maintainer",Cnf["Dinstall::MyEmailAddress"]));
971 bugs = changes["closes"].keys()
973 if dsc_name == changes["maintainername"]:
974 summary = summary + "Closing bugs: "
976 summary = summary + "%s " % (bug)
978 mail_message = """Return-Path: %s
980 To: %s-close@bugs.debian.org
981 Bcc: troup@auric.debian.org
982 Subject: Bug#%s: fixed in %s %s
984 We believe that the bug you reported is fixed in the latest version of
985 %s, which has been installed in the Debian FTP archive:
987 %s""" % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], bug, bug, changes["source"], changes["version"], changes["source"], short_summary)
989 if changes["distribution"].has_key("stable"):
990 mail_message = mail_message + """Note that this package is not part of the released stable Debian
991 distribution. It may have dependencies on other unreleased software,
992 or other instabilities. Please take care if you wish to install it.
993 The update will eventually make its way into the next released Debian
996 mail_message = mail_message + """A summary of the changes between this version and the previous one is
999 Thank you for reporting the bug, which will now be closed. If you
1000 have further comments please address them to %s@bugs.debian.org,
1001 and the maintainer will reopen the bug report if appropriate.
1003 Debian distribution maintenance software
1005 %s (supplier of updated %s package)
1007 (This message was generated automatically at their request; if you
1008 believe that there is a problem with it please contact the archive
1009 administrators by mailing ftpmaster@debian.org)
1012 %s""" % (bug, changes["maintainer"], changes["source"], changes["filecontents"])
1014 utils.send_mail (mail_message, "")
1016 summary = summary + "Setting bugs to severity fixed: "
1017 control_message = ""
1019 summary = summary + "%s " % (bug)
1020 control_message = control_message + "severity %s fixed\n" % (bug)
1021 if action and control_message != "":
1022 mail_message = """Return-Path: %s
1024 To: control@bugs.debian.org
1025 Bcc: troup@auric.debian.org, %s
1026 Subject: Fixed in NMU of %s %s
1031 This message was generated automatically in response to a
1032 non-maintainer upload. The .changes file follows.
1035 """ % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes["maintainer822"], changes["source"], changes["version"], control_message, changes["filecontents"])
1036 utils.send_mail (mail_message, "")
1037 summary = summary + "\n"
1041 ###############################################################################
1043 # reprocess is necessary for the case of foo_1.2-1 and foo_1.2-2 in
1044 # Incoming. -1 will reference the .orig.tar.gz, but -2 will not.
1045 # dsccheckdistrib() can find the .orig.tar.gz but it will not have
1046 # processed it during it's checks of -2. If -1 has been deleted or
1047 # otherwise not checked by da-install, the .orig.tar.gz will not have
1048 # been checked at all. To get round this, we force the .orig.tar.gz
1049 # into the .changes structure and reprocess the .changes file.
1051 def process_it (changes_file):
1052 global reprocess, orig_tar_id;
1057 # Absolutize the filename to avoid the requirement of being in the
1058 # same directory as the .changes file.
1059 changes_file = os.path.abspath(changes_file);
1061 # And since handling of installs to stable munges with the CWD;
1062 # save and restore it.
1065 check_signature (changes_file);
1066 check_changes (changes_file);
1073 action(changes_file);
1078 ###############################################################################
1081 global Cnf, projectB, reject_message, install_bytes, new_ack_old
1085 Cnf = apt_pkg.newConfiguration();
1086 apt_pkg.ReadConfigFileISC(Cnf,utils.which_conf_file());
1088 Arguments = [('a',"automatic","Dinstall::Options::Automatic"),
1089 ('d',"debug","Dinstall::Options::Debug", "IntVal"),
1090 ('h',"help","Dinstall::Options::Help"),
1091 ('k',"ack-new","Dinstall::Options::Ack-New"),
1092 ('m',"manual-reject","Dinstall::Options::Manual-Reject", "HasArg"),
1093 ('n',"no-action","Dinstall::Options::No-Action"),
1094 ('p',"no-lock", "Dinstall::Options::No-Lock"),
1095 ('r',"no-version-check", "Dinstall::Options::No-Version-Check"),
1096 ('s',"no-mail", "Dinstall::Options::No-Mail"),
1097 ('u',"override-distribution", "Dinstall::Options::Override-Distribution", "HasArg"),
1098 ('v',"version","Dinstall::Options::Version")];
1100 changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
1102 if Cnf["Dinstall::Options::Help"]:
1105 if Cnf["Dinstall::Options::Version"]:
1106 print "katie version 0.0000000000";
1109 postgresql_user = None; # Default == Connect as user running program.
1111 # -n/--dry-run invalidates some other options which would involve things happening
1112 if Cnf["Dinstall::Options::No-Action"]:
1113 Cnf["Dinstall::Options::Automatic"] = ""
1114 Cnf["Dinstall::Options::Ack-New"] = ""
1115 postgresql_user = Cnf["DB::ROUser"];
1117 projectB = pg.connect('projectb', Cnf["DB::Host"], int(Cnf["DB::Port"]), None, None, postgresql_user);
1119 db_access.init(Cnf, projectB);
1121 # Check that we aren't going to clash with the daily cron job
1123 if os.path.exists("%s/Archive_Maintenance_In_Progress" % (Cnf["Dir::RootDir"])) and not Cnf["Dinstall::Options::No-Lock"]:
1124 sys.stderr.write("Archive maintenance in progress. Try again later.\n");
1127 # Obtain lock if not in no-action mode
1129 if not Cnf["Dinstall::Options::No-Action"]:
1130 lock_fd = os.open(Cnf["Dinstall::LockFile"], os.O_RDWR);
1131 fcntl.lockf(lock_fd, FCNTL.F_TLOCK);
1133 # Read in the list of already-acknowledged NEW packages
1134 new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'r');
1136 for line in new_ack_list.readlines():
1137 new_ack_old[line[:-1]] = 1;
1138 new_ack_list.close();
1140 # Process the changes files
1141 for changes_file in changes_files:
1143 print "\n" + changes_file;
1144 process_it (changes_file);
1147 if install_bytes > 10000:
1148 install_bytes = install_bytes / 1000;
1149 install_mag = " Kb";
1150 if install_bytes > 10000:
1151 install_bytes = install_bytes / 1000;
1152 install_mag = " Mb";
1155 if install_count > 1:
1157 sys.stderr.write("Installed %d package %s, %d%s.\n" % (install_count, sets, int(install_bytes), install_mag))
1159 # Write out the list of already-acknowledged NEW packages
1160 if Cnf["Dinstall::Options::Ack-New"]:
1161 new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'w')
1162 for i in new_ack_new.keys():
1163 new_ack_list.write(i+'\n')
1164 new_ack_list.close()
1167 if __name__ == '__main__':