]> git.decadent.org.uk Git - dak.git/blob - dak/contents.py
Merge branch 'master' into content_generation
[dak.git] / dak / contents.py
1 #!/usr/bin/env python
2 """
3 Create all the contents files
4
5 @contact: Debian FTPMaster <ftpmaster@debian.org>
6 @copyright: 2008, 2009 Michael Casadevall <mcasadevall@debian.org>
7 @copyright: 2009 Mike O'Connor <stew@debian.org>
8 @license: GNU General Public License version 2 or later
9 """
10
11 ################################################################################
12
13 # This program is free software; you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation; either version 2 of the License, or
16 # (at your option) any later version.
17
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21 # GNU General Public License for more details.
22
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
26
27 ################################################################################
28
29 # <Ganneff> there is the idea to slowly replace contents files
30 # <Ganneff> with a new generation of such files.
31 # <Ganneff> having more info.
32
33 # <Ganneff> of course that wont help for now where we need to generate them :)
34
35 ################################################################################
36
37 import sys
38 import os
39 import logging
40 import math
41 import gzip
42 import apt_pkg
43 from daklib import utils
44 from daklib.binary import Binary
45 from daklib.config import Config
46 from daklib.dbconn import DBConn
47 ################################################################################
48
49 def usage (exit_code=0):
50     print """Usage: dak contents [options] command [arguments]
51
52 COMMANDS
53     generate
54         generate Contents-$arch.gz files
55
56     bootstrap
57         scan the debs in the existing pool and load contents in the the database
58
59     cruft
60         remove files/paths which are no longer referenced by a binary
61
62 OPTIONS
63      -h, --help
64         show this help and exit
65
66      -v, --verbose
67         show verbose information messages
68
69      -q, --quiet
70         supress all output but errors
71
72      -s, --suite={stable,testing,unstable,...}
73         only operate on a single suite
74
75      -a, --arch={i386,amd64}
76         only operate on a single architecture
77 """
78     sys.exit(exit_code)
79
80 ################################################################################
81
82 # where in dak.conf all of our configuration will be stowed
83
84 options_prefix = "Contents"
85 options_prefix = "%s::Options" % options_prefix
86
87 log = logging.getLogger()
88
89 ################################################################################
90
91 # get all the arches delivered for a given suite
92 # this should probably exist somehere common
93 arches_q = """PREPARE arches_q(int) as
94               SELECT s.architecture, a.arch_string
95               FROM suite_architectures s
96               JOIN architecture a ON (s.architecture=a.id)
97                   WHERE suite = $1"""
98
99 # find me the .deb for a given binary id
100 debs_q = """PREPARE debs_q(int, int) as
101               SELECT b.id, f.filename FROM bin_assoc_by_arch baa
102               JOIN binaries b ON baa.bin=b.id
103               JOIN files f ON b.file=f.id
104               WHERE suite = $1
105                   AND arch = $2"""
106
107 # ask if we already have contents associated with this binary
108 olddeb_q = """PREPARE olddeb_q(int) as
109               SELECT 1 FROM content_associations
110               WHERE binary_pkg = $1
111               LIMIT 1"""
112
113 # find me all of the contents for a given .deb
114 contents_q = """PREPARE contents_q(int,int,int,int) as
115                 SELECT (p.path||'/'||n.file) AS fn,
116                       comma_separated_list(s.section||'/'||b.package)
117               from content_associations c join content_file_paths p ON (c.filepath=p.id)
118               JOIN content_file_names n ON (c.filename=n.id)
119               JOIN binaries b ON (b.id=c.binary_pkg)
120               JOIN override o ON (o.package=b.package)
121               JOIN section s ON (s.id=o.section)
122               WHERE o.suite = $1 AND o.type = $2
123               AND b.id in (SELECT ba.bin from bin_associations ba join binaries b on b.id=ba.bin where (b.architecture=$3 or b.architecture=$4)and ba.suite=$1 and b.type='deb')
124               GROUP BY fn
125               ORDER BY fn;"""
126
127 # find me all of the contents for a given .udeb
128 udeb_contents_q = """PREPARE udeb_contents_q(int,int,int,int,int) as
129               SELECT (p.path||'/'||n.file) AS fn,
130                       comma_separated_list(s.section||'/'||b.package)
131               FROM content_file_paths p join content_associations c ON (c.filepath=p.id)
132               JOIN content_file_names n ON (c.filename=n.id)
133               JOIN binaries b ON (b.id=c.binary_pkg)
134               JOIN override o ON (o.package=b.package)
135               JOIN section s ON (s.id=o.section)
136               WHERE o.suite = $1 AND o.type = $2
137               AND s.id = $3
138               AND b.id in (SELECT ba.bin from bin_associations ba join binaries b on b.id=ba.bin where (b.architecture=$3 or b.architecture=$4)and ba.suite=$1 and b.type='udeb')
139               GROUP BY fn
140               ORDER BY fn;"""
141
142
143
144 # clear out all of the temporarily stored content associations
145 # this should be run only after p-a has run.  after a p-a
146 # run we should have either accepted or rejected every package
147 # so there should no longer be anything in the queue
148 remove_pending_contents_cruft_q = """DELETE FROM pending_content_associations"""
149
150 # delete any filenames we are storing which have no binary associated with them
151 remove_filename_cruft_q = """DELETE FROM content_file_names
152                              WHERE id IN (SELECT cfn.id FROM content_file_names cfn
153                                           LEFT JOIN content_associations ca
154                                             ON ca.filename=cfn.id
155                                           WHERE ca.id IS NULL)"""
156
157 # delete any paths we are storing which have no binary associated with them
158 remove_filepath_cruft_q = """DELETE FROM content_file_paths
159                              WHERE id IN (SELECT cfn.id FROM content_file_paths cfn
160                                           LEFT JOIN content_associations ca
161                                              ON ca.filepath=cfn.id
162                                           WHERE ca.id IS NULL)"""
163 class Contents(object):
164     """
165     Class capable of generating Contents-$arch.gz files
166
167     Usage GenerateContents().generateContents( ["main","contrib","non-free"] )
168     """
169
170     def __init__(self):
171         self.header = None
172
173     def reject(self, message):
174         log.error("E: %s" % message)
175
176     def _getHeader(self):
177         """
178         Internal method to return the header for Contents.gz files
179
180         This is boilerplate which explains the contents of the file and how
181         it can be used.
182         """
183         if self.header == None:
184             if Config().has_key("Contents::Header"):
185                 try:
186                     h = open(os.path.join( Config()["Dir::Templates"],
187                                            Config()["Contents::Header"] ), "r")
188                     self.header = h.read()
189                     h.close()
190                 except:
191                     log.error( "error opening header file: %d\n%s" % (Config()["Contents::Header"],
192                                                                       traceback.format_exc() ))
193                     self.header = False
194             else:
195                 self.header = False
196
197         return self.header
198
199     # goal column for section column
200     _goal_column = 54
201
202     def _write_content_file(self, cursor, filename):
203         """
204         Internal method for writing all the results to a given file.
205         The cursor should have a result set generated from a query already.
206         """
207         filepath = Config()["Contents::Root"] + filename
208         filedir = os.path.dirname(filepath)
209         if not os.path.isdir(filedir):
210             os.makedirs(filedir)
211         f = gzip.open(filepath, "w")
212         try:
213             header = self._getHeader()
214
215             if header:
216                 f.write(header)
217
218             while True:
219                 contents = cursor.fetchone()
220                 if not contents:
221                     return
222
223                 f.write("%s\t%s\n" % contents )
224
225         finally:
226             f.close()
227
228     def cruft(self):
229         """
230         remove files/paths from the DB which are no longer referenced
231         by binaries and clean the temporary table
232         """
233         cursor = DBConn().cursor();
234         cursor.execute( "BEGIN WORK" )
235         cursor.execute( remove_pending_contents_cruft_q )
236         cursor.execute( remove_filename_cruft_q )
237         cursor.execute( remove_filepath_cruft_q )
238         cursor.execute( "COMMIT" )
239
240
241     def bootstrap(self):
242         """
243         scan the existing debs in the pool to populate the contents database tables
244         """
245         pooldir = Config()[ 'Dir::Pool' ]
246
247         cursor = DBConn().cursor();
248         DBConn().prepare("debs_q",debs_q)
249         DBConn().prepare("olddeb_q",olddeb_q)
250         DBConn().prepare("arches_q",arches_q)
251
252         suites = self._suites()
253         for suite in [i.lower() for i in suites]:
254             suite_id = DBConn().get_suite_id(suite)
255
256             arch_list = self._arches(cursor, suite_id)
257             arch_all_id = DBConn().get_architecture_id("all")
258             for arch_id in arch_list:
259                 cursor.execute( "EXECUTE debs_q(%d, %d)" % ( suite_id, arch_id[0] ) )
260
261                 count = 0
262                 while True:
263                     deb = cursor.fetchone()
264                     if not deb:
265                         break
266                     count += 1
267                     cursor1 = DBConn().cursor();
268                     cursor1.execute( "EXECUTE olddeb_q(%d)" % (deb[0] ) )
269                     old = cursor1.fetchone()
270                     if old:
271                         log.debug( "already imported: %s" % (deb[1]) )
272                     else:
273                         log.debug( "scanning: %s" % (deb[1]) )
274                         debfile = os.path.join( pooldir, deb[1] )
275                         if os.path.exists( debfile ):
276                             Binary(debfile, self.reject).scan_package(deb[0],True)
277                         else:
278                             log.error("missing .deb: %s" % deb[1])
279
280     def generate(self):
281         """
282         Generate Contents-$arch.gz files for every available arch in each given suite.
283         """
284         cursor = DBConn().cursor()
285
286         DBConn().prepare("arches_q", arches_q)
287         DBConn().prepare("contents_q", contents_q)
288         DBConn().prepare("udeb_contents_q", udeb_contents_q)
289
290         debtype_id=DBConn().get_override_type_id("deb")
291         udebtype_id=DBConn().get_override_type_id("udeb")
292
293         suites = self._suites()
294
295         # Get our suites, and the architectures
296         for suite in [i.lower() for i in suites]:
297             suite_id = DBConn().get_suite_id(suite)
298             arch_list = self._arches(cursor, suite_id)
299
300             arch_all_id = DBConn().get_architecture_id("all")
301
302             for arch_id in arch_list:
303                 cursor.execute("EXECUTE contents_q(%d,%d,%d,%d)" % (suite_id, debtype_id, arch_all_id, arch_id[0] ))
304                 self._write_content_file(cursor, "dists/%s/Contents-%s.gz" % (suite, arch_id[1]))
305
306             # The MORE fun part. Ok, udebs need their own contents files, udeb, and udeb-nf (not-free)
307             # This is HORRIBLY debian specific :-/
308             for section, fn_pattern in [("debian-installer","dists/%s/Contents-udeb-%s.gz"),
309                                            ("non-free/debian-installer", "dists/%s/Contents-udeb-nf-%s.gz")]:
310
311                 for arch_id in arch_list:
312                     section_id = DBConn().get_section_id(section) # all udebs should be here)
313                     if section_id != -1:
314                         cursor.execute("EXECUTE udeb_contents_q(%d,%d,%d,%d,%d)" % (suite_id, udebtype_id, section_id, arch_id[0], arch_all_id))
315
316                         self._write_content_file(cursor, fn_pattern % (suite, arch_id[1]))
317
318
319 ################################################################################
320
321     def _suites(self):
322         """
323         return a list of suites to operate on
324         """
325         if Config().has_key( "%s::%s" %(options_prefix,"Suite")):
326             suites = utils.split_args(Config()[ "%s::%s" %(options_prefix,"Suite")])
327         else:
328             suites = Config().SubTree("Suite").List()
329
330         return suites
331
332     def _arches(self, cursor, suite):
333         """
334         return a list of archs to operate on
335         """
336         arch_list = [ ]
337         if Config().has_key( "%s::%s" %(options_prefix,"Arch")):
338             archs = utils.split_args(Config()[ "%s::%s" %(options_prefix,"Arch")])
339             for arch_name in archs:
340                 arch_list.append((DBConn().get_architecture_id(arch_name), arch_name))
341         else:
342             cursor.execute("EXECUTE arches_q(%d)" % (suite))
343             while True:
344                 r = cursor.fetchone()
345                 if not r:
346                     break
347
348                 if r[1] != "source" and r[1] != "all":
349                     arch_list.append((r[0], r[1]))
350
351         return arch_list
352
353 ################################################################################
354
355
356 def main():
357     cnf = Config()
358
359     arguments = [('h',"help", "%s::%s" % (options_prefix,"Help")),
360                  ('s',"suite", "%s::%s" % (options_prefix,"Suite"),"HasArg"),
361                  ('q',"quiet", "%s::%s" % (options_prefix,"Quiet")),
362                  ('v',"verbose", "%s::%s" % (options_prefix,"Verbose")),
363                  ('a',"arch", "%s::%s" % (options_prefix,"Arch"),"HasArg"),
364                 ]
365
366     commands = {'generate' : Contents.generate,
367                 'bootstrap' : Contents.bootstrap,
368                 'cruft' : Contents.cruft,
369                 }
370
371     args = apt_pkg.ParseCommandLine(cnf.Cnf, arguments,sys.argv)
372
373     if (len(args) < 1) or not commands.has_key(args[0]):
374         usage()
375
376     if cnf.has_key("%s::%s" % (options_prefix,"Help")):
377         usage()
378
379     level=logging.INFO
380     if cnf.has_key("%s::%s" % (options_prefix,"Quiet")):
381         level=logging.ERROR
382
383     elif cnf.has_key("%s::%s" % (options_prefix,"Verbose")):
384         level=logging.DEBUG
385
386
387     logging.basicConfig( level=level,
388                          format='%(asctime)s %(levelname)s %(message)s',
389                          stream = sys.stderr )
390
391     commands[args[0]](Contents())
392
393 if __name__ == '__main__':
394     main()