3 # Installs Debian packaes
4 # Copyright (C) 2000 James Troup <james@nocrew.org>
5 # $Id: katie,v 1.4 2000-11-27 03:15:26 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 # Find any old .dsc files
379 elif files[file]["type"] == "dsc":
380 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"
381 % (files[file]["package"], suite))
382 oldfiles = q.dictresult()
383 if len(oldfiles) >= 1:
384 files[file]["oldfiles"][suite] = oldfiles[0]
386 # Validate the component
387 component = files[file]["component"];
388 component_id = db_access.get_component_id(component);
389 if component_id == -1:
390 reject_message = reject_message + "Rejected: file '%s' has unknown component '%s'.\n" % (file, component);
393 # Check the md5sum & size against existing files (if any)
394 location = Cnf["Dir::PoolDir"];
395 files[file]["location id"] = db_access.get_location_id (location, component, archive);
396 files_id = db_access.get_files_id(component + '/' + file, files[file]["size"], files[file]["md5sum"], files[file]["location id"]);
398 reject_message = reject_message + "Rejected: INTERNAL ERROR, get_files_id() returned multiple matches for %s.\n" % (file)
400 reject_message = reject_message + "Rejected: md5sum and/or size mismatch on existing copy of %s.\n" % (file)
401 files[file]["files id"] = files_id
403 # Check for packages that have moved from one component to another
404 if files[file]["oldfiles"].has_key(suite) and files[file]["oldfiles"][suite]["name"] != files[file]["component"]:
405 files[file]["othercomponents"] = files[file]["oldfiles"][suite]["name"];
408 if string.find(reject_message, "Rejected:") != -1:
413 ###############################################################################
416 global dsc, dsc_files, reject_message, reprocess, orig_tar_id;
418 for file in files.keys():
419 if files[file]["type"] == "dsc":
421 dsc = utils.parse_changes(file)
422 except utils.cant_open_exc:
423 reject_message = reject_message + "Rejected: can't read changes file '%s'.\n" % (filename)
425 except utils.changes_parse_error_exc, line:
426 reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line)
429 dsc_files = utils.build_file_list(dsc, 1)
430 except utils.no_files_exc:
431 reject_message = reject_message + "Rejected: no Files: field in .dsc file.\n";
434 # Try and find all files mentioned in the .dsc. This has
435 # to work harder to cope with the multiple possible
436 # locations of an .orig.tar.gz.
437 for dsc_file in dsc_files.keys():
438 if files.has_key(dsc_file):
439 actual_md5 = files[dsc_file]["md5sum"]
440 found = "%s in incoming" % (dsc_file)
441 elif dsc_file[-12:] == ".orig.tar.gz":
443 # See comment above process_it() for explanation...
444 if os.access(dsc_file, os.R_OK) != 0:
445 files[dsc_file] = {};
446 files[dsc_file]["size"] = os.stat(dsc_file)[stat.ST_SIZE];
447 files[dsc_file]["md5sum"] = dsc_files[dsc_file]["md5sum"];
448 files[dsc_file]["section"] = files[file]["section"];
449 files[dsc_file]["priority"] = files[file]["priority"];
450 files[dsc_file]["component"] = files[file]["component"];
454 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));
457 old_file = ql[0][0] + ql[0][1];
458 actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
460 suite_type = ql[0][2];
462 if suite_type == "legacy" or suite_type == "legacy-mixed":
463 orig_tar_id = ql[0][3];
465 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);
468 reject_message = reject_message + "Rejected: %s refers to %s, but I can't find it in Incoming." % (file, dsc_file);
470 if actual_md5 != dsc_files[dsc_file]["md5sum"]:
471 reject_message = reject_message + "Rejected: md5sum for %s doesn't match %s.\n" % (found, file)
473 if string.find(reject_message, "Rejected:") != -1:
478 ###############################################################################
480 def check_md5sums ():
481 global reject_message;
483 for file in files.keys():
485 file_handle = utils.open_file(file,"r");
486 except utils.cant_open_exc:
489 if apt_pkg.md5sum(file_handle) != files[file]["md5sum"]:
490 reject_message = reject_message + "Rejected: md5sum check failed for %s.\n" % (file);
492 #####################################################################################################################
494 def action (changes_filename):
495 byhand = confirm = suites = summary = new = "";
497 # changes["distribution"] may not exist in corner cases
498 # (e.g. unreadable changes files)
499 if not changes.has_key("distribution"):
500 changes["distribution"] = {};
502 for suite in changes["distribution"].keys():
503 if Cnf.has_key("Suite::%s::Confirm"):
504 confirm = confirm + suite + ", "
505 suites = suites + suite + ", "
506 confirm = confirm[:-2]
509 for file in files.keys():
510 if files[file].has_key("byhand"):
512 summary = summary + file + " byhand\n"
513 elif files[file].has_key("new"):
515 summary = summary + "(new) %s %s %s\n" % (file, files[file]["priority"], files[file]["section"])
516 if files[file].has_key("othercomponents"):
517 summary = summary + "WARNING: Already present in %s distribution.\n" % (files[file]["othercomponents"])
518 if files[file]["type"] == "deb":
519 summary = summary + apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))["Description"] + '\n';
521 files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"])
522 destination = Cnf["Dir::PoolRoot"] + files[file]["pool name"] + file
523 summary = summary + file + "\n to " + destination + "\n"
525 short_summary = summary;
527 # This is for direport's benefit...
528 f = re_fdnic.sub("\n .\n", changes.get("changes",""));
530 if confirm or byhand or new:
531 summary = summary + "Changes: " + f;
533 summary = summary + announce (short_summary, 0)
535 (prompt, answer) = ("", "XXX")
536 if Cnf["Dinstall::Options::No-Action"] or Cnf["Dinstall::Options::Automatic"]:
539 if string.find(reject_message, "Rejected") != -1:
540 if time.time()-os.path.getmtime(changes_filename) < 86400:
541 print "SKIP (too new)\n" + reject_message,;
542 prompt = "[S]kip, Manual reject, Quit ?";
544 print "REJECT\n" + reject_message,;
545 prompt = "[R]eject, Manual reject, Skip, Quit ?";
546 if Cnf["Dinstall::Options::Automatic"]:
549 print "NEW to %s\n%s%s" % (suites, reject_message, summary),;
550 prompt = "[S]kip, New ack, Manual reject, Quit ?";
551 if Cnf["Dinstall::Options::Automatic"] and Cnf["Dinstall::Options::Ack-New"]:
554 print "BYHAND\n" + reject_message + summary,;
555 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
557 print "CONFIRM to %s\n%s%s" % (confirm, reject_message, summary),
558 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
560 print "INSTALL\n" + reject_message + summary,;
561 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
562 if Cnf["Dinstall::Options::Automatic"]:
565 while string.find(prompt, answer) == -1:
567 answer = utils.our_raw_input()
568 m = re_default_answer.match(prompt)
571 answer = string.upper(answer[:1])
574 reject (changes_filename, "");
576 manual_reject (changes_filename);
578 install (changes_filename, summary, short_summary);
580 acknowledge_new (changes_filename, summary);
584 #####################################################################################################################
586 def install (changes_filename, summary, short_summary):
587 global install_count, install_bytes
591 archive = utils.where_am_i();
593 # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
594 projectB.query("BEGIN WORK");
596 # Add the .dsc file to the DB
597 for file in files.keys():
598 if files[file]["type"] == "dsc":
599 package = dsc["source"]
600 version = dsc["version"] # NB: not files[file]["version"], that has no epoch
601 maintainer = dsc["maintainer"]
602 maintainer = string.replace(maintainer, "'", "\\'")
603 maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
604 filename = files[file]["pool name"] + file;
605 dsc_location_id = files[file]["location id"];
606 if not files[file]["files id"]:
607 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], dsc_location_id)
608 dsc_file_id = files[file]["files id"]
609 projectB.query("INSERT INTO source (source, version, maintainer, file) VALUES ('%s', '%s', %d, %d)"
610 % (package, version, maintainer_id, files[file]["files id"]))
612 for suite in changes["distribution"].keys():
613 suite_id = db_access.get_suite_id(suite);
614 projectB.query("INSERT INTO src_associations (suite, source) VALUES (%d, currval('source_id_seq'))" % (suite_id))
617 # Add the .diff.gz and {.orig,}.tar.gz files to the DB (files and dsc_files)
618 for file in files.keys():
619 if files[file]["type"] == "diff.gz" or files[file]["type"] == "orig.tar.gz" or files[file]["type"] == "tar.gz":
620 if not files[file]["files id"]:
621 filename = files[file]["pool name"] + file;
622 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], files[file]["location id"])
623 projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files[file]["files id"]));
625 # Add the .deb files to the DB
626 for file in files.keys():
627 if files[file]["type"] == "deb":
628 package = files[file]["package"]
629 version = files[file]["version"]
630 maintainer = files[file]["maintainer"]
631 maintainer = string.replace(maintainer, "'", "\\'")
632 maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
633 architecture = files[file]["architecture"]
634 architecture_id = db_access.get_architecture_id (architecture);
635 type = files[file]["dbtype"];
636 component = files[file]["component"]
637 source = files[file]["source"]
639 if string.find(source, "(") != -1:
640 m = utils.re_extract_src_version.match(source)
642 source_version = m.group(2)
643 if not source_version:
644 source_version = version
645 filename = files[file]["pool name"] + file;
646 if not files[file]["files id"]:
647 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], files[file]["location id"])
648 source_id = db_access.get_source_id (source, source_version);
650 projectB.query("INSERT INTO binaries (package, version, maintainer, source, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, %d, '%s')"
651 % (package, version, maintainer_id, source_id, architecture_id, files[file]["files id"], type));
653 projectB.query("INSERT INTO binaries (package, version, maintainer, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, '%s')"
654 % (package, version, maintainer_id, architecture_id, files[file]["files id"], type));
655 for suite in changes["distribution"].keys():
656 suite_id = db_access.get_suite_id(suite);
657 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES (%d, currval('binaries_id_seq'))" % (suite_id));
659 # Install the files into the pool
660 for file in files.keys():
661 if files[file].has_key("byhand"):
663 destination = Cnf["Dir::PoolDir"] + files[file]["pool name"] + file
664 destdir = os.path.dirname(destination)
665 utils.move (file, destination)
666 install_bytes = install_bytes + float(files[file]["size"])
668 # Copy the .changes file across for suite which need it.
669 for suite in changes["distribution"].keys():
670 if Cnf.has_key("Suite::%s::CopyChanges" % (suite)):
671 utils.copy (changes_filename, Cnf["Dir::RootDir"] + Cnf["Suite::%s::CopyChanges" % (suite)]);
673 # If the .orig.tar.gz is in a legacy directory we need to poolify
674 # it, so that apt-get source (and anything else that goes by the
675 # "Directory:" field in the Sources.gz file) works.
676 if orig_tar_id != None:
677 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));
680 # First move the files to the new location
681 legacy_filename = qid["path"]+qid["filename"];
682 pool_location = utils.poolify (files[file]["package"], files[file]["component"]);
683 pool_filename = pool_location + os.path.basename(qid["filename"]);
684 destination = Cnf["Dir::PoolDir"] + pool_location
685 utils.move(legacy_filename, destination);
686 # Update the DB: files table
687 new_files_id = db_access.set_files_id(pool_filename, qid["size"], qid["md5sum"], dsc_location_id);
688 # Update the DB: dsc_files table
689 projectB.query("INSERT INTO dsc_files (source, file) VALUES (%s, %s)" % (qid["source"], new_files_id));
690 # Update the DB: source table
691 if legacy_filename[-4:] == ".dsc":
692 projectB.query("UPDATE source SET file = %s WHERE id = %d" % (new_files_id, qid["source"]));
695 # Remove old data from the DB: dsc_files table
696 projectB.query("DELETE FROM dsc_files WHERE id = %s" % (qid["dsc_files_id"]));
697 # Remove old data from the DB: files table
698 projectB.query("DELETE FROM files WHERE id = %s" % (qid["files_id"]));
700 utils.move (changes_filename, Cnf["Dir::IncomingDir"] + 'DONE/' + os.path.basename(changes_filename))
702 projectB.query("COMMIT WORK");
704 install_count = install_count + 1;
706 if not Cnf["Dinstall::Options::No-Mail"]:
707 mail_message = """Return-Path: %s
710 Bcc: troup@auric.debian.org
711 Subject: %s INSTALLED
717 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, reject_message, summary, installed_footer)
718 utils.send_mail (mail_message, "")
719 announce (short_summary, 1)
721 #####################################################################################################################
723 def reject (changes_filename, manual_reject_mail_filename):
726 base_changes_filename = os.path.basename(changes_filename);
727 reason_filename = re_changes.sub("reason", base_changes_filename);
728 reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename);
730 # Move the .changes files and it's contents into REJECT/
731 utils.move (changes_filename, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], base_changes_filename));
732 for file in files.keys():
733 if os.access(file,os.R_OK) == 0:
734 utils.move (file, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], file));
736 # If this is not a manual rejection generate the .reason file and rejection mail message
737 if manual_reject_mail_filename == "":
738 if os.path.exists(reject_filename):
739 os.unlink(reject_filename);
740 fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
741 os.write(fd, reject_message);
743 reject_mail_message = """From: %s
745 Bcc: troup@auric.debian.org
750 %s""" % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, reject_message, reject_footer);
751 else: # Have a manual rejection file to use
752 reject_mail_message = ""; # avoid <undef>'s
754 # Send the rejection mail if appropriate
755 if not Cnf["Dinstall::Options::No-Mail"]:
756 utils.send_mail (reject_mail_message, manual_reject_mail_filename);
758 ##################################################################
760 def manual_reject (changes_filename):
761 # Build up the rejection email
762 user_email_address = string.replace(string.split(pwd.getpwuid(os.getuid())[4],',')[0], '.', '')
763 user_email_address = user_email_address + " <%s@%s>" % (pwd.getpwuid(os.getuid())[0], Cnf["Dinstall::MyHost"])
764 manual_reject_message = Cnf.get("Dinstall::Options::Manual-Reject", "")
766 reject_mail_message = """From: %s
769 Bcc: troup@auric.debian.org
775 %s""" % (user_email_address, Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, manual_reject_message, reject_message, reject_footer)
777 # Write the rejection email out as the <foo>.reason file
778 reason_filename = re_changes.sub("reason", os.path.basename(changes_filename));
779 reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename)
780 if os.path.exists(reject_filename):
781 os.unlink(reject_filename);
782 fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
783 os.write(fd, reject_mail_message);
786 # If we weren't given one, spawn an editor so the user can add one in
787 if manual_reject_message == "":
788 result = os.system("vi +6 %s" % (reject_file))
790 sys.stderr.write ("vi invocation failed for `%s'!" % (reject_file))
793 # Then process it as if it were an automatic rejection
794 reject (changes_filename, reject_filename)
796 #####################################################################################################################
798 def acknowledge_new (changes_filename, summary):
801 new_ack_new[changes_filename] = 1;
803 if new_ack_old.has_key(changes_filename):
804 print "Ack already sent.";
807 print "Sending new ack.";
808 if not Cnf["Dinstall::Options::No-Mail"]:
809 new_ack_message = """Return-Path: %s
812 Bcc: troup@auric.debian.org
816 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, summary, new_ack_footer);
817 utils.send_mail(new_ack_message,"");
819 #####################################################################################################################
821 def announce (short_summary, action):
822 # Only do announcements for source uploads with a recent dpkg-dev installed
823 if float(changes.get("format", 0)) < 1.6 or not changes["architecture"].has_key("source"):
829 for dist in changes["distribution"].keys():
830 list = Cnf.Find("Suite::%s::Announce" % (dist))
831 if list == None or lists_done.has_key(list):
834 summary = summary + "Announcing to %s\n" % (list)
837 mail_message = """Return-Path: %s
840 Bcc: troup@auric.debian.org
841 Subject: Installed %s %s (%s)
847 """ % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], list, changes["source"], changes["version"], string.join(changes["architecture"].keys(), ' ' ),
848 changes["filecontents"], short_summary)
849 utils.send_mail (mail_message, "")
851 (dsc_rfc822, dsc_name, dsc_email) = utils.fix_maintainer (dsc.get("maintainer",Cnf["Dinstall::MyEmailAddress"]));
852 bugs = changes["closes"].keys()
854 if dsc_name == changes["maintainername"]:
855 summary = summary + "Closing bugs: "
857 summary = summary + "%s " % (bug)
859 mail_message = """Return-Path: %s
861 To: %s-close@bugs.debian.org
862 Bcc: troup@auric.debian.org
863 Subject: Bug#%s: fixed in %s %s
865 We believe that the bug you reported is fixed in the latest version of
866 %s, which has been installed in the Debian FTP archive:
868 %s""" % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], bug, bug, changes["source"], changes["version"], changes["source"], short_summary)
870 if changes["distribution"].has_key("stable"):
871 mail_message = mail_message + """Note that this package is not part of the released stable Debian
872 distribution. It may have dependencies on other unreleased software,
873 or other instabilities. Please take care if you wish to install it.
874 The update will eventually make its way into the next released Debian
877 mail_message = mail_message + """A summary of the changes between this version and the previous one is
880 Thank you for reporting the bug, which will now be closed. If you
881 have further comments please address them to %s@bugs.debian.org,
882 and the maintainer will reopen the bug report if appropriate.
884 Debian distribution maintenance software
886 %s (supplier of updated %s package)
888 (This message was generated automatically at their request; if you
889 believe that there is a problem with it please contact the archive
890 administrators by mailing ftpmaster@debian.org)
893 %s""" % (bug, changes["maintainer"], changes["source"], changes["filecontents"])
895 utils.send_mail (mail_message, "")
897 summary = summary + "Setting bugs to severity fixed: "
900 summary = summary + "%s " % (bug)
901 control_message = control_message + "severity %s fixed\n" % (bug)
902 if action and control_message != "":
903 mail_message = """Return-Path: %s
905 To: control@bugs.debian.org
906 Bcc: troup@auric.debian.org, %s
907 Subject: Fixed in NMU of %s %s
912 This message was generated automatically in response to a
913 non-maintainer upload. The .changes file follows.
916 """ % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes["maintainer822"], changes["source"], changes["version"], control_message, changes["filecontents"])
917 utils.send_mail (mail_message, "")
918 summary = summary + "\n"
922 ###############################################################################
924 # reprocess is necessary for the case of foo_1.2-1 and foo_1.2-2 in
925 # Incoming. -1 will reference the .orig.tar.gz, but -2 will not.
926 # dsccheckdistrib() can find the .orig.tar.gz but it will not have
927 # processed it during it's checks of -2. If -1 has been deleted or
928 # otherwise not checked by da-install, the .orig.tar.gz will not have
929 # been checked at all. To get round this, we force the .orig.tar.gz
930 # into the .changes structure and reprocess the .changes file.
932 def process_it (changes_file):
933 global reprocess, orig_tar_id;
938 check_signature (changes_file);
939 check_changes (changes_file);
946 action(changes_file);
948 ###############################################################################
951 global Cnf, projectB, reject_message, install_bytes, new_ack_old
955 Cnf = apt_pkg.newConfiguration();
956 apt_pkg.ReadConfigFileISC(Cnf,utils.which_conf_file());
958 Arguments = [('a',"automatic","Dinstall::Options::Automatic"),
959 ('d',"debug","Dinstall::Options::Debug", "IntVal"),
960 ('h',"help","Dinstall::Options::Help"),
961 ('k',"ack-new","Dinstall::Options::Ack-New"),
962 ('m',"manual-reject","Dinstall::Options::Manual-Reject", "HasArg"),
963 ('n',"no-action","Dinstall::Options::No-Action"),
964 ('p',"no-lock", "Dinstall::Options::No-Lock"),
965 ('r',"no-version-check", "Dinstall::Options::No-Version-Check"),
966 ('s',"no-mail", "Dinstall::Options::No-Mail"),
967 ('u',"override-distribution", "Dinstall::Options::Override-Distribution", "HasArg"),
968 ('v',"version","Dinstall::Options::Version")];
970 changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
972 if Cnf["Dinstall::Options::Help"]:
975 if Cnf["Dinstall::Options::Version"]:
976 print "katie version 0.0000000000";
979 postgresql_user = None; # Default == Connect as user running program.
981 # -n/--dry-run invalidates some other options which would involve things happening
982 if Cnf["Dinstall::Options::No-Action"]:
983 Cnf["Dinstall::Options::Automatic"] = ""
984 Cnf["Dinstall::Options::Ack-New"] = ""
985 postgresql_user = Cnf["DB::ROUser"];
987 projectB = pg.connect('projectb', Cnf["DB::Host"], int(Cnf["DB::Port"]), None, None, postgresql_user);
989 db_access.init(Cnf, projectB);
991 # Check that we aren't going to clash with the daily cron job
993 if os.path.exists("%s/Archive_Maintenance_In_Progress" % (Cnf["Dir::RootDir"])) and not Cnf["Dinstall::Options::No-Lock"]:
994 sys.stderr.write("Archive maintenance in progress. Try again later.\n");
997 # Obtain lock if not in no-action mode
999 if not Cnf["Dinstall::Options::No-Action"]:
1000 lock_fd = os.open(Cnf["Dinstall::LockFile"], os.O_RDWR);
1001 fcntl.lockf(lock_fd, FCNTL.F_TLOCK);
1003 # Read in the list of already-acknowledged NEW packages
1004 new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'r');
1006 for line in new_ack_list.readlines():
1007 new_ack_old[line[:-1]] = 1;
1008 new_ack_list.close();
1010 # Process the changes files
1011 for changes_file in changes_files:
1013 print "\n" + changes_file;
1014 process_it (changes_file);
1017 if install_bytes > 10000:
1018 install_bytes = install_bytes / 1000;
1019 install_mag = " Kb";
1020 if install_bytes > 10000:
1021 install_bytes = install_bytes / 1000;
1022 install_mag = " Mb";
1025 if install_count > 1:
1027 sys.stderr.write("Installed %d package %s, %d%s.\n" % (install_count, sets, int(install_bytes), install_mag))
1029 # Write out the list of already-acknowledged NEW packages
1030 if Cnf["Dinstall::Options::Ack-New"]:
1031 new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'w')
1032 for i in new_ack_new.keys():
1033 new_ack_list.write(i+'\n')
1034 new_ack_list.close()
1037 if __name__ == '__main__':