]> git.decadent.org.uk Git - dak.git/blob - dak/contents.py
Merge branch 'master' into content_generation, make changes based on Joerg's review
[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 tempfile
40 import logging
41 import math
42 import gzip
43 import apt_pkg
44 from daklib import utils
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 signle suite
74
75      -a, --arch={i386,amd64}
76         only operate on a signle 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::Opitons" % options_prefix
86
87 log = logging.getLogger()
88
89 ################################################################################
90
91 # we unfortunately still have broken stuff in headers
92 latin1_q = """SET CLIENT_ENCODING TO 'LATIN1'"""
93
94 # get all the arches delivered for a given suite
95 # this should probably exist somehere common
96 arches_q = """PREPARE arches_q as
97               SELECT s.architecture, a.arch_string
98               FROM suite_architectures s
99               JOIN architecture a ON (s.architecture=a.id)
100                   WHERE suite = $1"""
101
102 # find me the .deb for a given binary id
103 debs_q = """PREPARE debs_q as
104               SELECT b.id, f.filename FROM bin_assoc_by_arch baa
105               JOIN binaries b ON baa.bin=b.id
106               JOIN files f ON b.file=f.id
107               WHERE suite = $1
108                   AND arch = $2"""
109
110 # ask if we already have contents associated with this binary
111 olddeb_q = """PREPARE olddeb_q as
112               SELECT 1 FROM content_associations
113               WHERE binary_pkg = $1
114               LIMIT 1"""
115
116 # find me all of the contents for a given .deb
117 contents_q = """PREPARE contents_q as
118               SELECT (p.path||'/'||n.file) AS fn,
119                       comma_separated_list(s.section||'/'||b.package)
120               FROM content_associations c
121               JOIN content_file_paths p ON (c.filepath=p.id)
122               JOIN content_file_names n ON (c.filename=n.id)
123               JOIN binaries b ON (b.id=c.binary_pkg)
124               JOIN bin_associations ba ON (b.id=ba.bin)
125               JOIN override o ON (o.package=b.package)
126               JOIN section s ON (s.id=o.section)
127               WHERE (b.architecture = $1 OR b.architecture = $2)
128                   AND ba.suite = $3
129                   AND o.suite = $4
130                   AND b.type = 'deb'
131                   AND o.type = '7'
132               GROUP BY fn
133               ORDER BY fn"""
134
135 # find me all of the contents for a given .udeb
136 udeb_contents_q = """PREPARE udeb_contents_q as
137               SELECT (p.path||'/'||n.file) as fn,
138                       comma_separated_list(s.section||'/'||b.package)
139               FROM content_associations c
140               JOIN content_file_paths p ON (c.filepath=p.id)
141               JOIN content_file_names n ON (c.filename=n.id)
142               JOIN binaries b ON (b.id=c.binary_pkg)
143               JOIN bin_associations ba ON (b.id=ba.bin)
144               JOIN override o ON (o.package=b.package)
145               JOIN section s ON (s.id=o.section)
146               WHERE s.id = $1
147                   AND ba.suite = $2
148                   AND o.suite = $3
149                   AND b.type = 'udeb'
150                   AND o.type = '8'
151               GROUP BY fn
152               ORDER BY fn"""
153
154 # clear out all of the temporarily stored content associations
155 # this should be run only after p-a has run.  after a p-a
156 # run we should have either accepted or rejected every package
157 # so there should no longer be anything in the queue
158 remove_temp_contents_cruft_q = """DELETE FROM temp_content_associations"""
159
160 # delete any filenames we are storing which have no binary associated with them
161 remove_filename_cruft_q = """DELETE FROM content_file_names
162                              WHERE id IN (SELECT cfn.id FROM content_file_names cfn
163                                           LEFT JOIN content_associations ca
164                                             ON ca.filename=cfn.id
165                                           WHERE ca.id IS NULL)""" );
166
167 # delete any paths we are storing which have no binary associated with them
168 remove_filepath_cruft_q = """DELETE FROM content_file_paths
169                              WHERE id IN (SELECT cfn.id FROM content_file_paths cfn
170                                           LEFT JOIN content_associations ca
171                                              ON ca.filepath=cfn.id
172                                           WHERE ca.id IS NULL)"""
173 class Contents(object):
174     """
175     Class capable of generating Contents-$arch.gz files
176
177     Usage GenerateContents().generateContents( ["main","contrib","non-free"] )
178     """
179
180     def __init__(self):
181         self.header = None
182
183     def _getHeader(self):
184         """
185         Internal method to return the header for Contents.gz files
186
187         This is boilerplate which explains the contents of the file and how
188         it can be used.
189         """
190         if self.header == None:
191             if Config().has_key("Contents::Header"):
192                 try:
193                     h = open(os.path.join( Config()["Dir::Templates"],
194                                            Config()["Contents::Header"] ), "r")
195                     self.header = h.read()
196                     print( "header: %s" % self.header )
197                     h.close()
198                 except:
199                     log.error( "error opening header file: %d\n%s" % (Config()["Contents::Header"],
200                                                                       traceback.format_exc() ))
201                     self.header = False
202             else:
203                 print( "no header" )
204                 self.header = False
205
206         return self.header
207
208     # goal column for section column
209     _goal_column = 54
210
211     def _write_content_file(self, cursor, filename):
212         """
213         Internal method for writing all the results to a given file.
214         The cursor should have a result set generated from a query already.
215         """
216         f = gzip.open(Config()["Dir::Root"] + filename, "w")
217         try:
218             header = self._getHeader()
219
220             if header:
221                 f.write(header)
222
223             while True:
224                 contents = cursor.fetchone()
225                 if not contents:
226                     return
227
228                 num_tabs = max(1,
229                                int( math.ceil( (self._goal_column - len(contents[0])) / 8) ) )
230                 f.write(contents[0] + ( '\t' * num_tabs ) + contents[-1] + "\n")
231
232         finally:
233             f.close()
234
235     def cruft(self):
236         """
237         remove files/paths from the DB which are no longer referenced
238         by binaries and clean the temporary table
239         """
240         cursor = DBConn().cursor();
241         cursor.execute( "BEGIN WORK" )
242         cursor.execute( remove_temp_contents_cruft_q )
243         cursor.execute( remove_filename_cruft_q )
244         cursor.execute( remove_filepath_cruft_q )
245         cursor.execute( "COMMIT" )
246
247
248     def bootstrap(self):
249         """
250         scan the existing debs in the pool to populate the contents database tables
251         """
252         pooldir = Config()[ 'Dir::Pool' ]
253
254         cursor = DBConn().cursor();
255         cursor.execute( latin1_q )
256         cursor.execute( debs_q )
257         cursor.execute( olddeb_q )
258         cursor.execute( arches_q )
259
260         suites = self._suites()
261         for suite in [i.lower() for i in suites]:
262             suite_id = DBConn().get_suite_id(suite)
263
264             arch_list = self._arches(cursor, suite_id)
265             arch_all_id = DBConn().get_architecture_id("all")
266             for arch_id in arch_list:
267                 cursor.execute( "EXECUTE debs_q(%d, %d)" % ( suite_id, arch_id[0] ) )
268
269                 debs = cursor.fetchall()
270                 count = 0
271                 for deb in debs:
272                     count += 1
273                     cursor.execute( "EXECUTE olddeb_q(%d)" % (deb[0] ) )
274                     old = cursor.fetchone()
275                     if old:
276                         log.debug( "already imported: %s" % deb[1] )
277                     else:
278                         debfile = os.path.join( pooldir, deb[1] )
279                         if os.path.exists( debfile ):
280                             contents = utils.generate_contents_information( debfile )
281                             DBConn().insert_content_paths(deb[0], contents)
282                             log.info( "imported (%d/%d): %s" % (count,len(debs),deb[1] ) )
283                         else:
284                             log.error( "missing .deb: %s" % deb[1] )
285
286     def generate(self):
287         """
288         Generate Contents-$arch.gz files for every aviailable arch in each given suite.
289         """
290         cursor = DBConn().cursor();
291
292         cursor.execute( arches_q )
293         cursor.execute( contents_q )
294         cursor.execute( udeb_contents_q )
295
296         suites = self._suites()
297
298         # Get our suites, and the architectures
299         for suite in [i.lower() for i in suites]:
300             suite_id = DBConn().get_suite_id(suite)
301             arch_list = self._arches(cursor, suite_id)
302
303             arch_all_id = DBConn().get_architecture_id("all")
304
305             for arch_id in arch_list:
306                 cursor.execute( "EXECUTE contents_q(%d,%d,%d,%d)" % (arch_id[0], arch_all_id, suite_id, suite_id))
307                 self._write_content_file(cursor, "dists/%s/Contents-%s.gz" % (suite, arch_id[1]))
308
309             # The MORE fun part. Ok, udebs need their own contents files, udeb, and udeb-nf (not-free)
310             # This is HORRIBLY debian specific :-/
311             # First off, udeb
312             section_id = DBConn().get_section_id('debian-installer') # all udebs should be here)
313             if section_id != -1:
314                 cursor.execute("EXECUTE udeb_contents_q(%d,%d,%d)" % (section_id, suite_id, suite_id))
315                 self._write_content_file(cursor, "dists/%s/Contents-udeb.gz" % suite)
316
317             # Once more, with non-free
318             section_id = DBConn().get_section_id('non-free/debian-installer') # all udebs should be here)
319
320             if section_id != -1:
321                 cursor.execute("EXECUTE udeb_contents_q(%d,%d,%d)" % (section_id, suite_id, suite_id))
322                 self._write_content_file(cursor, "dists/%s/Contents-udeb-nf.gz" % suite)
323
324
325 ################################################################################
326
327     def _suites(self):
328         """
329         return a list of suites to operate on
330         """
331         if Config().has_key( "%s::%s" %(options_prefix,"Suite")):
332             suites = utils.split_args(Config()[ "%s::%s" %(options_prefix,"Suite")])
333         else:
334             suites = Config().SubTree("Suite").List()
335
336         return suites
337
338     def _arches(self, cursor, suite):
339         """
340         return a list of archs to operate on
341         """
342         arch_list = [ ]
343         if Config().has_key( "%s::%s" %(options_prefix,"Arch")):
344             archs = utils.split_args(Config()[ "%s::%s" %(options_prefix,"Arch")])
345             for arch_name in archs:
346                 arch_list.append((DBConn().get_architecture_id(arch_name), arch_name))
347         else:
348             cursor.execute("EXECUTE arches_q(%d)" % (suite))
349             while True:
350                 r = cursor.fetchone()
351                 if not r:
352                     break
353
354                 if r[1] != "source" and r[1] != "all":
355                     arch_list.append((r[0], r[1]))
356
357         return arch_list
358
359 ################################################################################
360
361 def main():
362     cnf = Config()
363
364     arguments = [('h',"help", "%s::%s" % (options_prefix,"Help")),
365                  ('s',"suite", "%s::%s" % (options_prefix,"Suite"),"HasArg"),
366                  ('q',"quiet", "%s::%s" % (options_prefix,"Quiet")),
367                  ('v',"verbose", "%s::%s" % (options_prefix,"Verbose")),
368                  ('a',"arch", "%s::%s" % (options_prefix,"Arch"),"HasArg"),
369                 ]
370
371     commands = {'generate' : Contents.generate,
372                 'bootstrap' : Contents.bootstrap,
373                 'cruft' : Contents.cruft,
374                 }
375
376     level=logging.INFO
377     if cnf.has_key("%s::%s" % (options_prefix,"Quiet")):
378         level=logging.ERROR
379
380     elif cnf.has_key("%s::%s" % (options_prefix,"Verbose")):
381         level=logging.DEBUG
382
383
384     logging.basicConfig( level=level,
385                          format='%(asctime)s %(levelname)s %(message)s',
386                          stream = sys.stderr )
387
388     args = apt_pkg.ParseCommandLine(cnf.Cnf, arguments,sys.argv)
389
390     if (len(args) < 1) or not commands.has_key(args[0]):
391         usage()
392
393     if cnf.has_key("%s::%s" % (options_prefix,"Help")):
394         usage()
395
396     commands[args[0]](Contents())
397
398 if __name__ == '__main__':
399     main()