]> git.donarmstrong.com Git - neurodebian.git/blob - neurodebian/dde.py
Big rewrite using DDE and jinja insead of homebrew.
[neurodebian.git] / neurodebian / dde.py
1 #!/usr/bin/env python
2 """Tell me who you are!
3 """
4
5 import pysvn
6 import json
7 from debian_bundle import deb822
8 from ConfigParser import SafeConfigParser
9 from optparse import OptionParser, Option, OptionGroup, OptionConflictError
10 import sys
11 import os
12 import shutil
13 import urllib2
14 import urllib
15 import subprocess
16 # templating
17 from jinja2 import Environment, PackageLoader
18
19 from pprint import PrettyPrinter
20
21
22 class AptListsCache(object):
23     def __init__(self, cachedir='build/cache',
24                  ro_cachedirs=None,
25                  init_db=None):
26         self.cachedir = cachedir
27
28         if not ro_cachedirs is None:
29             self.ro_cachedirs = ro_cachedirs
30         else:
31             self.ro_cachedirs = []
32
33         # create cachedir
34         create_dir(self.cachedir)
35
36     def get(self, url, update=False):
37         """Looks in the cache if the file is there and takes the cached one.
38         Otherwise it is downloaded first.
39
40         Knows how to deal with http:// and svn:// URLs.
41
42         :Return:
43           file handler
44         """
45         # look whether it is compressed
46         cext = url.split('.')[-1]
47         if cext in ['gz', 'bz2']:
48             target_url = url[:-1 * len(cext) -1]
49         else:
50             # assume not compressed
51             target_url = url
52             cext = None
53
54         # turn url into a filename -- mimik what APT does for
55         # /var/lib/apt/lists/
56         tfilename = '_'.join(target_url.split('/')[2:])
57
58         # if we need to download anyway do not search
59         if update:
60             cfilename = os.path.join(self.cachedir, tfilename)
61         else:
62             # look for the uncompressed file anywhere in the cache
63             cfilename = None
64             for cp in [self.cachedir] + self.ro_cachedirs:
65                 if os.path.exists(os.path.join(cp, tfilename)):
66                     cfilename = os.path.join(cp, tfilename)
67
68         # nothing found?
69         if cfilename is None:
70             # add cache item
71             cfilename = os.path.join(self.cachedir, tfilename)
72             update = True
73
74         # if updated needed -- download
75         if update:
76             #print 'Caching file from %s' % url
77
78             if url.startswith('svn://'):
79                 # export from SVN
80                 pysvn.Client().export(url, cfilename)
81             if url.startswith('http://'):
82                 # download
83                 tempfile, ignored = urllib.urlretrieve(url)
84
85                 # decompress
86                 decompressor = None
87                 if cext == 'gz':
88                     decompressor = 'gzip'
89                 elif cext == 'bz2':
90                     decompressor = 'bzip2'
91                 elif cext == None:
92                     decompressor = None
93                 else:
94                     raise ValueError, \
95                           "Don't know how to decompress %s files" \
96                           % cext
97
98                 if not decompressor is None:
99                     if subprocess.call([decompressor, '-d', '-q', '-f',
100                                        tempfile]) == 1:
101                         raise RuntimeError, \
102                               "Something went wrong while decompressing '%s'" \
103                               % tempfile
104
105                 # move decompressed file into cache
106                 shutil.move(os.path.splitext(tempfile)[0], cfilename)
107
108                 # XXX do we need that if explicit filename is provided?
109                 urllib.urlcleanup()
110
111         # open cached file
112         fh = open(cfilename, 'r')
113
114         return fh
115
116
117 def add_pkgfromtaskfile(db, urls):
118     cache = AptListsCache()
119     pkgs = []
120
121     for task in urls:
122         fh = cache.get(task)
123
124         # loop over all stanzas
125         for stanza in deb822.Packages.iter_paragraphs(fh):
126             if stanza.has_key('Depends'):
127                 pkg = stanza['Depends']
128             elif stanza.has_key('Suggests'):
129                 pkg = stanza['Suggests']
130             else:
131                 continue
132
133             # account for multiple packages per line
134             if pkg.count(','):
135                 pkgs += [p.strip() for p in pkg.split(',')]
136             else:
137                 pkgs.append(pkg.strip())
138
139     for p in pkgs:
140         if not db.has_key(p):
141             db[p] = get_emptydbentry()
142
143     return db
144
145 def get_emptydbentry():
146     return {'main': {}}
147
148 def import_blendstask(db, url):
149     cache = AptListsCache()
150     fh = cache.get(url)
151     task_name = None
152
153     # figure out blend's task page URL, since they differ from blend to blend
154     urlsec = url.split('/')
155     blendname = urlsec[-3]
156     if blendname == 'debian-med':
157         taskpage_url = 'http://debian-med.alioth.debian.org/tasks/'
158     elif blendname == 'debian-science':
159         taskpage_url = 'http://blends.alioth.debian.org/science/tasks/' 
160     else:
161         raise ValueError('Unknown blend "%s"' % blendname)
162     taskpage_url += urlsec[-1]
163
164     for st in deb822.Packages.iter_paragraphs(fh):
165         if st.has_key('Task'):
166             task_name = st['Task']
167             task = (blendname, task_name, taskpage_url)
168
169         # do not stop unless we have a description
170         if not st.has_key('Pkg-Description'):
171             continue
172
173         if st.has_key('Depends'):
174             pkg = st['Depends']
175         elif st.has_key('Suggests'):
176             pkg = st['Suggests']
177         else:
178             print 'Warning: Cannot determine name of prospective package ' \
179                     '... ignoring.'
180             continue
181
182         if not db.has_key(pkg):
183             print 'Ignoring blend package "%s"' % pkg
184             continue
185
186         info = {}
187
188         # blends info
189         info['tasks'] = [task]
190         if st.has_key('License'):
191             info['license'] = st['License']
192         if st.has_key('Responsible'):
193             info['responsible'] = st['Responsible']
194
195         # pkg description
196         descr = st['Pkg-Description'].replace('%', '%%').split('\n')
197         info['description'] = descr[0].strip()
198         info['long_description'] = ' '.join([l.strip() for l in descr[1:]])
199
200         # charge the basic property set
201         db[pkg]['main']['description'] = info['description']
202         db[pkg]['main']['long_description'] = info['long_description']
203         if st.has_key('WNPP'):
204             db[pkg]['main']['debian_itp'] = st['WNPP']
205         if st.has_key('Pkg-URL'):
206             db[pkg]['main']['other_pkg'] = st['Pkg-URL']
207         if st.has_key('Homepage'):
208             db[pkg]['main']['homepage'] = st['Homepage']
209
210         # only store if there isn't something already
211         if not db[pkg].has_key('blends'):
212             db[pkg]['blends'] = info
213         else:
214             # just add this tasks name and id
215             db[pkg]['blends']['tasks'].append(task)
216
217     return db
218
219
220 def get_releaseinfo(rurl):
221     cache = AptListsCache()
222     # root URL of the repository
223     baseurl = '/'.join(rurl.split('/')[:-1])
224     # get the release file from the cache
225     release_file = cache.get(rurl)
226
227     # create parser instance
228     rp = deb822.Release(release_file)
229
230     # architectures on this dist
231     archs = rp['Architectures'].split()
232     components = rp['Components'].split()
233     # compile a new codename that also considers the repository label
234     # to distinguish between official and unofficial repos.
235     label = rp['Label']
236     origin = rp['Origin']
237     codename = rp['Codename']
238     labelcode = '_'.join([rp['Label'], rp['Codename']])
239
240     # cleanup
241     release_file.close()
242
243     return {'baseurl': baseurl, 'archs': archs, 'components': components,
244             'codename': codename, 'label': label, 'labelcode': labelcode,
245             'origin': origin}
246
247
248 def build_pkgsurl(baseurl, component, arch):
249     return '/'.join([baseurl, component, 'binary-' + arch, 'Packages.bz2'])
250
251
252 def import_release(cfg, db, rurl):
253     cache = AptListsCache()
254
255     ri = get_releaseinfo(rurl)
256
257     # compile the list of Packages files to parse and parse them
258     for c in ri['components']:
259         for a in ri['archs']:
260             # compile packages URL
261             pkgsurl = build_pkgsurl(ri['baseurl'], c, a)
262
263             # retrieve from cache
264             packages_file = cache.get(pkgsurl)
265
266             # parse
267             for stanza in deb822.Packages.iter_paragraphs(packages_file):
268                 db = _store_pkg(cfg, db, stanza, ri['origin'], ri['codename'], c, ri['baseurl'])
269
270             # cleanup
271             packages_file.close()
272
273     return db
274
275 def _store_pkg(cfg, db, st, origin, codename, component, baseurl):
276     """
277     :Parameter:
278       st: Package section
279     """
280     pkg = st['Package']
281
282     # only care for known packages
283     if not db.has_key(pkg):
284 #        print 'Ignoring NeuroDebian package "%s"' % pkg
285         return db
286
287     distkey = (trans_codename(codename, cfg), 'neurodebian-' + codename)
288
289     if db[pkg].has_key(distkey):
290         info = db[pkg][distkey]
291     else:
292         info = {'architecture': []}
293
294     # fill in data
295     if not st['Architecture'] in info['architecture']:
296         info['architecture'].append(st['Architecture'])
297     info['maintainer'] = st['Maintainer']
298     if st.has_key('Homepage'):
299         info['homepage'] = st['Homepage']
300     info['version'] = st['Version']
301
302     # origin
303     info['drc'] = '%s %s %s' % (origin, codename, component)
304
305     # pool url
306     info['poolurl'] = '/'.join([os.path.dirname(st['Filename'])])
307
308     # pkg description
309     descr = st['Description'].replace('%', '%%').split('\n')
310     info['description'] = descr[0].strip()
311     info['long_description'] = ' '.join([l.strip() for l in descr[1:]])
312
313     db[pkg][distkey] = info
314
315     # charge the basic property set
316     db[pkg]['main']['description'] = info['description']
317     db[pkg]['main']['long_description'] = info['long_description']
318     if st.has_key('Homepage'):
319         db[pkg]['main']['homepage'] = st['Homepage']
320
321     return db
322
323
324 def trans_codename(codename, cfg):
325     """Translate a known codename into a release description.
326
327     Unknown codenames will simply be returned as is.
328     """
329     # if we know something, tell
330     if codename in cfg.options('release codenames'):
331         return cfg.get('release codenames', codename)
332     else:
333         return codename
334
335
336 def create_dir(path):
337     if os.path.exists(path):
338         return
339
340     ps = path.split(os.path.sep)
341
342     for i in range(1,len(ps) + 1):
343         p = os.path.sep.join(ps[:i])
344
345         if not os.path.exists(p):
346             os.mkdir(p)
347
348
349 def dde_get(url):
350     try:
351         return json.read(urllib2.urlopen(url+"?t=json").read())['r']
352     except urllib2.HTTPError:
353         return False
354
355
356 def import_dde(cfg, db):
357     dists = cfg.get('dde', 'dists').split()
358     query_url = cfg.get('dde', 'pkgquery_url')
359     for p in db.keys():
360         # get freshest
361         q = dde_get(query_url + "/all/%s" % p)
362         if q:
363             db[p]['main'] = q
364         for d in dists:
365             q = dde_get(query_url + "/prio-%s/%s" % (d, p))
366             if q:
367                 db[p][(trans_codename(d.split('-')[1], cfg),d)] = q
368
369     return db
370
371
372 def generate_pkgpage(pkg, cfg, db, template, addenum_dir):
373     # local binding for ease of use
374     db = db[pkg]
375     # do nothing if there is not at least the very basic stuff
376     if not db['main'].has_key('description'):
377         return
378     title = '**%s** -- %s' % (pkg, db['main']['description'])
379     underline = '*' * (len(title) + 2)
380     title = '%s\n %s\n%s' % (underline, title, underline)
381
382     # preprocess long description
383     ld = db['main']['long_description']
384     ld = ' '.join([l.lstrip(' .') for l in ld.split('\n')])
385
386     page = template.render(pkg=pkg,
387                            title=title,
388                            long_description=ld,
389                            cfg=cfg,
390                            db=db)
391     # the following can be replaced by something like
392     # {% include "sidebar.html" ignore missing %}
393     # in the template whenever jinja 2.2 becomes available
394     addenum = os.path.join(os.path.abspath(addenum_dir), '%s.rst' % pkg)
395     if os.path.exists(addenum):
396         page += '\n\n.. include:: %s\n' % addenum
397     return page
398
399
400 def store_db(db, filename):
401     pp = PrettyPrinter(indent=2)
402     f = open(filename, 'w')
403     f.write(pp.pformat(db))
404     f.close()
405
406
407 def read_db(filename):
408     f = open(filename)
409     db = eval(f.read())
410     return db
411
412 def write_sourceslist(jinja_env, cfg, outdir):
413     create_dir(outdir)
414     create_dir(os.path.join(outdir, '_static'))
415
416     repos = {}
417     for release in cfg.options('release codenames'):
418         transrel = trans_codename(release, cfg)
419         repos[transrel] = []
420         for mirror in cfg.options('mirrors'):
421             listname = 'neurodebian.%s.%s.sources.list' % (release, mirror)
422             repos[transrel].append((mirror, listname))
423             lf = open(os.path.join(outdir, '_static', listname), 'w')
424             aptcfg = '%s %s main contrib non-free\n' % (cfg.get('mirrors', mirror),
425                                                       release)
426             lf.write('deb %s' % aptcfg)
427             lf.write('deb-src %s' % aptcfg)
428             lf.close()
429
430     srclist_template = jinja_env.get_template('sources_lists.rst')
431     sl = open(os.path.join(outdir, 'sources_lists'), 'w')
432     sl.write(srclist_template.render(repos=repos))
433     sl.close()
434
435
436 def write_pkgpages(jinja_env, cfg, db, outdir, addenum_dir):
437     create_dir(outdir)
438     create_dir(os.path.join(outdir, 'pkgs'))
439
440     # generate the TOC with all packages
441     toc_template = jinja_env.get_template('pkgs_toc.rst')
442     toc = open(os.path.join(outdir, 'pkgs.rst'), 'w')
443     toc.write(toc_template.render(pkgs=db.keys()))
444     toc.close()
445
446     # and now each individual package page
447     pkg_template = jinja_env.get_template('pkg.rst')
448     for p in db.keys():
449         page = generate_pkgpage(p, cfg, db, pkg_template, addenum_dir)
450         # when no page is available skip this package
451         if page is None:
452             continue
453         pf = open(os.path.join(outdir, 'pkgs', p + '.rst'), 'w')
454         pf.write(generate_pkgpage(p, cfg, db, pkg_template, addenum_dir))
455         pf.close()
456
457
458 def prepOptParser(op):
459     # use module docstring for help output
460     op.usage = "%s [OPTIONS]\n\n" % sys.argv[0] + __doc__
461
462     op.add_option("--db",
463                   action="store", type="string", dest="db",
464                   default=None,
465                   help="Database file to read. Default: None")
466
467     op.add_option("--cfg",
468                   action="store", type="string", dest="cfg",
469                   default=None,
470                   help="Repository config file.")
471
472     op.add_option("-o", "--outdir",
473                   action="store", type="string", dest="outdir",
474                   default=None,
475                   help="Target directory for ReST output. Default: None")
476
477     op.add_option("-r", "--release-url",
478                   action="append", dest="release_urls",
479                   help="None")
480
481     op.add_option("--pkgaddenum", action="store", dest="addenum_dir",
482                   type="string", default=None, help="None")
483
484
485 def main():
486     op = OptionParser(version="%prog 0.0.2")
487     prepOptParser(op)
488
489     (opts, args) = op.parse_args()
490
491     if len(args) != 1:
492         print('There needs to be exactly one command')
493         sys.exit(1)
494
495     cmd = args[0]
496
497     if opts.cfg is None:
498         print("'--cfg' option is mandatory.")
499         sys.exit(1)
500     if opts.db is None:
501         print("'--db' option is mandatory.")
502         sys.exit(1)
503
504
505     cfg = SafeConfigParser()
506     cfg.read(opts.cfg)
507
508     # load existing db, unless renew is requested
509     if cmd == 'updatedb':
510         db = {}
511         if cfg.has_option('packages', 'select taskfiles'):
512             db = add_pkgfromtaskfile(db, cfg.get('packages',
513                                                  'select taskfiles').split())
514
515         # add additional package names from config file
516         if cfg.has_option('packages', 'select names'):
517             for p in cfg.get('packages', 'select names').split():
518                 if not db.has_key(p):
519                     db[p] = get_emptydbentry()
520
521         # get info from task files
522         if cfg.has_option('packages', 'prospective'):
523             for url in cfg.get('packages', 'prospective').split():
524                 db = import_blendstask(db, url)
525
526         # parse NeuroDebian repository
527         if cfg.has_option('neurodebian', 'releases'):
528             for rurl in cfg.get('neurodebian', 'releases').split():
529                 db = import_release(cfg, db, rurl)
530
531         # collect package information from DDE
532         db = import_dde(cfg, db)
533         # store the new DB
534         store_db(db, opts.db)
535         # and be done
536         return
537
538     # load the db from file
539     db = read_db(opts.db)
540
541     # fire up jinja
542     jinja_env = Environment(loader=PackageLoader('neurodebian', 'templates'))
543
544     # generate package pages and TOC and write them to files
545     write_pkgpages(jinja_env, cfg, db, opts.outdir, opts.addenum_dir)
546
547     write_sourceslist(jinja_env, cfg, opts.outdir)
548
549 if __name__ == "__main__":
550     main()