2 """Tell me who you are!
7 from debian_bundle import deb822
9 from ConfigParser import SafeConfigParser
10 from optparse import OptionParser, Option, OptionGroup, OptionConflictError
20 from jinja2 import Environment, PackageLoader
22 from pprint import PrettyPrinter
25 class AptListsCache(object):
26 def __init__(self, cachedir='build/cache',
29 self.cachedir = cachedir
31 if not ro_cachedirs is None:
32 self.ro_cachedirs = ro_cachedirs
34 self.ro_cachedirs = []
37 create_dir(self.cachedir)
39 def get(self, url, update=False):
40 """Looks in the cache if the file is there and takes the cached one.
41 Otherwise it is downloaded first.
43 Knows how to deal with http:// and svn:// URLs.
48 # look whether it is compressed
49 cext = url.split('.')[-1]
50 if cext in ['gz', 'bz2']:
51 target_url = url[:-1 * len(cext) -1]
53 # assume not compressed
57 # turn url into a filename -- mimik what APT does for
59 tfilename = '_'.join(target_url.split('/')[2:])
61 # if we need to download anyway do not search
63 cfilename = os.path.join(self.cachedir, tfilename)
65 # look for the uncompressed file anywhere in the cache
67 for cp in [self.cachedir] + self.ro_cachedirs:
68 if os.path.exists(os.path.join(cp, tfilename)):
69 cfilename = os.path.join(cp, tfilename)
74 cfilename = os.path.join(self.cachedir, tfilename)
77 # if updated needed -- download
79 #print 'Caching file from %s' % url
81 if url.startswith('svn://'):
83 pysvn.Client().export(url, cfilename)
84 if url.startswith('http://'):
86 tempfile, ignored = urllib.urlretrieve(url)
93 decompressor = 'bzip2'
98 "Don't know how to decompress %s files" \
101 if not decompressor is None:
102 if subprocess.call([decompressor, '-d', '-q', '-f',
104 raise RuntimeError, \
105 "Something went wrong while decompressing '%s'" \
108 # move decompressed file into cache
109 shutil.move(os.path.splitext(tempfile)[0], cfilename)
111 # XXX do we need that if explicit filename is provided?
115 fh = codecs.open(cfilename, 'r', 'utf-8')
120 def add_pkgfromtaskfile(db, urls):
121 cache = AptListsCache()
127 # loop over all stanzas
128 for stanza in deb822.Packages.iter_paragraphs(fh):
129 if stanza.has_key('Depends'):
130 pkg = stanza['Depends']
131 elif stanza.has_key('Suggests'):
132 pkg = stanza['Suggests']
136 # account for multiple packages per line
138 pkgs += [p.strip() for p in pkg.split(',')]
140 pkgs.append(pkg.strip())
143 if not db.has_key(p):
144 db[p] = get_emptydbentry()
148 def get_emptydbentry():
151 def import_blendstask(db, url):
152 cache = AptListsCache()
156 # figure out blend's task page URL, since they differ from blend to blend
157 urlsec = url.split('/')
158 blendname = urlsec[-3]
159 if blendname == 'debian-med':
160 taskpage_url = 'http://debian-med.alioth.debian.org/tasks/'
161 elif blendname == 'debian-science':
162 taskpage_url = 'http://blends.alioth.debian.org/science/tasks/'
164 raise ValueError('Unknown blend "%s"' % blendname)
165 taskpage_url += urlsec[-1]
167 for st in deb822.Packages.iter_paragraphs(fh):
168 if st.has_key('Task'):
169 task_name = st['Task']
170 task = (blendname, task_name, taskpage_url)
172 if st.has_key('Depends'):
174 elif st.has_key('Suggests'):
177 # print 'Warning: Cannot determine name of prospective package ' \
178 # '... ignoring. Dump follows:'
182 # take care of pkg lists
183 for p in pkg.split(', '):
184 if not db.has_key(p):
185 print 'Ignoring blend package "%s"' % p
191 info['tasks'] = [task]
192 if st.has_key('License'):
193 info['license'] = st['License']
194 if st.has_key('Responsible'):
195 info['responsible'] = st['Responsible']
198 if st.has_key('Pkg-Description'):
199 descr = st['Pkg-Description'].split('\n')
200 info['description'] = descr[0].strip()
201 info['long_description'] = \
202 u'\n'.join(descr[1:])
204 # charge the basic property set
205 db[p]['main']['description'] = info['description']
206 db[p]['main']['long_description'] = info['long_description']
207 if st.has_key('WNPP'):
208 db[p]['main']['debian_itp'] = st['WNPP']
209 if st.has_key('Pkg-URL'):
210 db[p]['main']['other_pkg'] = st['Pkg-URL']
211 if st.has_key('Homepage'):
212 db[p]['main']['homepage'] = st['Homepage']
215 if st.has_key('Published-Title'):
216 pub = {'title': st['Published-Title']}
217 if st.has_key('Published-Authors'):
218 pub['authors'] = st['Published-Authors']
219 if st.has_key('Published-Year'):
220 pub['year'] = st['Published-Year']
221 if st.has_key('Published-In'):
222 pub['in'] = st['Published-In']
223 if st.has_key('Published-URL'):
224 pub['url'] = st['Published-URL']
225 if st.has_key('Published-DOI'):
226 pub['doi'] = st['Published-DOI']
227 # need at least one URL
228 if not pub.has_key('url'):
229 pub['url'] = st['Published-DOI']
231 db[p]['main']['publication'] = pub
234 if st.has_key('Registration'):
235 db[p]['main']['registration'] = st['Registration']
238 if st.has_key('Remark'):
239 # prepend a single space to make it look like a long description
240 info['remark'] = convert_longdescr(' ' + st['Remark'])
242 # only store if there isn't something already
243 if not db[p].has_key('blends'):
244 db[p]['blends'] = info
246 # just add this tasks name and id
247 db[p]['blends']['tasks'].append(task)
252 def get_releaseinfo(rurl):
253 cache = AptListsCache()
254 # root URL of the repository
255 baseurl = '/'.join(rurl.split('/')[:-1])
256 # get the release file from the cache
257 release_file = cache.get(rurl)
259 # create parser instance
260 rp = deb822.Release(release_file)
262 # architectures on this dist
263 archs = rp['Architectures'].split()
264 components = rp['Components'].split()
265 # compile a new codename that also considers the repository label
266 # to distinguish between official and unofficial repos.
268 origin = rp['Origin']
269 codename = rp['Codename']
270 labelcode = '_'.join([rp['Label'], rp['Codename']])
275 return {'baseurl': baseurl, 'archs': archs, 'components': components,
276 'codename': codename, 'label': label, 'labelcode': labelcode,
280 def build_pkgsurl(baseurl, component, arch):
281 return '/'.join([baseurl, component, 'binary-' + arch, 'Packages.bz2'])
284 def import_release(cfg, db, rurl):
285 cache = AptListsCache()
287 ri = get_releaseinfo(rurl)
289 # compile the list of Packages files to parse and parse them
290 for c in ri['components']:
291 for a in ri['archs']:
292 # compile packages URL
293 pkgsurl = build_pkgsurl(ri['baseurl'], c, a)
295 # retrieve from cache
296 packages_file = cache.get(pkgsurl)
299 for stanza in deb822.Packages.iter_paragraphs(packages_file):
300 db = _store_pkg(cfg, db, stanza, ri['origin'], ri['codename'], c, ri['baseurl'])
303 packages_file.close()
307 def _store_pkg(cfg, db, st, origin, codename, component, baseurl):
314 # only care for known packages
315 if not db.has_key(pkg):
316 # print 'Ignoring NeuroDebian package "%s"' % pkg
319 distkey = (trans_codename(codename, cfg), 'neurodebian-' + codename)
321 if db[pkg].has_key(distkey):
322 info = db[pkg][distkey]
324 info = {'architecture': []}
327 if not st['Architecture'] in info['architecture']:
328 info['architecture'].append(st['Architecture'])
329 info['maintainer'] = st['Maintainer']
330 if st.has_key('Homepage'):
331 info['homepage'] = st['Homepage']
332 info['version'] = st['Version']
335 info['distribution'] = origin
336 info['release'] = codename
337 info['component'] = component
340 info['poolurl'] = '/'.join([os.path.dirname(st['Filename'])])
343 descr = st['Description'].replace('%', '%%').split('\n')
344 info['description'] = descr[0].strip()
345 info['long_description'] = u'\n'.join(descr[1:])
347 db[pkg][distkey] = info
349 # charge the basic property set
350 db[pkg]['main']['description'] = info['description']
351 db[pkg]['main']['long_description'] = info['long_description']
352 if st.has_key('Source'):
353 db[pkg]['main']['sv'] = "%s %s" % (st['Source'], st['Version'])
355 db[pkg]['main']['sv'] = "%s %s" % (st['Package'], st['Version'])
356 if st.has_key('Homepage'):
357 db[pkg]['main']['homepage'] = st['Homepage']
358 if st.has_key('Recommends'):
359 db[pkg]['main']['recommends'] = st['Recommends']
364 def trans_codename(codename, cfg):
365 """Translate a known codename into a release description.
367 Unknown codenames will simply be returned as is.
369 # if we know something, tell
370 if codename in cfg.options('release codenames'):
371 return cfg.get('release codenames', codename)
376 def create_dir(path):
377 if os.path.exists(path):
380 ps = path.split(os.path.sep)
382 for i in range(1,len(ps) + 1):
383 p = os.path.sep.join(ps[:i])
385 if not os.path.exists(p):
389 def dde_get(url, fail=False):
390 # enforce delay to be friendly to DDE
393 data = json.read(urllib2.urlopen(url+"?t=json").read())['r']
394 print "SUCCESS:", url
396 except urllib2.HTTPError, e:
397 print "NOINFO:", url, type(e)
399 except urllib2.URLError, e:
400 print "URLERROR:", url, type(e)
402 print "Permanant failure"
404 print "Try again after 30 seconds..."
406 return dde_get(url, fail=True)
407 except (StopIteration):
412 def import_dde(cfg, db):
413 query_url = cfg.get('dde', 'pkgquery_url')
416 q = dde_get(query_url + "/packages/all/%s" % p)
418 # copy all stuff, while preserving non-overlapping information
419 for k, v in q.iteritems():
421 # get latest popcon info for debian and ubuntu
422 # cannot use origin field itself, since it is none for few packages
424 origin = q['drc'].split()[0]
425 if origin == 'ubuntu':
426 if q.has_key('popcon'):
427 db[p]['main']['ubuntu_popcon'] = q['popcon']
428 # if we have ubuntu, need to get debian
429 q = dde_get(query_url + "/packages/prio-debian-sid/%s" % p)
430 if q and q.has_key('popcon'):
431 db[p]['main']['debian_popcon'] = q['popcon']
432 elif origin == 'debian':
433 if q.has_key('popcon'):
434 db[p]['main']['debian_popcon'] = q['popcon']
435 # if we have debian, need to get ubuntu
436 q = dde_get(query_url + "/packages/prio-ubuntu-karmic/%s" % p)
437 if q and q.has_key('popcon'):
438 db[p]['main']['ubuntu_popcon'] = q['popcon']
440 print("Ignoring unkown origin '%s' for package '%s'." \
443 # now get info for package from all releases in UDD
444 q = dde_get(query_url + "/dist/p:%s" % p)
447 # hold all info about this package per distribution release
450 distkey = (trans_codename(cp['release'], cfg),
451 "%s-%s" % (cp['distribution'], cp['release']))
452 if not info.has_key(distkey):
454 # turn into a list to append others later
455 info[distkey]['architecture'] = [info[distkey]['architecture']]
456 # accumulate data for multiple over archs
458 comp = apt.VersionCompare(cp['version'],
459 info[distkey]['version'])
460 # found another arch for the same version
462 info[distkey]['architecture'].append(cp['architecture'])
463 # found newer version, dump the old ones
466 # turn into a list to append others later
467 info[distkey]['architecture'] = [info[distkey]['architecture']]
468 # simply ignore older versions
472 # finally assign the new package data
473 for k, v in info.iteritems():
479 def convert_longdescr(ld):
480 ld = ld.replace('% ', '%% ')
482 for i, l in enumerate(ld):
484 ld[i] = ' #NEWLINEMARKER#'
485 # look for embedded lists
486 elif len(l) >=3 and l[:2] == ' ' and l[2] in '-*':
487 ld[i] = ' #NEWLINEMARKER# ' + l[2:]
489 ld = u' '.join([l[1:] for l in ld])
490 ld = ld.replace('#NEWLINEMARKER# ', '\n\n')
491 # cleanup any leftover (e.g. trailing markers)
492 ld = ld.replace('#NEWLINEMARKER#', '')
496 def generate_pkgpage(pkg, cfg, db, template, addenum_dir):
497 # local binding for ease of use
499 # do nothing if there is not at least the very basic stuff
500 if not pkgdb['main'].has_key('description'):
502 title = '**%s** -- %s' % (pkg, pkgdb['main']['description'])
503 underline = '*' * (len(title) + 2)
504 title = '%s\n %s\n%s' % (underline, title, underline)
506 page = template.render(
509 long_description=convert_longdescr(pkgdb['main']['long_description']),
513 # the following can be replaced by something like
514 # {% include "sidebar.html" ignore missing %}
515 # in the template whenever jinja 2.2 becomes available
516 addenum = os.path.join(os.path.abspath(addenum_dir), '%s.rst' % pkg)
517 if os.path.exists(addenum):
518 page += '\n\n.. include:: %s\n' % addenum
522 def store_db(db, filename):
523 pp = PrettyPrinter(indent=2)
524 f = codecs.open(filename, 'w', 'utf-8')
525 f.write(pp.pformat(db))
529 def read_db(filename):
530 f = codecs.open(filename, 'r', 'utf-8')
534 def write_sourceslist(jinja_env, cfg, outdir):
536 create_dir(os.path.join(outdir, '_static'))
539 for release in cfg.options('release codenames'):
540 transrel = trans_codename(release, cfg)
542 for mirror in cfg.options('mirrors'):
543 listname = 'neurodebian.%s.%s.sources.list' % (release, mirror)
544 repos[transrel].append((mirror, listname))
545 lf = open(os.path.join(outdir, '_static', listname), 'w')
546 aptcfg = '%s %s main contrib non-free\n' % (cfg.get('mirrors', mirror),
548 lf.write('deb %s' % aptcfg)
549 lf.write('deb-src %s' % aptcfg)
552 srclist_template = jinja_env.get_template('sources_lists.rst')
553 sl = open(os.path.join(outdir, 'sources_lists'), 'w')
554 sl.write(srclist_template.render(repos=repos))
558 def write_pkgpages(jinja_env, cfg, db, outdir, addenum_dir):
560 create_dir(os.path.join(outdir, 'pkgs'))
562 # generate the TOC with all packages
563 toc_template = jinja_env.get_template('pkgs_toc.rst')
564 toc = codecs.open(os.path.join(outdir, 'pkgs.rst'), 'w', 'utf-8')
565 toc.write(toc_template.render(pkgs=db.keys()))
568 # and now each individual package page
569 pkg_template = jinja_env.get_template('pkg.rst')
571 page = generate_pkgpage(p, cfg, db, pkg_template, addenum_dir)
572 # when no page is available skip this package
575 pf = codecs.open(os.path.join(outdir, 'pkgs', p + '.rst'), 'w', 'utf-8')
580 def prepOptParser(op):
581 # use module docstring for help output
582 op.usage = "%s [OPTIONS]\n\n" % sys.argv[0] + __doc__
584 op.add_option("--db",
585 action="store", type="string", dest="db",
587 help="Database file to read. Default: None")
589 op.add_option("--cfg",
590 action="store", type="string", dest="cfg",
592 help="Repository config file.")
594 op.add_option("-o", "--outdir",
595 action="store", type="string", dest="outdir",
597 help="Target directory for ReST output. Default: None")
599 op.add_option("-r", "--release-url",
600 action="append", dest="release_urls",
603 op.add_option("--pkgaddenum", action="store", dest="addenum_dir",
604 type="string", default=None, help="None")
608 op = OptionParser(version="%prog 0.0.2")
611 (opts, args) = op.parse_args()
614 print('There needs to be exactly one command')
620 print("'--cfg' option is mandatory.")
623 print("'--db' option is mandatory.")
627 cfg = SafeConfigParser()
630 # load existing db, unless renew is requested
631 if cmd == 'updatedb':
633 if cfg.has_option('packages', 'select taskfiles'):
634 db = add_pkgfromtaskfile(db, cfg.get('packages',
635 'select taskfiles').split())
637 # add additional package names from config file
638 if cfg.has_option('packages', 'select names'):
639 for p in cfg.get('packages', 'select names').split():
640 if not db.has_key(p):
641 db[p] = get_emptydbentry()
643 # get info from task files
644 if cfg.has_option('packages', 'prospective'):
645 for url in cfg.get('packages', 'prospective').split():
646 db = import_blendstask(db, url)
648 # parse NeuroDebian repository
649 if cfg.has_option('neurodebian', 'releases'):
650 for rurl in cfg.get('neurodebian', 'releases').split():
651 db = import_release(cfg, db, rurl)
653 # collect package information from DDE
654 db = import_dde(cfg, db)
656 store_db(db, opts.db)
660 # load the db from file
661 db = read_db(opts.db)
664 jinja_env = Environment(loader=PackageLoader('neurodebian', 'templates'))
666 # generate package pages and TOC and write them to files
667 write_pkgpages(jinja_env, cfg, db, opts.outdir, opts.addenum_dir)
669 write_sourceslist(jinja_env, cfg, opts.outdir)
671 if __name__ == "__main__":