]> git.decadent.org.uk Git - dak.git/blob - dak/contents.py
prepare statements only once
[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
118               JOIN content_file_paths p ON (c.filepath=p.id)
119               JOIN content_file_names n ON (c.filename=n.id)
120               JOIN binaries b ON (b.id=c.binary_pkg)
121               JOIN bin_associations ba ON (b.id=ba.bin)
122               JOIN override o ON (o.package=b.package)
123               JOIN section s ON (s.id=o.section)
124               WHERE (b.architecture = $1 OR b.architecture = $2)
125                   AND ba.suite = $3
126                   AND o.suite = $3
127                   AND b.type = 'deb'
128                   AND o.type = $4
129               GROUP BY fn
130               ORDER BY fn"""
131
132 udeb_contents_q = """PREPARE udeb_contents_q(int,int,int,int,int) as
133               SELECT (p.path||'/'||n.file) as fn,
134                       comma_separated_list(s.section||'/'||b.package)
135               FROM content_associations c
136               JOIN content_file_paths p ON (c.filepath=p.id)
137               JOIN content_file_names n ON (c.filename=n.id)
138               JOIN binaries b ON (b.id=c.binary_pkg)
139               JOIN bin_associations ba ON (b.id=ba.bin)
140               JOIN override o ON (o.package=b.package)
141               JOIN section s ON (s.id=o.section)
142               WHERE (b.architecture = $1 OR b.architecture = $2)
143                   AND s.id = $3
144                   AND ba.suite = $4
145                   AND o.suite = $4
146                   AND b.type = 'udeb'
147                   AND o.type = $5
148               GROUP BY fn
149               ORDER BY fn"""
150
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 reject(self, message):
182         log.error("E: %s" % message)
183
184     def _getHeader(self):
185         """
186         Internal method to return the header for Contents.gz files
187
188         This is boilerplate which explains the contents of the file and how
189         it can be used.
190         """
191         if self.header == None:
192             if Config().has_key("Contents::Header"):
193                 try:
194                     h = open(os.path.join( Config()["Dir::Templates"],
195                                            Config()["Contents::Header"] ), "r")
196                     self.header = h.read()
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                 self.header = False
204
205         return self.header
206
207     # goal column for section column
208     _goal_column = 54
209
210     def _write_content_file(self, cursor, filename):
211         """
212         Internal method for writing all the results to a given file.
213         The cursor should have a result set generated from a query already.
214         """
215         filepath = Config()["Contents::Root"] + filename
216         filedir = os.path.dirname(filepath)
217         if not os.path.isdir(filedir):
218             os.makedirs(filedir)
219         f = gzip.open(filepath, "w")
220         try:
221             header = self._getHeader()
222
223             if header:
224                 f.write(header)
225
226             while True:
227                 contents = cursor.fetchone()
228                 if not contents:
229                     return
230
231                 num_tabs = max(1,
232                                int(math.ceil((self._goal_column - len(contents[0])-1) / 8)))
233                 f.write(contents[0] + ( '\t' * num_tabs ) + contents[-1] + "\n")
234
235         finally:
236             f.close()
237
238     def cruft(self):
239         """
240         remove files/paths from the DB which are no longer referenced
241         by binaries and clean the temporary table
242         """
243         cursor = DBConn().cursor();
244         cursor.execute( "BEGIN WORK" )
245         DBConn().prepare("remove_pending_contents_cruft_q", remove_pending_contents_cruft_q)
246         DBConn().prepare("remove_filename_cruft_q", remove_filename_cruft_q)
247         DBConn().prepare("remove_filepath_cruft_q", remove_filepath_cruft_q)
248         cursor.execute( "COMMIT" )
249
250
251     def bootstrap(self):
252         """
253         scan the existing debs in the pool to populate the contents database tables
254         """
255         pooldir = Config()[ 'Dir::Pool' ]
256
257         cursor = DBConn().cursor();
258         DBConn().prepare("debs_q",debs_q)
259         DBConn().prepare("olddeb_q",olddeb_q)
260         DBConn().prepare("arches_q",arches_q)
261
262         suites = self._suites()
263         for suite in [i.lower() for i in suites]:
264             suite_id = DBConn().get_suite_id(suite)
265
266             arch_list = self._arches(cursor, suite_id)
267             arch_all_id = DBConn().get_architecture_id("all")
268             for arch_id in arch_list:
269                 cursor.execute( "EXECUTE debs_q(%d, %d)" % ( suite_id, arch_id[0] ) )
270
271                 count = 0
272                 while True:
273                     deb = cursor.fetchone()
274                     if not deb:
275                         break
276                     count += 1
277                     cursor1 = DBConn().cursor();
278                     cursor1.execute( "EXECUTE olddeb_q(%d)" % (deb[0] ) )
279                     old = cursor1.fetchone()
280                     if old:
281                         log.debug( "already imported: %s" % deb[1] )
282                     else:
283                         debfile = os.path.join( pooldir, deb[1] )
284                         if os.path.exists( debfile ):
285                             Binary(debfile, self.reject).scan_package( deb[0] )
286                         else:
287                             log.error( "missing .deb: %s" % deb[1] )
288
289     def generate(self):
290         """
291         Generate Contents-$arch.gz files for every available arch in each given suite.
292         """
293         cursor = DBConn().cursor();
294
295         DBConn().prepare( "arches_q", arches_q )
296         DBConn().prepare( "contents_q", contents_q )
297         DBConn().prepare( "udeb_contents_q", udeb_contents_q )
298
299         debtype_id=DBConn().get_override_type_id("deb")
300         udebtype_id=DBConn().get_override_type_id("udeb")
301
302         suites = self._suites()
303
304         # Get our suites, and the architectures
305         for suite in [i.lower() for i in suites]:
306             suite_id = DBConn().get_suite_id(suite)
307             arch_list = self._arches(cursor, suite_id)
308
309             arch_all_id = DBConn().get_architecture_id("all")
310
311             for arch_id in arch_list:
312                 cursor.execute("EXECUTE contents_q(%d,%d,%d,%d)" % (arch_id[0], arch_all_id, suite_id, debtype_id))
313                 self._write_content_file(cursor, "dists/%s/Contents-%s.gz" % (suite, arch_id[1]))
314
315             # The MORE fun part. Ok, udebs need their own contents files, udeb, and udeb-nf (not-free)
316             # This is HORRIBLY debian specific :-/
317             for section, fn_pattern in [("debian-installer","dists/%s/Contents-udeb-%s.gz"),
318                                            ("non-free/debian-installer", "dists/%s/Contents-udeb-nf-%s.gz")]:
319
320                 for arch_id in arch_list:
321                     section_id = DBConn().get_section_id(section) # all udebs should be here)
322                     if section_id != -1:
323                         cursor.execute("EXECUTE udeb_contents_q(%d,%d,%d,%d,%d)" % (arch_id[0], arch_all_id, section_id, suite_id, udebtype_id))
324
325                         self._write_content_file(cursor, fn_pattern % (suite, arch_id[1]))
326
327
328 ################################################################################
329
330     def _suites(self):
331         """
332         return a list of suites to operate on
333         """
334         if Config().has_key( "%s::%s" %(options_prefix,"Suite")):
335             suites = utils.split_args(Config()[ "%s::%s" %(options_prefix,"Suite")])
336         else:
337             suites = Config().SubTree("Suite").List()
338
339         return suites
340
341     def _arches(self, cursor, suite):
342         """
343         return a list of archs to operate on
344         """
345         arch_list = [ ]
346         if Config().has_key( "%s::%s" %(options_prefix,"Arch")):
347             archs = utils.split_args(Config()[ "%s::%s" %(options_prefix,"Arch")])
348             for arch_name in archs:
349                 arch_list.append((DBConn().get_architecture_id(arch_name), arch_name))
350         else:
351             cursor.execute("EXECUTE arches_q(%d)" % (suite))
352             while True:
353                 r = cursor.fetchone()
354                 if not r:
355                     break
356
357                 if r[1] != "source" and r[1] != "all":
358                     arch_list.append((r[0], r[1]))
359
360         return arch_list
361
362 ################################################################################
363
364 def main():
365     cnf = Config()
366
367     arguments = [('h',"help", "%s::%s" % (options_prefix,"Help")),
368                  ('s',"suite", "%s::%s" % (options_prefix,"Suite"),"HasArg"),
369                  ('q',"quiet", "%s::%s" % (options_prefix,"Quiet")),
370                  ('v',"verbose", "%s::%s" % (options_prefix,"Verbose")),
371                  ('a',"arch", "%s::%s" % (options_prefix,"Arch"),"HasArg"),
372                 ]
373
374     commands = {'generate' : Contents.generate,
375                 'bootstrap' : Contents.bootstrap,
376                 'cruft' : Contents.cruft,
377                 }
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     args = apt_pkg.ParseCommandLine(cnf.Cnf, arguments,sys.argv)
392
393     if (len(args) < 1) or not commands.has_key(args[0]):
394         usage()
395
396     if cnf.has_key("%s::%s" % (options_prefix,"Help")):
397         usage()
398
399     commands[args[0]](Contents())
400
401 if __name__ == '__main__':
402     main()