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