]> git.decadent.org.uk Git - dak.git/blob - daklib/contents.py
Use 'dak contents generate_helper'.
[dak.git] / daklib / contents.py
1 #!/usr/bin/env python
2 """
3 Helper code for contents generation.
4
5 @contact: Debian FTPMaster <ftpmaster@debian.org>
6 @copyright: 2011 Torsten Werner <twerner@debian.org>
7 @license: GNU General Public License version 2 or later
8 """
9
10 ################################################################################
11
12 # This program is free software; you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation; either version 2 of the License, or
15 # (at your option) any later version.
16
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 # GNU General Public License for more details.
21
22 # You should have received a copy of the GNU General Public License
23 # along with this program; if not, write to the Free Software
24 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
25
26 ################################################################################
27
28 from daklib.dbconn import *
29 from daklib.config import Config
30 from daklib.threadpool import ThreadPool
31 from multiprocessing import Pool
32
33 from sqlalchemy import desc, or_
34 from subprocess import Popen, PIPE, call
35
36 import os.path
37
38 class ContentsWriter(object):
39     '''
40     ContentsWriter writes the Contents-$arch.gz files.
41     '''
42     def __init__(self, suite, architecture, overridetype, component = None):
43         '''
44         The constructor clones its arguments into a new session object to make
45         sure that the new ContentsWriter object can be executed in a different
46         thread.
47         '''
48         self.suite = suite
49         self.architecture = architecture
50         self.overridetype = overridetype
51         self.component = component
52         self.session = suite.session()
53
54     def query(self):
55         '''
56         Returns a query object that is doing most of the work.
57         '''
58         params = {
59             'suite':    self.suite.suite_id,
60             'arch_all': get_architecture('all', self.session).arch_id,
61             'arch':     self.architecture.arch_id,
62             'type_id':  self.overridetype.overridetype_id,
63             'type':     self.overridetype.overridetype,
64         }
65
66         if self.component is not None:
67             params['component'] = self.component.component_id
68             sql = '''
69 create temp table newest_binaries (
70     id integer primary key,
71     package text);
72
73 create index newest_binaries_by_package on newest_binaries (package);
74
75 insert into newest_binaries (id, package)
76     select distinct on (package) id, package from binaries
77         where type = :type and
78             (architecture = :arch_all or architecture = :arch) and
79             id in (select bin from bin_associations where suite = :suite)
80         order by package, version desc;
81
82 with
83
84 unique_override as
85     (select o.package, s.section
86         from override o, section s
87         where o.suite = :suite and o.type = :type_id and o.section = s.id and
88         o.component = :component)
89
90 select bc.file, substring(o.section from position('/' in o.section) + 1) || '/' || b.package as package
91     from newest_binaries b, bin_contents bc, unique_override o
92     where b.id = bc.binary_id and o.package = b.package
93     order by bc.file, b.package'''
94
95         else:
96             sql = '''
97 create temp table newest_binaries (
98     id integer primary key,
99     package text);
100
101 create index newest_binaries_by_package on newest_binaries (package);
102
103 insert into newest_binaries (id, package)
104     select distinct on (package) id, package from binaries
105         where type = :type and
106             (architecture = :arch_all or architecture = :arch) and
107             id in (select bin from bin_associations where suite = :suite)
108         order by package, version desc;
109
110 with
111
112 unique_override as
113     (select distinct on (o.package, s.section) o.package, s.section
114         from override o, section s
115         where o.suite = :suite and o.type = :type_id and o.section = s.id
116         order by o.package, s.section, o.modified desc)
117
118 select bc.file, substring(o.section from position('/' in o.section) + 1) || '/' || b.package as package
119     from newest_binaries b, bin_contents bc, unique_override o
120     where b.id = bc.binary_id and o.package = b.package
121     order by bc.file, b.package'''
122
123         return self.session.query("file", "package").from_statement(sql). \
124             params(params)
125
126     def formatline(self, filename, package_list):
127         '''
128         Returns a formatted string for the filename argument.
129         '''
130         package_list = ','.join(package_list)
131         return "%-55s %s\n" % (filename, package_list)
132
133     def fetch(self):
134         '''
135         Yields a new line of the Contents-$arch.gz file in filename order.
136         '''
137         last_filename = None
138         package_list = []
139         for filename, package in self.query().yield_per(100):
140             if filename != last_filename:
141                 if last_filename is not None:
142                     yield self.formatline(last_filename, package_list)
143                 last_filename = filename
144                 package_list = []
145             package_list.append(package)
146         if last_filename is not None:
147             yield self.formatline(last_filename, package_list)
148         # end transaction to return connection to pool
149         self.session.rollback()
150
151     def get_list(self):
152         '''
153         Returns a list of lines for the Contents-$arch.gz file.
154         '''
155         return [item for item in self.fetch()]
156
157     def output_filename(self):
158         '''
159         Returns the name of the output file.
160         '''
161         values = {
162             'root': Config()['Dir::Root'],
163             'suite': self.suite.suite_name,
164             'architecture': self.architecture.arch_string
165         }
166         if self.component is None:
167             return "%(root)s/dists/%(suite)s/Contents-%(architecture)s.gz" % values
168         values['component'] = self.component.component_name
169         return "%(root)s/dists/%(suite)s/%(component)s/Contents-%(architecture)s.gz" % values
170
171     def get_header(self):
172         '''
173         Returns the header for the Contents files as a string.
174         '''
175         header_file = None
176         try:
177             filename = os.path.join(Config()['Dir::Templates'], 'contents')
178             header_file = open(filename)
179             return header_file.read()
180         finally:
181             if header_file:
182                 header_file.close()
183
184     def write_file(self):
185         '''
186         Write the output file.
187         '''
188         command = ['gzip', '--rsyncable']
189         output_file = open(self.output_filename(), 'w')
190         gzip = Popen(command, stdin = PIPE, stdout = output_file)
191         gzip.stdin.write(self.get_header())
192         for item in self.fetch():
193             gzip.stdin.write(item)
194         gzip.stdin.close()
195         output_file.close()
196         gzip.wait()
197
198     @classmethod
199     def write_all(class_, suite_names = [], force = False):
200         '''
201         Writes all Contents files for suites in list suite_names which defaults
202         to all 'touchable' suites if not specified explicitely. Untouchable
203         suites will be included if the force argument is set to True.
204         '''
205         session = DBConn().session()
206         suite_query = session.query(Suite)
207         if len(suite_names) > 0:
208             suite_query = suite_query.filter(Suite.suite_name.in_(suite_names))
209         if not force:
210             suite_query = suite_query.filter_by(untouchable = False)
211         pool = Pool()
212         for suite in suite_query:
213             for architecture in suite.get_architectures(skipsrc = True, skipall = True):
214                 # handle 'deb' packages
215                 command = ['dak', 'contents', '-s', suite.suite_name, \
216                     'generate_helper', architecture.arch_string, 'deb']
217                 pool.apply_async(call, (command, ))
218                 # handle 'udeb' packages for 'main' and 'non-free'
219                 command = ['dak', 'contents', '-s', suite.suite_name, \
220                     'generate_helper', architecture.arch_string, 'udeb', 'main']
221                 pool.apply_async(call, (command, ))
222                 command = ['dak', 'contents', '-s', suite.suite_name, \
223                     'generate_helper', architecture.arch_string, 'udeb', 'non-free']
224                 pool.apply_async(call, (command, ))
225         pool.close()
226         pool.join()
227         session.close()
228
229
230 class ContentsScanner(object):
231     '''
232     ContentsScanner provides a threadsafe method scan() to scan the contents of
233     a DBBinary object.
234     '''
235     def __init__(self, binary):
236         '''
237         The argument binary is the actual DBBinary object that should be
238         scanned.
239         '''
240         self.binary_id = binary.binary_id
241
242     def scan(self, dummy_arg = None):
243         '''
244         This method does the actual scan and fills in the associated BinContents
245         property. It commits any changes to the database. The argument dummy_arg
246         is ignored but needed by our threadpool implementation.
247         '''
248         session = DBConn().session()
249         binary = session.query(DBBinary).get(self.binary_id)
250         for filename in binary.scan_contents():
251             binary.contents.append(BinContents(file = filename))
252         session.commit()
253         session.close()
254
255     @classmethod
256     def scan_all(class_, limit = None):
257         '''
258         The class method scan_all() scans all binaries using multiple threads.
259         The number of binaries to be scanned can be limited with the limit
260         argument. Returns the number of processed and remaining packages as a
261         dict.
262         '''
263         session = DBConn().session()
264         query = session.query(DBBinary).filter(DBBinary.contents == None)
265         remaining = query.count
266         if limit is not None:
267             query = query.limit(limit)
268         processed = query.count()
269         threadpool = ThreadPool()
270         for binary in query.yield_per(100):
271             threadpool.queueTask(ContentsScanner(binary).scan)
272         threadpool.joinAll()
273         remaining = remaining()
274         session.close()
275         return { 'processed': processed, 'remaining': remaining }