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