]> git.decadent.org.uk Git - dak.git/blob - dak/contents.py
don't import tempfile in contents.py
[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 = $4
127                   AND b.type = 'deb'
128                   AND o.type = '7'
129               GROUP BY fn
130               ORDER BY fn"""
131
132 # find me all of the contents for a given .udeb
133 udeb_contents_q = """PREPARE udeb_contents_q(int,int,int) as
134               SELECT (p.path||'/'||n.file) as fn,
135                       comma_separated_list(s.section||'/'||b.package)
136               FROM content_associations c
137               JOIN content_file_paths p ON (c.filepath=p.id)
138               JOIN content_file_names n ON (c.filename=n.id)
139               JOIN binaries b ON (b.id=c.binary_pkg)
140               JOIN bin_associations ba ON (b.id=ba.bin)
141               JOIN override o ON (o.package=b.package)
142               JOIN section s ON (s.id=o.section)
143               WHERE s.id = $1
144                   AND ba.suite = $2
145                   AND o.suite = $3
146                   AND b.type = 'udeb'
147                   AND o.type = '8'
148               GROUP BY fn
149               ORDER BY fn"""
150
151 # clear out all of the temporarily stored content associations
152 # this should be run only after p-a has run.  after a p-a
153 # run we should have either accepted or rejected every package
154 # so there should no longer be anything in the queue
155 remove_pending_contents_cruft_q = """DELETE FROM pending_content_associations"""
156
157 # delete any filenames we are storing which have no binary associated with them
158 remove_filename_cruft_q = """DELETE FROM content_file_names
159                              WHERE id IN (SELECT cfn.id FROM content_file_names cfn
160                                           LEFT JOIN content_associations ca
161                                             ON ca.filename=cfn.id
162                                           WHERE ca.id IS NULL)"""
163
164 # delete any paths we are storing which have no binary associated with them
165 remove_filepath_cruft_q = """DELETE FROM content_file_paths
166                              WHERE id IN (SELECT cfn.id FROM content_file_paths cfn
167                                           LEFT JOIN content_associations ca
168                                              ON ca.filepath=cfn.id
169                                           WHERE ca.id IS NULL)"""
170 class Contents(object):
171     """
172     Class capable of generating Contents-$arch.gz files
173
174     Usage GenerateContents().generateContents( ["main","contrib","non-free"] )
175     """
176
177     def __init__(self):
178         self.header = None
179
180     def reject(self, message):
181         log.error("E: %s" % message)
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                     h.close()
197                 except:
198                     log.error( "error opening header file: %d\n%s" % (Config()["Contents::Header"],
199                                                                       traceback.format_exc() ))
200                     self.header = False
201             else:
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         filepath = Config()["Contents::Root"] + filename
215         filedir = os.path.dirname(filepath)
216         if not os.path.isdir(filedir):
217             os.makedirs(filedir)
218         f = gzip.open(filepath, "w")
219         try:
220             header = self._getHeader()
221
222             if header:
223                 f.write(header)
224
225             while True:
226                 contents = cursor.fetchone()
227                 if not contents:
228                     return
229
230                 num_tabs = max(1,
231                                int(math.ceil((self._goal_column - len(contents[0])-1) / 8)))
232                 f.write(contents[0] + ( '\t' * num_tabs ) + contents[-1] + "\n")
233
234         finally:
235             f.close()
236
237     def cruft(self):
238         """
239         remove files/paths from the DB which are no longer referenced
240         by binaries and clean the temporary table
241         """
242         cursor = DBConn().cursor();
243         cursor.execute( "BEGIN WORK" )
244         cursor.execute( remove_pending_contents_cruft_q )
245         cursor.execute( remove_filename_cruft_q )
246         cursor.execute( remove_filepath_cruft_q )
247         cursor.execute( "COMMIT" )
248
249
250     def bootstrap(self):
251         """
252         scan the existing debs in the pool to populate the contents database tables
253         """
254         pooldir = Config()[ 'Dir::Pool' ]
255
256         cursor = DBConn().cursor();
257         cursor.execute( debs_q )
258         cursor.execute( olddeb_q )
259         cursor.execute( arches_q )
260
261         suites = self._suites()
262         for suite in [i.lower() for i in suites]:
263             suite_id = DBConn().get_suite_id(suite)
264
265             arch_list = self._arches(cursor, suite_id)
266             arch_all_id = DBConn().get_architecture_id("all")
267             for arch_id in arch_list:
268                 cursor.execute( "EXECUTE debs_q(%d, %d)" % ( suite_id, arch_id[0] ) )
269
270                 count = 0
271                 while True:
272                     deb = cursor.fetchone()
273                     if not deb:
274                         break
275                     count += 1
276                     cursor1 = DBConn().cursor();
277                     cursor1.execute( "EXECUTE olddeb_q(%d)" % (deb[0] ) )
278                     old = cursor1.fetchone()
279                     if old:
280                         log.debug( "already imported: %s" % deb[1] )
281                     else:
282                         debfile = os.path.join( pooldir, deb[1] )
283                         if os.path.exists( debfile ):
284                             Binary(debfile, self.reject).scan_package( deb[0] )
285                         else:
286                             log.error( "missing .deb: %s" % deb[1] )
287
288     def generate(self):
289         """
290         Generate Contents-$arch.gz files for every available arch in each given suite.
291         """
292         cursor = DBConn().cursor();
293
294         cursor.execute( arches_q )
295         cursor.execute( contents_q )
296         cursor.execute( udeb_contents_q )
297
298         suites = self._suites()
299
300         # Get our suites, and the architectures
301         for suite in [i.lower() for i in suites]:
302             suite_id = DBConn().get_suite_id(suite)
303             arch_list = self._arches(cursor, suite_id)
304
305             arch_all_id = DBConn().get_architecture_id("all")
306
307             for arch_id in arch_list:
308                 cursor.execute( "EXECUTE contents_q(%d,%d,%d,%d)" % (arch_id[0], arch_all_id, suite_id, suite_id))
309                 self._write_content_file(cursor, "dists/%s/Contents-%s.gz" % (suite, arch_id[1]))
310
311             # The MORE fun part. Ok, udebs need their own contents files, udeb, and udeb-nf (not-free)
312             # This is HORRIBLY debian specific :-/
313             for section_id, fn_pattern in [("debian-installer","dists/%s/Contents-udeb.gz"),
314                                            ("non-free/debian-installer", "dists/%s/Contents-udeb-nf.gz")]:
315
316                 section_id = DBConn().get_section_id(section_id) # all udebs should be here)
317                 if section_id != -1:
318                     cursor.execute("EXECUTE udeb_contents_q(%d,%d,%d)" % (section_id, suite_id, suite_id))
319                     self._write_content_file(cursor, fn_pattern % suite)
320
321
322 ################################################################################
323
324     def _suites(self):
325         """
326         return a list of suites to operate on
327         """
328         if Config().has_key( "%s::%s" %(options_prefix,"Suite")):
329             suites = utils.split_args(Config()[ "%s::%s" %(options_prefix,"Suite")])
330         else:
331             suites = Config().SubTree("Suite").List()
332
333         return suites
334
335     def _arches(self, cursor, suite):
336         """
337         return a list of archs to operate on
338         """
339         arch_list = [ ]
340         if Config().has_key( "%s::%s" %(options_prefix,"Arch")):
341             archs = utils.split_args(Config()[ "%s::%s" %(options_prefix,"Arch")])
342             for arch_name in archs:
343                 arch_list.append((DBConn().get_architecture_id(arch_name), arch_name))
344         else:
345             cursor.execute("EXECUTE arches_q(%d)" % (suite))
346             while True:
347                 r = cursor.fetchone()
348                 if not r:
349                     break
350
351                 if r[1] != "source" and r[1] != "all":
352                     arch_list.append((r[0], r[1]))
353
354         return arch_list
355
356 ################################################################################
357
358 def main():
359     cnf = Config()
360
361     arguments = [('h',"help", "%s::%s" % (options_prefix,"Help")),
362                  ('s',"suite", "%s::%s" % (options_prefix,"Suite"),"HasArg"),
363                  ('q',"quiet", "%s::%s" % (options_prefix,"Quiet")),
364                  ('v',"verbose", "%s::%s" % (options_prefix,"Verbose")),
365                  ('a',"arch", "%s::%s" % (options_prefix,"Arch"),"HasArg"),
366                 ]
367
368     commands = {'generate' : Contents.generate,
369                 'bootstrap' : Contents.bootstrap,
370                 'cruft' : Contents.cruft,
371                 }
372
373     level=logging.INFO
374     if cnf.has_key("%s::%s" % (options_prefix,"Quiet")):
375         level=logging.ERROR
376
377     elif cnf.has_key("%s::%s" % (options_prefix,"Verbose")):
378         level=logging.DEBUG
379
380
381     logging.basicConfig( level=level,
382                          format='%(asctime)s %(levelname)s %(message)s',
383                          stream = sys.stderr )
384
385     args = apt_pkg.ParseCommandLine(cnf.Cnf, arguments,sys.argv)
386
387     if (len(args) < 1) or not commands.has_key(args[0]):
388         usage()
389
390     if cnf.has_key("%s::%s" % (options_prefix,"Help")):
391         usage()
392
393     commands[args[0]](Contents())
394
395 if __name__ == '__main__':
396     main()