]> git.decadent.org.uk Git - dak.git/blob - dak/contents.py
get bootstrap working again
[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                 debs = cursor.fetchall()
267                 count = 0
268                 for deb in debs:
269                     count += 1
270                     cursor.execute( "EXECUTE olddeb_q(%d)" % (deb[0] ) )
271                     old = cursor.fetchone()
272                     if old:
273                         log.debug( "already imported: %s" % deb[1] )
274                     else:
275                         debfile = os.path.join( pooldir, deb[1] )
276                         if os.path.exists( debfile ):
277                             Binary(f).scan_package( deb[0] )
278                         else:
279                             log.error( "missing .deb: %s" % deb[1] )
280
281     def generate(self):
282         """
283         Generate Contents-$arch.gz files for every aviailable arch in each given suite.
284         """
285         cursor = DBConn().cursor();
286
287         cursor.execute( arches_q )
288         cursor.execute( contents_q )
289         cursor.execute( udeb_contents_q )
290
291         suites = self._suites()
292
293         # Get our suites, and the architectures
294         for suite in [i.lower() for i in suites]:
295             suite_id = DBConn().get_suite_id(suite)
296             arch_list = self._arches(cursor, suite_id)
297
298             arch_all_id = DBConn().get_architecture_id("all")
299
300             for arch_id in arch_list:
301                 cursor.execute( "EXECUTE contents_q(%d,%d,%d,%d)" % (arch_id[0], arch_all_id, suite_id, suite_id))
302                 self._write_content_file(cursor, "dists/%s/Contents-%s.gz" % (suite, arch_id[1]))
303
304             # The MORE fun part. Ok, udebs need their own contents files, udeb, and udeb-nf (not-free)
305             # This is HORRIBLY debian specific :-/
306             for section_id, fn_pattern in [("debian-installer","dists/%s/Contents-udeb.gz"),
307                                            ("non-free/debian-installer", "dists/%s/Contents-udeb-nf.gz")]:
308
309                 section_id = DBConn().get_section_id(section_id) # all udebs should be here)
310                 if section_id != -1:
311                     cursor.execute("EXECUTE udeb_contents_q(%d,%d,%d)" % (section_id, suite_id, suite_id))
312                     self._write_content_file(cursor, fn_pattern % suite)
313
314
315 ################################################################################
316
317     def _suites(self):
318         """
319         return a list of suites to operate on
320         """
321         if Config().has_key( "%s::%s" %(options_prefix,"Suite")):
322             suites = utils.split_args(Config()[ "%s::%s" %(options_prefix,"Suite")])
323         else:
324             suites = Config().SubTree("Suite").List()
325
326         return suites
327
328     def _arches(self, cursor, suite):
329         """
330         return a list of archs to operate on
331         """
332         arch_list = [ ]
333         if Config().has_key( "%s::%s" %(options_prefix,"Arch")):
334             archs = utils.split_args(Config()[ "%s::%s" %(options_prefix,"Arch")])
335             for arch_name in archs:
336                 arch_list.append((DBConn().get_architecture_id(arch_name), arch_name))
337         else:
338             cursor.execute("EXECUTE arches_q(%d)" % (suite))
339             while True:
340                 r = cursor.fetchone()
341                 if not r:
342                     break
343
344                 if r[1] != "source" and r[1] != "all":
345                     arch_list.append((r[0], r[1]))
346
347         return arch_list
348
349 ################################################################################
350
351 def main():
352     cnf = Config()
353
354     arguments = [('h',"help", "%s::%s" % (options_prefix,"Help")),
355                  ('s',"suite", "%s::%s" % (options_prefix,"Suite"),"HasArg"),
356                  ('q',"quiet", "%s::%s" % (options_prefix,"Quiet")),
357                  ('v',"verbose", "%s::%s" % (options_prefix,"Verbose")),
358                  ('a',"arch", "%s::%s" % (options_prefix,"Arch"),"HasArg"),
359                 ]
360
361     commands = {'generate' : Contents.generate,
362                 'bootstrap' : Contents.bootstrap,
363                 'cruft' : Contents.cruft,
364                 }
365
366     level=logging.INFO
367     if cnf.has_key("%s::%s" % (options_prefix,"Quiet")):
368         level=logging.ERROR
369
370     elif cnf.has_key("%s::%s" % (options_prefix,"Verbose")):
371         level=logging.DEBUG
372
373
374     logging.basicConfig( level=level,
375                          format='%(asctime)s %(levelname)s %(message)s',
376                          stream = sys.stderr )
377
378     args = apt_pkg.ParseCommandLine(cnf.Cnf, arguments,sys.argv)
379
380     if (len(args) < 1) or not commands.has_key(args[0]):
381         usage()
382
383     if cnf.has_key("%s::%s" % (options_prefix,"Help")):
384         usage()
385
386     commands[args[0]](Contents())
387
388 if __name__ == '__main__':
389     main()