]> git.donarmstrong.com Git - neurodebian.git/blob - reblender
Refactored into sphinx, addenum and reblender (separate).
[neurodebian.git] / reblender
1 #!/usr/bin/env python
2 """Tell me who you are!
3 """
4
5 import urllib
6 import apt
7 from debian_bundle import deb822
8 from debian_bundle import debtags
9 from ConfigParser import SafeConfigParser
10 import subprocess
11 import os
12 import sys
13 import shutil
14 import pysvn
15 from optparse import OptionParser, Option, OptionGroup, OptionConflictError
16
17 codename2descr = {
18     'apsy_etch': 'Debian GNU/Linux 4.0 (etch)',
19     'apsy_lenny': 'Debian GNU/Linux 5.0 (lenny)',
20     'apsy_squeeze': 'Debian testing (squeeze)',
21     'apsy_sid': 'Debian unstable (sid)',
22     'apsy_dapper': 'Ubuntu 6.06 LTS "Dapper Drake" (dapper)',
23     'apsy_edgy': 'Ubuntu 6.10 "Edgy Eft" (edgy)',
24     'apsy_feisty': 'Ubuntu 7.04 "Feisty Fawn" (feisty)',
25     'apsy_gutsy': 'Ubuntu 7.10 "Gutsy Gibbon" (gutsy)',
26     'apsy_hardy': 'Ubuntu 8.04 LTS "Hardy Heron" (hardy)',
27     'apsy_intrepid': 'Ubuntu 8.10 "Intrepid Ibex" (intrepid)',
28     'apsy_jaunty': 'Ubuntu 9.04 "Jaunty Jackalope" (jaunty)',
29     }
30
31 def transCodename(codename):
32     """Translate a known codename into a release description.
33
34     Unknown codenames will simply be returned as is.
35     """
36     if codename in codename2descr.keys():
37         return codename2descr[codename]
38     else:
39         return codename
40
41
42 def createDir(path):
43     if os.path.exists(path):
44         return
45
46     ps = path.split(os.path.sep)
47
48     for i in range(1,len(ps) + 1):
49         p = os.path.sep.join(ps[:i])
50
51         if not os.path.exists(p):
52             os.mkdir(p)
53
54
55 class AptListsCache(object):
56     def __init__(self, cachedir='cache', ro_cachedirs=None, init_db=None):
57         self.cachedir = cachedir
58
59         if not ro_cachedirs is None:
60             self.ro_cachedirs = ro_cachedirs
61         else:
62             self.ro_cachedirs = []
63
64         # always use system cache
65         self.ro_cachedirs.append('/var/lib/apt/lists/')
66
67         # create cachedir
68         createDir(self.cachedir)
69
70
71     def get(self, url, update=False):
72         """Looks in the cache if the file is there and takes the cached one.
73         Otherwise it is downloaded first.
74
75         Knows how to deal with http:// and svn:// URLs.
76
77         :Return:
78           file handler
79         """
80         # look whether it is compressed
81         cext = url.split('.')[-1]
82         if cext in ['gz', 'bz2']:
83             target_url = url[:-1 * len(cext) -1]
84         else:
85             # assume not compressed
86             target_url = url
87             cext = None
88
89         # turn url into a filename -- mimik what APT does for
90         # /var/lib/apt/lists/
91         tfilename = '_'.join(target_url.split('/')[2:])
92
93         # if we need to download anyway do not search
94         if update:
95             cfilename = os.path.join(self.cachedir, tfilename)
96         else:
97             # look for the uncompressed file anywhere in the cache
98             cfilename = None
99             for cp in [self.cachedir] + self.ro_cachedirs:
100                 if os.path.exists(os.path.join(cp, tfilename)):
101                     cfilename = os.path.join(cp, tfilename)
102
103         # nothing found?
104         if cfilename is None:
105             # add cache item
106             cfilename = os.path.join(self.cachedir, tfilename)
107             update = True
108
109         # if updated needed -- download
110         if update:
111             print 'Caching file from %s' % url
112
113             if url.startswith('svn://'):
114                 # export from SVN
115                 pysvn.Client().export(url, cfilename)
116             if url.startswith('http://'):
117                 # download
118                 tempfile, ignored = urllib.urlretrieve(url)
119
120                 # decompress
121                 decompressor = None
122                 if cext == 'gz':
123                     decompressor = 'gzip'
124                 elif cext == 'bz2':
125                     decompressor = 'bzip2'
126                 elif cext == None:
127                     decompressor = None
128                 else:
129                     raise ValueError, \
130                           "Don't know how to decompress %s files" \
131                           % cext
132
133                 if not decompressor is None:
134                     if subprocess.call([decompressor, '-d', '-q', '-f',
135                                        tempfile]) == 1:
136                         raise RuntimeError, \
137                               "Something went wrong while decompressing '%s'" \
138                               % tempfile
139
140                 # move decompressed file into cache
141                 shutil.move(os.path.splitext(tempfile)[0], cfilename)
142
143                 # XXX do we need that if explicit filename is provided?
144                 urllib.urlcleanup()
145
146         # open cached file
147         fh = open(cfilename, 'r')
148
149         return fh
150
151
152
153
154 class DebianPkgArchive(SafeConfigParser):
155     """
156     """
157     def __init__(self, cache=None, init_db=None):
158         """
159         :Parameter:
160         """
161         SafeConfigParser.__init__(self)
162
163         # read an existing database if provided
164         if not init_db is None:
165             self.read(init_db)
166
167         # use provided file cache or use fresh one
168         if not cache is None:
169             self.cache = cache
170         else:
171             self.cache = AptListsCache()
172
173         # init debtags DB
174         self.dtags = debtags.DB()
175         self.dtags.read(open('/var/lib/debtags/package-tags'))
176
177         # init package filter
178         self.pkgfilter = None
179
180         self._updateReleases()
181
182
183     def _updateReleases(self):
184         self.releases = {}
185
186         for p in self.sections():
187             if not self.has_option(p, 'releases'):
188                 continue
189
190             # for all releases of this package
191             for r in \
192               [rel.strip() for rel in self.get(p, 'releases').split(',')]:
193                   # push release code
194                   if not self.releases.has_key(r):
195                       self.releases[r] = []
196
197                   # store component
198                   component = self.get(p, 'component %s' % r)
199
200                   if not component in self.releases[r]:
201                       self.releases[r].append(component)
202
203
204     def __repr__(self):
205         """Generate INI file content for current content.
206         """
207         # make adaptor to use str as file-like (needed for ConfigParser.write()
208         class file2str(object):
209             def __init__(self):
210                 self.__s = ''
211             def write(self, val):
212                 self.__s += val
213             def str(self):
214                 return self.__s
215
216         r = file2str()
217         self.write(r)
218
219         return r.str()
220
221
222     def save(self, filename):
223         """Write current content to a file.
224         """
225         f = open(filename, 'w')
226         self.write(f)
227         f.close()
228
229
230     def ensureUnique(self, section, option, value):
231         if not self.has_option(section, option):
232             self.set(section, option, value)
233         else:
234             if not self.get(section, option) == value:
235                 raise ValueError, "%s: %s is not unique (%s != %s)" \
236                                   % (section, option,
237                                      self.get(section, option), value)
238
239
240     def appendUniqueCSV(self, section, option, value):
241         """
242         """
243         if not self.has_option(section, option):
244             self.set(section, option, value)
245         else:
246             l = self.get(section, option).split(', ')
247             if not value in l:
248                 self.set(section, option, ', '.join(l + [value]))
249
250
251     def importRelease(self, rurl, force_update=False):
252         # root URL of the repository
253         baseurl = '/'.join(rurl.split('/')[:-1])
254         # get the release file from the cache
255         release_file = self.cache.get(rurl, update=force_update)
256
257         # create parser instance
258         rp = deb822.Release(release_file)
259
260         # architectures on this dist
261         archs = rp['Architectures'].split()
262         components = rp['Components'].split()
263         # compile a new codename that also considers the repository label
264         # to distinguish between official and unofficial repos.
265         codename = '_'.join([rp['Label'], rp['Codename']])
266
267         # compile the list of Packages files to parse and parse them
268         for c in components:
269             for a in archs:
270                 # compile packages URL
271                 pkgsurl = '/'.join([baseurl, c, 'binary-' + a, 'Packages.bz2'])
272
273                 # retrieve from cache
274                 packages_file = self.cache.get(pkgsurl,
275                                                update=force_update)
276
277                 # parse
278                 self._parsePkgsFile(packages_file, codename, c, baseurl)
279
280                 # cleanup
281                 packages_file.close()
282
283         # cleanup
284         release_file.close()
285
286         self._updateReleases()
287
288
289     def _parsePkgsFile(self, fh, codename, component, baseurl):
290         """
291         :Parameters:
292           fh: file handler
293             Packages list file
294           codename: str
295             Codename of the release
296           component: str
297             The archive component this packages file corresponds to.
298         """
299         for stanza in deb822.Packages.iter_paragraphs(fh):
300             self._storePkg(stanza, codename, component, baseurl)
301
302
303     def _storePkg(self, st, codename, component, baseurl):
304         """
305         :Parameter:
306           st: Package section
307         """
308         pkg = st['Package']
309
310         if not self.has_section(pkg):
311             self.add_section(pkg)
312
313         # do nothing if package is not in filter if there is any
314         if not self.pkgfilter is None and not pkg in self.pkgfilter:
315             self.ensureUnique(pkg, 'visibility', 'shadowed')
316         else:
317             self.ensureUnique(pkg, 'visibility', 'featured')
318
319         # which releases
320         self.appendUniqueCSV(pkg, "releases", codename)
321
322         # arch listing
323         self.appendUniqueCSV(pkg, "archs %s" % codename, st['Architecture'])
324
325         # versions
326         self.ensureUnique(pkg,
327                           "version %s %s" % (codename, st['Architecture']),
328                           st['Version'])
329
330         # link to .deb
331         self.ensureUnique(pkg,
332                           "file %s %s" % (codename, st['Architecture']),
333                           '/'.join(baseurl.split('/')[:-2] + [st['Filename']]))
334
335         # component
336         self.ensureUnique(pkg, 'component ' + codename, component)
337
338         # store the pool url
339         self.ensureUnique(pkg, "poolurl %s" % codename,
340                  '/'.join(baseurl.split('/')[:-2] \
341                          + [os.path.dirname(st['Filename'])]))
342
343
344         # now the stuff where a single variant is sufficient and where we go for
345         # the latest available one
346         if self.has_option(pkg, "newest version") \
347             and apt.VersionCompare(st['Version'],
348                                    self.get(pkg, "newest version")) < 0:
349             return
350
351         # everything from here will overwrite existing ones
352
353         # we seems to have an updated package
354         self.set(pkg, "newest version", st['Version'])
355
356         # description
357         self.set(pkg, "description", st['Description'].replace('%', '%%'))
358
359         # maintainer
360         self.set(pkg, "maintainer", st['Maintainer'])
361
362         # optional stuff
363         if st.has_key('Homepage'):
364             self.set(pkg, 'homepage', st['Homepage'])
365
366         # query debtags
367         debtags = self.dtags.tagsOfPackage(pkg)
368         if debtags:
369             self.set(pkg, 'debtags', ', '.join(debtags))
370
371
372     def writeSourcesLists(self, outdir):
373         createDir(outdir)
374         createDir(os.path.join(outdir, 'static'))
375
376         fl = open(os.path.join(outdir, 'sources_lists'), 'w')
377         for trans, r in sorted([(transCodename(k), k) 
378                 for k in self.releases.keys()]):
379             # need to turn 'apsy_lenny' back into 'lenny'
380             debneuro_r = r.split('_')[1]
381
382             f = open(os.path.join(outdir, 'static',
383                                   'debneuro.%s.sources.list' % debneuro_r),
384                      'w')
385             f.write("deb http://apsy.gse.uni-magdeburg.de/debian %s %s\n" \
386                     % (debneuro_r, ' '.join(self.releases[r])))
387             f.write("deb-src http://apsy.gse.uni-magdeburg.de/debian %s %s\n" \
388                     % (debneuro_r, ' '.join(self.releases[r])))
389             # XXX use :download: role from sphinx 0.6 on
390             #fl.write('* `%s <http://apsy.gse.uni-magdeburg.de/debian/html/_static/debneuro.%s.sources.list>`_\n' \
391             fl.write('* `%s <_static/debneuro.%s.sources.list>`_\n' \
392                      % (trans, debneuro_r))
393             f.close()
394         fl.close()
395
396
397     def importProspectivePkgsFromTaskFile(self, url):
398         fh = self.cache.get(url)
399
400         for st in deb822.Packages.iter_paragraphs(fh):
401             # do not stop unless we have a description
402             if not st.has_key('Pkg-Description'):
403                 continue
404
405             if st.has_key('Depends'):
406                 pkg = st['Depends']
407             elif st.has_key('Suggests'):
408                 pkg = st['Suggests']
409             else:
410                 print 'Warning: Cannot determine name of prospective package ' \
411                       '... ignoring.'
412                 continue
413
414             # store pkg info
415             if not self.has_section(pkg):
416                 self.add_section(pkg)
417
418             # prospective ones are always featured
419             self.ensureUnique(pkg, 'visibility', 'featured')
420
421             # pkg description
422             self.set(pkg, "description",
423                      st['Pkg-Description'].replace('%', '%%'))
424
425             # optional stuff
426             if st.has_key('Homepage'):
427                 self.set(pkg, 'homepage', st['Homepage'])
428
429             if st.has_key('Pkg-URL'):
430                 self.set(pkg, 'external pkg url', st['Pkg-URL'])
431
432             if st.has_key('WNPP'):
433                 self.set(pkg, 'wnpp debian', st['WNPP'])
434
435             if st.has_key('License'):
436                 self.set(pkg, 'license', st['License'])
437
438             # treat responsible as maintainer
439             if st.has_key('Responsible'):
440                 self.set(pkg, "maintainer", st['Responsible'])
441
442
443     def setPkgFilterFromTaskFile(self, urls):
444         pkgs = []
445
446         for task in urls:
447             fh = self.cache.get(task)
448
449
450             # loop over all stanzas
451             for stanza in deb822.Packages.iter_paragraphs(fh):
452                 if stanza.has_key('Depends'):
453                     pkg = stanza['Depends']
454                 elif stanza.has_key('Suggests'):
455                     pkg = stanza['Suggests']
456                 else:
457                     continue
458
459                 # account for multiple packages per line
460                 if pkg.count(','):
461                     pkgs += [p.strip() for p in pkg.split(',')]
462                 else:
463                     pkgs.append(pkg.strip())
464
465         # activate filter
466         self.pkgfilter = pkgs
467
468
469 def genPkgPage(db, pkg):
470     """
471     :Parameters:
472       db: database
473       pkg: str
474         Package name
475     """
476     descr = db.get(pkg, 'description').split('\n')
477
478     s = ''
479
480     # only put index markup for featured packages
481     if db.get(pkg, 'visibility') == 'featured':
482         s = '.. index:: %s, ' % pkg
483         s += '\n'
484
485         if db.has_option(pkg, 'debtags'):
486             # filter tags
487             tags = [t for t in db.get(pkg, 'debtags').split(', ')
488                         if t.split('::')[0] in ['field', 'works-with']]
489             if len(tags):
490                 s += '.. index:: %s\n\n' % ', '.join(tags)
491
492     # main ref target for this package
493     s += '.. _deb_' + pkg + ':\n'
494
495     # separate header from the rest
496     s += '\n\n\n'
497
498     header = '%s -- %s' % (pkg, descr[0])
499     s += '*' * (len(header) + 2)
500     s += '\n ' + header + '\n'
501     s += '*' * (len(header) + 2) + '\n\n'
502
503     # put description
504     s += '\n'.join([l.lstrip(' .') for l in descr[1:]])
505     s += '\n'
506
507     if db.has_option(pkg, 'homepage'):
508         s += '\n**Homepage**: %s\n' % db.get(pkg, 'homepage')
509
510     s += '\nBinary packages'\
511         '\n===============\n'
512
513     s += genMaintainerSection(db, pkg)
514
515     if db.has_option(pkg, 'wnpp debian'):
516         s += 'A Debian packaging effort has been officially announced. ' \
517              'Please see the corresponding `intent-to-package bug report`_ ' \
518              'for more information about its current status.\n\n' \
519              '.. _intent-to-package bug report: http://bugs.debian.org/%s\n\n' \
520              % db.get(pkg, 'wnpp debian')
521
522     s += genBinaryPackageSummary(db, pkg, 'DebNeuro repository')
523
524 #    if db.has_option(pkg, 'external pkg url'):
525 #        s += 'Other unofficial ressources\n' \
526 #             '---------------------------\n\n'
527 #        s += 'An unofficial package is available from %s\ .\n\n' \
528 #                % db.get(pkg, 'external pkg url')
529     return s
530
531
532 def genMaintainerSection(db, pkg):
533     s = ''
534
535     if not db.has_option(pkg, 'maintainer'):
536         s += '\nCurrently, nobody seems to be responsible for creating or ' \
537              'maintaining Debian packages of this software.\n\n'
538         return s
539
540     # there is someone responsible
541     maintainer = db.get(pkg, 'maintainer')
542
543     # do we have actual packages, or is it just a note
544     if not db.has_option(pkg, 'releases'):
545         s += '\nThere are currently no binary packages available. However, ' \
546              'the last known packaging effort was started by %s which ' \
547              'meanwhile might have led to an initial unofficial Debian ' \
548              'packaging.\n\n' % maintainer
549         return s
550
551     s += '\n**Maintainer**: %s\n\n' % maintainer
552
553     if not maintainer.startswith('Michael Hanke'):
554         s += '\n.. note::\n'
555         s += '  Do not contact the original package maintainer regarding ' \
556              '  bugs in this unofficial binary package. Instead, contact ' \
557              '  the repository maintainer at michael.hanke@gmail.com\ .'
558
559     return s
560
561
562 def genBinaryPackageSummary(db, pkg, reposname):
563     # do nothing if the are no packages
564     if not db.has_option(pkg, 'releases'):
565         return ''
566
567     s = '\n%s\n%s\n' % (reposname, '-' * len(reposname))
568
569     s += 'The repository contains binary packages for the following ' \
570          'distribution releases and system architectures. Note, that the ' \
571          'corresponding source packages are of course available too. Please ' \
572          'click on the release name to access them.\n\n'
573
574     # for all releases this package is part of
575     for rel in db.get(pkg, 'releases').split(', '):
576         # write release description and component
577         s += '\n`%s <%s>`_:\n  ' \
578                 % (transCodename(rel),
579                    db.get(pkg, 'poolurl %s' % rel))
580
581         s += '[%s] ' % db.get(pkg, 'component ' + rel)
582
583         # archs this package is available for
584         archs = db.get(pkg, 'archs ' + rel).split(', ')
585
586         # extract all present versions for any arch
587         versions =  [db.get(pkg, 'version %s %s' % (rel, arch))
588                         for arch in archs]
589
590         # if there is only a single version for all of them, simplify the list
591         single_ver = versions.count(versions[0]) == len(versions)
592
593         if single_ver:
594             # only one version string for all
595             s += ', '.join(['`%s <%s>`_' \
596                     % (arch, db.get(pkg, 'file %s %s' % (rel, arch)))
597                         for arch in archs])
598             s += ' (%s)' % versions[0]
599         else:
600             # a separate version string for each arch
601             s += ', '.join(['`%s <%s>`_ (%s)' \
602                     % (arch,
603                        db.get(pkg, 'file %s %s' % (rel, arch)),
604                        db.get(pkg, 'version %s %s' % (rel, arch)))
605                         for arch in archs])
606
607         s += '\n'
608
609     return s
610
611 def maintainer2email(maint):
612     return maint.split('<')[1].rstrip('>')
613
614
615 def writePkgsBy(db, key, value2id, outdir, heading):
616     createDir(outdir)
617     nwkey = key.replace(' ', '')
618     createDir(os.path.join(outdir, 'by%s' % nwkey))
619
620     collector = {}
621
622     # get packages by maintainer
623     for p in db.sections():
624         # only featured packages
625         if db.get(p, 'visibility') == 'shadowed':
626             continue
627
628         if db.has_option(p, key):
629             by = db.get(p, key)
630
631             if not collector.has_key(by):
632                 collector[by] = (value2id(by), [p])
633             else:
634                 collector[by][1].append(p)
635
636     toc = open(os.path.join(outdir, 'by%s.rst' % nwkey), 'w')
637     toc.write('.. index:: Packages by %s\n.. _by%s:\n\n' % (key, key))
638
639     toc_heading = 'Packages by %s' % key
640     toc.write('%s\n%s\n\n' % (toc_heading, '=' * len(toc_heading)))
641     toc.write('.. toctree::\n  :maxdepth: 1\n\n')
642
643     # summary page per maintainer
644     for by in sorted(collector.keys()):
645         toc.write('  by%s/%s\n' % (nwkey, collector[by][0]))
646
647         fh = open(os.path.join(outdir,
648                                'by%s' % nwkey,
649                                collector[by][0] + '.rst'), 'w')
650
651         fh.write('.. index:: %s\n.. _%s:\n\n' % (by, by))
652
653         hdr = heading.replace('<ITEM>', by)
654         fh.write(hdr + '\n')
655         fh.write('=' * len(hdr) + '\n\n')
656
657         # write sorted list of packages
658         for p in sorted(collector[by][1]):
659             fh.write('* :ref:`deb_%s`\n' % p)
660
661         fh.close()
662
663     toc.close()
664
665
666 def writeRst(db, outdir, addenum_dir=None):
667     createDir(outdir)
668     createDir(os.path.join(outdir, 'pkgs'))
669
670     # open pkgs toctree
671     toc = open(os.path.join(outdir, 'pkgs.rst'), 'w')
672     # write header
673     toc.write('.. _full_pkg_list:\n\n')
674     toc.write('Archive content\n===============\n\n'
675               '.. toctree::\n  :maxdepth: 1\n\n')
676
677     for p in sorted(db.sections()):
678         print "Generating page for '%s'" % p
679         pf = open(os.path.join(outdir, 'pkgs', '%s.rst' % p), 'w')
680         pf.write(genPkgPage(db, p))
681
682         # check for doc addons
683         if addenum_dir is not None:
684             addenum = os.path.join(os.path.abspath(addenum_dir), '%s.rst' % p)
685             if os.path.exists(addenum):
686                 pf.write('\n\n.. include:: %s\n' % addenum)
687         pf.close()
688         toc.write('  pkgs/%s\n' % p)
689
690
691     toc.close()
692
693
694 def prepOptParser(op):
695     # use module docstring for help output
696     op.usage = "%s [OPTIONS]\n\n" % sys.argv[0] + __doc__
697
698     op.add_option("--db",
699                   action="store", type="string", dest="db",
700                   default=None,
701                   help="Database file to read. Default: None")
702
703     op.add_option("-o", "--outdir",
704                   action="store", type="string", dest="outdir",
705                   default=None,
706                   help="Target directory for ReST output. Default: None")
707
708     op.add_option("-r", "--release-url",
709                   action="append", dest="release_urls",
710                   help="None")
711
712     op.add_option("-t", "--taskfile-url",
713                   action="append", dest="taskfile_urls",
714                   help="None")
715
716     op.add_option("-f", "--featured",
717                   action="append", dest="featured_pkgs",
718                   help="None")
719
720     op.add_option("-p", "--prospective",
721                   action="append", dest="prospective_pkgs",
722                   help="None")
723
724     op.add_option("--pkgaddenum", action="store", dest="addenum_dir",
725                   type="string", default=None, help="None")
726
727
728 def main():
729     op = OptionParser(version="%prog 0.0.1")
730     prepOptParser(op)
731
732     (opts, args) = op.parse_args()
733
734     dpa = DebianPkgArchive(init_db=opts.db)
735
736     if not opts.taskfile_urls is None:
737         dpa.setPkgFilterFromTaskFile(opts.taskfile_urls)
738
739     if not opts.featured_pkgs is None:
740         dpa.pkgfilter += opts.featured_pkgs
741
742     if not opts.prospective_pkgs is None:
743         for p in opts.prospective_pkgs:
744             dpa.importProspectivePkgsFromTaskFile(p)
745
746     if not opts.release_urls is None:
747         for rurl in opts.release_urls:
748             dpa.importRelease(rurl, force_update=False)
749
750     if not opts.outdir is None:
751         dpa.writeSourcesLists(opts.outdir)
752         writeRst(dpa, opts.outdir, opts.addenum_dir)
753         writePkgsBy(dpa, 'maintainer', maintainer2email, opts.outdir,
754                     'Packages maintained by <ITEM>')
755
756     if not opts.db is None:
757         dpa.save(opts.db)
758
759
760 if __name__ == "__main__":
761     main()
762