3 # Installs Debian packaes
4 # Copyright (C) 2000 James Troup <james@nocrew.org>
5 # $Id: katie,v 1.1.1.1 2000-11-24 00:20:08 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 --load-extension rsaref --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 # FIXME: nasty non-US speficic hack
137 if string.lower(component[:7]) == "non-us/":
138 component = component[7:];
139 if not overrides.has_key(suite) or not overrides[suite].has_key(component):
140 if not overrides.has_key(suite):
141 overrides[suite] = {}
142 if not overrides[suite].has_key(component):
143 overrides[suite][component] = {}
144 if Cnf.has_key("Suite::%s::SingleOverrideFile" % (suite)): # legacy mixed suite (i.e. experimental)
145 override_filename = Cnf["Dir::OverrideDir"] + 'override.' + Cnf["Suite::%s::OverrideCodeName" % (suite)];
146 read_override_file (override_filename, suite, component);
148 for src in ("", ".src"):
149 override_filename = Cnf["Dir::OverrideDir"] + 'override.' + Cnf["Suite::%s::OverrideCodeName" % (suite)] + '.' + component + src;
150 read_override_file (override_filename, suite, component);
152 return overrides[suite][component].get(package, None);
154 #####################################################################################################################
156 def check_changes(filename):
157 global reject_message, changes, files
159 # Parse the .changes field into a dictionary [FIXME - need to trap errors, pass on to reject_message etc.]
161 changes = utils.parse_changes(filename)
162 except utils.cant_open_exc:
163 reject_message = "Rejected: can't read changes file '%s'.\n" % (filename)
165 except utils.changes_parse_error_exc, line:
166 reject_message = "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line)
167 changes["maintainer822"] = Cnf["Dinstall::MyEmailAddress"];
170 # Parse the Files field from the .changes into another dictionary [FIXME need to trap errors as above]
171 files = utils.build_file_list(changes, "")
173 # Check for mandatory fields
174 for i in ("source", "binary", "architecture", "version", "distribution","maintainer", "files"):
175 if not changes.has_key(i):
176 reject_message = "Rejected: Missing field `%s' in changes file." % (i)
177 return 0 # Avoid <undef> errors during later tests
179 # Fix the Maintainer: field to be RFC822 compatible
180 (changes["maintainer822"], changes["maintainername"], changes["maintaineremail"]) = utils.fix_maintainer (changes["maintainer"])
182 # Override the Distribution: field if appropriate
183 if Cnf["Dinstall::Options::Override-Distribution"] != "":
184 reject_message = reject_message + "Warning: Distribution was overriden from %s to %s.\n" % (changes["distribution"], Cnf["Dinstall::Options::Override-Distribution"])
185 changes["distribution"] = Cnf["Dinstall::Options::Override-Distribution"]
187 # Split multi-value fields into a lower-level dictionary
188 for i in ("architecture", "distribution", "binary", "closes"):
189 o = changes.get(i, "")
193 for j in string.split(o):
196 # Ensure all the values in Closes: are numbers
197 if changes.has_key("closes"):
198 for i in changes["closes"].keys():
199 if re_isanum.match (i) == None:
200 reject_message = reject_message + "Rejected: `%s' from Closes field isn't a number.\n" % (i)
203 # Map frozen to unstable if frozen doesn't exist
204 if changes["distribution"].has_key("frozen") and not Cnf.has_key("Suite::Frozen"):
205 del changes["distribution"]["frozen"]
206 reject_message = reject_message + "Mapping frozen to unstable.\n"
208 # Ensure target distributions exist
209 for i in changes["distribution"].keys():
210 if not Cnf.has_key("Suite::%s" % (i)):
211 reject_message = reject_message + "Rejected: Unknown distribution `%s'.\n" % (i)
213 # Map unreleased arches from stable to unstable
214 if changes["distribution"].has_key("stable"):
215 for i in changes["architecture"].keys():
216 if not Cnf.has_key("Suite::Stable::Architectures::%s" % (i)):
217 reject_message = reject_message + "Mapping stable to unstable for unreleased arch `%s'.\n" % (i)
218 del changes["distribution"]["stable"]
220 # Map arches not being released from frozen to unstable
221 if changes["distribution"].has_key("frozen"):
222 for i in changes["architecture"].keys():
223 if not Cnf.has_key("Suite::Frozen::Architectures::%s" % (i)):
224 reject_message = reject_message + "Mapping frozen to unstable for non-releasing arch `%s'.\n" % (i)
225 del changes["distribution"]["frozen"]
227 # Handle uploads to stable
228 if changes["distribution"].has_key("stable"):
229 # If running from within proposed-updates kill non-stable distributions
230 if string.find(os.getcwd(), 'proposed-updates') != -1:
231 for i in ("frozen", "unstable"):
232 if changes["distributions"].has_key(i):
233 reject_message = reject_message + "Removing %s from distribution list.\n"
234 del changes["distribution"][i]
235 # Otherwise (normal case) map stable to updates
237 reject_message = reject_message + "Mapping stable to updates.\n";
238 del changes["distribution"]["stable"];
239 changes["distribution"]["proposed-updates"] = 1;
241 # chopversion = no epoch; chopversion2 = no epoch and no revision (e.g. for .orig.tar.gz comparison)
242 changes["chopversion"] = utils.re_no_epoch.sub('', changes["version"])
243 changes["chopversion2"] = utils.re_no_revision.sub('', changes["chopversion"])
245 if string.find(reject_message, "Rejected:") != -1:
251 global reject_message
253 archive = utils.where_am_i();
255 for file in files.keys():
256 # Check the file is readable
257 if os.access(file,os.R_OK) == 0:
258 reject_message = reject_message + "Rejected: Can't read `%s'.\n" % (file)
259 files[file]["type"] = "unreadable";
261 # If it's byhand skip remaining checks
262 if files[file]["section"] == "byhand":
263 files[file]["byhand"] = 1;
264 files[file]["type"] = "byhand";
265 # Checks for a binary package...
266 elif re_isadeb.match(file) != None:
267 # Extract package information using dpkg-deb
268 control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))
270 # Check for mandatory fields
271 if control.Find("Package") == None:
272 reject_message = reject_message + "Rejected: %s: No package field in control.\n" % (file)
273 if control.Find("Architecture") == None:
274 reject_message = reject_message + "Rejected: %s: No architecture field in control.\n" % (file)
275 if control.Find("Version") == None:
276 reject_message = reject_message + "Rejected: %s: No version field in control.\n" % (file)
278 # Ensure the package name matches the one give in the .changes
279 if not changes["binary"].has_key(control.Find("Package", "")):
280 reject_message = reject_message + "Rejected: %s: control file lists name as `%s', which isn't in changes file.\n" % (file, control.Find("Package", ""))
282 # Validate the architecture
283 if not Cnf.has_key("Suite::Unstable::Architectures::%s" % (control.Find("Architecture", ""))):
284 reject_message = reject_message + "Rejected: Unknown architecture '%s'.\n" % (control.Find("Architecture", ""))
286 # Check the architecture matches the one given in the .changes
287 if not changes["architecture"].has_key(control.Find("Architecture", "")):
288 reject_message = reject_message + "Rejected: %s: control file lists arch as `%s', which isn't in changes file.\n" % (file, control.Find("Architecture", ""))
289 # Check the section & priority match those given in the .changes (non-fatal)
290 if control.Find("Section") != None and files[file]["section"] != "" and files[file]["section"] != control.Find("Section"):
291 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"])
292 if control.Find("Priority") != None and files[file]["priority"] != "" and files[file]["priority"] != control.Find("Priority"):
293 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"])
295 epochless_version = utils.re_no_epoch.sub('', control.Find("Version", ""))
297 files[file]["package"] = control.Find("Package");
298 files[file]["architecture"] = control.Find("Architecture");
299 files[file]["version"] = control.Find("Version");
300 files[file]["maintainer"] = control.Find("Maintainer", "");
301 if file[-5:] == ".udeb":
302 files[file]["dbtype"] = "udeb";
303 elif file[-4:] == ".deb":
304 files[file]["dbtype"] = "deb";
306 reject_message = reject_message + "Rejected: %s is neither a .deb or a .udeb.\n " % (file);
307 files[file]["type"] = "deb";
308 files[file]["fullname"] = "%s_%s_%s.deb" % (control.Find("Package", ""), epochless_version, control.Find("Architecture", ""))
309 files[file]["source"] = control.Find("Source", "");
310 if files[file]["source"] == "":
311 files[file]["source"] = files[file]["package"];
312 # Checks for a source package...
314 m = re_issource.match(file)
316 files[file]["package"] = m.group(1)
317 files[file]["version"] = m.group(2)
318 files[file]["type"] = m.group(3)
320 # Ensure the source package name matches the Source filed in the .changes
321 if changes["source"] != files[file]["package"]:
322 reject_message = reject_message + "Rejected: %s: changes file doesn't say %s for Source\n" % (file, files[file]["package"])
324 # Ensure the source version matches the version in the .changes file
325 if files[file]["type"] == "orig.tar.gz":
326 changes_version = changes["chopversion2"]
328 changes_version = changes["chopversion"]
329 if changes_version != files[file]["version"]:
330 reject_message = reject_message + "Rejected: %s: should be %s according to changes file.\n" % (file, changes_version)
332 # Ensure the .changes lists source in the Architecture field
333 if not changes["architecture"].has_key("source"):
334 reject_message = reject_message + "Rejected: %s: changes file doesn't list `source' in Architecture field.\n" % (file)
336 # Check the signature of a .dsc file
337 if files[file]["type"] == "dsc":
338 check_signature(file)
340 files[file]["fullname"] = file
342 # Not a binary or source package? Assume byhand...
344 files[file]["byhand"] = 1;
345 files[file]["type"] = "byhand";
347 files[file]["oldfiles"] = {}
348 for suite in changes["distribution"].keys():
350 if files[file].has_key("byhand"):
353 if Cnf.has_key("Suite:%s::Components" % (suite)) and not Cnf.has_key("Suite::%s::Components::%s" % (suite, files[file]["component"])):
354 reject_message = reject_message + "Rejected: unknown component `%s' for suite `%s'.\n" % (files[file]["component"], suite)
357 # See if the package is NEW
358 if not in_override_p(files[file]["package"], files[file]["component"], suite):
359 files[file]["new"] = 1
361 # Find any old binary packages
362 if files[file]["type"] == "deb":
363 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"
364 % (files[file]["package"], suite, files[file]["architecture"]))
365 oldfiles = q.dictresult()
366 for oldfile in oldfiles:
367 files[file]["oldfiles"][suite] = oldfile
368 # Check versions [NB: per-suite only; no cross-suite checking done (yet)]
369 if apt_pkg.VersionCompare(files[file]["version"], oldfile["version"]) != 1:
370 if Cnf["Dinstall::Options::No-Version-Check"]:
371 reject_message = reject_message + "Overriden rejection"
373 reject_message = reject_message + "Rejected"
374 reject_message = reject_message + ": %s Old version `%s' >= new version `%s'.\n" % (file, oldfile["version"], files[file]["version"])
375 # Find any old .dsc files
376 elif files[file]["type"] == "dsc":
377 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"
378 % (files[file]["package"], suite))
379 oldfiles = q.dictresult()
380 if len(oldfiles) >= 1:
381 files[file]["oldfiles"][suite] = oldfiles[0]
383 # Validate the component
384 component = files[file]["component"];
385 component_id = db_access.get_component_id(component);
386 if component_id == -1:
387 reject_message = reject_message + "Rejected: file '%s' has unknown component '%s'.\n" % (file, component);
390 # Check the md5sum & size against existing files (if any)
391 location = Cnf["Dir::PoolDir"];
392 files[file]["location id"] = db_access.get_location_id (location, component, archive);
393 files_id = db_access.get_files_id(component + '/' + file, files[file]["size"], files[file]["md5sum"], files[file]["location id"]);
395 reject_message = reject_message + "Rejected: INTERNAL ERROR, get_files_id() returned multiple matches for %s.\n" % (file)
397 reject_message = reject_message + "Rejected: md5sum and/or size mismatch on existing copy of %s.\n" % (file)
398 files[file]["files id"] = files_id
400 # Check for packages that have moved from one component to another
401 if files[file]["oldfiles"].has_key(suite) and files[file]["oldfiles"][suite]["name"] != files[file]["component"]:
402 files[file]["othercomponents"] = files[file]["oldfiles"][suite]["name"];
405 if string.find(reject_message, "Rejected:") != -1:
410 ###############################################################################
413 global dsc, dsc_files, reject_message, reprocess, orig_tar_id;
415 for file in files.keys():
416 if files[file]["type"] == "dsc":
418 dsc = utils.parse_changes(file)
419 except utils.cant_open_exc:
420 reject_message = reject_message + "Rejected: can't read changes file '%s'.\n" % (filename)
422 except utils.changes_parse_error_exc, line:
423 reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line)
426 dsc_files = utils.build_file_list(dsc, 1)
427 except utils.no_files_exc:
428 reject_message = reject_message + "Rejected: no Files: field in .dsc file.\n";
431 # Try and find all files mentioned in the .dsc. This has
432 # to work harder to cope with the multiple possible
433 # locations of an .orig.tar.gz.
434 for dsc_file in dsc_files.keys():
435 if files.has_key(dsc_file):
436 actual_md5 = files[dsc_file]["md5sum"]
437 found = "%s in incoming" % (dsc_file)
438 elif dsc_file[-12:] == ".orig.tar.gz":
440 # See comment above process_it() for explanation...
441 if os.access(dsc_file, os.R_OK) != 0:
442 files[dsc_file] = {};
443 files[dsc_file]["size"] = os.stat(dsc_file)[stat.ST_SIZE];
444 files[dsc_file]["md5sum"] = dsc_files[dsc_file]["md5sum"];
445 files[dsc_file]["section"] = files[file]["section"];
446 files[dsc_file]["priority"] = files[file]["priority"];
447 files[dsc_file]["component"] = files[file]["component"];
451 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));
454 old_file = ql[0][0] + ql[0][1];
455 actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
457 suite_type = ql[0][2];
459 if suite_type == "legacy" or suite_type == "legacy-mixed":
460 orig_tar_id = ql[0][3];
462 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);
465 reject_message = reject_message + "Rejected: %s refers to %s, but I can't find it in Incoming." % (file, dsc_file);
467 if actual_md5 != dsc_files[dsc_file]["md5sum"]:
468 reject_message = reject_message + "Rejected: md5sum for %s doesn't match %s.\n" % (found, file)
470 if string.find(reject_message, "Rejected:") != -1:
475 ###############################################################################
477 def check_md5sums ():
478 global reject_message;
480 for file in files.keys():
482 file_handle = utils.open_file(file,"r");
483 except utils.cant_open_exc:
486 if apt_pkg.md5sum(file_handle) != files[file]["md5sum"]:
487 reject_message = reject_message + "Rejected: md5sum check failed for %s.\n" % (file);
489 #####################################################################################################################
491 def action (changes_filename):
492 byhand = confirm = suites = summary = new = "";
494 # changes["distribution"] may not exist in corner cases
495 # (e.g. unreadable changes files)
496 if not changes.has_key("distribution"):
497 changes["distribution"] = {};
499 for suite in changes["distribution"].keys():
500 if Cnf.has_key("Suite::%s::Confirm"):
501 confirm = confirm + suite + ", "
502 suites = suites + suite + ", "
503 confirm = confirm[:-2]
506 for file in files.keys():
507 if files[file].has_key("byhand"):
509 summary = summary + file + " byhand\n"
510 elif files[file].has_key("new"):
512 summary = summary + "(new) %s %s %s\n" % (file, files[file]["priority"], files[file]["section"])
513 if files[file].has_key("othercomponents"):
514 summary = summary + "WARNING: Already present in %s distribution.\n" % (files[file]["othercomponents"])
515 if files[file]["type"] == "deb":
516 summary = summary + apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))["Description"] + '\n';
518 files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"])
519 destination = Cnf["Dir::PoolRoot"] + files[file]["pool name"] + file
520 summary = summary + file + "\n to " + destination + "\n"
522 short_summary = summary;
524 # This is for direport's benefit...
525 f = re_fdnic.sub("\n .\n", changes.get("changes",""));
527 if confirm or byhand or new:
528 summary = summary + "Changes: " + f;
530 summary = summary + announce (short_summary, 0)
532 (prompt, answer) = ("", "XXX")
533 if Cnf["Dinstall::Options::No-Action"] or Cnf["Dinstall::Options::Automatic"]:
536 if string.find(reject_message, "Rejected") != -1:
537 if time.time()-os.path.getmtime(changes_filename) < 86400:
538 print "SKIP (too new)\n" + reject_message,;
539 prompt = "[S]kip, Manual reject, Quit ?";
541 print "REJECT\n" + reject_message,;
542 prompt = "[R]eject, Manual reject, Skip, Quit ?";
543 if Cnf["Dinstall::Options::Automatic"]:
546 print "NEW to %s\n%s%s" % (suites, reject_message, summary),;
547 prompt = "[S]kip, New ack, Manual reject, Quit ?";
548 if Cnf["Dinstall::Options::Automatic"] and Cnf["Dinstall::Options::Ack-New"]:
551 print "BYHAND\n" + reject_message + summary,;
552 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
554 print "CONFIRM to %s\n%s%s" % (confirm, reject_message, summary),
555 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
557 print "INSTALL\n" + reject_message + summary,;
558 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
559 if Cnf["Dinstall::Options::Automatic"]:
562 while string.find(prompt, answer) == -1:
564 answer = utils.our_raw_input()
565 m = re_default_answer.match(prompt)
568 answer = string.upper(answer[:1])
571 reject (changes_filename, "");
573 manual_reject (changes_filename);
575 install (changes_filename, summary, short_summary);
577 acknowledge_new (changes_filename, summary);
581 #####################################################################################################################
583 def install (changes_filename, summary, short_summary):
584 global install_count, install_bytes
588 archive = utils.where_am_i();
590 # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
591 projectB.query("BEGIN WORK");
593 # Add the .dsc file to the DB
594 for file in files.keys():
595 if files[file]["type"] == "dsc":
596 package = dsc["source"]
597 version = dsc["version"] # NB: not files[file]["version"], that has no epoch
598 maintainer = dsc["maintainer"]
599 maintainer = string.replace(maintainer, "'", "\\'")
600 maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
601 filename = files[file]["pool name"] + file;
602 dsc_location_id = files[file]["location id"];
603 if not files[file]["files id"]:
604 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], dsc_location_id)
605 dsc_file_id = files[file]["files id"]
606 projectB.query("INSERT INTO source (source, version, maintainer, file) VALUES ('%s', '%s', %d, %d)"
607 % (package, version, maintainer_id, files[file]["files id"]))
609 for suite in changes["distribution"].keys():
610 suite_id = db_access.get_suite_id(suite);
611 projectB.query("INSERT INTO src_associations (suite, source) VALUES (%d, currval('source_id_seq'))" % (suite_id))
614 # Add the .diff.gz and {.orig,}.tar.gz files to the DB (files and dsc_files)
615 for file in files.keys():
616 if files[file]["type"] == "diff.gz" or files[file]["type"] == "orig.tar.gz" or files[file]["type"] == "tar.gz":
617 if not files[file]["files id"]:
618 filename = files[file]["pool name"] + file;
619 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], files[file]["location id"])
620 projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files[file]["files id"]));
622 # Add the .deb files to the DB
623 for file in files.keys():
624 if files[file]["type"] == "deb":
625 package = files[file]["package"]
626 version = files[file]["version"]
627 maintainer = files[file]["maintainer"]
628 maintainer = string.replace(maintainer, "'", "\\'")
629 maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
630 architecture = files[file]["architecture"]
631 architecture_id = db_access.get_architecture_id (architecture);
632 type = files[file]["dbtype"];
633 component = files[file]["component"]
634 source = files[file]["source"]
636 if string.find(source, "(") != -1:
637 m = utils.re_extract_src_version.match(source)
639 source_version = m.group(2)
640 if not source_version:
641 source_version = version
642 filename = files[file]["pool name"] + file;
643 if not files[file]["files id"]:
644 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], files[file]["location id"])
645 source_id = db_access.get_source_id (source, source_version);
647 projectB.query("INSERT INTO binaries (package, version, maintainer, source, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, %d, '%s')"
648 % (package, version, maintainer_id, source_id, architecture_id, files[file]["files id"], type));
650 projectB.query("INSERT INTO binaries (package, version, maintainer, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, '%s')"
651 % (package, version, maintainer_id, architecture_id, files[file]["files id"], type));
652 for suite in changes["distribution"].keys():
653 suite_id = db_access.get_suite_id(suite);
654 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES (%d, currval('binaries_id_seq'))" % (suite_id));
656 # Install the files into the pool
657 for file in files.keys():
658 if files[file].has_key("byhand"):
660 destination = Cnf["Dir::PoolDir"] + files[file]["pool name"] + file
661 destdir = os.path.dirname(destination)
662 utils.move (file, destination)
663 install_bytes = install_bytes + float(files[file]["size"])
665 # Copy the .changes file across for suite which need it.
666 for suite in changes["distribution"].keys():
667 if Cnf.has_key("Suties::%s::CopyChanges" % (suite)):
668 destination = Cnf["Dir::RootDir"] + Cnf["Suite::%s::CopyChanges" % (suite)] + os.path.basename(changes_filename)
669 copy_file (changes_filename, destination)
671 # If the .orig.tar.gz is in a legacy directory we need to poolify
672 # it, so that apt-get source (and anything else that goes by the
673 # "Directory:" field in the Sources.gz file) works.
674 if orig_tar_id != None:
675 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));
678 # First move the files to the new location
679 legacy_filename = qid["path"]+qid["filename"];
680 pool_location = utils.poolify (files[file]["package"], files[file]["component"]);
681 pool_filename = pool_location + os.path.basename(qid["filename"]);
682 destination = Cnf["Dir::PoolDir"] + pool_location
683 utils.move(legacy_filename, destination);
684 # Update the DB: files table
685 new_files_id = db_access.set_files_id(pool_filename, qid["size"], qid["md5sum"], dsc_location_id);
686 # Update the DB: dsc_files table
687 projectB.query("INSERT INTO dsc_files (source, file) VALUES (%s, %s)" % (qid["source"], new_files_id));
688 # Update the DB: source table
689 if legacy_filename[-4:] == ".dsc":
690 projectB.query("UPDATE source SET file = %s WHERE id = %d" % (new_files_id, qid["source"]));
693 # Remove old data from the DB: dsc_files table
694 projectB.query("DELETE FROM dsc_files WHERE id = %s" % (qid["dsc_files_id"]));
695 # Remove old data from the DB: files table
696 projectB.query("DELETE FROM files WHERE id = %s" % (qid["files_id"]));
698 utils.move (changes_filename, Cnf["Dir::IncomingDir"] + 'DONE/' + os.path.basename(changes_filename))
700 projectB.query("COMMIT WORK");
702 install_count = install_count + 1;
704 if not Cnf["Dinstall::Options::No-Mail"]:
705 mail_message = """Return-Path: %s
708 Bcc: troup@auric.debian.org
709 Subject: %s INSTALLED
715 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, reject_message, summary, installed_footer)
716 utils.send_mail (mail_message, "")
717 announce (short_summary, 1)
719 #####################################################################################################################
721 def reject (changes_filename, manual_reject_mail_filename):
724 base_changes_filename = os.path.basename(changes_filename);
725 reason_filename = re_changes.sub("reason", base_changes_filename);
726 reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename);
728 # Move the .changes files and it's contents into REJECT/
729 utils.move (changes_filename, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], base_changes_filename));
730 for file in files.keys():
731 if os.access(file,os.R_OK) == 0:
732 utils.move (file, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], file));
734 # If this is not a manual rejection generate the .reason file and rejection mail message
735 if manual_reject_mail_filename == "":
736 if os.path.exists(reject_filename):
737 os.unlink(reject_filename);
738 fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
739 os.write(fd, reject_message);
741 reject_mail_message = """From: %s
743 Bcc: troup@auric.debian.org
748 %s""" % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, reject_message, reject_footer);
749 else: # Have a manual rejection file to use
750 reject_mail_message = ""; # avoid <undef>'s
752 # Send the rejection mail if appropriate
753 if not Cnf["Dinstall::Options::No-Mail"]:
754 utils.send_mail (reject_mail_message, manual_reject_mail_filename);
756 ##################################################################
758 def manual_reject (changes_filename):
759 # Build up the rejection email
760 user_email_address = string.replace(string.split(pwd.getpwuid(os.getuid())[4],',')[0], '.', '')
761 user_email_address = user_email_address + " <%s@%s>" % (pwd.getpwuid(os.getuid())[0], Cnf["Dinstall::MyHost"])
762 manual_reject_message = Cnf.get("Dinstall::Options::Manual-Reject", "")
764 reject_mail_message = """From: %s
767 Bcc: troup@auric.debian.org
773 %s""" % (user_email_address, Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, manual_reject_message, reject_message, reject_footer)
775 # Write the rejection email out as the <foo>.reason file
776 reason_filename = re_changes.sub("reason", os.path.basename(changes_filename));
777 reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename)
778 if os.path.exists(reject_filename):
779 os.unlink(reject_filename);
780 fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
781 os.write(fd, reject_mail_message);
784 # If we weren't given one, spawn an editor so the user can add one in
785 if manual_reject_message == "":
786 result = os.system("vi +6 %s" % (reject_file))
788 sys.stderr.write ("vi invocation failed for `%s'!" % (reject_file))
791 # Then process it as if it were an automatic rejection
792 reject (changes_filename, reject_filename)
794 #####################################################################################################################
796 def acknowledge_new (changes_filename, summary):
799 new_ack_new[changes_filename] = 1;
801 if new_ack_old.has_key(changes_filename):
802 print "Ack already sent.";
805 print "Sending new ack.";
806 if not Cnf["Dinstall::Options::No-Mail"]:
807 new_ack_message = """Return-Path: %s
810 Bcc: troup@auric.debian.org
814 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, summary, new_ack_footer);
815 utils.send_mail(new_ack_message,"");
817 #####################################################################################################################
819 def announce (short_summary, action):
820 # Only do announcements for source uploads with a recent dpkg-dev installed
821 if float(changes.get("format", 0)) < 1.6 or not changes["architecture"].has_key("source"):
827 for dist in changes["distribution"].keys():
828 list = Cnf["Suite::%s::Announce" % (dist)]
829 if lists_done.has_key(list):
832 summary = summary + "Announcing to %s\n" % (list)
835 mail_message = """Return-Path: %s
838 Bcc: troup@auric.debian.org
839 Subject: Installed %s %s (%s)
845 """ % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], list, changes["source"], changes["version"], string.join(changes["architecture"].keys(), ' ' ),
846 changes["filecontents"], short_summary)
847 utils.send_mail (mail_message, "")
849 (dsc_rfc822, dsc_name, dsc_email) = utils.fix_maintainer (dsc.get("maintainer",Cnf["Dinstall::MyEmailAddress"]));
850 bugs = changes["closes"].keys()
852 if dsc_name == changes["maintainername"]:
853 summary = summary + "Closing bugs: "
855 summary = summary + "%s " % (bug)
857 mail_message = """Return-Path: %s
859 To: %s-close@bugs.debian.org
860 Bcc: troup@auric.debian.org
861 Subject: Bug#%s: fixed in %s %s
863 We believe that the bug you reported is fixed in the latest version of
864 %s, which has been installed in the Debian FTP archive:
866 %s""" % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], bug, bug, changes["source"], changes["version"], changes["source"], short_summary)
868 if changes["distribution"].has_key("stable"):
869 mail_message = mail_message + """Note that this package is not part of the released stable Debian
870 distribution. It may have dependencies on other unreleased software,
871 or other instabilities. Please take care if you wish to install it.
872 The update will eventually make its way into the next released Debian
875 mail_message = mail_message + """A summary of the changes between this version and the previous one is
878 Thank you for reporting the bug, which will now be closed. If you
879 have further comments please address them to %s@bugs.debian.org,
880 and the maintainer will reopen the bug report if appropriate.
882 Debian distribution maintenance software
884 %s (supplier of updated %s package)
886 (This message was generated automatically at their request; if you
887 believe that there is a problem with it please contact the archive
888 administrators by mailing ftpmaster@debian.org)
891 %s""" % (bug, changes["maintainer"], changes["source"], changes["filecontents"])
893 utils.send_mail (mail_message, "")
895 summary = summary + "Setting bugs to severity fixed: "
898 summary = summary + "%s " % (bug)
899 control_message = control_message + "severity %s fixed\n" % (bug)
900 if action and control_message != "":
901 mail_message = """Return-Path: %s
903 To: control@bugs.debian.org
904 Bcc: troup@auric.debian.org, %s
905 Subject: Fixed in NMU of %s %s
910 This message was generated automatically in response to a
911 non-maintainer upload. The .changes file follows.
914 """ % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes["maintainer822"], changes["source"], changes["version"], control_message, changes["filecontents"])
915 utils.send_mail (mail_message, "")
916 summary = summary + "\n"
920 ###############################################################################
922 # reprocess is necessary for the case of foo_1.2-1 and foo_1.2-2 in
923 # Incoming. -1 will reference the .orig.tar.gz, but -2 will not.
924 # dsccheckdistrib() can find the .orig.tar.gz but it will not have
925 # processed it during it's checks of -2. If -1 has been deleted or
926 # otherwise not checked by da-install, the .orig.tar.gz will not have
927 # been checked at all. To get round this, we force the .orig.tar.gz
928 # into the .changes structure and reprocess the .changes file.
930 def process_it (changes_file):
931 global reprocess, orig_tar_id;
936 check_signature (changes_file);
937 check_changes (changes_file);
944 action(changes_file);
946 ###############################################################################
949 global Cnf, projectB, reject_message, install_bytes, new_ack_old
953 Cnf = apt_pkg.newConfiguration();
954 apt_pkg.ReadConfigFileISC(Cnf,utils.which_conf_file());
956 Arguments = [('a',"automatic","Dinstall::Options::Automatic"),
957 ('d',"debug","Dinstall::Options::Debug", "IntVal"),
958 ('h',"help","Dinstall::Options::Help"),
959 ('k',"ack-new","Dinstall::Options::Ack-New"),
960 ('m',"manual-reject","Dinstall::Options::Manual-Reject", "HasArg"),
961 ('n',"no-action","Dinstall::Options::No-Action"),
962 ('p',"no-lock", "Dinstall::Options::No-Lock"),
963 ('r',"no-version-check", "Dinstall::Options::No-Version-Check"),
964 ('s',"no-mail", "Dinstall::Options::No-Mail"),
965 ('u',"override-distribution", "Dinstall::Options::Override-Distribution", "HasArg"),
966 ('v',"version","Dinstall::Options::Version")];
968 changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
970 if Cnf["Dinstall::Options::Help"]:
973 if Cnf["Dinstall::Options::Version"]:
974 print "katie version 0.0000000000";
977 postgresql_user = None; # Default == Connect as user running program.
979 # -n/--dry-run invalidates some other options which would involve things happening
980 if Cnf["Dinstall::Options::No-Action"]:
981 Cnf["Dinstall::Options::Automatic"] = ""
982 Cnf["Dinstall::Options::Ack-New"] = ""
983 postgresql_user = Cnf["DB::ROUser"];
985 projectB = pg.connect('projectb', Cnf["DB::Host"], int(Cnf["DB::Port"]), None, None, postgresql_user);
987 db_access.init(Cnf, projectB);
989 # Check that we aren't going to clash with the daily cron job
991 if os.path.exists("%s/Archive_Maintenance_In_Progress" % (Cnf["Dir::RootDir"])) and not Cnf["Dinstall::Options::No-Lock"]:
992 sys.stderr.write("Archive maintenance in progress. Try again later.\n");
995 # Obtain lock if not in no-action mode
997 if not Cnf["Dinstall::Options::No-Action"]:
998 lock_fd = os.open(Cnf["Dinstall::LockFile"], os.O_RDWR);
999 fcntl.lockf(lock_fd, FCNTL.F_TLOCK);
1001 # Read in the list of already-acknowledged NEW packages
1002 new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'r');
1004 for line in new_ack_list.readlines():
1005 new_ack_old[line[:-1]] = 1;
1006 new_ack_list.close();
1008 # Process the changes files
1009 for changes_file in changes_files:
1011 print "\n" + changes_file;
1012 process_it (changes_file);
1015 if install_bytes > 10000:
1016 install_bytes = install_bytes / 1000;
1017 install_mag = " Kb";
1018 if install_bytes > 10000:
1019 install_bytes = install_bytes / 1000;
1020 install_mag = " Mb";
1023 if install_count > 1:
1025 sys.stderr.write("Installed %d package %s, %d%s.\n" % (install_count, sets, int(install_bytes), install_mag))
1027 # Write out the list of already-acknowledged NEW packages
1028 if Cnf["Dinstall::Options::Ack-New"]:
1029 new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'w')
1030 for i in new_ack_new.keys():
1031 new_ack_list.write(i+'\n')
1032 new_ack_list.close()
1035 if __name__ == '__main__':