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