3 # Installs Debian packaes
4 # Copyright (C) 2000 James Troup <james@nocrew.org>
5 # $Id: katie,v 1.16 2000-12-20 08:25:56 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, binary_type):
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][binary_type][override_package] = 1
131 # See if a given package is in the override file. Caches and only loads override files on demand.
133 def in_override_p (package, component, suite, binary_type):
136 if binary_type == "" or binary_type == "deb":
139 # Avoid <undef> on unknown distributions
140 if db_access.get_suite_id(suite) == -1:
143 # FIXME: nasty non-US speficic hack
144 if string.lower(component[:7]) == "non-us/":
145 component = component[7:];
146 if not overrides.has_key(suite) or not overrides[suite].has_key(component) or not overrides[suite][component].has_key(binary_type):
147 if not overrides.has_key(suite):
148 overrides[suite] = {}
149 if not overrides[suite].has_key(component):
150 overrides[suite][component] = {}
151 if not overrides[suite][component].has_key(binary_type):
152 overrides[suite][component][binary_type] = {}
153 if Cnf.has_key("Suite::%s::SingleOverrideFile" % (suite)): # legacy mixed suite (i.e. experimental)
154 override_filename = Cnf["Dir::OverrideDir"] + 'override.' + Cnf["Suite::%s::OverrideCodeName" % (suite)];
155 read_override_file (override_filename, suite, component, binary_type);
157 if binary_type == "udeb":
158 override_filename = Cnf["Dir::OverrideDir"] + 'override.' + Cnf["Suite::%s::OverrideCodeName" % (suite)] + '.debian-installer.' + component;
159 read_override_file (override_filename, suite, component, binary_type);
161 for src in ("", ".src"):
162 override_filename = Cnf["Dir::OverrideDir"] + 'override.' + Cnf["Suite::%s::OverrideCodeName" % (suite)] + '.' + component + src;
163 read_override_file (override_filename, suite, component, binary_type);
165 return overrides[suite][component][binary_type].get(package, None);
167 #####################################################################################################################
169 def check_changes(filename):
170 global reject_message, changes, files
172 # Parse the .changes field into a dictionary [FIXME - need to trap errors, pass on to reject_message etc.]
174 changes = utils.parse_changes(filename)
175 except utils.cant_open_exc:
176 reject_message = "Rejected: can't read changes file '%s'.\n" % (filename)
178 except utils.changes_parse_error_exc, line:
179 reject_message = "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line)
180 changes["maintainer822"] = Cnf["Dinstall::MyEmailAddress"];
183 # Parse the Files field from the .changes into another dictionary [FIXME need to trap errors as above]
184 files = utils.build_file_list(changes, "")
186 # Check for mandatory fields
187 for i in ("source", "binary", "architecture", "version", "distribution","maintainer", "files"):
188 if not changes.has_key(i):
189 reject_message = "Rejected: Missing field `%s' in changes file." % (i)
190 return 0 # Avoid <undef> errors during later tests
192 # Fix the Maintainer: field to be RFC822 compatible
193 (changes["maintainer822"], changes["maintainername"], changes["maintaineremail"]) = utils.fix_maintainer (changes["maintainer"])
195 # Override the Distribution: field if appropriate
196 if Cnf["Dinstall::Options::Override-Distribution"] != "":
197 reject_message = reject_message + "Warning: Distribution was overriden from %s to %s.\n" % (changes["distribution"], Cnf["Dinstall::Options::Override-Distribution"])
198 changes["distribution"] = Cnf["Dinstall::Options::Override-Distribution"]
200 # Split multi-value fields into a lower-level dictionary
201 for i in ("architecture", "distribution", "binary", "closes"):
202 o = changes.get(i, "")
206 for j in string.split(o):
209 # Ensure all the values in Closes: are numbers
210 if changes.has_key("closes"):
211 for i in changes["closes"].keys():
212 if re_isanum.match (i) == None:
213 reject_message = reject_message + "Rejected: `%s' from Closes field isn't a number.\n" % (i)
215 # Map frozen to unstable if frozen doesn't exist
216 if changes["distribution"].has_key("frozen") and not Cnf.has_key("Suite::Frozen"):
217 del changes["distribution"]["frozen"]
218 reject_message = reject_message + "Mapping frozen to unstable.\n"
220 # Map testing to unstable
221 if changes["distribution"].has_key("testing"):
222 del changes["distribution"]["testing"]
223 reject_message = reject_message + "Mapping testing to unstable.\n"
225 # Ensure target distributions exist
226 for i in changes["distribution"].keys():
227 if not Cnf.has_key("Suite::%s" % (i)):
228 reject_message = reject_message + "Rejected: Unknown distribution `%s'.\n" % (i)
230 # Map unreleased arches from stable to unstable
231 if changes["distribution"].has_key("stable"):
232 for i in changes["architecture"].keys():
233 if not Cnf.has_key("Suite::Stable::Architectures::%s" % (i)):
234 reject_message = reject_message + "Mapping stable to unstable for unreleased arch `%s'.\n" % (i)
235 del changes["distribution"]["stable"]
237 # Map arches not being released from frozen to unstable
238 if changes["distribution"].has_key("frozen"):
239 for i in changes["architecture"].keys():
240 if not Cnf.has_key("Suite::Frozen::Architectures::%s" % (i)):
241 reject_message = reject_message + "Mapping frozen to unstable for non-releasing arch `%s'.\n" % (i)
242 del changes["distribution"]["frozen"]
244 # Handle uploads to stable
245 if changes["distribution"].has_key("stable"):
246 # If running from within proposed-updates; assume an install to stable
247 if string.find(os.getcwd(), 'proposed-updates') != -1:
248 # FIXME: should probably remove anything that != stable
249 for i in ("frozen", "unstable"):
250 if changes["distribution"].has_key(i):
251 reject_message = reject_message + "Removing %s from distribution list.\n"
252 del changes["distribution"][i]
253 changes["stable upload"] = 1;
254 # If we can't find a file from the .changes; assume it's a package already in the pool and move into the pool
255 file = files.keys()[0];
256 if os.access(file, os.R_OK) == 0:
257 pool_dir = Cnf["Dir::PoolDir"] + '/' + utils.poolify(changes["source"], files[file]["component"]);
259 # Otherwise (normal case) map stable to updates
261 reject_message = reject_message + "Mapping stable to updates.\n";
262 del changes["distribution"]["stable"];
263 changes["distribution"]["proposed-updates"] = 1;
265 # chopversion = no epoch; chopversion2 = no epoch and no revision (e.g. for .orig.tar.gz comparison)
266 changes["chopversion"] = utils.re_no_epoch.sub('', changes["version"])
267 changes["chopversion2"] = utils.re_no_revision.sub('', changes["chopversion"])
269 if string.find(reject_message, "Rejected:") != -1:
275 global reject_message
277 archive = utils.where_am_i();
279 for file in files.keys():
280 # Check the file is readable
281 if os.access(file,os.R_OK) == 0:
282 reject_message = reject_message + "Rejected: Can't read `%s'.\n" % (file)
283 files[file]["type"] = "unreadable";
285 # If it's byhand skip remaining checks
286 if files[file]["section"] == "byhand":
287 files[file]["byhand"] = 1;
288 files[file]["type"] = "byhand";
289 # Checks for a binary package...
290 elif re_isadeb.match(file) != None:
291 # Extract package information using dpkg-deb
292 control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))
294 # Check for mandatory fields
295 if control.Find("Package") == None:
296 reject_message = reject_message + "Rejected: %s: No package field in control.\n" % (file)
297 if control.Find("Architecture") == None:
298 reject_message = reject_message + "Rejected: %s: No architecture field in control.\n" % (file)
299 if control.Find("Version") == None:
300 reject_message = reject_message + "Rejected: %s: No version field in control.\n" % (file)
302 # Ensure the package name matches the one give in the .changes
303 if not changes["binary"].has_key(control.Find("Package", "")):
304 reject_message = reject_message + "Rejected: %s: control file lists name as `%s', which isn't in changes file.\n" % (file, control.Find("Package", ""))
306 # Validate the architecture
307 if not Cnf.has_key("Suite::Unstable::Architectures::%s" % (control.Find("Architecture", ""))):
308 reject_message = reject_message + "Rejected: Unknown architecture '%s'.\n" % (control.Find("Architecture", ""))
310 # Check the architecture matches the one given in the .changes
311 if not changes["architecture"].has_key(control.Find("Architecture", "")):
312 reject_message = reject_message + "Rejected: %s: control file lists arch as `%s', which isn't in changes file.\n" % (file, control.Find("Architecture", ""))
313 # Check the section & priority match those given in the .changes (non-fatal)
314 if control.Find("Section") != None and files[file]["section"] != "" and files[file]["section"] != control.Find("Section"):
315 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"])
316 if control.Find("Priority") != None and files[file]["priority"] != "" and files[file]["priority"] != control.Find("Priority"):
317 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"])
319 epochless_version = utils.re_no_epoch.sub('', control.Find("Version", ""))
321 files[file]["package"] = control.Find("Package");
322 files[file]["architecture"] = control.Find("Architecture");
323 files[file]["version"] = control.Find("Version");
324 files[file]["maintainer"] = control.Find("Maintainer", "");
325 if file[-5:] == ".udeb":
326 files[file]["dbtype"] = "udeb";
327 elif file[-4:] == ".deb":
328 files[file]["dbtype"] = "deb";
330 reject_message = reject_message + "Rejected: %s is neither a .deb or a .udeb.\n " % (file);
331 files[file]["type"] = "deb";
332 files[file]["fullname"] = "%s_%s_%s.deb" % (control.Find("Package", ""), epochless_version, control.Find("Architecture", ""))
333 files[file]["source"] = control.Find("Source", "");
334 if files[file]["source"] == "":
335 files[file]["source"] = files[file]["package"];
336 # Checks for a source package...
338 m = re_issource.match(file)
340 files[file]["package"] = m.group(1)
341 files[file]["version"] = m.group(2)
342 files[file]["type"] = m.group(3)
344 # Ensure the source package name matches the Source filed in the .changes
345 if changes["source"] != files[file]["package"]:
346 reject_message = reject_message + "Rejected: %s: changes file doesn't say %s for Source\n" % (file, files[file]["package"])
348 # Ensure the source version matches the version in the .changes file
349 if files[file]["type"] == "orig.tar.gz":
350 changes_version = changes["chopversion2"]
352 changes_version = changes["chopversion"]
353 if changes_version != files[file]["version"]:
354 reject_message = reject_message + "Rejected: %s: should be %s according to changes file.\n" % (file, changes_version)
356 # Ensure the .changes lists source in the Architecture field
357 if not changes["architecture"].has_key("source"):
358 reject_message = reject_message + "Rejected: %s: changes file doesn't list `source' in Architecture field.\n" % (file)
360 # Check the signature of a .dsc file
361 if files[file]["type"] == "dsc":
362 check_signature(file)
364 files[file]["fullname"] = file
366 # Not a binary or source package? Assume byhand...
368 files[file]["byhand"] = 1;
369 files[file]["type"] = "byhand";
371 files[file]["oldfiles"] = {}
372 for suite in changes["distribution"].keys():
374 if files[file].has_key("byhand"):
377 if Cnf.has_key("Suite:%s::Components" % (suite)) and not Cnf.has_key("Suite::%s::Components::%s" % (suite, files[file]["component"])):
378 reject_message = reject_message + "Rejected: unknown component `%s' for suite `%s'.\n" % (files[file]["component"], suite)
381 # See if the package is NEW
382 if not in_override_p(files[file]["package"], files[file]["component"], suite, files[file].get("dbtype","")):
383 files[file]["new"] = 1
385 # Find any old binary packages
386 if files[file]["type"] == "deb":
387 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"
388 % (files[file]["package"], suite, files[file]["architecture"]))
389 oldfiles = q.dictresult()
390 for oldfile in oldfiles:
391 files[file]["oldfiles"][suite] = oldfile
392 # Check versions [NB: per-suite only; no cross-suite checking done (yet)]
393 if apt_pkg.VersionCompare(files[file]["version"], oldfile["version"]) != 1:
394 if Cnf["Dinstall::Options::No-Version-Check"]:
395 reject_message = reject_message + "Overriden rejection"
397 reject_message = reject_message + "Rejected"
398 reject_message = reject_message + ": %s Old version `%s' >= new version `%s'.\n" % (file, oldfile["version"], files[file]["version"])
399 # Check for existing copies of the file
400 if not changes.has_key("stable upload"):
401 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"]))
402 if q.getresult() != []:
403 reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (file)
405 # Find any old .dsc files
406 elif files[file]["type"] == "dsc":
407 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"
408 % (files[file]["package"], suite))
409 oldfiles = q.dictresult()
410 if len(oldfiles) >= 1:
411 files[file]["oldfiles"][suite] = oldfiles[0]
413 # Validate the component
414 component = files[file]["component"];
415 component_id = db_access.get_component_id(component);
416 if component_id == -1:
417 reject_message = reject_message + "Rejected: file '%s' has unknown component '%s'.\n" % (file, component);
420 # Check the md5sum & size against existing files (if any)
421 location = Cnf["Dir::PoolDir"];
422 files[file]["location id"] = db_access.get_location_id (location, component, archive);
424 files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"]);
425 files_id = db_access.get_files_id(files[file]["pool name"] + file, files[file]["size"], files[file]["md5sum"], files[file]["location id"]);
427 reject_message = reject_message + "Rejected: INTERNAL ERROR, get_files_id() returned multiple matches for %s.\n" % (file)
429 reject_message = reject_message + "Rejected: md5sum and/or size mismatch on existing copy of %s.\n" % (file)
430 files[file]["files id"] = files_id
432 # Check for packages that have moved from one component to another
433 if files[file]["oldfiles"].has_key(suite) and files[file]["oldfiles"][suite]["name"] != files[file]["component"]:
434 files[file]["othercomponents"] = files[file]["oldfiles"][suite]["name"];
437 if string.find(reject_message, "Rejected:") != -1:
442 ###############################################################################
445 global dsc, dsc_files, reject_message, reprocess, orig_tar_id;
447 for file in files.keys():
448 if files[file]["type"] == "dsc":
450 dsc = utils.parse_changes(file)
451 except utils.cant_open_exc:
452 reject_message = reject_message + "Rejected: can't read changes file '%s'.\n" % (filename)
454 except utils.changes_parse_error_exc, line:
455 reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line)
458 dsc_files = utils.build_file_list(dsc, 1)
459 except utils.no_files_exc:
460 reject_message = reject_message + "Rejected: no Files: field in .dsc file.\n";
463 # Try and find all files mentioned in the .dsc. This has
464 # to work harder to cope with the multiple possible
465 # locations of an .orig.tar.gz.
466 for dsc_file in dsc_files.keys():
467 if files.has_key(dsc_file):
468 actual_md5 = files[dsc_file]["md5sum"];
469 actual_size = int(files[dsc_file]["size"]);
470 found = "%s in incoming" % (dsc_file)
471 # Check the file does not already exist in the archive
472 if not changes.has_key("stable upload"):
473 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)));
474 if q.getresult() != []:
475 reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (dsc_file)
476 elif dsc_file[-12:] == ".orig.tar.gz":
478 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)));
481 old_file = ql[0][0] + ql[0][1];
482 actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
483 actual_size = os.stat(old_file)[stat.ST_SIZE];
485 suite_type = ql[0][2];
486 dsc_files[dsc_file]["files id"] = ql[0][3]; # need this for updating dsc_files in install()
488 if suite_type == "legacy" or suite_type == "legacy-mixed":
489 orig_tar_id = ql[0][3];
491 # Not there? Check in Incoming...
492 # [See comment above process_it() for explanation
493 # of why this is necessary...]
494 if os.access(dsc_file, os.R_OK) != 0:
495 files[dsc_file] = {};
496 files[dsc_file]["size"] = os.stat(dsc_file)[stat.ST_SIZE];
497 files[dsc_file]["md5sum"] = dsc_files[dsc_file]["md5sum"];
498 files[dsc_file]["section"] = files[file]["section"];
499 files[dsc_file]["priority"] = files[file]["priority"];
500 files[dsc_file]["component"] = files[file]["component"];
504 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);
507 reject_message = reject_message + "Rejected: %s refers to %s, but I can't find it in Incoming." % (file, dsc_file);
509 if actual_md5 != dsc_files[dsc_file]["md5sum"]:
510 reject_message = reject_message + "Rejected: md5sum for %s doesn't match %s.\n" % (found, file);
511 if actual_size != int(dsc_files[dsc_file]["size"]):
512 reject_message = reject_message + "Rejected: size for %s doesn't match %s.\n" % (found, file);
514 if string.find(reject_message, "Rejected:") != -1:
519 ###############################################################################
521 def check_md5sums ():
522 global reject_message;
524 for file in files.keys():
526 file_handle = utils.open_file(file,"r");
527 except utils.cant_open_exc:
530 if apt_pkg.md5sum(file_handle) != files[file]["md5sum"]:
531 reject_message = reject_message + "Rejected: md5sum check failed for %s.\n" % (file);
533 #####################################################################################################################
535 def action (changes_filename):
536 byhand = confirm = suites = summary = new = "";
538 # changes["distribution"] may not exist in corner cases
539 # (e.g. unreadable changes files)
540 if not changes.has_key("distribution"):
541 changes["distribution"] = {};
543 for suite in changes["distribution"].keys():
544 if Cnf.has_key("Suite::%s::Confirm"):
545 confirm = confirm + suite + ", "
546 suites = suites + suite + ", "
547 confirm = confirm[:-2]
550 for file in files.keys():
551 if files[file].has_key("byhand"):
553 summary = summary + file + " byhand\n"
554 elif files[file].has_key("new"):
556 summary = summary + "(new) %s %s %s\n" % (file, files[file]["priority"], files[file]["section"])
557 if files[file].has_key("othercomponents"):
558 summary = summary + "WARNING: Already present in %s distribution.\n" % (files[file]["othercomponents"])
559 if files[file]["type"] == "deb":
560 summary = summary + apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))["Description"] + '\n';
562 files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"])
563 destination = Cnf["Dir::PoolRoot"] + files[file]["pool name"] + file
564 summary = summary + file + "\n to " + destination + "\n"
566 short_summary = summary;
568 # This is for direport's benefit...
569 f = re_fdnic.sub("\n .\n", changes.get("changes",""));
571 if confirm or byhand or new:
572 summary = summary + "Changes: " + f;
574 summary = summary + announce (short_summary, 0)
576 (prompt, answer) = ("", "XXX")
577 if Cnf["Dinstall::Options::No-Action"] or Cnf["Dinstall::Options::Automatic"]:
580 if string.find(reject_message, "Rejected") != -1:
581 if time.time()-os.path.getmtime(changes_filename) < 86400:
582 print "SKIP (too new)\n" + reject_message,;
583 prompt = "[S]kip, Manual reject, Quit ?";
585 print "REJECT\n" + reject_message,;
586 prompt = "[R]eject, Manual reject, Skip, Quit ?";
587 if Cnf["Dinstall::Options::Automatic"]:
590 print "NEW to %s\n%s%s" % (suites, reject_message, summary),;
591 prompt = "[S]kip, New ack, Manual reject, Quit ?";
592 if Cnf["Dinstall::Options::Automatic"] and Cnf["Dinstall::Options::Ack-New"]:
595 print "BYHAND\n" + reject_message + summary,;
596 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
598 print "CONFIRM to %s\n%s%s" % (confirm, reject_message, summary),
599 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
601 print "INSTALL\n" + reject_message + summary,;
602 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
603 if Cnf["Dinstall::Options::Automatic"]:
606 while string.find(prompt, answer) == -1:
608 answer = utils.our_raw_input()
609 m = re_default_answer.match(prompt)
612 answer = string.upper(answer[:1])
615 reject (changes_filename, "");
617 manual_reject (changes_filename);
619 install (changes_filename, summary, short_summary);
621 acknowledge_new (changes_filename, summary);
625 #####################################################################################################################
627 def install (changes_filename, summary, short_summary):
628 global install_count, install_bytes
630 # Stable uploads are a special case
631 if changes.has_key("stable upload"):
632 stable_install (changes_filename, summary, short_summary);
637 archive = utils.where_am_i();
639 # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
640 projectB.query("BEGIN WORK");
642 # Add the .dsc file to the DB
643 for file in files.keys():
644 if files[file]["type"] == "dsc":
645 package = dsc["source"]
646 version = dsc["version"] # NB: not files[file]["version"], that has no epoch
647 maintainer = dsc["maintainer"]
648 maintainer = string.replace(maintainer, "'", "\\'")
649 maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
650 filename = files[file]["pool name"] + file;
651 dsc_location_id = files[file]["location id"];
652 if not files[file]["files id"]:
653 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], dsc_location_id)
654 projectB.query("INSERT INTO source (source, version, maintainer, file) VALUES ('%s', '%s', %d, %d)"
655 % (package, version, maintainer_id, files[file]["files id"]))
657 for suite in changes["distribution"].keys():
658 suite_id = db_access.get_suite_id(suite);
659 projectB.query("INSERT INTO src_associations (suite, source) VALUES (%d, currval('source_id_seq'))" % (suite_id))
661 # Add the source files to the DB (files and dsc_files)
662 projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files[file]["files id"]));
663 for dsc_file in dsc_files.keys():
664 filename = files[file]["pool name"] + dsc_file;
665 # If the .orig.tar.gz is already in the pool, it's
666 # files id is stored in dsc_files by check_dsc().
667 files_id = dsc_files[dsc_file].get("files id", None);
669 files_id = db_access.get_files_id(filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], dsc_location_id);
670 # FIXME: needs to check for -1/-2 and or handle exception
672 files_id = db_access.set_files_id (filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], dsc_location_id);
673 projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files_id));
675 # Add the .deb files to the DB
676 for file in files.keys():
677 if files[file]["type"] == "deb":
678 package = files[file]["package"]
679 version = files[file]["version"]
680 maintainer = files[file]["maintainer"]
681 maintainer = string.replace(maintainer, "'", "\\'")
682 maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
683 architecture = files[file]["architecture"]
684 architecture_id = db_access.get_architecture_id (architecture);
685 type = files[file]["dbtype"];
686 component = files[file]["component"]
687 source = files[file]["source"]
689 if string.find(source, "(") != -1:
690 m = utils.re_extract_src_version.match(source)
692 source_version = m.group(2)
693 if not source_version:
694 source_version = version
695 filename = files[file]["pool name"] + file;
696 if not files[file]["files id"]:
697 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], files[file]["location id"])
698 source_id = db_access.get_source_id (source, source_version);
700 projectB.query("INSERT INTO binaries (package, version, maintainer, source, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, %d, '%s')"
701 % (package, version, maintainer_id, source_id, architecture_id, files[file]["files id"], type));
703 projectB.query("INSERT INTO binaries (package, version, maintainer, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, '%s')"
704 % (package, version, maintainer_id, architecture_id, files[file]["files id"], type));
705 for suite in changes["distribution"].keys():
706 suite_id = db_access.get_suite_id(suite);
707 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES (%d, currval('binaries_id_seq'))" % (suite_id));
709 # If the .orig.tar.gz is in a legacy directory we need to poolify
710 # it, so that apt-get source (and anything else that goes by the
711 # "Directory:" field in the Sources.gz file) works.
712 if orig_tar_id != None:
713 q = projectB.query("SELECT DISTINCT ON (f.id) 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 AND (l.type = 'legacy' OR l.type = 'legacy-mixed')" % (orig_tar_id));
716 # First move the files to the new location
717 legacy_filename = qid["path"]+qid["filename"];
718 pool_location = utils.poolify (changes["source"], files[file]["component"]);
719 pool_filename = pool_location + os.path.basename(qid["filename"]);
720 destination = Cnf["Dir::PoolDir"] + pool_location
721 utils.move(legacy_filename, destination);
722 # Then Update the DB's files table
723 q = projectB.query("UPDATE files SET filename = '%s', location = '%s' WHERE id = '%s'" % (pool_filename, dsc_location_id, qid["files_id"]));
725 # Install the files into the pool
726 for file in files.keys():
727 if files[file].has_key("byhand"):
729 destination = Cnf["Dir::PoolDir"] + files[file]["pool name"] + file
730 destdir = os.path.dirname(destination)
731 utils.move (file, destination)
732 install_bytes = install_bytes + float(files[file]["size"])
734 # Copy the .changes file across for suite which need it.
735 for suite in changes["distribution"].keys():
736 if Cnf.has_key("Suite::%s::CopyChanges" % (suite)):
737 utils.copy (changes_filename, Cnf["Dir::RootDir"] + Cnf["Suite::%s::CopyChanges" % (suite)]);
739 projectB.query("COMMIT WORK");
741 utils.move (changes_filename, Cnf["Dir::IncomingDir"] + 'DONE/' + os.path.basename(changes_filename))
743 install_count = install_count + 1;
745 if not Cnf["Dinstall::Options::No-Mail"]:
746 mail_message = """Return-Path: %s
749 Bcc: troup@auric.debian.org
750 Subject: %s INSTALLED
756 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], os.path.basename(changes_filename), reject_message, summary, installed_footer)
757 utils.send_mail (mail_message, "")
758 announce (short_summary, 1)
760 #####################################################################################################################
762 def stable_install (changes_filename, summary, short_summary):
763 global install_count, install_bytes
765 print "Installing to stable."
767 archive = utils.where_am_i();
769 # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
770 projectB.query("BEGIN WORK");
772 # Add the .dsc file to the DB
773 for file in files.keys():
774 if files[file]["type"] == "dsc":
775 package = dsc["source"]
776 version = dsc["version"] # NB: not files[file]["version"], that has no epoch
777 q = projectB.query("SELECT id FROM source WHERE source = '%s' AND version = '%s'" % (package, version))
780 sys.stderr.write("INTERNAL ERROR: couldn't find '%s' (%s) in source table.\n" % (package, version));
782 source_id = ql[0][0];
783 suite_id = db_access.get_suite_id('proposed-updates');
784 projectB.query("DELETE FROM src_associations WHERE suite = '%s' AND source = '%s'" % (suite_id, source_id));
785 suite_id = db_access.get_suite_id('stable');
786 projectB.query("INSERT INTO src_associations (suite, source) VALUES ('%s', '%s')" % (suite_id, source_id));
788 # Add the .deb files to the DB
789 for file in files.keys():
790 if files[file]["type"] == "deb":
791 package = files[file]["package"]
792 version = files[file]["version"]
793 architecture = files[file]["architecture"]
794 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))
797 sys.stderr.write("INTERNAL ERROR: couldn't find '%s' (%s for %s architecture) in binaries table.\n" % (package, version, architecture));
799 binary_id = ql[0][0];
800 suite_id = db_access.get_suite_id('proposed-updates');
801 projectB.query("DELETE FROM bin_associations WHERE suite = '%s' AND bin = '%s'" % (suite_id, binary_id));
802 suite_id = db_access.get_suite_id('stable');
803 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES ('%s', '%s')" % (suite_id, binary_id));
805 projectB.query("COMMIT WORK");
807 utils.move (changes_filename, Cnf["Rhona::Morgue"] + os.path.basename(changes_filename));
809 # Update the Stable ChangeLog file
811 new_changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + ".ChangeLog";
812 changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + "ChangeLog";
813 if os.path.exists(new_changelog_filename):
814 os.unlink (new_changelog_filename);
816 new_changelog = utils.open_file(new_changelog_filename, 'w');
817 for file in files.keys():
818 if files[file]["type"] == "deb":
819 new_changelog.write("stable/%s/binary-%s/%s\n" % (files[file]["component"], files[file]["architecture"], file));
820 elif re_issource.match(file) != None:
821 new_changelog.write("stable/%s/source/%s\n" % (files[file]["component"], file));
823 new_changelog.write("%s\n" % (file));
824 chop_changes = re_fdnic.sub("\n", changes["changes"]);
825 new_changelog.write(chop_changes + '\n\n');
826 if os.access(changelog_filename, os.R_OK) != 0:
827 changelog = utils.open_file(changelog_filename, 'r');
828 new_changelog.write(changelog.read());
829 new_changelog.close();
830 if os.access(changelog_filename, os.R_OK) != 0:
831 os.unlink(changelog_filename);
832 utils.move(new_changelog_filename, changelog_filename);
834 install_count = install_count + 1;
836 if not Cnf["Dinstall::Options::No-Mail"]:
837 mail_message = """Return-Path: %s
840 Bcc: troup@auric.debian.org
841 Subject: %s INSTALLED into stable
847 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], os.path.basename(changes_filename), reject_message, summary, installed_footer)
848 utils.send_mail (mail_message, "")
849 announce (short_summary, 1)
851 #####################################################################################################################
853 def reject (changes_filename, manual_reject_mail_filename):
856 base_changes_filename = os.path.basename(changes_filename);
857 reason_filename = re_changes.sub("reason", base_changes_filename);
858 reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename);
860 # Move the .changes files and it's contents into REJECT/ (if we can; errors are ignored)
862 utils.move (changes_filename, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], base_changes_filename));
863 except utils.cant_overwrite_exc:
864 sys.stderr.write("W: couldn't overwrite existing file '%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], base_changes_filename));
866 for file in files.keys():
867 if os.path.exists(file):
869 utils.move (file, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], file));
870 except utils.cant_overwrite_exc:
871 sys.stderr.write("W: couldn't overwrite existing file '%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], file));
874 # If this is not a manual rejection generate the .reason file and rejection mail message
875 if manual_reject_mail_filename == "":
876 if os.path.exists(reject_filename):
877 os.unlink(reject_filename);
878 fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
879 os.write(fd, reject_message);
881 reject_mail_message = """From: %s
883 Bcc: troup@auric.debian.org
888 %s""" % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], os.path.basename(changes_filename), reject_message, reject_footer);
889 else: # Have a manual rejection file to use
890 reject_mail_message = ""; # avoid <undef>'s
892 # Send the rejection mail if appropriate
893 if not Cnf["Dinstall::Options::No-Mail"]:
894 utils.send_mail (reject_mail_message, manual_reject_mail_filename);
896 ##################################################################
898 def manual_reject (changes_filename):
899 # Build up the rejection email
900 user_email_address = string.replace(string.split(pwd.getpwuid(os.getuid())[4],',')[0], '.', '')
901 user_email_address = user_email_address + " <%s@%s>" % (pwd.getpwuid(os.getuid())[0], Cnf["Dinstall::MyHost"])
902 manual_reject_message = Cnf.get("Dinstall::Options::Manual-Reject", "")
904 reject_mail_message = """From: %s
907 Bcc: troup@auric.debian.org
913 %s""" % (user_email_address, Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], os.path.basename(changes_filename), manual_reject_message, reject_message, reject_footer)
915 # Write the rejection email out as the <foo>.reason file
916 reason_filename = re_changes.sub("reason", os.path.basename(changes_filename));
917 reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename)
918 if os.path.exists(reject_filename):
919 os.unlink(reject_filename);
920 fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
921 os.write(fd, reject_mail_message);
924 # If we weren't given one, spawn an editor so the user can add one in
925 if manual_reject_message == "":
926 result = os.system("vi +6 %s" % (reject_file))
928 sys.stderr.write ("vi invocation failed for `%s'!" % (reject_file))
931 # Then process it as if it were an automatic rejection
932 reject (changes_filename, reject_filename)
934 #####################################################################################################################
936 def acknowledge_new (changes_filename, summary):
939 changes_filename = os.path.basename(changes_filename);
941 new_ack_new[changes_filename] = 1;
943 if new_ack_old.has_key(changes_filename):
944 print "Ack already sent.";
947 print "Sending new ack.";
948 if not Cnf["Dinstall::Options::No-Mail"]:
949 new_ack_message = """Return-Path: %s
952 Bcc: troup@auric.debian.org
956 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, summary, new_ack_footer);
957 utils.send_mail(new_ack_message,"");
959 #####################################################################################################################
961 def announce (short_summary, action):
962 # Only do announcements for source uploads with a recent dpkg-dev installed
963 if float(changes.get("format", 0)) < 1.6 or not changes["architecture"].has_key("source"):
969 for dist in changes["distribution"].keys():
970 list = Cnf.Find("Suite::%s::Announce" % (dist))
971 if list == None or lists_done.has_key(list):
974 summary = summary + "Announcing to %s\n" % (list)
977 mail_message = """Return-Path: %s
980 Bcc: troup@auric.debian.org
981 Subject: Installed %s %s (%s)
987 """ % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], list, changes["source"], changes["version"], string.join(changes["architecture"].keys(), ' ' ),
988 changes["filecontents"], short_summary)
989 utils.send_mail (mail_message, "")
991 (dsc_rfc822, dsc_name, dsc_email) = utils.fix_maintainer (dsc.get("maintainer",Cnf["Dinstall::MyEmailAddress"]));
992 bugs = changes["closes"].keys()
994 if dsc_name == changes["maintainername"]:
995 summary = summary + "Closing bugs: "
997 summary = summary + "%s " % (bug)
999 mail_message = """Return-Path: %s
1001 To: %s-close@bugs.debian.org
1002 Bcc: troup@auric.debian.org
1003 Subject: Bug#%s: fixed in %s %s
1005 We believe that the bug you reported is fixed in the latest version of
1006 %s, which has been installed in the Debian FTP archive:
1008 %s""" % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], bug, bug, changes["source"], changes["version"], changes["source"], short_summary)
1010 if changes["distribution"].has_key("stable"):
1011 mail_message = mail_message + """Note that this package is not part of the released stable Debian
1012 distribution. It may have dependencies on other unreleased software,
1013 or other instabilities. Please take care if you wish to install it.
1014 The update will eventually make its way into the next released Debian
1017 mail_message = mail_message + """A summary of the changes between this version and the previous one is
1020 Thank you for reporting the bug, which will now be closed. If you
1021 have further comments please address them to %s@bugs.debian.org,
1022 and the maintainer will reopen the bug report if appropriate.
1024 Debian distribution maintenance software
1026 %s (supplier of updated %s package)
1028 (This message was generated automatically at their request; if you
1029 believe that there is a problem with it please contact the archive
1030 administrators by mailing ftpmaster@debian.org)
1033 %s""" % (bug, changes["maintainer"], changes["source"], changes["filecontents"])
1035 utils.send_mail (mail_message, "")
1037 summary = summary + "Setting bugs to severity fixed: "
1038 control_message = ""
1040 summary = summary + "%s " % (bug)
1041 control_message = control_message + "severity %s fixed\n" % (bug)
1042 if action and control_message != "":
1043 mail_message = """Return-Path: %s
1045 To: control@bugs.debian.org
1046 Bcc: troup@auric.debian.org, %s
1047 Subject: Fixed in NMU of %s %s
1052 This message was generated automatically in response to a
1053 non-maintainer upload. The .changes file follows.
1056 """ % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes["maintainer822"], changes["source"], changes["version"], control_message, changes["filecontents"])
1057 utils.send_mail (mail_message, "")
1058 summary = summary + "\n"
1062 ###############################################################################
1064 # reprocess is necessary for the case of foo_1.2-1 and foo_1.2-2 in
1065 # Incoming. -1 will reference the .orig.tar.gz, but -2 will not.
1066 # dsccheckdistrib() can find the .orig.tar.gz but it will not have
1067 # processed it during it's checks of -2. If -1 has been deleted or
1068 # otherwise not checked by da-install, the .orig.tar.gz will not have
1069 # been checked at all. To get round this, we force the .orig.tar.gz
1070 # into the .changes structure and reprocess the .changes file.
1072 def process_it (changes_file):
1073 global reprocess, orig_tar_id, changes, dsc, dsc_files, files;
1077 # Reset some globals
1084 # Absolutize the filename to avoid the requirement of being in the
1085 # same directory as the .changes file.
1086 changes_file = os.path.abspath(changes_file);
1088 # And since handling of installs to stable munges with the CWD;
1089 # save and restore it.
1092 check_signature (changes_file);
1093 check_changes (changes_file);
1100 action(changes_file);
1105 ###############################################################################
1108 global Cnf, projectB, reject_message, install_bytes, new_ack_old
1112 Cnf = apt_pkg.newConfiguration();
1113 apt_pkg.ReadConfigFileISC(Cnf,utils.which_conf_file());
1115 Arguments = [('a',"automatic","Dinstall::Options::Automatic"),
1116 ('d',"debug","Dinstall::Options::Debug", "IntVal"),
1117 ('h',"help","Dinstall::Options::Help"),
1118 ('k',"ack-new","Dinstall::Options::Ack-New"),
1119 ('m',"manual-reject","Dinstall::Options::Manual-Reject", "HasArg"),
1120 ('n',"no-action","Dinstall::Options::No-Action"),
1121 ('p',"no-lock", "Dinstall::Options::No-Lock"),
1122 ('r',"no-version-check", "Dinstall::Options::No-Version-Check"),
1123 ('s',"no-mail", "Dinstall::Options::No-Mail"),
1124 ('u',"override-distribution", "Dinstall::Options::Override-Distribution", "HasArg"),
1125 ('v',"version","Dinstall::Options::Version")];
1127 changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
1129 if Cnf["Dinstall::Options::Help"]:
1132 if Cnf["Dinstall::Options::Version"]:
1133 print "katie version 0.0000000000";
1136 postgresql_user = None; # Default == Connect as user running program.
1138 # -n/--dry-run invalidates some other options which would involve things happening
1139 if Cnf["Dinstall::Options::No-Action"]:
1140 Cnf["Dinstall::Options::Automatic"] = ""
1141 Cnf["Dinstall::Options::Ack-New"] = ""
1142 postgresql_user = Cnf["DB::ROUser"];
1144 projectB = pg.connect('projectb', Cnf["DB::Host"], int(Cnf["DB::Port"]), None, None, postgresql_user);
1146 db_access.init(Cnf, projectB);
1148 # Check that we aren't going to clash with the daily cron job
1150 if os.path.exists("%s/Archive_Maintenance_In_Progress" % (Cnf["Dir::RootDir"])) and not Cnf["Dinstall::Options::No-Lock"]:
1151 sys.stderr.write("Archive maintenance in progress. Try again later.\n");
1154 # Obtain lock if not in no-action mode
1156 if not Cnf["Dinstall::Options::No-Action"]:
1157 lock_fd = os.open(Cnf["Dinstall::LockFile"], os.O_RDWR);
1158 fcntl.lockf(lock_fd, FCNTL.F_TLOCK);
1160 # Read in the list of already-acknowledged NEW packages
1161 new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'r');
1163 for line in new_ack_list.readlines():
1164 new_ack_old[line[:-1]] = 1;
1165 new_ack_list.close();
1167 # Process the changes files
1168 for changes_file in changes_files:
1170 print "\n" + changes_file;
1171 process_it (changes_file);
1175 if install_count > 1:
1177 sys.stderr.write("Installed %d package %s, %s.\n" % (install_count, sets, utils.size_type(int(install_bytes))));
1179 # Write out the list of already-acknowledged NEW packages
1180 if Cnf["Dinstall::Options::Ack-New"]:
1181 new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'w')
1182 for i in new_ack_new.keys():
1183 new_ack_list.write(i+'\n')
1184 new_ack_list.close()
1187 if __name__ == '__main__':