]> git.decadent.org.uk Git - dak.git/blob - dak/process_unchecked.py
Merge commit 'stew/content_generation' into merge
[dak.git] / dak / process_unchecked.py
1 #!/usr/bin/env python
2
3 """ Checks Debian packages from Incoming """
4 # Copyright (C) 2000, 2001, 2002, 2003, 2004, 2005, 2006  James Troup <james@nocrew.org>
5
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
20 # Originally based on dinstall by Guy Maor <maor@debian.org>
21
22 ################################################################################
23
24 # Computer games don't affect kids. I mean if Pacman affected our generation as
25 # kids, we'd all run around in a darkened room munching pills and listening to
26 # repetitive music.
27 #         -- Unknown
28
29 ################################################################################
30
31 import commands
32 import errno
33 import fcntl
34 import os
35 import re
36 import shutil
37 import stat
38 import sys
39 import time
40 import traceback
41 import tarfile
42 import apt_inst
43 import apt_pkg
44 from debian_bundle import deb822
45 from daklib.dbconn import DBConn
46 from daklib.binary import Binary
47 from daklib import logging
48 from daklib import queue
49 from daklib import utils
50 from daklib.dak_exceptions import *
51 from daklib.regexes import re_valid_version, re_valid_pkg_name, re_changelog_versions, \
52                            re_strip_revision, re_strip_srcver, re_spacestrip, \
53                            re_isanum, re_no_epoch, re_no_revision, re_taint_free, \
54                            re_isadeb, re_extract_src_version, re_issource, re_default_answer
55
56 from types import *
57
58 ################################################################################
59
60
61 ################################################################################
62
63 # Globals
64 Cnf = None
65 Options = None
66 Logger = None
67 Upload = None
68
69 reprocess = 0
70 in_holding = {}
71
72 # Aliases to the real vars in the Upload class; hysterical raisins.
73 reject_message = ""
74 changes = {}
75 dsc = {}
76 dsc_files = {}
77 files = {}
78 pkg = {}
79
80 ###############################################################################
81
82 def init():
83     global Cnf, Options, Upload, changes, dsc, dsc_files, files, pkg
84
85     apt_pkg.init()
86
87     Cnf = apt_pkg.newConfiguration()
88     apt_pkg.ReadConfigFileISC(Cnf,utils.which_conf_file())
89
90     Arguments = [('a',"automatic","Dinstall::Options::Automatic"),
91                  ('h',"help","Dinstall::Options::Help"),
92                  ('n',"no-action","Dinstall::Options::No-Action"),
93                  ('p',"no-lock", "Dinstall::Options::No-Lock"),
94                  ('s',"no-mail", "Dinstall::Options::No-Mail"),
95                  ('d',"directory", "Dinstall::Options::Directory", "HasArg")]
96
97     for i in ["automatic", "help", "no-action", "no-lock", "no-mail",
98               "override-distribution", "version", "directory"]:
99         Cnf["Dinstall::Options::%s" % (i)] = ""
100
101     changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv)
102     Options = Cnf.SubTree("Dinstall::Options")
103
104     if Options["Help"]:
105         usage()
106
107     # If we have a directory flag, use it to find our files
108     if Cnf["Dinstall::Options::Directory"] != "":
109         # Note that we clobber the list of files we were given in this case
110         # so warn if the user has done both
111         if len(changes_files) > 0:
112             utils.warn("Directory provided so ignoring files given on command line")
113
114         changes_files = utils.get_changes_files(Cnf["Dinstall::Options::Directory"])
115
116     Upload = queue.Upload(Cnf)
117
118     changes = Upload.pkg.changes
119     dsc = Upload.pkg.dsc
120     dsc_files = Upload.pkg.dsc_files
121     files = Upload.pkg.files
122     pkg = Upload.pkg
123
124     return changes_files
125
126 ################################################################################
127
128 def usage (exit_code=0):
129     print """Usage: dinstall [OPTION]... [CHANGES]...
130   -a, --automatic           automatic run
131   -h, --help                show this help and exit.
132   -n, --no-action           don't do anything
133   -p, --no-lock             don't check lockfile !! for cron.daily only !!
134   -s, --no-mail             don't send any mail
135   -V, --version             display the version number and exit"""
136     sys.exit(exit_code)
137
138 ################################################################################
139
140 def reject (str, prefix="Rejected: "):
141     global reject_message
142     if str:
143         reject_message += prefix + str + "\n"
144
145 ################################################################################
146
147 def copy_to_holding(filename):
148     global in_holding
149
150     base_filename = os.path.basename(filename)
151
152     dest = Cnf["Dir::Queue::Holding"] + '/' + base_filename
153     try:
154         fd = os.open(dest, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0640)
155         os.close(fd)
156     except OSError, e:
157         # Shouldn't happen, but will if, for example, someone lists a
158         # file twice in the .changes.
159         if errno.errorcode[e.errno] == 'EEXIST':
160             reject("%s: already exists in holding area; can not overwrite." % (base_filename))
161             return
162         raise
163
164     try:
165         shutil.copy(filename, dest)
166     except IOError, e:
167         # In either case (ENOENT or EACCES) we want to remove the
168         # O_CREAT | O_EXCLed ghost file, so add the file to the list
169         # of 'in holding' even if it's not the real file.
170         if errno.errorcode[e.errno] == 'ENOENT':
171             reject("%s: can not copy to holding area: file not found." % (base_filename))
172             os.unlink(dest)
173             return
174         elif errno.errorcode[e.errno] == 'EACCES':
175             reject("%s: can not copy to holding area: read permission denied." % (base_filename))
176             os.unlink(dest)
177             return
178         raise
179
180     in_holding[base_filename] = ""
181
182 ################################################################################
183
184 def clean_holding():
185     global in_holding
186
187     cwd = os.getcwd()
188     os.chdir(Cnf["Dir::Queue::Holding"])
189     for f in in_holding.keys():
190         if os.path.exists(f):
191             if f.find('/') != -1:
192                 utils.fubar("WTF? clean_holding() got a file ('%s') with / in it!" % (f))
193             else:
194                 os.unlink(f)
195     in_holding = {}
196     os.chdir(cwd)
197
198 ################################################################################
199
200 def check_changes():
201     filename = pkg.changes_file
202
203     # Parse the .changes field into a dictionary
204     try:
205         changes.update(utils.parse_changes(filename))
206     except CantOpenError:
207         reject("%s: can't read file." % (filename))
208         return 0
209     except ParseChangesError, line:
210         reject("%s: parse error, can't grok: %s." % (filename, line))
211         return 0
212     except ChangesUnicodeError:
213         reject("%s: changes file not proper utf-8" % (filename))
214         return 0
215
216     # Parse the Files field from the .changes into another dictionary
217     try:
218         files.update(utils.build_file_list(changes))
219     except ParseChangesError, line:
220         reject("%s: parse error, can't grok: %s." % (filename, line))
221     except UnknownFormatError, format:
222         reject("%s: unknown format '%s'." % (filename, format))
223         return 0
224
225     # Check for mandatory fields
226     for i in ("source", "binary", "architecture", "version", "distribution",
227               "maintainer", "files", "changes", "description"):
228         if not changes.has_key(i):
229             reject("%s: Missing mandatory field `%s'." % (filename, i))
230             return 0    # Avoid <undef> errors during later tests
231
232     # Strip a source version in brackets from the source field
233     if re_strip_srcver.search(changes["source"]):
234         changes["source"] = re_strip_srcver.sub('', changes["source"])
235
236     # Ensure the source field is a valid package name.
237     if not re_valid_pkg_name.match(changes["source"]):
238         reject("%s: invalid source name '%s'." % (filename, changes["source"]))
239
240     # Split multi-value fields into a lower-level dictionary
241     for i in ("architecture", "distribution", "binary", "closes"):
242         o = changes.get(i, "")
243         if o != "":
244             del changes[i]
245         changes[i] = {}
246         for j in o.split():
247             changes[i][j] = 1
248
249     # Fix the Maintainer: field to be RFC822/2047 compatible
250     try:
251         (changes["maintainer822"], changes["maintainer2047"],
252          changes["maintainername"], changes["maintaineremail"]) = \
253          utils.fix_maintainer (changes["maintainer"])
254     except ParseMaintError, msg:
255         reject("%s: Maintainer field ('%s') failed to parse: %s" \
256                % (filename, changes["maintainer"], msg))
257
258     # ...likewise for the Changed-By: field if it exists.
259     try:
260         (changes["changedby822"], changes["changedby2047"],
261          changes["changedbyname"], changes["changedbyemail"]) = \
262          utils.fix_maintainer (changes.get("changed-by", ""))
263     except ParseMaintError, msg:
264         (changes["changedby822"], changes["changedby2047"],
265          changes["changedbyname"], changes["changedbyemail"]) = \
266          ("", "", "", "")
267         reject("%s: Changed-By field ('%s') failed to parse: %s" \
268                % (filename, changes["changed-by"], msg))
269
270     # Ensure all the values in Closes: are numbers
271     if changes.has_key("closes"):
272         for i in changes["closes"].keys():
273             if re_isanum.match (i) == None:
274                 reject("%s: `%s' from Closes field isn't a number." % (filename, i))
275
276
277     # chopversion = no epoch; chopversion2 = no epoch and no revision (e.g. for .orig.tar.gz comparison)
278     changes["chopversion"] = re_no_epoch.sub('', changes["version"])
279     changes["chopversion2"] = re_no_revision.sub('', changes["chopversion"])
280
281     # Check there isn't already a changes file of the same name in one
282     # of the queue directories.
283     base_filename = os.path.basename(filename)
284     for d in [ "Accepted", "Byhand", "Done", "New", "ProposedUpdates", "OldProposedUpdates" ]:
285         if os.path.exists(Cnf["Dir::Queue::%s" % (d) ]+'/'+base_filename):
286             reject("%s: a file with this name already exists in the %s directory." % (base_filename, d))
287
288     # Check the .changes is non-empty
289     if not files:
290         reject("%s: nothing to do (Files field is empty)." % (base_filename))
291         return 0
292
293     return 1
294
295 ################################################################################
296
297 def check_distributions():
298     "Check and map the Distribution field of a .changes file."
299
300     # Handle suite mappings
301     for m in Cnf.ValueList("SuiteMappings"):
302         args = m.split()
303         mtype = args[0]
304         if mtype == "map" or mtype == "silent-map":
305             (source, dest) = args[1:3]
306             if changes["distribution"].has_key(source):
307                 del changes["distribution"][source]
308                 changes["distribution"][dest] = 1
309                 if mtype != "silent-map":
310                     reject("Mapping %s to %s." % (source, dest),"")
311             if changes.has_key("distribution-version"):
312                 if changes["distribution-version"].has_key(source):
313                     changes["distribution-version"][source]=dest
314         elif mtype == "map-unreleased":
315             (source, dest) = args[1:3]
316             if changes["distribution"].has_key(source):
317                 for arch in changes["architecture"].keys():
318                     if arch not in DBConn().get_suite_architectures(source):
319                         reject("Mapping %s to %s for unreleased architecture %s." % (source, dest, arch),"")
320                         del changes["distribution"][source]
321                         changes["distribution"][dest] = 1
322                         break
323         elif mtype == "ignore":
324             suite = args[1]
325             if changes["distribution"].has_key(suite):
326                 del changes["distribution"][suite]
327                 reject("Ignoring %s as a target suite." % (suite), "Warning: ")
328         elif mtype == "reject":
329             suite = args[1]
330             if changes["distribution"].has_key(suite):
331                 reject("Uploads to %s are not accepted." % (suite))
332         elif mtype == "propup-version":
333             # give these as "uploaded-to(non-mapped) suites-to-add-when-upload-obsoletes"
334             #
335             # changes["distribution-version"] looks like: {'testing': 'testing-proposed-updates'}
336             if changes["distribution"].has_key(args[1]):
337                 changes.setdefault("distribution-version", {})
338                 for suite in args[2:]: changes["distribution-version"][suite]=suite
339
340     # Ensure there is (still) a target distribution
341     if changes["distribution"].keys() == []:
342         reject("no valid distribution.")
343
344     # Ensure target distributions exist
345     for suite in changes["distribution"].keys():
346         if not Cnf.has_key("Suite::%s" % (suite)):
347             reject("Unknown distribution `%s'." % (suite))
348
349 ################################################################################
350
351 def check_files():
352     global reprocess
353
354     archive = utils.where_am_i()
355     file_keys = files.keys()
356
357     # if reprocess is 2 we've already done this and we're checking
358     # things again for the new .orig.tar.gz.
359     # [Yes, I'm fully aware of how disgusting this is]
360     if not Options["No-Action"] and reprocess < 2:
361         cwd = os.getcwd()
362         os.chdir(pkg.directory)
363         for f in file_keys:
364             copy_to_holding(f)
365         os.chdir(cwd)
366
367     # Check there isn't already a .changes or .dak file of the same name in
368     # the proposed-updates "CopyChanges" or "CopyDotDak" storage directories.
369     # [NB: this check must be done post-suite mapping]
370     base_filename = os.path.basename(pkg.changes_file)
371     dot_dak_filename = base_filename[:-8]+".dak"
372     for suite in changes["distribution"].keys():
373         copychanges = "Suite::%s::CopyChanges" % (suite)
374         if Cnf.has_key(copychanges) and \
375                os.path.exists(Cnf[copychanges]+"/"+base_filename):
376             reject("%s: a file with this name already exists in %s" \
377                    % (base_filename, Cnf[copychanges]))
378
379         copy_dot_dak = "Suite::%s::CopyDotDak" % (suite)
380         if Cnf.has_key(copy_dot_dak) and \
381                os.path.exists(Cnf[copy_dot_dak]+"/"+dot_dak_filename):
382             reject("%s: a file with this name already exists in %s" \
383                    % (dot_dak_filename, Cnf[copy_dot_dak]))
384
385     reprocess = 0
386     has_binaries = 0
387     has_source = 0
388
389     cursor = DBConn().cursor()
390     # Check for packages that have moved from one component to another
391     # STU: this should probably be changed to not join on architecture, suite tables but instead to used their cached name->id mappings from DBConn
392     DBConn().prepare("moved_pkg_q", """
393         PREPARE moved_pkg_q(text,text,text) AS
394         SELECT c.name FROM binaries b, bin_associations ba, suite s, location l,
395                     component c, architecture a, files f
396         WHERE b.package = $1 AND s.suite_name = $2
397           AND (a.arch_string = $3 OR a.arch_string = 'all')
398           AND ba.bin = b.id AND ba.suite = s.id AND b.architecture = a.id
399           AND f.location = l.id
400           AND l.component = c.id
401           AND b.file = f.id""")
402
403     for f in file_keys:
404         # Ensure the file does not already exist in one of the accepted directories
405         for d in [ "Accepted", "Byhand", "New", "ProposedUpdates", "OldProposedUpdates", "Embargoed", "Unembargoed" ]:
406             if not Cnf.has_key("Dir::Queue::%s" % (d)): continue
407             if os.path.exists(Cnf["Dir::Queue::%s" % (d) ] + '/' + f):
408                 reject("%s file already exists in the %s directory." % (f, d))
409         if not re_taint_free.match(f):
410             reject("!!WARNING!! tainted filename: '%s'." % (f))
411         # Check the file is readable
412         if os.access(f, os.R_OK) == 0:
413             # When running in -n, copy_to_holding() won't have
414             # generated the reject_message, so we need to.
415             if Options["No-Action"]:
416                 if os.path.exists(f):
417                     reject("Can't read `%s'. [permission denied]" % (f))
418                 else:
419                     reject("Can't read `%s'. [file not found]" % (f))
420             files[f]["type"] = "unreadable"
421             continue
422         # If it's byhand skip remaining checks
423         if files[f]["section"] == "byhand" or files[f]["section"][:4] == "raw-":
424             files[f]["byhand"] = 1
425             files[f]["type"] = "byhand"
426         # Checks for a binary package...
427         elif re_isadeb.match(f):
428             has_binaries = 1
429             files[f]["type"] = "deb"
430
431             # Extract package control information
432             deb_file = utils.open_file(f)
433             try:
434                 control = apt_pkg.ParseSection(apt_inst.debExtractControl(deb_file))
435             except:
436                 reject("%s: debExtractControl() raised %s." % (f, sys.exc_type))
437                 deb_file.close()
438                 # Can't continue, none of the checks on control would work.
439                 continue
440             deb_file.close()
441
442             # Check for mandatory fields
443             for field in [ "Package", "Architecture", "Version" ]:
444                 if control.Find(field) == None:
445                     reject("%s: No %s field in control." % (f, field))
446                     # Can't continue
447                     continue
448
449             # Ensure the package name matches the one give in the .changes
450             if not changes["binary"].has_key(control.Find("Package", "")):
451                 reject("%s: control file lists name as `%s', which isn't in changes file." % (f, control.Find("Package", "")))
452
453             # Validate the package field
454             package = control.Find("Package")
455             if not re_valid_pkg_name.match(package):
456                 reject("%s: invalid package name '%s'." % (f, package))
457
458             # Validate the version field
459             version = control.Find("Version")
460             if not re_valid_version.match(version):
461                 reject("%s: invalid version number '%s'." % (f, version))
462
463             # Ensure the architecture of the .deb is one we know about.
464             default_suite = Cnf.get("Dinstall::DefaultSuite", "Unstable")
465             architecture = control.Find("Architecture")
466             upload_suite = changes["distribution"].keys()[0]
467             if architecture not in DBConn().get_suite_architectures(default_suite) and architecture not in DBConn().get_suite_architectures(upload_suite):
468                 reject("Unknown architecture '%s'." % (architecture))
469
470             # Ensure the architecture of the .deb is one of the ones
471             # listed in the .changes.
472             if not changes["architecture"].has_key(architecture):
473                 reject("%s: control file lists arch as `%s', which isn't in changes file." % (f, architecture))
474
475             # Sanity-check the Depends field
476             depends = control.Find("Depends")
477             if depends == '':
478                 reject("%s: Depends field is empty." % (f))
479
480             # Sanity-check the Provides field
481             provides = control.Find("Provides")
482             if provides:
483                 provide = re_spacestrip.sub('', provides)
484                 if provide == '':
485                     reject("%s: Provides field is empty." % (f))
486                 prov_list = provide.split(",")
487                 for prov in prov_list:
488                     if not re_valid_pkg_name.match(prov):
489                         reject("%s: Invalid Provides field content %s." % (f, prov))
490
491
492             # Check the section & priority match those given in the .changes (non-fatal)
493             if control.Find("Section") and files[f]["section"] != "" and files[f]["section"] != control.Find("Section"):
494                 reject("%s control file lists section as `%s', but changes file has `%s'." % (f, control.Find("Section", ""), files[f]["section"]), "Warning: ")
495             if control.Find("Priority") and files[f]["priority"] != "" and files[f]["priority"] != control.Find("Priority"):
496                 reject("%s control file lists priority as `%s', but changes file has `%s'." % (f, control.Find("Priority", ""), files[f]["priority"]),"Warning: ")
497
498             files[f]["package"] = package
499             files[f]["architecture"] = architecture
500             files[f]["version"] = version
501             files[f]["maintainer"] = control.Find("Maintainer", "")
502             if f.endswith(".udeb"):
503                 files[f]["dbtype"] = "udeb"
504             elif f.endswith(".deb"):
505                 files[f]["dbtype"] = "deb"
506             else:
507                 reject("%s is neither a .deb or a .udeb." % (f))
508             files[f]["source"] = control.Find("Source", files[f]["package"])
509             # Get the source version
510             source = files[f]["source"]
511             source_version = ""
512             if source.find("(") != -1:
513                 m = re_extract_src_version.match(source)
514                 source = m.group(1)
515                 source_version = m.group(2)
516             if not source_version:
517                 source_version = files[f]["version"]
518             files[f]["source package"] = source
519             files[f]["source version"] = source_version
520
521             # Ensure the filename matches the contents of the .deb
522             m = re_isadeb.match(f)
523             #  package name
524             file_package = m.group(1)
525             if files[f]["package"] != file_package:
526                 reject("%s: package part of filename (%s) does not match package name in the %s (%s)." % (f, file_package, files[f]["dbtype"], files[f]["package"]))
527             epochless_version = re_no_epoch.sub('', control.Find("Version"))
528             #  version
529             file_version = m.group(2)
530             if epochless_version != file_version:
531                 reject("%s: version part of filename (%s) does not match package version in the %s (%s)." % (f, file_version, files[f]["dbtype"], epochless_version))
532             #  architecture
533             file_architecture = m.group(3)
534             if files[f]["architecture"] != file_architecture:
535                 reject("%s: architecture part of filename (%s) does not match package architecture in the %s (%s)." % (f, file_architecture, files[f]["dbtype"], files[f]["architecture"]))
536
537             # Check for existent source
538             source_version = files[f]["source version"]
539             source_package = files[f]["source package"]
540             if changes["architecture"].has_key("source"):
541                 if source_version != changes["version"]:
542                     reject("source version (%s) for %s doesn't match changes version %s." % (source_version, f, changes["version"]))
543             else:
544                 # Check in the SQL database
545                 if not Upload.source_exists(source_package, source_version, changes["distribution"].keys()):
546                     # Check in one of the other directories
547                     source_epochless_version = re_no_epoch.sub('', source_version)
548                     dsc_filename = "%s_%s.dsc" % (source_package, source_epochless_version)
549                     if os.path.exists(Cnf["Dir::Queue::Byhand"] + '/' + dsc_filename):
550                         files[f]["byhand"] = 1
551                     elif os.path.exists(Cnf["Dir::Queue::New"] + '/' + dsc_filename):
552                         files[f]["new"] = 1
553                     else:
554                         dsc_file_exists = 0
555                         for myq in ["Accepted", "Embargoed", "Unembargoed", "ProposedUpdates", "OldProposedUpdates"]:
556                             if Cnf.has_key("Dir::Queue::%s" % (myq)):
557                                 if os.path.exists(Cnf["Dir::Queue::"+myq] + '/' + dsc_filename):
558                                     dsc_file_exists = 1
559                                     break
560                         if not dsc_file_exists:
561                             reject("no source found for %s %s (%s)." % (source_package, source_version, f))
562             # Check the version and for file overwrites
563             reject(Upload.check_binary_against_db(f),"")
564
565             Binary(f, reject).scan_package( )
566
567         # Checks for a source package...
568         else:
569             m = re_issource.match(f)
570             if m:
571                 has_source = 1
572                 files[f]["package"] = m.group(1)
573                 files[f]["version"] = m.group(2)
574                 files[f]["type"] = m.group(3)
575
576                 # Ensure the source package name matches the Source filed in the .changes
577                 if changes["source"] != files[f]["package"]:
578                     reject("%s: changes file doesn't say %s for Source" % (f, files[f]["package"]))
579
580                 # Ensure the source version matches the version in the .changes file
581                 if files[f]["type"] == "orig.tar.gz":
582                     changes_version = changes["chopversion2"]
583                 else:
584                     changes_version = changes["chopversion"]
585                 if changes_version != files[f]["version"]:
586                     reject("%s: should be %s according to changes file." % (f, changes_version))
587
588                 # Ensure the .changes lists source in the Architecture field
589                 if not changes["architecture"].has_key("source"):
590                     reject("%s: changes file doesn't list `source' in Architecture field." % (f))
591
592                 # Check the signature of a .dsc file
593                 if files[f]["type"] == "dsc":
594                     dsc["fingerprint"] = utils.check_signature(f, reject)
595
596                 files[f]["architecture"] = "source"
597
598             # Not a binary or source package?  Assume byhand...
599             else:
600                 files[f]["byhand"] = 1
601                 files[f]["type"] = "byhand"
602
603         # Per-suite file checks
604         files[f]["oldfiles"] = {}
605         for suite in changes["distribution"].keys():
606             # Skip byhand
607             if files[f].has_key("byhand"):
608                 continue
609
610             # Handle component mappings
611             for m in Cnf.ValueList("ComponentMappings"):
612                 (source, dest) = m.split()
613                 if files[f]["component"] == source:
614                     files[f]["original component"] = source
615                     files[f]["component"] = dest
616
617             # Ensure the component is valid for the target suite
618             if Cnf.has_key("Suite:%s::Components" % (suite)) and \
619                files[f]["component"] not in Cnf.ValueList("Suite::%s::Components" % (suite)):
620                 reject("unknown component `%s' for suite `%s'." % (files[f]["component"], suite))
621                 continue
622
623             # Validate the component
624             component = files[f]["component"]
625             component_id = DBConn().get_component_id(component)
626             if component_id == -1:
627                 reject("file '%s' has unknown component '%s'." % (f, component))
628                 continue
629
630             # See if the package is NEW
631             if not Upload.in_override_p(files[f]["package"], files[f]["component"], suite, files[f].get("dbtype",""), f):
632                 files[f]["new"] = 1
633
634             # Validate the priority
635             if files[f]["priority"].find('/') != -1:
636                 reject("file '%s' has invalid priority '%s' [contains '/']." % (f, files[f]["priority"]))
637
638             # Determine the location
639             location = Cnf["Dir::Pool"]
640             location_id = DBConn().get_location_id(location, component, archive)
641             if location_id == -1:
642                 reject("[INTERNAL ERROR] couldn't determine location (Component: %s, Archive: %s)" % (component, archive))
643             files[f]["location id"] = location_id
644
645             # Check the md5sum & size against existing files (if any)
646             files[f]["pool name"] = utils.poolify (changes["source"], files[f]["component"])
647             files_id = DBConn().get_files_id(files[f]["pool name"] + f, files[f]["size"], files[f]["md5sum"], files[f]["location id"])
648             if files_id == -1:
649                 reject("INTERNAL ERROR, get_files_id() returned multiple matches for %s." % (f))
650             elif files_id == -2:
651                 reject("md5sum and/or size mismatch on existing copy of %s." % (f))
652             files[f]["files id"] = files_id
653
654             # Check for packages that have moved from one component to another
655             files[f]['suite'] = suite
656             cursor.execute("""EXECUTE moved_pkg_q( %(package)s, %(suite)s, %(architecture)s )""", ( files[f] ) )
657             ql = cursor.fetchone()
658             if ql:
659                 files[f]["othercomponents"] = ql[0][0]
660
661     # If the .changes file says it has source, it must have source.
662     if changes["architecture"].has_key("source"):
663         if not has_source:
664             reject("no source found and Architecture line in changes mention source.")
665
666         if not has_binaries and Cnf.FindB("Dinstall::Reject::NoSourceOnly"):
667             reject("source only uploads are not supported.")
668
669 ###############################################################################
670
671 def check_dsc():
672     global reprocess
673
674     # Ensure there is source to check
675     if not changes["architecture"].has_key("source"):
676         return 1
677
678     # Find the .dsc
679     dsc_filename = None
680     for f in files.keys():
681         if files[f]["type"] == "dsc":
682             if dsc_filename:
683                 reject("can not process a .changes file with multiple .dsc's.")
684                 return 0
685             else:
686                 dsc_filename = f
687
688     # If there isn't one, we have nothing to do. (We have reject()ed the upload already)
689     if not dsc_filename:
690         reject("source uploads must contain a dsc file")
691         return 0
692
693     # Parse the .dsc file
694     try:
695         dsc.update(utils.parse_changes(dsc_filename, signing_rules=1))
696     except CantOpenError:
697         # if not -n copy_to_holding() will have done this for us...
698         if Options["No-Action"]:
699             reject("%s: can't read file." % (dsc_filename))
700     except ParseChangesError, line:
701         reject("%s: parse error, can't grok: %s." % (dsc_filename, line))
702     except InvalidDscError, line:
703         reject("%s: syntax error on line %s." % (dsc_filename, line))
704     except ChangesUnicodeError:
705         reject("%s: dsc file not proper utf-8." % (dsc_filename))
706
707     # Build up the file list of files mentioned by the .dsc
708     try:
709         dsc_files.update(utils.build_file_list(dsc, is_a_dsc=1))
710     except NoFilesFieldError:
711         reject("%s: no Files: field." % (dsc_filename))
712         return 0
713     except UnknownFormatError, format:
714         reject("%s: unknown format '%s'." % (dsc_filename, format))
715         return 0
716     except ParseChangesError, line:
717         reject("%s: parse error, can't grok: %s." % (dsc_filename, line))
718         return 0
719
720     # Enforce mandatory fields
721     for i in ("format", "source", "version", "binary", "maintainer", "architecture", "files"):
722         if not dsc.has_key(i):
723             reject("%s: missing mandatory field `%s'." % (dsc_filename, i))
724             return 0
725
726     # Validate the source and version fields
727     if not re_valid_pkg_name.match(dsc["source"]):
728         reject("%s: invalid source name '%s'." % (dsc_filename, dsc["source"]))
729     if not re_valid_version.match(dsc["version"]):
730         reject("%s: invalid version number '%s'." % (dsc_filename, dsc["version"]))
731
732     # Bumping the version number of the .dsc breaks extraction by stable's
733     # dpkg-source.  So let's not do that...
734     if dsc["format"] != "1.0":
735         reject("%s: incompatible 'Format' version produced by a broken version of dpkg-dev 1.9.1{3,4}." % (dsc_filename))
736
737     # Validate the Maintainer field
738     try:
739         utils.fix_maintainer (dsc["maintainer"])
740     except ParseMaintError, msg:
741         reject("%s: Maintainer field ('%s') failed to parse: %s" \
742                % (dsc_filename, dsc["maintainer"], msg))
743
744     # Validate the build-depends field(s)
745     for field_name in [ "build-depends", "build-depends-indep" ]:
746         field = dsc.get(field_name)
747         if field:
748             # Check for broken dpkg-dev lossage...
749             if field.startswith("ARRAY"):
750                 reject("%s: invalid %s field produced by a broken version of dpkg-dev (1.10.11)" % (dsc_filename, field_name.title()))
751
752             # Have apt try to parse them...
753             try:
754                 apt_pkg.ParseSrcDepends(field)
755             except:
756                 reject("%s: invalid %s field (can not be parsed by apt)." % (dsc_filename, field_name.title()))
757                 pass
758
759     # Ensure the version number in the .dsc matches the version number in the .changes
760     epochless_dsc_version = re_no_epoch.sub('', dsc["version"])
761     changes_version = files[dsc_filename]["version"]
762     if epochless_dsc_version != files[dsc_filename]["version"]:
763         reject("version ('%s') in .dsc does not match version ('%s') in .changes." % (epochless_dsc_version, changes_version))
764
765     # Ensure there is a .tar.gz in the .dsc file
766     has_tar = 0
767     for f in dsc_files.keys():
768         m = re_issource.match(f)
769         if not m:
770             reject("%s: %s in Files field not recognised as source." % (dsc_filename, f))
771             continue
772         ftype = m.group(3)
773         if ftype == "orig.tar.gz" or ftype == "tar.gz":
774             has_tar = 1
775     if not has_tar:
776         reject("%s: no .tar.gz or .orig.tar.gz in 'Files' field." % (dsc_filename))
777
778     # Ensure source is newer than existing source in target suites
779     reject(Upload.check_source_against_db(dsc_filename),"")
780
781     (reject_msg, is_in_incoming) = Upload.check_dsc_against_db(dsc_filename)
782     reject(reject_msg, "")
783     if is_in_incoming:
784         if not Options["No-Action"]:
785             copy_to_holding(is_in_incoming)
786         orig_tar_gz = os.path.basename(is_in_incoming)
787         files[orig_tar_gz] = {}
788         files[orig_tar_gz]["size"] = os.stat(orig_tar_gz)[stat.ST_SIZE]
789         files[orig_tar_gz]["md5sum"] = dsc_files[orig_tar_gz]["md5sum"]
790         files[orig_tar_gz]["sha1sum"] = dsc_files[orig_tar_gz]["sha1sum"]
791         files[orig_tar_gz]["sha256sum"] = dsc_files[orig_tar_gz]["sha256sum"]
792         files[orig_tar_gz]["section"] = files[dsc_filename]["section"]
793         files[orig_tar_gz]["priority"] = files[dsc_filename]["priority"]
794         files[orig_tar_gz]["component"] = files[dsc_filename]["component"]
795         files[orig_tar_gz]["type"] = "orig.tar.gz"
796         reprocess = 2
797
798     return 1
799
800 ################################################################################
801
802 def get_changelog_versions(source_dir):
803     """Extracts a the source package and (optionally) grabs the
804     version history out of debian/changelog for the BTS."""
805
806     # Find the .dsc (again)
807     dsc_filename = None
808     for f in files.keys():
809         if files[f]["type"] == "dsc":
810             dsc_filename = f
811
812     # If there isn't one, we have nothing to do. (We have reject()ed the upload already)
813     if not dsc_filename:
814         return
815
816     # Create a symlink mirror of the source files in our temporary directory
817     for f in files.keys():
818         m = re_issource.match(f)
819         if m:
820             src = os.path.join(source_dir, f)
821             # If a file is missing for whatever reason, give up.
822             if not os.path.exists(src):
823                 return
824             ftype = m.group(3)
825             if ftype == "orig.tar.gz" and pkg.orig_tar_gz:
826                 continue
827             dest = os.path.join(os.getcwd(), f)
828             os.symlink(src, dest)
829
830     # If the orig.tar.gz is not a part of the upload, create a symlink to the
831     # existing copy.
832     if pkg.orig_tar_gz:
833         dest = os.path.join(os.getcwd(), os.path.basename(pkg.orig_tar_gz))
834         os.symlink(pkg.orig_tar_gz, dest)
835
836     # Extract the source
837     cmd = "dpkg-source -sn -x %s" % (dsc_filename)
838     (result, output) = commands.getstatusoutput(cmd)
839     if (result != 0):
840         reject("'dpkg-source -x' failed for %s [return code: %s]." % (dsc_filename, result))
841         reject(utils.prefix_multi_line_string(output, " [dpkg-source output:] "), "")
842         return
843
844     if not Cnf.Find("Dir::Queue::BTSVersionTrack"):
845         return
846
847     # Get the upstream version
848     upstr_version = re_no_epoch.sub('', dsc["version"])
849     if re_strip_revision.search(upstr_version):
850         upstr_version = re_strip_revision.sub('', upstr_version)
851
852     # Ensure the changelog file exists
853     changelog_filename = "%s-%s/debian/changelog" % (dsc["source"], upstr_version)
854     if not os.path.exists(changelog_filename):
855         reject("%s: debian/changelog not found in extracted source." % (dsc_filename))
856         return
857
858     # Parse the changelog
859     dsc["bts changelog"] = ""
860     changelog_file = utils.open_file(changelog_filename)
861     for line in changelog_file.readlines():
862         m = re_changelog_versions.match(line)
863         if m:
864             dsc["bts changelog"] += line
865     changelog_file.close()
866
867     # Check we found at least one revision in the changelog
868     if not dsc["bts changelog"]:
869         reject("%s: changelog format not recognised (empty version tree)." % (dsc_filename))
870
871 ########################################
872
873 def check_source():
874     # Bail out if:
875     #    a) there's no source
876     # or b) reprocess is 2 - we will do this check next time when orig.tar.gz is in 'files'
877     # or c) the orig.tar.gz is MIA
878     if not changes["architecture"].has_key("source") or reprocess == 2 \
879        or pkg.orig_tar_gz == -1:
880         return
881
882     tmpdir = utils.temp_dirname()
883
884     # Move into the temporary directory
885     cwd = os.getcwd()
886     os.chdir(tmpdir)
887
888     # Get the changelog version history
889     get_changelog_versions(cwd)
890
891     # Move back and cleanup the temporary tree
892     os.chdir(cwd)
893     try:
894         shutil.rmtree(tmpdir)
895     except OSError, e:
896         if errno.errorcode[e.errno] != 'EACCES':
897             utils.fubar("%s: couldn't remove tmp dir for source tree." % (dsc["source"]))
898
899         reject("%s: source tree could not be cleanly removed." % (dsc["source"]))
900         # We probably have u-r or u-w directories so chmod everything
901         # and try again.
902         cmd = "chmod -R u+rwx %s" % (tmpdir)
903         result = os.system(cmd)
904         if result != 0:
905             utils.fubar("'%s' failed with result %s." % (cmd, result))
906         shutil.rmtree(tmpdir)
907     except:
908         utils.fubar("%s: couldn't remove tmp dir for source tree." % (dsc["source"]))
909
910 ################################################################################
911
912 # FIXME: should be a debian specific check called from a hook
913
914 def check_urgency ():
915     if changes["architecture"].has_key("source"):
916         if not changes.has_key("urgency"):
917             changes["urgency"] = Cnf["Urgency::Default"]
918         changes["urgency"] = changes["urgency"].lower()
919         if changes["urgency"] not in Cnf.ValueList("Urgency::Valid"):
920             reject("%s is not a valid urgency; it will be treated as %s by testing." % (changes["urgency"], Cnf["Urgency::Default"]), "Warning: ")
921             changes["urgency"] = Cnf["Urgency::Default"]
922
923 ################################################################################
924
925 def check_hashes ():
926     utils.check_hash(".changes", files, "md5", apt_pkg.md5sum)
927     utils.check_size(".changes", files)
928     utils.check_hash(".dsc", dsc_files, "md5", apt_pkg.md5sum)
929     utils.check_size(".dsc", dsc_files)
930
931     # This is stupid API, but it'll have to do for now until
932     # we actually have proper abstraction
933     for m in utils.ensure_hashes(changes, dsc, files, dsc_files):
934         reject(m)
935
936 ################################################################################
937
938 # Sanity check the time stamps of files inside debs.
939 # [Files in the near future cause ugly warnings and extreme time
940 #  travel can cause errors on extraction]
941
942 def check_timestamps():
943     class Tar:
944         def __init__(self, future_cutoff, past_cutoff):
945             self.reset()
946             self.future_cutoff = future_cutoff
947             self.past_cutoff = past_cutoff
948
949         def reset(self):
950             self.future_files = {}
951             self.ancient_files = {}
952
953         def callback(self, Kind,Name,Link,Mode,UID,GID,Size,MTime,Major,Minor):
954             if MTime > self.future_cutoff:
955                 self.future_files[Name] = MTime
956             if MTime < self.past_cutoff:
957                 self.ancient_files[Name] = MTime
958     ####
959
960     future_cutoff = time.time() + int(Cnf["Dinstall::FutureTimeTravelGrace"])
961     past_cutoff = time.mktime(time.strptime(Cnf["Dinstall::PastCutoffYear"],"%Y"))
962     tar = Tar(future_cutoff, past_cutoff)
963     for filename in files.keys():
964         if files[filename]["type"] == "deb":
965             tar.reset()
966             try:
967                 deb_file = utils.open_file(filename)
968                 apt_inst.debExtract(deb_file,tar.callback,"control.tar.gz")
969                 deb_file.seek(0)
970                 try:
971                     apt_inst.debExtract(deb_file,tar.callback,"data.tar.gz")
972                 except SystemError, e:
973                     # If we can't find a data.tar.gz, look for data.tar.bz2 instead.
974                     if not re.search(r"Cannot f[ui]nd chunk data.tar.gz$", str(e)):
975                         raise
976                     deb_file.seek(0)
977                     apt_inst.debExtract(deb_file,tar.callback,"data.tar.bz2")
978                 deb_file.close()
979                 #
980                 future_files = tar.future_files.keys()
981                 if future_files:
982                     num_future_files = len(future_files)
983                     future_file = future_files[0]
984                     future_date = tar.future_files[future_file]
985                     reject("%s: has %s file(s) with a time stamp too far into the future (e.g. %s [%s])."
986                            % (filename, num_future_files, future_file,
987                               time.ctime(future_date)))
988                 #
989                 ancient_files = tar.ancient_files.keys()
990                 if ancient_files:
991                     num_ancient_files = len(ancient_files)
992                     ancient_file = ancient_files[0]
993                     ancient_date = tar.ancient_files[ancient_file]
994                     reject("%s: has %s file(s) with a time stamp too ancient (e.g. %s [%s])."
995                            % (filename, num_ancient_files, ancient_file,
996                               time.ctime(ancient_date)))
997             except:
998                 reject("%s: deb contents timestamp check failed [%s: %s]" % (filename, sys.exc_type, sys.exc_value))
999
1000 ################################################################################
1001
1002 def lookup_uid_from_fingerprint(fpr):
1003     """
1004     Return the uid,name,isdm for a given gpg fingerprint
1005
1006     @ptype fpr: string
1007     @param fpr: a 40 byte GPG fingerprint
1008
1009     @return (uid, name, isdm)
1010     """
1011     cursor = DBConn().cursor()
1012     cursor.execute( "SELECT u.uid, u.name, k.debian_maintainer FROM fingerprint f JOIN keyrings k ON (f.keyring=k.id), uid u WHERE f.uid = u.id AND f.fingerprint = '%s'" % (fpr))
1013     qs = cursor.fetchone()
1014     if qs:
1015         return qs
1016     else:
1017         return (None, None, None)
1018
1019 def check_signed_by_key():
1020     """Ensure the .changes is signed by an authorized uploader."""
1021
1022     (uid, uid_name, is_dm) = lookup_uid_from_fingerprint(changes["fingerprint"])
1023     if uid_name == None:
1024         uid_name = ""
1025
1026     # match claimed name with actual name:
1027     if uid == None:
1028         uid, uid_email = changes["fingerprint"], uid
1029         may_nmu, may_sponsor = 1, 1
1030         # XXX by default new dds don't have a fingerprint/uid in the db atm,
1031         #     and can't get one in there if we don't allow nmu/sponsorship
1032     elif is_dm is "t":
1033         uid_email = uid
1034         may_nmu, may_sponsor = 0, 0
1035     else:
1036         uid_email = "%s@debian.org" % (uid)
1037         may_nmu, may_sponsor = 1, 1
1038
1039     if uid_email in [changes["maintaineremail"], changes["changedbyemail"]]:
1040         sponsored = 0
1041     elif uid_name in [changes["maintainername"], changes["changedbyname"]]:
1042         sponsored = 0
1043         if uid_name == "": sponsored = 1
1044     else:
1045         sponsored = 1
1046         if ("source" in changes["architecture"] and
1047             uid_email and utils.is_email_alias(uid_email)):
1048             sponsor_addresses = utils.gpg_get_key_addresses(changes["fingerprint"])
1049             if (changes["maintaineremail"] not in sponsor_addresses and
1050                 changes["changedbyemail"] not in sponsor_addresses):
1051                 changes["sponsoremail"] = uid_email
1052
1053     if sponsored and not may_sponsor:
1054         reject("%s is not authorised to sponsor uploads" % (uid))
1055
1056     if not sponsored and not may_nmu:
1057         source_ids = []
1058         cursor.execute( "SELECT s.id, s.version FROM source s JOIN src_associations sa ON (s.id = sa.source) WHERE s.source = %(source)s AND s.dm_upload_allowed = 'yes'", changes )
1059
1060         highest_sid, highest_version = None, None
1061
1062         should_reject = True
1063         while True:
1064             si = cursor.fetchone()
1065             if not si:
1066                 break
1067
1068             if highest_version == None or apt_pkg.VersionCompare(si[1], highest_version) == 1:
1069                  highest_sid = si[0]
1070                  highest_version = si[1]
1071
1072         if highest_sid == None:
1073            reject("Source package %s does not have 'DM-Upload-Allowed: yes' in its most recent version" % changes["source"])
1074         else:
1075
1076             cursor.execute("SELECT m.name FROM maintainer m WHERE m.id IN (SELECT su.maintainer FROM src_uploaders su JOIN source s ON (s.id = su.source) WHERE su.source = %s)" % (highest_sid))
1077
1078             while True:
1079                 m = cursor.fetchone()
1080                 if not m:
1081                     break
1082
1083                 (rfc822, rfc2047, name, email) = utils.fix_maintainer(m[0])
1084                 if email == uid_email or name == uid_name:
1085                     should_reject=False
1086                     break
1087
1088         if should_reject == True:
1089             reject("%s is not in Maintainer or Uploaders of source package %s" % (uid, changes["source"]))
1090
1091         for b in changes["binary"].keys():
1092             for suite in changes["distribution"].keys():
1093                 suite_id = DBConn().get_suite_id(suite)
1094
1095                 cursor.execute("SELECT DISTINCT s.source FROM source s JOIN binaries b ON (s.id = b.source) JOIN bin_associations ba On (b.id = ba.bin) WHERE b.package = %(package)s AND ba.suite = %(suite)s" , {'package':b, 'suite':suite_id} )
1096                 while True:
1097                     s = cursor.fetchone()
1098                     if not s:
1099                         break
1100
1101                     if s[0] != changes["source"]:
1102                         reject("%s may not hijack %s from source package %s in suite %s" % (uid, b, s, suite))
1103
1104         for f in files.keys():
1105             if files[f].has_key("byhand"):
1106                 reject("%s may not upload BYHAND file %s" % (uid, f))
1107             if files[f].has_key("new"):
1108                 reject("%s may not upload NEW file %s" % (uid, f))
1109
1110
1111 ################################################################################
1112 ################################################################################
1113
1114 # If any file of an upload has a recent mtime then chances are good
1115 # the file is still being uploaded.
1116
1117 def upload_too_new():
1118     too_new = 0
1119     # Move back to the original directory to get accurate time stamps
1120     cwd = os.getcwd()
1121     os.chdir(pkg.directory)
1122     file_list = pkg.files.keys()
1123     file_list.extend(pkg.dsc_files.keys())
1124     file_list.append(pkg.changes_file)
1125     for f in file_list:
1126         try:
1127             last_modified = time.time()-os.path.getmtime(f)
1128             if last_modified < int(Cnf["Dinstall::SkipTime"]):
1129                 too_new = 1
1130                 break
1131         except:
1132             pass
1133     os.chdir(cwd)
1134     return too_new
1135
1136 ################################################################################
1137
1138 def action ():
1139     # changes["distribution"] may not exist in corner cases
1140     # (e.g. unreadable changes files)
1141     if not changes.has_key("distribution") or not isinstance(changes["distribution"], DictType):
1142         changes["distribution"] = {}
1143
1144     (summary, short_summary) = Upload.build_summaries()
1145
1146     # q-unapproved hax0ring
1147     queue_info = {
1148          "New": { "is": is_new, "process": acknowledge_new },
1149          "Autobyhand" : { "is" : is_autobyhand, "process": do_autobyhand },
1150          "Byhand" : { "is": is_byhand, "process": do_byhand },
1151          "OldStableUpdate" : { "is": is_oldstableupdate,
1152                                 "process": do_oldstableupdate },
1153          "StableUpdate" : { "is": is_stableupdate, "process": do_stableupdate },
1154          "Unembargo" : { "is": is_unembargo, "process": queue_unembargo },
1155          "Embargo" : { "is": is_embargo, "process": queue_embargo },
1156     }
1157     queues = [ "New", "Autobyhand", "Byhand" ]
1158     if Cnf.FindB("Dinstall::SecurityQueueHandling"):
1159         queues += [ "Unembargo", "Embargo" ]
1160     else:
1161         queues += [ "OldStableUpdate", "StableUpdate" ]
1162
1163     (prompt, answer) = ("", "XXX")
1164     if Options["No-Action"] or Options["Automatic"]:
1165         answer = 'S'
1166
1167     queuekey = ''
1168
1169     if reject_message.find("Rejected") != -1:
1170         if upload_too_new():
1171             print "SKIP (too new)\n" + reject_message,
1172             prompt = "[S]kip, Quit ?"
1173         else:
1174             print "REJECT\n" + reject_message,
1175             prompt = "[R]eject, Skip, Quit ?"
1176             if Options["Automatic"]:
1177                 answer = 'R'
1178     else:
1179         qu = None
1180         for q in queues:
1181             if queue_info[q]["is"]():
1182                 qu = q
1183                 break
1184         if qu:
1185             print "%s for %s\n%s%s" % (
1186                 qu.upper(), ", ".join(changes["distribution"].keys()),
1187                 reject_message, summary),
1188             queuekey = qu[0].upper()
1189             if queuekey in "RQSA":
1190                 queuekey = "D"
1191                 prompt = "[D]ivert, Skip, Quit ?"
1192             else:
1193                 prompt = "[%s]%s, Skip, Quit ?" % (queuekey, qu[1:].lower())
1194             if Options["Automatic"]:
1195                 answer = queuekey
1196         else:
1197             print "ACCEPT\n" + reject_message + summary,
1198             prompt = "[A]ccept, Skip, Quit ?"
1199             if Options["Automatic"]:
1200                 answer = 'A'
1201
1202     while prompt.find(answer) == -1:
1203         answer = utils.our_raw_input(prompt)
1204         m = re_default_answer.match(prompt)
1205         if answer == "":
1206             answer = m.group(1)
1207         answer = answer[:1].upper()
1208
1209     if answer == 'R':
1210         os.chdir (pkg.directory)
1211         Upload.do_reject(0, reject_message)
1212     elif answer == 'A':
1213         accept(summary, short_summary)
1214         remove_from_unchecked()
1215     elif answer == queuekey:
1216         queue_info[qu]["process"](summary, short_summary)
1217         remove_from_unchecked()
1218     elif answer == 'Q':
1219         sys.exit(0)
1220
1221 def remove_from_unchecked():
1222     os.chdir (pkg.directory)
1223     for f in files.keys():
1224         os.unlink(f)
1225     os.unlink(pkg.changes_file)
1226
1227 ################################################################################
1228
1229 def accept (summary, short_summary):
1230     Upload.accept(summary, short_summary)
1231     Upload.check_override()
1232
1233 ################################################################################
1234
1235 def move_to_dir (dest, perms=0660, changesperms=0664):
1236     utils.move (pkg.changes_file, dest, perms=changesperms)
1237     file_keys = files.keys()
1238     for f in file_keys:
1239         utils.move (f, dest, perms=perms)
1240
1241 ################################################################################
1242
1243 def is_unembargo ():
1244     cursor = DBConn().cursor()
1245     cursor.execute( "SELECT package FROM disembargo WHERE package = %(source)s AND version = %(version)s", changes )
1246     if cursor.fetchone():
1247         return 1
1248
1249     oldcwd = os.getcwd()
1250     os.chdir(Cnf["Dir::Queue::Disembargo"])
1251     disdir = os.getcwd()
1252     os.chdir(oldcwd)
1253
1254     if pkg.directory == disdir:
1255         if changes["architecture"].has_key("source"):
1256             if Options["No-Action"]: return 1
1257
1258             cursor.execute( "INSERT INTO disembargo (package, version) VALUES ('%(package)s', '%(version)s')",
1259                             changes )
1260             cursor.execute( "COMMIT" )
1261             return 1
1262
1263     return 0
1264
1265 def queue_unembargo (summary, short_summary):
1266     print "Moving to UNEMBARGOED holding area."
1267     Logger.log(["Moving to unembargoed", pkg.changes_file])
1268
1269     Upload.dump_vars(Cnf["Dir::Queue::Unembargoed"])
1270     move_to_dir(Cnf["Dir::Queue::Unembargoed"])
1271     Upload.queue_build("unembargoed", Cnf["Dir::Queue::Unembargoed"])
1272
1273     # Check for override disparities
1274     Upload.Subst["__SUMMARY__"] = summary
1275     Upload.check_override()
1276
1277     # Send accept mail, announce to lists, close bugs and check for
1278     # override disparities
1279     if not Cnf["Dinstall::Options::No-Mail"]:
1280         Upload.Subst["__SUITE__"] = ""
1281         mail_message = utils.TemplateSubst(Upload.Subst,Cnf["Dir::Templates"]+"/process-unchecked.accepted")
1282         utils.send_mail(mail_message)
1283         Upload.announce(short_summary, 1)
1284
1285 ################################################################################
1286
1287 def is_embargo ():
1288     # if embargoed queues are enabled always embargo
1289     return 1
1290
1291 def queue_embargo (summary, short_summary):
1292     print "Moving to EMBARGOED holding area."
1293     Logger.log(["Moving to embargoed", pkg.changes_file])
1294
1295     Upload.dump_vars(Cnf["Dir::Queue::Embargoed"])
1296     move_to_dir(Cnf["Dir::Queue::Embargoed"])
1297     Upload.queue_build("embargoed", Cnf["Dir::Queue::Embargoed"])
1298
1299     # Check for override disparities
1300     Upload.Subst["__SUMMARY__"] = summary
1301     Upload.check_override()
1302
1303     # Send accept mail, announce to lists, close bugs and check for
1304     # override disparities
1305     if not Cnf["Dinstall::Options::No-Mail"]:
1306         Upload.Subst["__SUITE__"] = ""
1307         mail_message = utils.TemplateSubst(Upload.Subst,Cnf["Dir::Templates"]+"/process-unchecked.accepted")
1308         utils.send_mail(mail_message)
1309         Upload.announce(short_summary, 1)
1310
1311 ################################################################################
1312
1313 def is_stableupdate ():
1314     if not changes["distribution"].has_key("proposed-updates"):
1315         return 0
1316
1317     if not changes["architecture"].has_key("source"):
1318         pusuite = DBConn().get_suite_id("proposed-updates")
1319         cursor = DBConn().cursor()
1320         cursor.execute( """SELECT 1 FROM source s
1321                            JOIN src_associations sa ON (s.id = sa.source)
1322                            WHERE s.source = %(source)s
1323                               AND s.version = '%(version)s'
1324                               AND sa.suite = %(suite)d""",
1325                         {'source' : changes['source'],
1326                          'version' : changes['version'],
1327                          'suite' : pusuite})
1328
1329         if cursor.fetchone():
1330             # source is already in proposed-updates so no need to hold
1331             return 0
1332
1333     return 1
1334
1335 def do_stableupdate (summary, short_summary):
1336     print "Moving to PROPOSED-UPDATES holding area."
1337     Logger.log(["Moving to proposed-updates", pkg.changes_file])
1338
1339     Upload.dump_vars(Cnf["Dir::Queue::ProposedUpdates"])
1340     move_to_dir(Cnf["Dir::Queue::ProposedUpdates"], perms=0664)
1341
1342     # Check for override disparities
1343     Upload.Subst["__SUMMARY__"] = summary
1344     Upload.check_override()
1345
1346 ################################################################################
1347
1348 def is_oldstableupdate ():
1349     if not changes["distribution"].has_key("oldstable-proposed-updates"):
1350         return 0
1351
1352     if not changes["architecture"].has_key("source"):
1353         pusuite = DBConn().get_suite_id("oldstable-proposed-updates")
1354         cursor = DBConn().cursor()
1355         cursor.execute( """"SELECT 1 FROM source s
1356                             JOIN src_associations sa ON (s.id = sa.source)
1357                             WHERE s.source = %(source)s
1358                               AND s.version = %(version)s
1359                                AND sa.suite = %d""",
1360                         {'source' : changes['source'],
1361                          'version' : changes['version'],
1362                          'suite' : pusuite})
1363         if cursor.fetchone():
1364             return 0
1365
1366     return 1
1367
1368 def do_oldstableupdate (summary, short_summary):
1369     print "Moving to OLDSTABLE-PROPOSED-UPDATES holding area."
1370     Logger.log(["Moving to oldstable-proposed-updates", pkg.changes_file])
1371
1372     Upload.dump_vars(Cnf["Dir::Queue::OldProposedUpdates"])
1373     move_to_dir(Cnf["Dir::Queue::OldProposedUpdates"], perms=0664)
1374
1375     # Check for override disparities
1376     Upload.Subst["__SUMMARY__"] = summary
1377     Upload.check_override()
1378
1379 ################################################################################
1380
1381 def is_autobyhand ():
1382     all_auto = 1
1383     any_auto = 0
1384     for f in files.keys():
1385         if files[f].has_key("byhand"):
1386             any_auto = 1
1387
1388             # filename is of form "PKG_VER_ARCH.EXT" where PKG, VER and ARCH
1389             # don't contain underscores, and ARCH doesn't contain dots.
1390             # further VER matches the .changes Version:, and ARCH should be in
1391             # the .changes Architecture: list.
1392             if f.count("_") < 2:
1393                 all_auto = 0
1394                 continue
1395
1396             (pckg, ver, archext) = f.split("_", 2)
1397             if archext.count(".") < 1 or changes["version"] != ver:
1398                 all_auto = 0
1399                 continue
1400
1401             ABH = Cnf.SubTree("AutomaticByHandPackages")
1402             if not ABH.has_key(pckg) or \
1403               ABH["%s::Source" % (pckg)] != changes["source"]:
1404                 print "not match %s %s" % (pckg, changes["source"])
1405                 all_auto = 0
1406                 continue
1407
1408             (arch, ext) = archext.split(".", 1)
1409             if arch not in changes["architecture"]:
1410                 all_auto = 0
1411                 continue
1412
1413             files[f]["byhand-arch"] = arch
1414             files[f]["byhand-script"] = ABH["%s::Script" % (pckg)]
1415
1416     return any_auto and all_auto
1417
1418 def do_autobyhand (summary, short_summary):
1419     print "Attempting AUTOBYHAND."
1420     byhandleft = 0
1421     for f in files.keys():
1422         byhandfile = f
1423         if not files[f].has_key("byhand"):
1424             continue
1425         if not files[f].has_key("byhand-script"):
1426             byhandleft = 1
1427             continue
1428
1429         os.system("ls -l %s" % byhandfile)
1430         result = os.system("%s %s %s %s %s" % (
1431                 files[f]["byhand-script"], byhandfile,
1432                 changes["version"], files[f]["byhand-arch"],
1433                 os.path.abspath(pkg.changes_file)))
1434         if result == 0:
1435             os.unlink(byhandfile)
1436             del files[f]
1437         else:
1438             print "Error processing %s, left as byhand." % (f)
1439             byhandleft = 1
1440
1441     if byhandleft:
1442         do_byhand(summary, short_summary)
1443     else:
1444         accept(summary, short_summary)
1445
1446 ################################################################################
1447
1448 def is_byhand ():
1449     for f in files.keys():
1450         if files[f].has_key("byhand"):
1451             return 1
1452     return 0
1453
1454 def do_byhand (summary, short_summary):
1455     print "Moving to BYHAND holding area."
1456     Logger.log(["Moving to byhand", pkg.changes_file])
1457
1458     Upload.dump_vars(Cnf["Dir::Queue::Byhand"])
1459     move_to_dir(Cnf["Dir::Queue::Byhand"])
1460
1461     # Check for override disparities
1462     Upload.Subst["__SUMMARY__"] = summary
1463     Upload.check_override()
1464
1465 ################################################################################
1466
1467 def is_new ():
1468     for f in files.keys():
1469         if files[f].has_key("new"):
1470             return 1
1471     return 0
1472
1473 def acknowledge_new (summary, short_summary):
1474     Subst = Upload.Subst
1475
1476     print "Moving to NEW holding area."
1477     Logger.log(["Moving to new", pkg.changes_file])
1478
1479     Upload.dump_vars(Cnf["Dir::Queue::New"])
1480     move_to_dir(Cnf["Dir::Queue::New"])
1481
1482     if not Options["No-Mail"]:
1483         print "Sending new ack."
1484         Subst["__SUMMARY__"] = summary
1485         new_ack_message = utils.TemplateSubst(Subst,Cnf["Dir::Templates"]+"/process-unchecked.new")
1486         utils.send_mail(new_ack_message)
1487
1488 ################################################################################
1489
1490 # reprocess is necessary for the case of foo_1.2-1 and foo_1.2-2 in
1491 # Incoming. -1 will reference the .orig.tar.gz, but -2 will not.
1492 # Upload.check_dsc_against_db() can find the .orig.tar.gz but it will
1493 # not have processed it during it's checks of -2.  If -1 has been
1494 # deleted or otherwise not checked by 'dak process-unchecked', the
1495 # .orig.tar.gz will not have been checked at all.  To get round this,
1496 # we force the .orig.tar.gz into the .changes structure and reprocess
1497 # the .changes file.
1498
1499 def process_it (changes_file):
1500     global reprocess, reject_message
1501
1502     # Reset some globals
1503     reprocess = 1
1504     Upload.init_vars()
1505     # Some defaults in case we can't fully process the .changes file
1506     changes["maintainer2047"] = Cnf["Dinstall::MyEmailAddress"]
1507     changes["changedby2047"] = Cnf["Dinstall::MyEmailAddress"]
1508     reject_message = ""
1509
1510     # Absolutize the filename to avoid the requirement of being in the
1511     # same directory as the .changes file.
1512     pkg.changes_file = os.path.abspath(changes_file)
1513
1514     # Remember where we are so we can come back after cd-ing into the
1515     # holding directory.
1516     pkg.directory = os.getcwd()
1517
1518     try:
1519         # If this is the Real Thing(tm), copy things into a private
1520         # holding directory first to avoid replacable file races.
1521         if not Options["No-Action"]:
1522             os.chdir(Cnf["Dir::Queue::Holding"])
1523             copy_to_holding(pkg.changes_file)
1524             # Relativize the filename so we use the copy in holding
1525             # rather than the original...
1526             pkg.changes_file = os.path.basename(pkg.changes_file)
1527         changes["fingerprint"] = utils.check_signature(pkg.changes_file, reject)
1528         if changes["fingerprint"]:
1529             valid_changes_p = check_changes()
1530         else:
1531             valid_changes_p = 0
1532         if valid_changes_p:
1533             while reprocess:
1534                 check_distributions()
1535                 check_files()
1536                 valid_dsc_p = check_dsc()
1537                 if valid_dsc_p:
1538                     check_source()
1539                 check_hashes()
1540                 check_urgency()
1541                 check_timestamps()
1542                 check_signed_by_key()
1543         Upload.update_subst(reject_message)
1544         action()
1545     except SystemExit:
1546         raise
1547     except:
1548         print "ERROR"
1549         traceback.print_exc(file=sys.stderr)
1550         pass
1551
1552     # Restore previous WD
1553     os.chdir(pkg.directory)
1554
1555 ###############################################################################
1556
1557 def main():
1558     global Cnf, Options, Logger
1559
1560     changes_files = init()
1561
1562     # -n/--dry-run invalidates some other options which would involve things happening
1563     if Options["No-Action"]:
1564         Options["Automatic"] = ""
1565
1566     # Ensure all the arguments we were given are .changes files
1567     for f in changes_files:
1568         if not f.endswith(".changes"):
1569             utils.warn("Ignoring '%s' because it's not a .changes file." % (f))
1570             changes_files.remove(f)
1571
1572     if changes_files == []:
1573         utils.fubar("Need at least one .changes file as an argument.")
1574
1575     # Check that we aren't going to clash with the daily cron job
1576
1577     if not Options["No-Action"] and os.path.exists("%s/daily.lock" % (Cnf["Dir::Lock"])) and not Options["No-Lock"]:
1578         utils.fubar("Archive maintenance in progress.  Try again later.")
1579
1580     # Obtain lock if not in no-action mode and initialize the log
1581
1582     if not Options["No-Action"]:
1583         lock_fd = os.open(Cnf["Dinstall::LockFile"], os.O_RDWR | os.O_CREAT)
1584         try:
1585             fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1586         except IOError, e:
1587             if errno.errorcode[e.errno] == 'EACCES' or errno.errorcode[e.errno] == 'EAGAIN':
1588                 utils.fubar("Couldn't obtain lock; assuming another 'dak process-unchecked' is already running.")
1589             else:
1590                 raise
1591         Logger = Upload.Logger = logging.Logger(Cnf, "process-unchecked")
1592
1593     # debian-{devel-,}-changes@lists.debian.org toggles writes access based on this header
1594     bcc = "X-DAK: dak process-unchecked\nX-Katie: $Revision: 1.65 $"
1595     if Cnf.has_key("Dinstall::Bcc"):
1596         Upload.Subst["__BCC__"] = bcc + "\nBcc: %s" % (Cnf["Dinstall::Bcc"])
1597     else:
1598         Upload.Subst["__BCC__"] = bcc
1599
1600
1601     # Sort the .changes files so that we process sourceful ones first
1602     changes_files.sort(utils.changes_compare)
1603
1604     # Process the changes files
1605     for changes_file in changes_files:
1606         print "\n" + changes_file
1607         try:
1608             process_it (changes_file)
1609         finally:
1610             if not Options["No-Action"]:
1611                 clean_holding()
1612
1613     accept_count = Upload.accept_count
1614     accept_bytes = Upload.accept_bytes
1615     if accept_count:
1616         sets = "set"
1617         if accept_count > 1:
1618             sets = "sets"
1619         print "Accepted %d package %s, %s." % (accept_count, sets, utils.size_type(int(accept_bytes)))
1620         Logger.log(["total",accept_count,accept_bytes])
1621
1622     if not Options["No-Action"]:
1623         Logger.close()
1624
1625 ################################################################################
1626
1627 if __name__ == '__main__':
1628     main()