]> git.donarmstrong.com Git - neurodebian.git/blob - tools/blends-inject
working version of blends-inject, without VCS links though yet
[neurodebian.git] / tools / blends-inject
1 #!/usr/bin/python
2 #emacs: -*- mode: python-mode; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*-
3 #ex: set sts=4 ts=4 sw=4 noet:
4 """Script to help maintaining package definitions within Debian blends
5 task files.
6
7 Often it becomes necessary to duplicate the same information about a
8 package within debian packaging and multiple blends task files,
9 possibly of different blends.  This script allows to automate:
10
11 - construction of entries
12 - injection into task files
13 - modification of existing entries if things changed
14 - removal of previously injected entries
15
16 Possible TODOs:
17 ---------------
18
19 Whenever processing multiple files, figure out topdir automatically,
20 so we could do
21
22 blends-inject */debian/blends
23
24 """
25
26 """
27 Configuration
28 -------------
29
30 Paths to the blends top directories, containing tasks directories are
31 specified in ~/.blends-inject.cfg file, e.g.::
32
33  [debian-med]
34  path = /home/yoh/deb/debian-med/
35
36  [debian-science]
37  path = /home/yoh/deb/debian-science/
38
39
40 Definition of the fields for task files by default are looked up
41 within debian/blends, or files provided in the command line.
42
43 Format of debian/blends
44 -----------------------
45
46 TODO:
47
48 """
49
50
51 import re, os, sys
52 from os.path import join, exists, expanduser, dirname, basename
53
54 from ConfigParser import ConfigParser
55 from optparse import OptionParser, Option
56
57 from copy import deepcopy
58 #from debian_bundle import deb822
59 from debian import deb822
60 from debian.changelog import Changelog
61
62 __author__ = 'Yaroslav Halchenko'
63 __prog__ = os.path.basename(sys.argv[0])
64 __version__ = '0.0.1'
65 __copyright__ = 'Copyright (c) 2010 Yaroslav Halchenko'
66 __license__ = 'GPL'
67
68 # What fields initiate new package description
69 PKG_FIELDS = ('depends', 'recommends', 'suggests', 'ignore', 'removed')
70
71 # We might need to resort to assure some what a canonical order
72 FIELDS_ORDER = ('depends', 'recommends', 'suggests', 'ignore',
73                 'homepage', 'language', 'wnpp', 'responsible', 'license',
74                 'vcs-', 'pkg-url', 'pkg-description',
75                 'published-', 'x-', 'registration', 'remark')
76
77 verbosity = None
78
79 def error(msg, exit_code):
80     sys.stderr.write(msg + '\n')
81     sys.exit(exit_code)
82
83 def verbose(level, msg):
84     if level <= verbosity:
85         print " "*level, msg
86
87
88 def parse_debian_blends(f='debian/blends'):
89     """Parses debian/blends file
90
91     Returns unprocessed list of customized Deb822 entries
92     """
93     # Linearize all the paragraphs since we are not using them
94     items = []
95     for p in deb822.Deb822.iter_paragraphs(open(f)):
96         items += p.items()
97
98     # Traverse and collect things
99     format_ = 'plain'
100     format_clean = False # do not propagate fields into a new pkg if True
101     pkg, source = None, None
102     pkgs = []
103     tasks = []
104
105     for k, v in items:
106         kl = k.lower()
107         if kl == 'source':
108             source = v.strip()
109         elif kl == 'format':
110             format_ = v.strip()
111             format_clean = format_.endswith('-clean')
112             if format_clean:
113                 format_ = format_[:-6]
114         elif kl == 'tasks':
115             tasks = v.split(',')
116             newtasks = True                 # either we need to provide tune-ups
117                                             # for current package
118         elif kl in PKG_FIELDS: # new package
119             if format_clean or pkg is None:
120                 pkg = deb822.Deb822()
121             else:
122                 pkg = deepcopy(pkg)
123                 for k_ in PKG_FIELDS:   # prune older depends
124                     pkg.pop(k_, None)
125             pkg['Pkg-Name'] = pkg[k] = v
126             if source is None:
127                 source = v
128             pkg['Pkg-Source'] = source
129             pkgs.append(pkg)
130             pkg.tasks = dict( (t.strip(), deb822.OrderedSet()) for t in tasks )
131             pkg.format = format_
132             newtasks = False
133         else:
134             if newtasks:
135                 # Add customization
136                 for t in tasks:
137                     if not t in pkg.tasks:
138                         pkg.tasks[t] = deb822.Deb822Dict()
139                     pkg.tasks[t][k] = v
140             else:
141                 # just store the key in the pkg itself
142                 pkg[k] = v
143     return pkgs
144
145
146 def expand_pkgs(pkgs, topdir='.'):
147     """In-place modification of pkgs taking if necessary additional
148     information from Debian materials, and pruning empty fields
149     """
150     debianm = None
151     # Expand packages which format is extended
152     for pkg in pkgs:
153         if pkg.format == 'extended':
154             # expanding, for that we need debian/control
155             if debianm is None:
156                 debianm = DebianMaterials(topdir)
157             for k, m in (('License', lambda: debianm.get_license(pkg['Pkg-Name'])),
158                          ('WNPP', debianm.get_wnpp),
159                          ('Pkg-description',
160                           lambda: debianm.binaries[pkg['Pkg-Name']]['Description']),
161                          ('Responsible', debianm.get_responsible),
162                          ('Homepage', lambda: debianm.source.get('Homepage', None))):
163                 if pkg.get(k, None):
164                     continue
165                 v = m()
166                 if v:
167                     pkg[k] = v
168             # VCS fields
169             pkg.update(debianm.get_vcsfields())
170
171 def key_prefix_compare(x, y, order, strict=False, case=False):
172     """Little helper to help with sorting
173
174     Sorts according to the order of string prefixes as given by
175     `order`.  If `strict`, then if no matching prefix found, would
176     raise KeyError; otherwise provides least priority to those keys
177     which were not found in `order`
178     """
179     if not case:
180         order = [v.lower() for v in order]
181
182     def prefix_index(t, order, strict=True, case=False):
183         x = t[0]
184         if not case:
185             x = x.lower()
186         for i, v in enumerate(order):
187             if x.startswith(v):
188                 return i
189
190         if strict:
191             raise IndexError(
192                 "Could not find location for %s as specified by %s" %
193                 (x, order))
194         return 10000                    #  some large number ;)
195
196     cmp_res =  cmp(prefix_index(x, order, strict, case),
197                    prefix_index(y, order, strict, case))
198     if not cmp_res:                     # still unknown
199         return cmp(x, y)
200     return cmp_res
201
202
203 def group_packages_into_tasks(pkgs):
204     """Given a list of packages (with .tasks) group them per each
205     task and perform necessary customizations stored in .tasks
206     """
207     # Time to take care about packages and tasks
208     # Unroll pkgs into a collection of pkgs per known task
209     tasks = {}
210     for pkg in pkgs:
211         # Lets just create deepcopies with tune-ups for each task
212         for itask, (task, fields) in enumerate(pkg.tasks.iteritems()):
213             pkg_ = deepcopy(pkg)
214             pkg_.update(fields)
215
216             # Perform string completions and removals
217             for k,v in pkg_.iteritems():
218                 pkg_[k] = v % pkg_
219                 if v is None or not len(v.strip()):
220                     pkg_.pop(k)
221
222             # Sort the fields according to FIELDS_ORDER. Unfortunately
223             # silly Deb822* cannot create from list of tuples, so will do
224             # manually
225             pkg__ = deb822.Deb822()
226             for k,v in sorted(pkg_.items(),
227                               cmp=lambda x, y:
228                               key_prefix_compare(x, y, order=FIELDS_ORDER)):
229                 pkg__[k] = v
230
231             # Move Pkg-source/name into attributes
232             pkg__.source = pkg__.pop('Pkg-Source')
233             pkg__.name = pkg__.pop('Pkg-name')
234
235             tasks[task] = tasks.get(task, []) + [pkg__]
236
237     return tasks
238
239 def inject_tasks(tasks, config):
240     # Now go through task files and replace/add entries
241     for task, pkgs in tasks.iteritems():
242         verbose(2, "Task %s with %d packages" % (task, len(pkgs)))
243         blend, puretask = task.split('/')
244         taskfile = join(config.get(blend, 'path'), 'tasks', puretask)
245
246         # Load the file
247         stats = dict(Added=[], Modified=[])
248         for pkg in pkgs:
249             msgs = {'Name': pkg.name.strip(), 'Action': None}
250             # Find either it is known to the task file already
251
252             # Load entirely so we could simply manipulate
253             entries = open(taskfile).readlines()
254             known = False
255             # We need to search by name and by source
256             # We need to search for every possible type of dependecy
257             regexp = re.compile('^ *(%s) *: *(%s) *$' %
258                                 ('|'.join(PKG_FIELDS),
259                                 '|'.join((pkg.name, pkg.source))),
260                                 re.I)
261             for istart, e in enumerate(entries):
262                 if regexp.search(e):
263                     verbose(4, "Found %s in position %i: %s" %
264                             (pkg.name, istart, e.rstrip()))
265                     known = True
266                     break
267
268             descr = ' ; Added by %s %s.  Modified manually: False\n' % \
269                     (__prog__,  __version__)
270             # Replace existing entry
271             if known:
272                 # TODO: Check if previous copy does not have our preceding comment
273                 # Find the previous end
274                 icount = 1
275                 try:
276                     while entries[istart+icount].strip() != '':
277                         icount += 1
278                 except IndexError, e:
279                     pass                # if we go beyond
280
281                 # Lets not change file without necessity, if entry is identical --
282                 # do nothing
283                 entry = pkg.dump()
284                 old_entry = entries[istart:istart+icount]
285
286                 if u''.join(old_entry) == entry:
287                    pass
288                 else: # Rewrite the entry
289                    if __prog__ in entries[istart-1]:
290                        istart -= 1
291                        icount += 2
292                    if not 'Removed' in pkg.keys():
293                        entries = entries[:istart] + [descr + entry] + entries[istart+icount:]
294                        msgs['Action'] = 'Changed'
295                    else:
296                        while entries[istart-1].strip() == '':
297                            istart -=1
298                            icount +=2
299                        entries = entries[:istart] + entries[istart+icount:]
300                        msgs['Action'] = 'Removed'
301                    open(taskfile, 'w').write(''.join(entries))
302             elif not 'removed' in pkg:  # or Append one
303                 msgs['Action'] = 'Added'
304                 # could be as simple as
305                 open(taskfile, 'a').write('\n%s%s' % (descr, pkg.dump(),))
306
307             if msgs['Action']:
308                 verbose(3, "%(Action)s %(Name)s" % msgs)
309
310
311 class DebianMaterials(object):
312     """Extract selected information from an existing debian/
313     """
314     _WNPP_RE = re.compile('^ *\* *Initial release.*closes:? #(?P<bug>[0-9]*).*', re.I)
315
316     def __init__(self, topdir):
317         #self.topdir = topdir
318         self._debiandir = join(topdir, 'debian')
319         self._source = None
320         self._binaries = None
321
322     @property
323     def source(self):
324         if self._source is None:
325             self._assign_packages()
326         return self._source
327
328     @property
329     def binaries(self):
330         if self._binaries is None:
331             self._assign_packages()
332         return self._binaries
333
334     def fpath(self, name):
335         return join(self._debiandir, name)
336
337     def _assign_packages(self):
338         try:
339             control = deb822.Deb822.iter_paragraphs(
340                 open(self.fpath('control')))
341         except Exception, e:
342             raise RuntimeError(
343                   "Cannot parse %s file necessary for the %s package entry. Error: %s"
344                   % (control_file, pkg['Pkg-Name'], str(e)))
345         self._binaries = {}
346         self._source = None
347         for v in control:
348             if v.get('Source', None):
349                 self._source = v
350             else:
351                 self._binaries[v['Package']] = v
352
353     def get_license(self, package=None, first_only=True):
354         """Return a license(s). Parsed out from debian/copyright if it is
355         in machine readable format
356         """
357         licenses = []
358         # may be package should carry custom copyright file
359         copyright_file_ = self.fpath('%s.copyright' % package)
360         if package and exists(copyright_file_):
361             copyright_file = copyright_file_
362         else:
363             copyright_file = self.fpath('copyright')
364
365         try:
366             for p in deb822.Deb822.iter_paragraphs(open(copyright_file)):
367                 if not 'Files' in p or p['Files'].strip().startswith('debian/'):
368                     continue
369                 l = p['License']
370                 # Take only the short version of first line
371                 l = re.sub('\n.*', '', l).strip()
372                 if not len(l):
373                     l = 'custom'
374                 if not l in licenses:
375                     licenses.append(l)
376                     if first_only:
377                         break
378         except Exception, e:
379             # print e
380             return None
381         return ', '.join(licenses)
382
383     def get_wnpp(self):
384         """Search for a template changelog entry closing "Initial bug
385         """
386         for l in open(self.fpath('changelog')):
387             rr = self._WNPP_RE.match(l)
388             if rr:
389                 return rr.groupdict()['bug']
390         return None
391
392     def get_responsible(self):
393         """Returns responsible, atm -- maintainer
394         """
395         return self.source['Maintainer']
396
397     def get_vcsfields(self):
398         vcs = deb822.Deb822()
399         for f,v in self._source.iteritems():
400             if f.lower().startswith('vcs-'):
401                 vcs[f] = v
402         return vcs
403
404
405 def main():
406
407     p = OptionParser(
408                 usage="%s [OPTIONS] [blends_files]\n\n" % __prog__ + __doc__,
409                 version="%prog " + __version__)
410
411     p.add_option(
412         Option("-d", "--topdir", action="store",
413                dest="topdir", default=None,
414                help="Top directory of a Debian package. It is used to locate "
415                "'debian/blends' if none is specified, and where to look for "
416                "extended information."))
417
418     p.add_option(
419         Option("-c", "--config-file", action="store",
420                dest="config_file", default=join(expanduser('~'), '.%s.cfg' % __prog__),
421                help="Noise level."))
422
423     p.add_option(
424         Option("-v", "--verbosity", action="store", type="int",
425                dest="verbosity", default=1, help="Noise level."))
426
427     (options, infiles) = p.parse_args()
428     global verbosity; verbosity = options.verbosity
429
430     if not len(infiles):
431         infiles = [join(options.topdir or './', 'debian/blends')]     #  default one
432
433     # Load configuration
434     config = ConfigParser()
435     config.read(options.config_file)
436
437     for blends_file in infiles:
438         verbose(1, "Processing %s" % blends_file)
439         if not exists(blends_file):
440             error("Cannot find a file %s.  Either provide a file or specify top "
441                   "debian directory with -d." % blends_file, 1)
442         pkgs = parse_debian_blends(blends_file)
443         if options.topdir is None:
444             if dirname(blends_file).endswith('/debian'):
445                 topdir = dirname(dirname(blends_file))
446             else:
447                 topdir = '.'            # and hope for the best
448         else:
449             topdir = options.topdir
450         expand_pkgs(pkgs, topdir=topdir)
451         tasks = group_packages_into_tasks(pkgs)
452         inject_tasks(tasks, config)
453
454     #for t,v in tasks.iteritems():
455     #    print "-------TASK: ", t
456     #    print ''.join(str(t_) for t_ in v)
457     #print pkgs[0]
458     #print tasks['debian-med/documentation'][0]
459
460 if __name__ == '__main__':
461     main()
462