2 """Tell me who you are!
7 from debian_bundle import deb822
9 # Lets first assure no guarding (but annoying) warnings
11 warnings.simplefilter('ignore', FutureWarning)
12 warnings.filterwarnings('ignore', 'Module debian_bundle was already imported.*', UserWarning)
15 from ConfigParser import SafeConfigParser
16 from optparse import OptionParser, Option, OptionGroup, OptionConflictError
27 from jinja2 import Environment, PackageLoader
29 from pprint import PrettyPrinter
32 class AptListsCache(object):
33 def __init__(self, cachedir='build/cache',
36 self.cachedir = cachedir
38 if not ro_cachedirs is None:
39 self.ro_cachedirs = ro_cachedirs
41 self.ro_cachedirs = []
44 create_dir(self.cachedir)
46 def get(self, url, update=False):
47 """Looks in the cache if the file is there and takes the cached one.
48 Otherwise it is downloaded first.
50 Knows how to deal with http:// and svn:// URLs.
55 # look whether it is compressed
56 cext = url.split('.')[-1]
57 if cext in ['gz', 'bz2']:
58 target_url = url[:-1 * len(cext) -1]
60 # assume not compressed
64 # turn url into a filename -- mimik what APT does for
66 tfilename = '_'.join(target_url.split('/')[2:])
68 # if we need to download anyway do not search
70 cfilename = os.path.join(self.cachedir, tfilename)
72 # look for the uncompressed file anywhere in the cache
74 for cp in [self.cachedir] + self.ro_cachedirs:
75 if os.path.exists(os.path.join(cp, tfilename)):
76 cfilename = os.path.join(cp, tfilename)
81 cfilename = os.path.join(self.cachedir, tfilename)
84 # if updated needed -- download
86 #print 'Caching file from %s' % url
88 if url.startswith('svn://'):
90 pysvn.Client().export(url, cfilename)
91 if url.startswith('http://'):
93 tempfile, ignored = urllib.urlretrieve(url)
100 decompressor = 'bzip2'
105 "Don't know how to decompress %s files" \
108 if not decompressor is None:
109 if subprocess.call([decompressor, '-d', '-q', '-f',
111 raise RuntimeError, \
112 "Something went wrong while decompressing '%s'" \
115 # move decompressed file into cache
116 shutil.move(os.path.splitext(tempfile)[0], cfilename)
118 # XXX do we need that if explicit filename is provided?
122 fh = codecs.open(cfilename, 'r', 'utf-8')
127 def add_pkgfromtaskfile(db, urls):
128 cache = AptListsCache()
134 # loop over all stanzas
135 for stanza in deb822.Packages.iter_paragraphs(fh):
136 if stanza.has_key('Depends'):
137 pkg = stanza['Depends']
138 elif stanza.has_key('Suggests'):
139 pkg = stanza['Suggests']
143 # account for multiple packages per line
145 pkgs += [p.strip() for p in pkg.split(',')]
147 pkgs.append(pkg.strip())
150 if not db.has_key(p):
151 db[p] = get_emptydbentry()
155 def get_emptydbentry():
158 def import_blendstask(cfg, db, url):
159 cache = AptListsCache()
163 # figure out blend's task page URL, since they differ from blend to blend
164 urlsec = url.split('/')
165 blendname = urlsec[-3]
166 if blendname == 'debian-med':
167 taskpage_url = 'http://debian-med.alioth.debian.org/tasks/'
168 elif blendname == 'debian-science':
169 taskpage_url = 'http://blends.alioth.debian.org/science/tasks/'
171 raise ValueError('Unknown blend "%s"' % blendname)
172 taskpage_url += urlsec[-1]
174 for st in deb822.Packages.iter_paragraphs(fh):
175 if st.has_key('Task'):
176 task_name = st['Task']
177 task = (blendname, task_name, taskpage_url)
179 if st.has_key('Depends'):
181 elif st.has_key('Suggests'):
184 # print 'Warning: Cannot determine name of prospective package ' \
185 # '... ignoring. Dump follows:'
189 # take care of pkg lists
190 for p in pkg.split(', '):
191 if not db.has_key(p):
192 print 'Ignoring blend package "%s"' % p
198 info['tasks'] = [task]
199 if st.has_key('License'):
200 info['license'] = st['License']
201 if st.has_key('Responsible'):
202 info['responsible'] = st['Responsible']
205 if st.has_key('Pkg-Description'):
206 descr = st['Pkg-Description'].split('\n')
207 info['description'] = descr[0].strip()
208 info['long_description'] = \
209 u'\n'.join(descr[1:])
211 # charge the basic property set
212 db[p]['main']['description'] = info['description']
213 db[p]['main']['long_description'] = info['long_description']
214 if st.has_key('WNPP'):
215 db[p]['main']['debian_itp'] = st['WNPP']
216 if st.has_key('Pkg-URL'):
217 db[p]['main']['other_pkg'] = st['Pkg-URL']
218 if st.has_key('Homepage'):
219 db[p]['main']['homepage'] = st['Homepage']
222 if st.has_key('Published-Title'):
223 title = st['Published-Title']
225 # trip trailing dot -- added later
226 pub = {'title': title[:-1]}
228 pub = {'title': title}
229 if st.has_key('Published-Authors'):
230 pub['authors'] = st['Published-Authors']
231 if st.has_key('Published-Year'):
232 pub['year'] = st['Published-Year']
233 if st.has_key('Published-In'):
234 pub['in'] = st['Published-In']
235 if st.has_key('Published-URL'):
236 pub['url'] = st['Published-URL']
237 if st.has_key('Published-DOI'):
238 pub['doi'] = st['Published-DOI']
239 # need at least one URL
240 if not pub.has_key('url'):
241 pub['url'] = "http://dx.doi.org/%s" % st['Published-DOI']
243 db[p]['main']['publication'] = pub
246 if st.has_key('Registration'):
247 db[p]['main']['registration'] = st['Registration']
250 if st.has_key('Remark'):
251 # prepend a single space to make it look like a long description
252 info['remark'] = convert_longdescr(' ' + st['Remark'])
254 # only store if there isn't something already
255 if not db[p].has_key('blends'):
256 db[p]['blends'] = info
258 # just add this tasks name and id
259 db[p]['blends']['tasks'].append(task)
261 # handle pkg name aliases
262 if p in cfg.options('blend package aliases'):
263 src_entry = db[p].copy()
264 # remove original entry
266 # copy the entry into all aliases
267 for alias in cfg.get('blend package aliases', p).split():
268 print "Aliasing %s to %s" % (p, alias)
269 db[alias] = copy.deepcopy(src_entry)
274 def get_releaseinfo(rurl):
275 cache = AptListsCache()
276 # root URL of the repository
277 baseurl = '/'.join(rurl.split('/')[:-1])
278 # get the release file from the cache
279 release_file = cache.get(rurl)
281 # create parser instance
282 rp = deb822.Release(release_file)
284 # architectures on this dist
285 archs = rp['Architectures'].split()
286 components = rp['Components'].split()
287 # compile a new codename that also considers the repository label
288 # to distinguish between official and unofficial repos.
290 origin = rp['Origin']
291 codename = rp['Codename']
292 labelcode = '_'.join([rp['Label'], rp['Codename']])
297 return {'baseurl': baseurl, 'archs': archs, 'components': components,
298 'codename': codename, 'label': label, 'labelcode': labelcode,
302 def build_pkgsurl(baseurl, component, arch):
303 return '/'.join([baseurl, component, 'binary-' + arch, 'Packages.bz2'])
306 def import_release(cfg, db, rurl):
307 cache = AptListsCache()
309 ri = get_releaseinfo(rurl)
311 # compile the list of Packages files to parse and parse them
312 for c in ri['components']:
313 for a in ri['archs']:
314 # compile packages URL
315 pkgsurl = build_pkgsurl(ri['baseurl'], c, a)
317 # retrieve from cache
318 packages_file = cache.get(pkgsurl)
321 for stanza in deb822.Packages.iter_paragraphs(packages_file):
322 db = _store_pkg(cfg, db, stanza, ri['origin'], ri['codename'], c, ri['baseurl'])
325 packages_file.close()
329 def _store_pkg(cfg, db, st, origin, codename, component, baseurl):
336 # only care for known packages
337 if not db.has_key(pkg):
338 # print 'Ignoring NeuroDebian package "%s"' % pkg
341 distkey = (trans_codename(codename, cfg), 'neurodebian-' + codename)
343 if db[pkg].has_key(distkey):
344 info = db[pkg][distkey]
346 info = {'architecture': []}
349 if not st['Architecture'] in info['architecture']:
350 info['architecture'].append(st['Architecture'])
351 info['maintainer'] = st['Maintainer']
352 if st.has_key('Homepage'):
353 info['homepage'] = st['Homepage']
354 info['version'] = st['Version']
357 info['distribution'] = origin
358 info['release'] = codename
359 info['component'] = component
362 info['poolurl'] = '/'.join([os.path.dirname(st['Filename'])])
365 descr = st['Description'].replace('%', '%%').split('\n')
366 info['description'] = descr[0].strip()
367 info['long_description'] = u'\n'.join(descr[1:])
369 db[pkg][distkey] = info
371 # charge the basic property set
372 db[pkg]['main']['description'] = info['description']
373 db[pkg]['main']['long_description'] = info['long_description']
374 if st.has_key('Source'):
375 db[pkg]['main']['sv'] = "%s %s" % (st['Source'], st['Version'])
377 db[pkg]['main']['sv'] = "%s %s" % (st['Package'], st['Version'])
378 if st.has_key('Homepage'):
379 db[pkg]['main']['homepage'] = st['Homepage']
380 if st.has_key('Recommends'):
381 db[pkg]['main']['recommends'] = st['Recommends']
386 def trans_codename(codename, cfg):
387 """Translate a known codename into a release description.
389 Unknown codenames will simply be returned as is.
391 # if we know something, tell
392 if codename in cfg.options('release codenames'):
393 return cfg.get('release codenames', codename)
398 def create_dir(path):
399 if os.path.exists(path):
402 ps = path.split(os.path.sep)
404 for i in range(1,len(ps) + 1):
405 p = os.path.sep.join(ps[:i])
407 if not os.path.exists(p):
411 def dde_get(url, fail=False):
412 # enforce delay to be friendly to DDE
415 data = json.read(urllib2.urlopen(url+"?t=json").read())['r']
416 print "SUCCESS:", url
418 except urllib2.HTTPError, e:
419 print "NOINFO:", url, type(e)
421 except urllib2.URLError, e:
422 print "URLERROR:", url, type(e)
424 print "Permanant failure"
426 print "Try again after 30 seconds..."
428 return dde_get(url, fail=True)
429 except (StopIteration):
432 except json.ReadException, e:
433 print "UDD-DOWN?:", url, type(e)
437 def nitrc_get(spec, fail=False):
438 nitrc_url = 'http://www.nitrc.org/export/site/projects.json.php'
440 # change into this from python 2.6 on
441 #data = json.loads(urllib2.urlopen(nitrc_url + '?spec=%s' % spec).read())
442 data = json.read(urllib2.urlopen(nitrc_url + '?spec=%s' % spec).read())
443 print "NITRC-SUCCESS:", spec
444 except urllib2.HTTPError, e:
445 print "NITRC-NOINFO:", spec, type(e)
447 except urllib2.URLError, e:
448 print "NITRC-URLERROR:", spec, type(e)
450 print "Permanant failure"
452 print "Try again after 30 seconds..."
454 return nitrc_get(spec, fail=True)
458 def parse_nitrc(data):
461 # simplify -- there is only one project in the data
462 project = data['projects'][0]
463 nitrc_filtered = {'downloads': 0,
465 for pkg in project['packages']:
466 for release in pkg['releases']:
467 for file in release['files']:
468 nitrc_filtered['downloads'] += file['download_count']
469 return nitrc_filtered
472 def import_nitrc(cfg, db):
474 if not cfg.has_option("nitrc ids", p):
476 nitrc_spec = cfg.get("nitrc ids", p)
477 nitrc_data = nitrc_get(nitrc_spec)
478 nitrc_excerpt = parse_nitrc(nitrc_data)
479 if not nitrc_excerpt is None:
480 db[p]['nitrc'] = nitrc_excerpt
484 def import_dde(cfg, db):
485 query_url = cfg.get('dde', 'pkgquery_url')
488 q = dde_get(query_url + "/packages/all/%s" % p)
490 # copy all stuff, while preserving non-overlapping information
491 for k, v in q.iteritems():
493 # get latest popcon info for debian and ubuntu
494 # cannot use origin field itself, since it is none for few packages
496 origin = q['drc'].split()[0]
497 if origin == 'ubuntu':
498 if q.has_key('popcon'):
499 db[p]['main']['ubuntu_popcon'] = q['popcon']
500 # if we have ubuntu, need to get debian
501 q = dde_get(query_url + "/packages/prio-debian-sid/%s" % p)
502 if q and q.has_key('popcon'):
503 db[p]['main']['debian_popcon'] = q['popcon']
504 elif origin == 'debian':
505 if q.has_key('popcon'):
506 db[p]['main']['debian_popcon'] = q['popcon']
507 # if we have debian, need to get ubuntu
508 q = dde_get(query_url + "/packages/prio-ubuntu-natty/%s" % p)
509 if q and q.has_key('popcon'):
510 db[p]['main']['ubuntu_popcon'] = q['popcon']
512 print("Ignoring unkown origin '%s' for package '%s'." \
515 # now get info for package from all releases in UDD
516 q = dde_get(query_url + "/dist/p:%s" % p)
519 # hold all info about this package per distribution release
522 distkey = (trans_codename(cp['release'], cfg),
523 "%s-%s" % (cp['distribution'], cp['release']))
524 if not info.has_key(distkey):
526 # turn into a list to append others later
527 info[distkey]['architecture'] = [info[distkey]['architecture']]
528 # accumulate data for multiple over archs
530 comp = apt.VersionCompare(cp['version'],
531 info[distkey]['version'])
532 # found another arch for the same version
534 info[distkey]['architecture'].append(cp['architecture'])
535 # found newer version, dump the old ones
538 # turn into a list to append others later
539 info[distkey]['architecture'] = [info[distkey]['architecture']]
540 # simply ignore older versions
544 # finally assign the new package data
545 for k, v in info.iteritems():
551 def convert_longdescr(ld):
552 ld = ld.replace('% ', '%% ')
554 for i, l in enumerate(ld):
556 ld[i] = ' #NEWLINEMARKER#'
557 # look for embedded lists
558 elif len(l) >=3 and l[:2] == ' ' and l[2] in '-*':
559 ld[i] = ' #NEWLINEMARKER# ' + l[2:]
561 ld = u' '.join([l[1:] for l in ld])
562 ld = ld.replace('#NEWLINEMARKER# ', '\n\n')
563 # cleanup any leftover (e.g. trailing markers)
564 ld = ld.replace('#NEWLINEMARKER#', '')
568 def generate_pkgpage(pkg, cfg, db, template, addenum_dir):
569 # local binding for ease of use
571 # do nothing if there is not at least the very basic stuff
572 if not pkgdb['main'].has_key('description'):
574 title = '**%s** -- %s' % (pkg, pkgdb['main']['description'])
575 underline = '*' * (len(title) + 2)
576 title = '%s\n %s\n%s' % (underline, title, underline)
578 page = template.render(
581 long_description=convert_longdescr(pkgdb['main']['long_description']),
585 # the following can be replaced by something like
586 # {% include "sidebar.html" ignore missing %}
587 # in the template whenever jinja 2.2 becomes available
588 addenum = os.path.join(os.path.abspath(addenum_dir), '%s.rst' % pkg)
589 if os.path.exists(addenum):
590 page += '\n\n.. include:: %s\n' % addenum
594 def store_db(db, filename):
595 pp = PrettyPrinter(indent=2)
596 f = codecs.open(filename, 'w', 'utf-8')
597 f.write(pp.pformat(db))
601 def read_db(filename):
602 f = codecs.open(filename, 'r', 'utf-8')
606 def write_sourceslist(jinja_env, cfg, outdir):
608 create_dir(os.path.join(outdir, '_static'))
611 for release in cfg.options('release codenames'):
612 if release == 'data':
613 # no seperate list for the data archive
615 transrel = trans_codename(release, cfg)
617 for mirror in cfg.options('mirrors'):
618 listname = 'neurodebian.%s.%s.sources.list' % (release, mirror)
619 repos[transrel].append((mirror, listname))
620 lf = open(os.path.join(outdir, '_static', listname), 'w')
621 for rel in ('data', release):
622 aptcfg = '%s %s main contrib non-free\n' % (cfg.get('mirrors', mirror),
624 lf.write('deb %s' % aptcfg)
625 lf.write('#deb-src %s' % aptcfg)
628 srclist_template = jinja_env.get_template('sources_lists.rst')
629 sl = open(os.path.join(outdir, 'sources_lists'), 'w')
630 sl.write(srclist_template.render(repos=repos))
634 def write_pkgpages(jinja_env, cfg, db, outdir, addenum_dir):
636 create_dir(os.path.join(outdir, 'pkgs'))
638 # generate the TOC with all packages
639 toc_template = jinja_env.get_template('pkgs_toc.rst')
640 toc = codecs.open(os.path.join(outdir, 'pkgs.rst'), 'w', 'utf-8')
641 # this is a fragile test
642 toc.write(toc_template.render(
643 pkgs=[k for k in db.keys()
644 if not ('Datasets', 'neurodebian-data') in db[k]]))
646 # and now only for dataset packages
647 toc_template = jinja_env.get_template('datasets_toc.rst')
648 toc = codecs.open(os.path.join(outdir, 'datasets.rst'), 'w', 'utf-8')
649 # this is a fragile test
650 toc.write(toc_template.render(
651 pkgs=[k for k in db.keys()
652 if ('Datasets', 'neurodebian-data') in db[k]]))
656 # and now each individual package page
657 pkg_template = jinja_env.get_template('pkg.rst')
659 page = generate_pkgpage(p, cfg, db, pkg_template, addenum_dir)
660 # when no page is available skip this package
663 pf = codecs.open(os.path.join(outdir, 'pkgs', p + '.rst'), 'w', 'utf-8')
668 def prepOptParser(op):
669 # use module docstring for help output
670 op.usage = "%s [OPTIONS]\n\n" % sys.argv[0] + __doc__
672 op.add_option("--db",
673 action="store", type="string", dest="db",
675 help="Database file to read. Default: None")
677 op.add_option("--cfg",
678 action="store", type="string", dest="cfg",
680 help="Repository config file.")
682 op.add_option("-o", "--outdir",
683 action="store", type="string", dest="outdir",
685 help="Target directory for ReST output. Default: None")
687 op.add_option("-r", "--release-url",
688 action="append", dest="release_urls",
691 op.add_option("--pkgaddenum", action="store", dest="addenum_dir",
692 type="string", default=None, help="None")
696 op = OptionParser(version="%prog 0.0.2")
699 (opts, args) = op.parse_args()
702 print('There needs to be exactly one command')
708 print("'--cfg' option is mandatory.")
711 print("'--db' option is mandatory.")
715 cfg = SafeConfigParser()
718 # load existing db, unless renew is requested
719 if cmd == 'updatedb':
721 if cfg.has_option('packages', 'select taskfiles'):
722 db = add_pkgfromtaskfile(db, cfg.get('packages',
723 'select taskfiles').split())
725 # add additional package names from config file
726 if cfg.has_option('packages', 'select names'):
727 for p in cfg.get('packages', 'select names').split():
728 if not db.has_key(p):
729 db[p] = get_emptydbentry()
731 # get info from task files
732 if cfg.has_option('packages', 'prospective'):
733 for url in cfg.get('packages', 'prospective').split():
734 db = import_blendstask(cfg, db, url)
736 # parse NeuroDebian repository
737 if cfg.has_option('neurodebian', 'releases'):
738 for rurl in cfg.get('neurodebian', 'releases').split():
739 db = import_release(cfg, db, rurl)
741 # collect package information from DDE
742 db = import_dde(cfg, db)
743 # get info from NITRC
744 db = import_nitrc(cfg, db)
746 store_db(db, opts.db)
750 # load the db from file
751 db = read_db(opts.db)
754 jinja_env = Environment(loader=PackageLoader('neurodebian', 'templates'))
756 # generate package pages and TOC and write them to files
757 write_pkgpages(jinja_env, cfg, db, opts.outdir, opts.addenum_dir)
759 write_sourceslist(jinja_env, cfg, opts.outdir)
761 if __name__ == "__main__":