]> git.decadent.org.uk Git - dak.git/blob - dak/contents.py
Revert "for the time we test, modify the path to a non-world-visible one"
[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                     h.close()
195                 except:
196                     log.error( "error opening header file: %d\n%s" % (Config()["Contents::Header"],
197                                                                       traceback.format_exc() ))
198                     self.header = False
199             else:
200                 self.header = False
201
202         return self.header
203
204     # goal column for section column
205     _goal_column = 54
206
207     def _write_content_file(self, cursor, filename):
208         """
209         Internal method for writing all the results to a given file.
210         The cursor should have a result set generated from a query already.
211         """
212         filepath = Config()["Contents::Root"] + filename
213         filedir = os.path.dirname(filepath)
214         if not os.path.isdir(filedir):
215             os.makedirs(filedir)
216         f = gzip.open(filepath, "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_pending_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( debs_q )
256         cursor.execute( olddeb_q )
257         cursor.execute( arches_q )
258
259         suites = self._suites()
260         for suite in [i.lower() for i in suites]:
261             suite_id = DBConn().get_suite_id(suite)
262
263             arch_list = self._arches(cursor, suite_id)
264             arch_all_id = DBConn().get_architecture_id("all")
265             for arch_id in arch_list:
266                 cursor.execute( "EXECUTE debs_q(%d, %d)" % ( suite_id, arch_id[0] ) )
267
268                 count = 0
269                 while True:
270                     deb = cursor.fetchone()
271                     if not deb:
272                         break
273                     count += 1
274                     cursor1 = DBConn().cursor();
275                     cursor1.execute( "EXECUTE olddeb_q(%d)" % (deb[0] ) )
276                     old = cursor1.fetchone()
277                     if old:
278                         log.debug( "already imported: %s" % deb[1] )
279                     else:
280                         debfile = os.path.join( pooldir, deb[1] )
281                         if os.path.exists( debfile ):
282                             Binary(debfile).scan_package( deb[0] )
283                         else:
284                             log.error( "missing .deb: %s" % deb[1] )
285
286     def generate(self):
287         """
288         Generate Contents-$arch.gz files for every available 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             for section_id, fn_pattern in [("debian-installer","dists/%s/Contents-udeb.gz"),
312                                            ("non-free/debian-installer", "dists/%s/Contents-udeb-nf.gz")]:
313
314                 section_id = DBConn().get_section_id(section_id) # all udebs should be here)
315                 if section_id != -1:
316                     cursor.execute("EXECUTE udeb_contents_q(%d,%d,%d)" % (section_id, suite_id, suite_id))
317                     self._write_content_file(cursor, fn_pattern % suite)
318
319
320 ################################################################################
321
322     def _suites(self):
323         """
324         return a list of suites to operate on
325         """
326         if Config().has_key( "%s::%s" %(options_prefix,"Suite")):
327             suites = utils.split_args(Config()[ "%s::%s" %(options_prefix,"Suite")])
328         else:
329             suites = Config().SubTree("Suite").List()
330
331         return suites
332
333     def _arches(self, cursor, suite):
334         """
335         return a list of archs to operate on
336         """
337         arch_list = [ ]
338         if Config().has_key( "%s::%s" %(options_prefix,"Arch")):
339             archs = utils.split_args(Config()[ "%s::%s" %(options_prefix,"Arch")])
340             for arch_name in archs:
341                 arch_list.append((DBConn().get_architecture_id(arch_name), arch_name))
342         else:
343             cursor.execute("EXECUTE arches_q(%d)" % (suite))
344             while True:
345                 r = cursor.fetchone()
346                 if not r:
347                     break
348
349                 if r[1] != "source" and r[1] != "all":
350                     arch_list.append((r[0], r[1]))
351
352         return arch_list
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     level=logging.INFO
372     if cnf.has_key("%s::%s" % (options_prefix,"Quiet")):
373         level=logging.ERROR
374
375     elif cnf.has_key("%s::%s" % (options_prefix,"Verbose")):
376         level=logging.DEBUG
377
378
379     logging.basicConfig( level=level,
380                          format='%(asctime)s %(levelname)s %(message)s',
381                          stream = sys.stderr )
382
383     args = apt_pkg.ParseCommandLine(cnf.Cnf, arguments,sys.argv)
384
385     if (len(args) < 1) or not commands.has_key(args[0]):
386         usage()
387
388     if cnf.has_key("%s::%s" % (options_prefix,"Help")):
389         usage()
390
391     commands[args[0]](Contents())
392
393 if __name__ == '__main__':
394     main()