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