]> git.donarmstrong.com Git - neurodebian.git/blob - neurodebian/dde.py
Adjust to changes in DDE and build for more releases.
[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         for d in dists:
368             dist, release = d.split('-')
369             q = dde_get(query_url + "/dist/d:%s/r:%s/p:%s" % (dist, release, p))
370             if not q:
371                 continue
372             # accumulate data for multiple over archs
373             item = None
374             for i in q:
375                 if item is None:
376                     item = i
377                     # turn into a list to append others later
378                     item['architecture'] = [item['architecture']]
379                 else:
380                     comp = apt.VersionCompare(i['version'], item['version'])
381                     # found another arch for the same version
382                     if comp == 0:
383                         item['architecture'].append(i['architecture'])
384                     # found newer version, dump the old ones
385                     elif comp > 0:
386                         item = i
387                         # turn into a list to append others later
388                         item['architecture'] = [item['architecture']]
389                     # simply ignore older versions
390                     else:
391                         pass
392
393             # finally assign the new package data
394             db[p][(trans_codename(d.split('-')[1], cfg),d)] = item
395
396     return db
397
398
399 def generate_pkgpage(pkg, cfg, db, template, addenum_dir):
400     # local binding for ease of use
401     db = db[pkg]
402     # do nothing if there is not at least the very basic stuff
403     if not db['main'].has_key('description'):
404         return
405     title = '**%s** -- %s' % (pkg, db['main']['description'])
406     underline = '*' * (len(title) + 2)
407     title = '%s\n %s\n%s' % (underline, title, underline)
408
409     # preprocess long description
410     ld = db['main']['long_description']
411     ld = ' '.join([l.lstrip(' .') for l in ld.split('\n')])
412
413     page = template.render(pkg=pkg,
414                            title=title,
415                            long_description=ld,
416                            cfg=cfg,
417                            db=db)
418     # the following can be replaced by something like
419     # {% include "sidebar.html" ignore missing %}
420     # in the template whenever jinja 2.2 becomes available
421     addenum = os.path.join(os.path.abspath(addenum_dir), '%s.rst' % pkg)
422     if os.path.exists(addenum):
423         page += '\n\n.. include:: %s\n' % addenum
424     return page
425
426
427 def store_db(db, filename):
428     pp = PrettyPrinter(indent=2)
429     f = open(filename, 'w')
430     f.write(pp.pformat(db))
431     f.close()
432
433
434 def read_db(filename):
435     f = open(filename)
436     db = eval(f.read())
437     return db
438
439 def write_sourceslist(jinja_env, cfg, outdir):
440     create_dir(outdir)
441     create_dir(os.path.join(outdir, '_static'))
442
443     repos = {}
444     for release in cfg.options('release codenames'):
445         transrel = trans_codename(release, cfg)
446         repos[transrel] = []
447         for mirror in cfg.options('mirrors'):
448             listname = 'neurodebian.%s.%s.sources.list' % (release, mirror)
449             repos[transrel].append((mirror, listname))
450             lf = open(os.path.join(outdir, '_static', listname), 'w')
451             aptcfg = '%s %s main contrib non-free\n' % (cfg.get('mirrors', mirror),
452                                                       release)
453             lf.write('deb %s' % aptcfg)
454             lf.write('deb-src %s' % aptcfg)
455             lf.close()
456
457     srclist_template = jinja_env.get_template('sources_lists.rst')
458     sl = open(os.path.join(outdir, 'sources_lists'), 'w')
459     sl.write(srclist_template.render(repos=repos))
460     sl.close()
461
462
463 def write_pkgpages(jinja_env, cfg, db, outdir, addenum_dir):
464     create_dir(outdir)
465     create_dir(os.path.join(outdir, 'pkgs'))
466
467     # generate the TOC with all packages
468     toc_template = jinja_env.get_template('pkgs_toc.rst')
469     toc = open(os.path.join(outdir, 'pkgs.rst'), 'w')
470     toc.write(toc_template.render(pkgs=db.keys()))
471     toc.close()
472
473     # and now each individual package page
474     pkg_template = jinja_env.get_template('pkg.rst')
475     for p in db.keys():
476         page = generate_pkgpage(p, cfg, db, pkg_template, addenum_dir)
477         # when no page is available skip this package
478         if page is None:
479             continue
480         pf = open(os.path.join(outdir, 'pkgs', p + '.rst'), 'w')
481         pf.write(generate_pkgpage(p, cfg, db, pkg_template, addenum_dir))
482         pf.close()
483
484
485 def prepOptParser(op):
486     # use module docstring for help output
487     op.usage = "%s [OPTIONS]\n\n" % sys.argv[0] + __doc__
488
489     op.add_option("--db",
490                   action="store", type="string", dest="db",
491                   default=None,
492                   help="Database file to read. Default: None")
493
494     op.add_option("--cfg",
495                   action="store", type="string", dest="cfg",
496                   default=None,
497                   help="Repository config file.")
498
499     op.add_option("-o", "--outdir",
500                   action="store", type="string", dest="outdir",
501                   default=None,
502                   help="Target directory for ReST output. Default: None")
503
504     op.add_option("-r", "--release-url",
505                   action="append", dest="release_urls",
506                   help="None")
507
508     op.add_option("--pkgaddenum", action="store", dest="addenum_dir",
509                   type="string", default=None, help="None")
510
511
512 def main():
513     op = OptionParser(version="%prog 0.0.2")
514     prepOptParser(op)
515
516     (opts, args) = op.parse_args()
517
518     if len(args) != 1:
519         print('There needs to be exactly one command')
520         sys.exit(1)
521
522     cmd = args[0]
523
524     if opts.cfg is None:
525         print("'--cfg' option is mandatory.")
526         sys.exit(1)
527     if opts.db is None:
528         print("'--db' option is mandatory.")
529         sys.exit(1)
530
531
532     cfg = SafeConfigParser()
533     cfg.read(opts.cfg)
534
535     # load existing db, unless renew is requested
536     if cmd == 'updatedb':
537         db = {}
538         if cfg.has_option('packages', 'select taskfiles'):
539             db = add_pkgfromtaskfile(db, cfg.get('packages',
540                                                  'select taskfiles').split())
541
542         # add additional package names from config file
543         if cfg.has_option('packages', 'select names'):
544             for p in cfg.get('packages', 'select names').split():
545                 if not db.has_key(p):
546                     db[p] = get_emptydbentry()
547
548         # get info from task files
549         if cfg.has_option('packages', 'prospective'):
550             for url in cfg.get('packages', 'prospective').split():
551                 db = import_blendstask(db, url)
552
553         # parse NeuroDebian repository
554         if cfg.has_option('neurodebian', 'releases'):
555             for rurl in cfg.get('neurodebian', 'releases').split():
556                 db = import_release(cfg, db, rurl)
557
558         # collect package information from DDE
559         db = import_dde(cfg, db)
560         # store the new DB
561         store_db(db, opts.db)
562         # and be done
563         return
564
565     # load the db from file
566     db = read_db(opts.db)
567
568     # fire up jinja
569     jinja_env = Environment(loader=PackageLoader('neurodebian', 'templates'))
570
571     # generate package pages and TOC and write them to files
572     write_pkgpages(jinja_env, cfg, db, opts.outdir, opts.addenum_dir)
573
574     write_sourceslist(jinja_env, cfg, opts.outdir)
575
576 if __name__ == "__main__":
577     main()