]> git.decadent.org.uk Git - dak.git/blob - katie
life sucks
[dak.git] / katie
1 #!/usr/bin/env python
2
3 # Installs Debian packaes
4 # Copyright (C) 2000  James Troup <james@nocrew.org>
5 # $Id: katie,v 1.9 2000-12-05 04:27:48 troup Exp $
6
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.
11
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.
16
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
20
21 # Based (almost entirely) on dinstall by Guy Maor <maor@debian.org>
22
23 #########################################################################################
24
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."
28 #
29 #    Kyle: "But Cartman, we're trying to..."
30 #
31 #    Cartman: "uhh.. screw you guys... home."
32
33 #########################################################################################
34
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
38
39 ###############################################################################
40
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");
53
54 ###############################################################################
55
56 #
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.
59
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/."""
64 #
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.
68
69 You may have gotten the distribution wrong.  You'll get warnings above
70 if files already exist in other distributions."""
71 #
72 installed_footer = """If the override file requires editing, file a bug on ftp.debian.org.
73
74 Thank you for your contribution to Debian GNU."""
75
76 #########################################################################################
77
78 # Globals
79 Cnf = None;
80 reject_message = "";
81 changes = {};
82 dsc = {};
83 dsc_files = {};
84 files = {};
85 projectB = None;
86 new_ack_new = {};
87 new_ack_old = {};
88 overrides = {};
89 install_count = 0;
90 install_bytes = 0.0;
91 reprocess = 0;
92 orig_tar_id = None;
93
94 #########################################################################################
95
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'"""
106     sys.exit(exit_code)
107
108 def check_signature (filename):
109     global reject_message
110
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))
112     if (result != 0):
113         reject_message = "Rejected: GPG signature check failed on `%s'.\n%s\n" % (filename, output)
114         return 0
115     return 1
116
117 #####################################################################################################################
118
119 def read_override_file (filename, suite, component):
120     global overrides;
121
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
128     file.close()
129
130
131 # See if a given package is in the override file.  Caches and only loads override files on demand.
132
133 def in_override_p (package, component, suite):
134     global overrides;
135
136     # Avoid <undef> on unknown distributions
137     if db_access.get_suite_id(suite) == -1:
138         return None;
139
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);
151         else: # all others.
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);
155
156     return overrides[suite][component].get(package, None);
157
158 #####################################################################################################################
159
160 def check_changes(filename):
161     global reject_message, changes, files
162
163     # Parse the .changes field into a dictionary [FIXME - need to trap errors, pass on to reject_message etc.]
164     try:
165         changes = utils.parse_changes(filename)
166     except utils.cant_open_exc:
167         reject_message = "Rejected: can't read changes file '%s'.\n" % (filename)
168         return 0;
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"];
172         return 0;
173
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, "")
176
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
182
183     # Fix the Maintainer: field to be RFC822 compatible
184     (changes["maintainer822"], changes["maintainername"], changes["maintaineremail"]) = utils.fix_maintainer (changes["maintainer"])
185
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"]
190
191     # Split multi-value fields into a lower-level dictionary
192     for i in ("architecture", "distribution", "binary", "closes"):
193         o = changes.get(i, "")
194         if o != "":
195             del changes[i]
196         changes[i] = {}
197         for j in string.split(o):
198             changes[i][j] = 1
199
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)
205
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"
210
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)
215
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"]
222     
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"]
229
230     # Handle uploads to stable
231     if changes["distribution"].has_key("stable"):
232         # If running from within proposed-updates; assume an install to stable
233         if string.find(os.getcwd(), 'proposed-updates') != -1:
234             # FIXME: should probably remove anything that != stable
235             for i in ("frozen", "unstable"):
236                 if changes["distribution"].has_key(i):
237                     reject_message = reject_message + "Removing %s from distribution list.\n"
238                     del changes["distribution"][i]
239             changes["stable upload"] = 1;
240             # If we can't find a file from the .changes; assume it's a package already in the pool and move into the pool
241             file = files.keys()[0];
242             if os.access(file, os.R_OK) == 0:
243                 pool_dir = Cnf["Dir::PoolDir"] + '/' + utils.poolify(changes["source"], files[file]["component"]);
244                 os.chdir(pool_dir);
245         # Otherwise (normal case) map stable to updates
246         else:
247             reject_message = reject_message + "Mapping stable to updates.\n";
248             del changes["distribution"]["stable"];
249             changes["distribution"]["proposed-updates"] = 1;
250
251     # chopversion = no epoch; chopversion2 = no epoch and no revision (e.g. for .orig.tar.gz comparison)
252     changes["chopversion"] = utils.re_no_epoch.sub('', changes["version"])
253     changes["chopversion2"] = utils.re_no_revision.sub('', changes["chopversion"])
254     
255     if string.find(reject_message, "Rejected:") != -1:
256         return 0
257     else: 
258         return 1
259
260 def check_files():
261     global reject_message
262     
263     archive = utils.where_am_i();
264
265     for file in files.keys():
266         # Check the file is readable
267         if os.access(file,os.R_OK) == 0:
268             reject_message = reject_message + "Rejected: Can't read `%s'.\n" % (file)
269             files[file]["type"] = "unreadable";
270             continue
271         # If it's byhand skip remaining checks
272         if files[file]["section"] == "byhand":
273             files[file]["byhand"] = 1;
274             files[file]["type"] = "byhand";
275         # Checks for a binary package...
276         elif re_isadeb.match(file) != None:
277             # Extract package information using dpkg-deb
278             control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))
279
280             # Check for mandatory fields
281             if control.Find("Package") == None:
282                 reject_message = reject_message + "Rejected: %s: No package field in control.\n" % (file)
283             if control.Find("Architecture") == None:
284                 reject_message = reject_message + "Rejected: %s: No architecture field in control.\n" % (file)
285             if control.Find("Version") == None:
286                 reject_message = reject_message + "Rejected: %s: No version field in control.\n" % (file)
287                 
288             # Ensure the package name matches the one give in the .changes
289             if not changes["binary"].has_key(control.Find("Package", "")):
290                 reject_message = reject_message + "Rejected: %s: control file lists name as `%s', which isn't in changes file.\n" % (file, control.Find("Package", ""))
291
292             # Validate the architecture
293             if not Cnf.has_key("Suite::Unstable::Architectures::%s" % (control.Find("Architecture", ""))):
294                 reject_message = reject_message + "Rejected: Unknown architecture '%s'.\n" % (control.Find("Architecture", ""))
295
296             # Check the architecture matches the one given in the .changes
297             if not changes["architecture"].has_key(control.Find("Architecture", "")):
298                 reject_message = reject_message + "Rejected: %s: control file lists arch as `%s', which isn't in changes file.\n" % (file, control.Find("Architecture", ""))
299             # Check the section & priority match those given in the .changes (non-fatal)
300             if control.Find("Section") != None and files[file]["section"] != "" and files[file]["section"] != control.Find("Section"):
301                 reject_message = reject_message + "Warning: %s control file lists section as `%s', but changes file has `%s'.\n" % (file, control.Find("Section", ""), files[file]["section"])
302             if control.Find("Priority") != None and files[file]["priority"] != "" and files[file]["priority"] != control.Find("Priority"):
303                 reject_message = reject_message + "Warning: %s control file lists priority as `%s', but changes file has `%s'.\n" % (file, control.Find("Priority", ""), files[file]["priority"])
304
305             epochless_version = utils.re_no_epoch.sub('', control.Find("Version", ""))
306
307             files[file]["package"] = control.Find("Package");
308             files[file]["architecture"] = control.Find("Architecture");
309             files[file]["version"] = control.Find("Version");
310             files[file]["maintainer"] = control.Find("Maintainer", "");
311             if file[-5:] == ".udeb":
312                 files[file]["dbtype"] = "udeb";
313             elif file[-4:] == ".deb":
314                 files[file]["dbtype"] = "deb";
315             else:
316                 reject_message = reject_message + "Rejected: %s is neither a .deb or a .udeb.\n " % (file);
317             files[file]["type"] = "deb";
318             files[file]["fullname"] = "%s_%s_%s.deb" % (control.Find("Package", ""), epochless_version, control.Find("Architecture", ""))
319             files[file]["source"] = control.Find("Source", "");
320             if files[file]["source"] == "":
321                 files[file]["source"] = files[file]["package"];
322         # Checks for a source package...
323         else:
324             m = re_issource.match(file)
325             if m != None:
326                 files[file]["package"] = m.group(1)
327                 files[file]["version"] = m.group(2)
328                 files[file]["type"] = m.group(3)
329                 
330                 # Ensure the source package name matches the Source filed in the .changes
331                 if changes["source"] != files[file]["package"]:
332                     reject_message = reject_message + "Rejected: %s: changes file doesn't say %s for Source\n" % (file, files[file]["package"])
333
334                 # Ensure the source version matches the version in the .changes file
335                 if files[file]["type"] == "orig.tar.gz":
336                     changes_version = changes["chopversion2"]
337                 else:
338                     changes_version = changes["chopversion"]
339                 if changes_version != files[file]["version"]:
340                     reject_message = reject_message + "Rejected: %s: should be %s according to changes file.\n" % (file, changes_version)
341
342                 # Ensure the .changes lists source in the Architecture field
343                 if not changes["architecture"].has_key("source"):
344                     reject_message = reject_message + "Rejected: %s: changes file doesn't list `source' in Architecture field.\n" % (file)
345
346                 # Check the signature of a .dsc file
347                 if files[file]["type"] == "dsc":
348                     check_signature(file)
349
350                 files[file]["fullname"] = file
351
352             # Not a binary or source package?  Assume byhand...
353             else:
354                 files[file]["byhand"] = 1;
355                 files[file]["type"] = "byhand";
356                 
357         files[file]["oldfiles"] = {}
358         for suite in changes["distribution"].keys():
359             # Skip byhand
360             if files[file].has_key("byhand"):
361                 continue
362
363             if Cnf.has_key("Suite:%s::Components" % (suite)) and not Cnf.has_key("Suite::%s::Components::%s" % (suite, files[file]["component"])):
364                 reject_message = reject_message + "Rejected: unknown component `%s' for suite `%s'.\n" % (files[file]["component"], suite)
365                 continue
366
367             # See if the package is NEW
368             if not in_override_p(files[file]["package"], files[file]["component"], suite):
369                 files[file]["new"] = 1
370                 
371             # Find any old binary packages
372             if files[file]["type"] == "deb":
373                 q = projectB.query("SELECT b.id, b.version, f.filename, l.path, c.name FROM binaries b, bin_associations ba, suite s, location l, component c, architecture a, files f WHERE b.package = '%s' AND s.suite_name = '%s' AND a.arch_string = '%s' AND ba.bin = b.id AND ba.suite = s.id AND b.architecture = a.id AND f.location = l.id AND l.component = c.id AND b.file = f.id"
374                                    % (files[file]["package"], suite, files[file]["architecture"]))
375                 oldfiles = q.dictresult()
376                 for oldfile in oldfiles:
377                     files[file]["oldfiles"][suite] = oldfile
378                     # Check versions [NB: per-suite only; no cross-suite checking done (yet)]
379                     if apt_pkg.VersionCompare(files[file]["version"], oldfile["version"]) != 1:
380                         if Cnf["Dinstall::Options::No-Version-Check"]:
381                             reject_message = reject_message + "Overriden rejection"
382                         else:
383                             reject_message = reject_message + "Rejected"
384                         reject_message = reject_message + ": %s Old version `%s' >= new version `%s'.\n" % (file, oldfile["version"], files[file]["version"])
385                 # Check for existing copies of the file
386                 if not changes.has_key("stable upload"):
387                     q = projectB.query("SELECT b.id FROM binaries b, architecture a WHERE b.package = '%s' AND b.version = '%s' AND a.arch_string = '%s' AND a.id = b.architecture" % (files[file]["package"], files[file]["version"], files[file]["architecture"]))
388                     if q.getresult() != []:
389                         reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (file)
390
391             # Find any old .dsc files
392             elif files[file]["type"] == "dsc":
393                 q = projectB.query("SELECT s.id, s.version, f.filename, l.path, c.name FROM source s, src_associations sa, suite su, location l, component c, files f WHERE s.source = '%s' AND su.suite_name = '%s' AND sa.source = s.id AND sa.suite = su.id AND f.location = l.id AND l.component = c.id AND f.id = s.file"
394                                    % (files[file]["package"], suite))
395                 oldfiles = q.dictresult()
396                 if len(oldfiles) >= 1:
397                     files[file]["oldfiles"][suite] = oldfiles[0]
398
399             # Validate the component
400             component = files[file]["component"];
401             component_id = db_access.get_component_id(component);
402             if component_id == -1:
403                 reject_message = reject_message + "Rejected: file '%s' has unknown component '%s'.\n" % (file, component);
404                 continue;
405             
406             # Check the md5sum & size against existing files (if any)
407             location = Cnf["Dir::PoolDir"];
408             files[file]["location id"] = db_access.get_location_id (location, component, archive);
409             files_id = db_access.get_files_id(component + '/' + file, files[file]["size"], files[file]["md5sum"], files[file]["location id"]);
410             if files_id == -1:
411                 reject_message = reject_message + "Rejected: INTERNAL ERROR, get_files_id() returned multiple matches for %s.\n" % (file)
412             elif files_id == -2:
413                 reject_message = reject_message + "Rejected: md5sum and/or size mismatch on existing copy of %s.\n" % (file)
414             files[file]["files id"] = files_id
415
416             # Check for packages that have moved from one component to another
417             if files[file]["oldfiles"].has_key(suite) and files[file]["oldfiles"][suite]["name"] != files[file]["component"]:
418                 files[file]["othercomponents"] = files[file]["oldfiles"][suite]["name"];
419
420                 
421     if string.find(reject_message, "Rejected:") != -1:
422         return 0
423     else: 
424         return 1
425
426 ###############################################################################
427
428 def check_dsc ():
429     global dsc, dsc_files, reject_message, reprocess, orig_tar_id;
430     
431     for file in files.keys():
432         if files[file]["type"] == "dsc":
433             try:
434                 dsc = utils.parse_changes(file)
435             except utils.cant_open_exc:
436                 reject_message = reject_message + "Rejected: can't read changes file '%s'.\n" % (filename)
437                 return 0;
438             except utils.changes_parse_error_exc, line:
439                 reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line)
440                 return 0;
441             try:
442                 dsc_files = utils.build_file_list(dsc, 1)
443             except utils.no_files_exc:
444                 reject_message = reject_message + "Rejected: no Files: field in .dsc file.\n";
445                 continue;
446
447             # Try and find all files mentioned in the .dsc.  This has
448             # to work harder to cope with the multiple possible
449             # locations of an .orig.tar.gz.
450             for dsc_file in dsc_files.keys():
451                 if files.has_key(dsc_file):
452                     actual_md5 = files[dsc_file]["md5sum"]
453                     found = "%s in incoming" % (dsc_file)
454                     # Check the file does not already exist in the archive
455                     if not changes.has_key("stable upload"):
456                         q = projectB.query("SELECT f.id FROM files f, location l WHERE f.filename ~ '%s' AND l.id = f.location" % (utils.regex_safe(dsc_file)));
457                         if q.getresult() != []:
458                             reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (dsc_file)
459                 elif dsc_file[-12:] == ".orig.tar.gz":
460                     # Check in the pool
461                     q = projectB.query("SELECT l.path, f.filename, l.type, f.id FROM files f, location l WHERE f.filename ~ '%s' AND l.id = f.location" % (utils.regex_safe(dsc_file)));
462                     ql = q.getresult();
463                     if len(ql) > 0:
464                         old_file = ql[0][0] + ql[0][1];
465                         actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
466                         found = old_file;
467                         suite_type = ql[0][2];
468                         # See install()...
469                         if suite_type == "legacy" or suite_type == "legacy-mixed":
470                             orig_tar_id = ql[0][3];
471                     else:
472                         # Not there? Check in Incoming...
473                         # [See comment above process_it() for explanation
474                         #  of why this is necessary...]
475                         if os.access(dsc_file, os.R_OK) != 0:
476                             files[dsc_file] = {};
477                             files[dsc_file]["size"] = os.stat(dsc_file)[stat.ST_SIZE];
478                             files[dsc_file]["md5sum"] = dsc_files[dsc_file]["md5sum"];
479                             files[dsc_file]["section"] = files[file]["section"];
480                             files[dsc_file]["priority"] = files[file]["priority"];
481                             files[dsc_file]["component"] = files[file]["component"];
482                             reprocess = 1;
483                             return 1;
484                         else:
485                             reject_message = reject_message + "Rejected: %s refers to %s, but I can't find it in Incoming or in the pool.\n" % (file, dsc_file);
486                             continue;
487                 else:
488                     reject_message = reject_message + "Rejected: %s refers to %s, but I can't find it in Incoming." % (file, dsc_file);
489                     continue;
490                 if actual_md5 != dsc_files[dsc_file]["md5sum"]:
491                     reject_message = reject_message + "Rejected: md5sum for %s doesn't match %s.\n" % (found, file)
492
493     if string.find(reject_message, "Rejected:") != -1:
494         return 0
495     else: 
496         return 1
497
498 ###############################################################################
499
500 def check_md5sums ():
501     global reject_message;
502
503     for file in files.keys():
504         try:
505             file_handle = utils.open_file(file,"r");
506         except utils.cant_open_exc:
507             pass;
508         else:
509             if apt_pkg.md5sum(file_handle) != files[file]["md5sum"]:
510                 reject_message = reject_message + "Rejected: md5sum check failed for %s.\n" % (file);
511
512 #####################################################################################################################
513
514 def action (changes_filename):
515     byhand = confirm = suites = summary = new = "";
516
517     # changes["distribution"] may not exist in corner cases
518     # (e.g. unreadable changes files)
519     if not changes.has_key("distribution"):
520         changes["distribution"] = {};
521     
522     for suite in changes["distribution"].keys():
523         if Cnf.has_key("Suite::%s::Confirm"):
524             confirm = confirm + suite + ", "
525         suites = suites + suite + ", "
526     confirm = confirm[:-2]
527     suites = suites[:-2]
528
529     for file in files.keys():
530         if files[file].has_key("byhand"):
531             byhand = 1
532             summary = summary + file + " byhand\n"
533         elif files[file].has_key("new"):
534             new = 1
535             summary = summary + "(new) %s %s %s\n" % (file, files[file]["priority"], files[file]["section"])
536             if files[file].has_key("othercomponents"):
537                 summary = summary + "WARNING: Already present in %s distribution.\n" % (files[file]["othercomponents"])
538             if files[file]["type"] == "deb":
539                 summary = summary + apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))["Description"] + '\n';
540         else:
541             files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"])
542             destination = Cnf["Dir::PoolRoot"] + files[file]["pool name"] + file
543             summary = summary + file + "\n  to " + destination + "\n"
544
545     short_summary = summary;
546
547     # This is for direport's benefit...
548     f = re_fdnic.sub("\n .\n", changes.get("changes",""));
549
550     if confirm or byhand or new:
551         summary = summary + "Changes: " + f;
552
553     summary = summary + announce (short_summary, 0)
554     
555     (prompt, answer) = ("", "XXX")
556     if Cnf["Dinstall::Options::No-Action"] or Cnf["Dinstall::Options::Automatic"]:
557         answer = 'S'
558
559     if string.find(reject_message, "Rejected") != -1:
560         if time.time()-os.path.getmtime(changes_filename) < 86400:
561             print "SKIP (too new)\n" + reject_message,;
562             prompt = "[S]kip, Manual reject, Quit ?";
563         else:
564             print "REJECT\n" + reject_message,;
565             prompt = "[R]eject, Manual reject, Skip, Quit ?";
566             if Cnf["Dinstall::Options::Automatic"]:
567                 answer = 'R';
568     elif new:
569         print "NEW to %s\n%s%s" % (suites, reject_message, summary),;
570         prompt = "[S]kip, New ack, Manual reject, Quit ?";
571         if Cnf["Dinstall::Options::Automatic"] and Cnf["Dinstall::Options::Ack-New"]:
572             answer = 'N';
573     elif byhand:
574         print "BYHAND\n" + reject_message + summary,;
575         prompt = "[I]nstall, Manual reject, Skip, Quit ?";
576     elif confirm:
577         print "CONFIRM to %s\n%s%s" % (confirm, reject_message, summary),
578         prompt = "[I]nstall, Manual reject, Skip, Quit ?";
579     else:
580         print "INSTALL\n" + reject_message + summary,;
581         prompt = "[I]nstall, Manual reject, Skip, Quit ?";
582         if Cnf["Dinstall::Options::Automatic"]:
583             answer = 'I';
584
585     while string.find(prompt, answer) == -1:
586         print prompt,;
587         answer = utils.our_raw_input()
588         m = re_default_answer.match(prompt)
589         if answer == "":
590             answer = m.group(1)
591         answer = string.upper(answer[:1])
592
593     if answer == 'R':
594         reject (changes_filename, "");
595     elif answer == 'M':
596         manual_reject (changes_filename);
597     elif answer == 'I':
598         install (changes_filename, summary, short_summary);
599     elif answer == 'N':
600         acknowledge_new (changes_filename, summary);
601     elif answer == 'Q':
602         sys.exit(0)
603
604 #####################################################################################################################
605
606 def install (changes_filename, summary, short_summary):
607     global install_count, install_bytes
608
609     # Stable uploads are a special case
610     if changes.has_key("stable upload"):
611         stable_install (changes_filename, summary, short_summary);
612         return;
613     
614     print "Installing."
615
616     archive = utils.where_am_i();
617
618     # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
619     projectB.query("BEGIN WORK");
620
621     # Add the .dsc file to the DB
622     for file in files.keys():
623         if files[file]["type"] == "dsc":
624             package = dsc["source"]
625             version = dsc["version"]  # NB: not files[file]["version"], that has no epoch
626             maintainer = dsc["maintainer"]
627             maintainer = string.replace(maintainer, "'", "\\'")
628             maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
629             filename = files[file]["pool name"] + file;
630             dsc_location_id = files[file]["location id"];
631             if not files[file]["files id"]:
632                 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], dsc_location_id)
633             projectB.query("INSERT INTO source (source, version, maintainer, file) VALUES ('%s', '%s', %d, %d)"
634                            % (package, version, maintainer_id, files[file]["files id"]))
635             
636             for suite in changes["distribution"].keys():
637                 suite_id = db_access.get_suite_id(suite);
638                 projectB.query("INSERT INTO src_associations (suite, source) VALUES (%d, currval('source_id_seq'))" % (suite_id))
639
640             # Add the source files to the DB (files and dsc_files)
641             projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files[file]["files id"]));
642             for dsc_file in dsc_files.keys():
643                 filename = files[file]["pool name"] + dsc_file;
644                 files_id = db_access.get_files_id(filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], files[file]["location id"]);
645                 # FIXME: needs to check for -1/-2 and or handle exception
646                 if files_id == None:
647                     files_id = db_access.set_files_id (filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], files[file]["location id"]);
648                 projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files_id));
649             
650             
651     # Add the .deb files to the DB
652     for file in files.keys():
653         if files[file]["type"] == "deb":
654             package = files[file]["package"]
655             version = files[file]["version"]
656             maintainer = files[file]["maintainer"]
657             maintainer = string.replace(maintainer, "'", "\\'")
658             maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
659             architecture = files[file]["architecture"]
660             architecture_id = db_access.get_architecture_id (architecture);
661             type = files[file]["dbtype"];
662             component = files[file]["component"]
663             source = files[file]["source"]
664             source_version = ""
665             if string.find(source, "(") != -1:
666                 m = utils.re_extract_src_version.match(source)
667                 source = m.group(1)
668                 source_version = m.group(2)
669             if not source_version:
670                 source_version = version
671             filename = files[file]["pool name"] + file;
672             if not files[file]["files id"]:
673                 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], files[file]["location id"])
674             source_id = db_access.get_source_id (source, source_version);
675             if source_id:
676                 projectB.query("INSERT INTO binaries (package, version, maintainer, source, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, %d, '%s')"
677                                % (package, version, maintainer_id, source_id, architecture_id, files[file]["files id"], type));
678             else:
679                 projectB.query("INSERT INTO binaries (package, version, maintainer, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, '%s')"
680                                % (package, version, maintainer_id, architecture_id, files[file]["files id"], type));
681             for suite in changes["distribution"].keys():
682                 suite_id = db_access.get_suite_id(suite);
683                 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES (%d, currval('binaries_id_seq'))" % (suite_id));
684
685     # Install the files into the pool
686     for file in files.keys():
687         if files[file].has_key("byhand"):
688             continue
689         destination = Cnf["Dir::PoolDir"] + files[file]["pool name"] + file
690         destdir = os.path.dirname(destination)
691         utils.move (file, destination)
692         install_bytes = install_bytes + float(files[file]["size"])
693
694     # Copy the .changes file across for suite which need it.
695     for suite in changes["distribution"].keys():
696         if Cnf.has_key("Suite::%s::CopyChanges" % (suite)):
697             utils.copy (changes_filename, Cnf["Dir::RootDir"] + Cnf["Suite::%s::CopyChanges" % (suite)]);
698
699     # If the .orig.tar.gz is in a legacy directory we need to poolify
700     # it, so that apt-get source (and anything else that goes by the
701     # "Directory:" field in the Sources.gz file) works.
702     if orig_tar_id != None:
703         q = projectB.query("SELECT l.path, f.filename, f.id as files_id, df.source, df.id as dsc_files_id, f.size, f.md5sum FROM files f, dsc_files df, location l WHERE df.source IN (SELECT source FROM dsc_files WHERE file = %s) AND f.id = df.file AND l.id = f.location" % (orig_tar_id));
704         qd = q.dictresult();
705         for qid in qd:
706             # First move the files to the new location
707             legacy_filename = qid["path"]+qid["filename"];
708             pool_location = utils.poolify (changes["source"], files[file]["component"]);
709             pool_filename = pool_location + os.path.basename(qid["filename"]);
710             destination = Cnf["Dir::PoolDir"] + pool_location
711             utils.move(legacy_filename, destination);
712             # Update the DB: files table
713             new_files_id = db_access.set_files_id(pool_filename, qid["size"], qid["md5sum"], dsc_location_id);
714             # Update the DB: dsc_files table
715             projectB.query("INSERT INTO dsc_files (source, file) VALUES (%s, %s)" % (qid["source"], new_files_id));
716             # Update the DB: source table
717             if legacy_filename[-4:] == ".dsc":
718                 projectB.query("UPDATE source SET file = %s WHERE id = %d" % (new_files_id, qid["source"]));
719
720         for qid in qd:
721             # Remove old data from the DB: dsc_files table
722             projectB.query("DELETE FROM dsc_files WHERE id = %s" % (qid["dsc_files_id"]));
723             # Remove old data from the DB: files table
724             projectB.query("DELETE FROM files WHERE id = %s" % (qid["files_id"]));
725
726     projectB.query("COMMIT WORK");
727
728     utils.move (changes_filename, Cnf["Dir::IncomingDir"] + 'DONE/' + os.path.basename(changes_filename))
729
730     install_count = install_count + 1;
731
732     if not Cnf["Dinstall::Options::No-Mail"]:
733         mail_message = """Return-Path: %s
734 From: %s
735 To: %s
736 Bcc: troup@auric.debian.org
737 Subject: %s INSTALLED
738
739 %s
740 Installing:
741 %s
742
743 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], os.path.basename(changes_filename), reject_message, summary, installed_footer)
744         utils.send_mail (mail_message, "")
745         announce (short_summary, 1)
746
747 #####################################################################################################################
748
749 def stable_install (changes_filename, summary, short_summary):
750     global install_count, install_bytes
751     
752     print "Installing to stable."
753
754     archive = utils.where_am_i();
755
756     # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
757     projectB.query("BEGIN WORK");
758
759     # Add the .dsc file to the DB
760     for file in files.keys():
761         if files[file]["type"] == "dsc":
762             package = dsc["source"]
763             version = dsc["version"]  # NB: not files[file]["version"], that has no epoch
764             q = projectB.query("SELECT id FROM source WHERE source = '%s' AND version = '%s'" % (package, version))
765             ql = q.getresult()
766             if ql == []:
767                 sys.stderr.write("INTERNAL ERROR: couldn't find '%s' (%s) in source table.\n" % (package, version));
768                 sys.exit(1);
769             source_id = ql[0][0];
770             suite_id = db_access.get_suite_id('proposed-updates');
771             projectB.query("DELETE FROM src_associations WHERE suite = '%s' AND source = '%s'" % (suite_id, source_id));
772             suite_id = db_access.get_suite_id('stable');
773             projectB.query("INSERT INTO src_associations (suite, source) VALUES ('%s', '%s')" % (suite_id, source_id));
774                 
775     # Add the .deb files to the DB
776     for file in files.keys():
777         if files[file]["type"] == "deb":
778             package = files[file]["package"]
779             version = files[file]["version"]
780             architecture = files[file]["architecture"]
781             q = projectB.query("SELECT b.id FROM binaries b, architecture a WHERE b.package = '%s' AND b.version = '%s' AND (a.arch_string = '%s' OR a.arch_string = 'all') AND b.architecture = a.id" % (package, version, architecture))
782             ql = q.getresult()
783             if ql == []:
784                 sys.stderr.write("INTERNAL ERROR: couldn't find '%s' (%s for %s architecture) in binaries table.\n" % (package, version, architecture));
785                 sys.exit(1);
786             binary_id = ql[0][0];
787             suite_id = db_access.get_suite_id('proposed-updates');
788             projectB.query("DELETE FROM bin_associations WHERE suite = '%s' AND bin = '%s'" % (suite_id, binary_id));
789             suite_id = db_access.get_suite_id('stable');
790             projectB.query("INSERT INTO bin_associations (suite, bin) VALUES ('%s', '%s')" % (suite_id, binary_id));
791
792     projectB.query("COMMIT WORK");
793
794     utils.move (changes_filename, Cnf["Rhona::Morgue"] + os.path.basename(changes_filename));
795
796     # Update the Stable ChangeLog file
797
798     new_changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + ".ChangeLog";
799     changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + "ChangeLog";
800     if os.path.exists(new_changelog_filename):
801         os.unlink (new_changelog_filename);
802     
803     new_changelog = utils.open_file(new_changelog_filename, 'w');
804     for file in files.keys():
805         if files[file]["type"] == "deb":
806             new_changelog.write("stable/%s/binary-%s/%s\n" % (files[file]["component"], files[file]["architecture"], file));
807         elif re_issource.match(file) != None:
808             new_changelog.write("stable/%s/source/%s\n" % (files[file]["component"], file));
809         else:
810             new_changelog.write("%s\n" % (file));
811     chop_changes = re_fdnic.sub("\n", changes["changes"]);
812     new_changelog.write(chop_changes + '\n\n');
813     if os.access(changelog_filename, os.R_OK) != 0:
814         changelog = utils.open_file(changelog_filename, 'r');
815         new_changelog.write(changelog.read());
816     new_changelog.close();
817     if os.access(changelog_filename, os.R_OK) != 0:
818         os.unlink(changelog_filename);
819     utils.move(new_changelog_filename, changelog_filename);
820
821     install_count = install_count + 1;
822
823     if not Cnf["Dinstall::Options::No-Mail"]:
824         mail_message = """Return-Path: %s
825 From: %s
826 To: %s
827 Bcc: troup@auric.debian.org
828 Subject: %s INSTALLED into stable
829
830 %s
831 Installing:
832 %s
833
834 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], os.path.basename(changes_filename), reject_message, summary, installed_footer)
835         utils.send_mail (mail_message, "")
836         announce (short_summary, 1)
837
838 #####################################################################################################################
839
840 def reject (changes_filename, manual_reject_mail_filename):
841     print "Rejecting.\n"
842
843     base_changes_filename = os.path.basename(changes_filename);
844     reason_filename = re_changes.sub("reason", base_changes_filename);
845     reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename);
846
847     # Move the .changes files and it's contents into REJECT/
848     utils.move (changes_filename, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], base_changes_filename));
849     for file in files.keys():
850         if os.access(file,os.R_OK) == 0:
851             utils.move (file, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], file));
852
853     # If this is not a manual rejection generate the .reason file and rejection mail message
854     if manual_reject_mail_filename == "":
855         if os.path.exists(reject_filename):
856             os.unlink(reject_filename);
857         fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
858         os.write(fd, reject_message);
859         os.close(fd);
860         reject_mail_message = """From: %s
861 To: %s
862 Bcc: troup@auric.debian.org
863 Subject: %s REJECTED
864
865 %s
866 ===
867 %s""" % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, reject_message, reject_footer);
868     else: # Have a manual rejection file to use
869         reject_mail_message = ""; # avoid <undef>'s
870         
871     # Send the rejection mail if appropriate
872     if not Cnf["Dinstall::Options::No-Mail"]:
873         utils.send_mail (reject_mail_message, manual_reject_mail_filename);
874
875 ##################################################################
876
877 def manual_reject (changes_filename):
878     # Build up the rejection email 
879     user_email_address = string.replace(string.split(pwd.getpwuid(os.getuid())[4],',')[0], '.', '')
880     user_email_address = user_email_address + " <%s@%s>" % (pwd.getpwuid(os.getuid())[0], Cnf["Dinstall::MyHost"])
881     manual_reject_message = Cnf.get("Dinstall::Options::Manual-Reject", "")
882
883     reject_mail_message = """From: %s
884 Cc: %s
885 To: %s
886 Bcc: troup@auric.debian.org
887 Subject: %s REJECTED
888
889 %s
890 %s
891 ===
892 %s""" % (user_email_address, Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, manual_reject_message, reject_message, reject_footer)
893     
894     # Write the rejection email out as the <foo>.reason file
895     reason_filename = re_changes.sub("reason", os.path.basename(changes_filename));
896     reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename)
897     if os.path.exists(reject_filename):
898         os.unlink(reject_filename);
899     fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
900     os.write(fd, reject_mail_message);
901     os.close(fd);
902     
903     # If we weren't given one, spawn an editor so the user can add one in
904     if manual_reject_message == "":
905         result = os.system("vi +6 %s" % (reject_file))
906         if result != 0:
907             sys.stderr.write ("vi invocation failed for `%s'!" % (reject_file))
908             sys.exit(result)
909
910     # Then process it as if it were an automatic rejection
911     reject (changes_filename, reject_filename)
912
913 #####################################################################################################################
914  
915 def acknowledge_new (changes_filename, summary):
916     global new_ack_new;
917
918     changes_filename = os.path.basename(changes_filename);
919
920     new_ack_new[changes_filename] = 1;
921
922     if new_ack_old.has_key(changes_filename):
923         print "Ack already sent.";
924         return;
925
926     print "Sending new ack.";
927     if not Cnf["Dinstall::Options::No-Mail"]:
928         new_ack_message = """Return-Path: %s
929 From: %s
930 To: %s
931 Bcc: troup@auric.debian.org
932 Subject: %s is NEW
933
934 %s
935 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, summary, new_ack_footer);
936         utils.send_mail(new_ack_message,"");
937
938 #####################################################################################################################
939
940 def announce (short_summary, action):
941     # Only do announcements for source uploads with a recent dpkg-dev installed
942     if float(changes.get("format", 0)) < 1.6 or not changes["architecture"].has_key("source"):
943         return ""
944
945     lists_done = {}
946     summary = ""
947
948     for dist in changes["distribution"].keys():
949         list = Cnf.Find("Suite::%s::Announce" % (dist))
950         if list == None or lists_done.has_key(list):
951             continue
952         lists_done[list] = 1
953         summary = summary + "Announcing to %s\n" % (list)
954
955         if action:
956             mail_message = """Return-Path: %s
957 From: %s
958 To: %s
959 Bcc: troup@auric.debian.org
960 Subject: Installed %s %s (%s)
961
962 %s
963
964 Installed:
965 %s
966 """ % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], list, changes["source"], changes["version"], string.join(changes["architecture"].keys(), ' ' ),
967        changes["filecontents"], short_summary)
968             utils.send_mail (mail_message, "")
969
970     (dsc_rfc822, dsc_name, dsc_email) = utils.fix_maintainer (dsc.get("maintainer",Cnf["Dinstall::MyEmailAddress"]));
971     bugs = changes["closes"].keys()
972     bugs.sort()
973     if dsc_name == changes["maintainername"]:
974         summary = summary + "Closing bugs: "
975         for bug in bugs:
976             summary = summary + "%s " % (bug)
977             if action:
978                 mail_message = """Return-Path: %s
979 From: %s
980 To: %s-close@bugs.debian.org
981 Bcc: troup@auric.debian.org
982 Subject: Bug#%s: fixed in %s %s
983
984 We believe that the bug you reported is fixed in the latest version of
985 %s, which has been installed in the Debian FTP archive:
986
987 %s""" % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], bug, bug, changes["source"], changes["version"], changes["source"], short_summary)
988
989                 if changes["distribution"].has_key("stable"):
990                     mail_message = mail_message + """Note that this package is not part of the released stable Debian
991 distribution.  It may have dependencies on other unreleased software,
992 or other instabilities.  Please take care if you wish to install it.
993 The update will eventually make its way into the next released Debian
994 distribution."""
995
996                 mail_message = mail_message + """A summary of the changes between this version and the previous one is
997 attached.
998
999 Thank you for reporting the bug, which will now be closed.  If you
1000 have further comments please address them to %s@bugs.debian.org,
1001 and the maintainer will reopen the bug report if appropriate.
1002
1003 Debian distribution maintenance software
1004 pp.
1005 %s (supplier of updated %s package)
1006
1007 (This message was generated automatically at their request; if you
1008 believe that there is a problem with it please contact the archive
1009 administrators by mailing ftpmaster@debian.org)
1010
1011
1012 %s""" % (bug, changes["maintainer"], changes["source"], changes["filecontents"])
1013
1014                 utils.send_mail (mail_message, "")
1015     else:                     # NMU
1016         summary = summary + "Setting bugs to severity fixed: "
1017         control_message = ""
1018         for bug in bugs:
1019             summary = summary + "%s " % (bug)
1020             control_message = control_message + "severity %s fixed\n" % (bug)
1021         if action and control_message != "":
1022             mail_message = """Return-Path: %s
1023 From: %s
1024 To: control@bugs.debian.org
1025 Bcc: troup@auric.debian.org, %s
1026 Subject: Fixed in NMU of %s %s
1027
1028 %s
1029 quit
1030
1031 This message was generated automatically in response to a
1032 non-maintainer upload.  The .changes file follows.
1033
1034 %s
1035 """ % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes["maintainer822"], changes["source"], changes["version"], control_message, changes["filecontents"])
1036             utils.send_mail (mail_message, "")
1037     summary = summary + "\n"
1038
1039     return summary
1040
1041 ###############################################################################
1042
1043 # reprocess is necessary for the case of foo_1.2-1 and foo_1.2-2 in
1044 # Incoming. -1 will reference the .orig.tar.gz, but -2 will not.
1045 # dsccheckdistrib() can find the .orig.tar.gz but it will not have
1046 # processed it during it's checks of -2.  If -1 has been deleted or
1047 # otherwise not checked by da-install, the .orig.tar.gz will not have
1048 # been checked at all.  To get round this, we force the .orig.tar.gz
1049 # into the .changes structure and reprocess the .changes file.
1050
1051 def process_it (changes_file):
1052     global reprocess, orig_tar_id;
1053
1054     reprocess = 1;
1055     orig_tar_id = None;
1056
1057     # Absolutize the filename to avoid the requirement of being in the
1058     # same directory as the .changes file.
1059     changes_file = os.path.abspath(changes_file);
1060
1061     # And since handling of installs to stable munges with the CWD;
1062     # save and restore it.
1063     cwd = os.getcwd();
1064
1065     check_signature (changes_file);
1066     check_changes (changes_file);
1067     while reprocess:
1068         reprocess = 0;
1069         check_files ();
1070         check_md5sums ();
1071         check_dsc ();
1072         
1073     action(changes_file);
1074
1075     # Restore CWD
1076     os.chdir(cwd);
1077
1078 ###############################################################################
1079
1080 def main():
1081     global Cnf, projectB, reject_message, install_bytes, new_ack_old
1082
1083     apt_pkg.init();
1084     
1085     Cnf = apt_pkg.newConfiguration();
1086     apt_pkg.ReadConfigFileISC(Cnf,utils.which_conf_file());
1087
1088     Arguments = [('a',"automatic","Dinstall::Options::Automatic"),
1089                  ('d',"debug","Dinstall::Options::Debug", "IntVal"),
1090                  ('h',"help","Dinstall::Options::Help"),
1091                  ('k',"ack-new","Dinstall::Options::Ack-New"),
1092                  ('m',"manual-reject","Dinstall::Options::Manual-Reject", "HasArg"),
1093                  ('n',"no-action","Dinstall::Options::No-Action"),
1094                  ('p',"no-lock", "Dinstall::Options::No-Lock"),
1095                  ('r',"no-version-check", "Dinstall::Options::No-Version-Check"),
1096                  ('s',"no-mail", "Dinstall::Options::No-Mail"),
1097                  ('u',"override-distribution", "Dinstall::Options::Override-Distribution", "HasArg"),
1098                  ('v',"version","Dinstall::Options::Version")];
1099     
1100     changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
1101
1102     if Cnf["Dinstall::Options::Help"]:
1103         usage(0);
1104         
1105     if Cnf["Dinstall::Options::Version"]:
1106         print "katie version 0.0000000000";
1107         usage(0);
1108
1109     postgresql_user = None; # Default == Connect as user running program.
1110
1111     # -n/--dry-run invalidates some other options which would involve things happening
1112     if Cnf["Dinstall::Options::No-Action"]:
1113         Cnf["Dinstall::Options::Automatic"] = ""
1114         Cnf["Dinstall::Options::Ack-New"] = ""
1115         postgresql_user = Cnf["DB::ROUser"];
1116
1117     projectB = pg.connect('projectb', Cnf["DB::Host"], int(Cnf["DB::Port"]), None, None, postgresql_user);
1118
1119     db_access.init(Cnf, projectB);
1120
1121     # Check that we aren't going to clash with the daily cron job
1122
1123     if os.path.exists("%s/Archive_Maintenance_In_Progress" % (Cnf["Dir::RootDir"])) and not Cnf["Dinstall::Options::No-Lock"]:
1124         sys.stderr.write("Archive maintenance in progress.  Try again later.\n");
1125         sys.exit(2);
1126     
1127     # Obtain lock if not in no-action mode
1128
1129     if not Cnf["Dinstall::Options::No-Action"]:
1130         lock_fd = os.open(Cnf["Dinstall::LockFile"], os.O_RDWR);
1131         fcntl.lockf(lock_fd, FCNTL.F_TLOCK);
1132
1133     # Read in the list of already-acknowledged NEW packages
1134     new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'r');
1135     new_ack_old = {};
1136     for line in new_ack_list.readlines():
1137         new_ack_old[line[:-1]] = 1;
1138     new_ack_list.close();
1139
1140     # Process the changes files
1141     for changes_file in changes_files:
1142         reject_message = ""
1143         print "\n" + changes_file;
1144         process_it (changes_file);
1145
1146     install_mag = " b";
1147     if install_bytes > 10000:
1148         install_bytes = install_bytes / 1000;
1149         install_mag = " Kb";
1150     if install_bytes > 10000:
1151         install_bytes = install_bytes / 1000;
1152         install_mag = " Mb";
1153     if install_count:
1154         sets = "set"
1155         if install_count > 1:
1156             sets = "sets"
1157         sys.stderr.write("Installed %d package %s, %d%s.\n" % (install_count, sets, int(install_bytes), install_mag))
1158
1159     # Write out the list of already-acknowledged NEW packages
1160     if Cnf["Dinstall::Options::Ack-New"]:
1161         new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'w')
1162         for i in new_ack_new.keys():
1163             new_ack_list.write(i+'\n')
1164         new_ack_list.close()
1165     
1166             
1167 if __name__ == '__main__':
1168     main()
1169