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