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
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:
11 - construction of entries
12 - injection into task files
13 - modification of existing entries if things changed
14 - removal of previously injected entries
19 Whenever processing multiple files, figure out topdir automatically,
22 blends-inject */debian/blends
30 Paths to the blends top directories, containing tasks directories are
31 specified in ~/.blends-inject.cfg file, e.g.::
34 path = /home/yoh/deb/debian-med/
37 path = /home/yoh/deb/debian-science/
40 Definition of the fields for task files by default are looked up
41 within debian/blends, or files provided in the command line.
43 Format of debian/blends
44 -----------------------
52 from os.path import join, exists, expanduser, dirname, basename
54 from ConfigParser import ConfigParser
55 from optparse import OptionParser, Option
57 from copy import deepcopy
58 #from debian_bundle import deb822
59 from debian import deb822
60 from debian.changelog import Changelog
62 __author__ = 'Yaroslav Halchenko'
63 __prog__ = os.path.basename(sys.argv[0])
65 __copyright__ = 'Copyright (c) 2010 Yaroslav Halchenko'
68 # What fields initiate new package description
69 PKG_FIELDS = ('depends', 'recommends', 'suggests', 'ignore', 'removed')
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')
79 def error(msg, exit_code):
80 sys.stderr.write(msg + '\n')
83 def verbose(level, msg):
84 if level <= verbosity:
88 def parse_debian_blends(f='debian/blends'):
89 """Parses debian/blends file
91 Returns unprocessed list of customized Deb822 entries
93 # Linearize all the paragraphs since we are not using them
95 for p in deb822.Deb822.iter_paragraphs(open(f)):
98 # Traverse and collect things
100 format_clean = False # do not propagate fields into a new pkg if True
101 pkg, source = None, None
111 format_clean = format_.endswith('-clean')
113 format_ = format_[:-6]
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()
123 for k_ in PKG_FIELDS: # prune older depends
125 pkg['Pkg-Name'] = pkg[k] = v
128 pkg['Pkg-Source'] = source
130 pkg.tasks = dict( (t.strip(), deb822.OrderedSet()) for t in tasks )
137 if not t in pkg.tasks:
138 pkg.tasks[t] = deb822.Deb822Dict()
141 # just store the key in the pkg itself
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
151 # Expand packages which format is extended
153 if pkg.format == 'extended':
154 # expanding, for that we need debian/control
156 debianm = DebianMaterials(topdir)
157 for k, m in (('License', lambda: debianm.get_license(pkg['Pkg-Name'])),
158 ('WNPP', debianm.get_wnpp),
160 lambda: debianm.binaries[pkg['Pkg-Name']]['Description']),
161 ('Responsible', debianm.get_responsible),
162 ('Homepage', lambda: debianm.source.get('Homepage', None))):
169 pkg.update(debianm.get_vcsfields())
171 def key_prefix_compare(x, y, order, strict=False, case=False):
172 """Little helper to help with sorting
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`
180 order = [v.lower() for v in order]
182 def prefix_index(t, order, strict=True, case=False):
186 for i, v in enumerate(order):
192 "Could not find location for %s as specified by %s" %
194 return 10000 # some large number ;)
196 cmp_res = cmp(prefix_index(x, order, strict, case),
197 prefix_index(y, order, strict, case))
198 if not cmp_res: # still unknown
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
207 # Time to take care about packages and tasks
208 # Unroll pkgs into a collection of pkgs per known task
211 # Lets just create deepcopies with tune-ups for each task
212 for itask, (task, fields) in enumerate(pkg.tasks.iteritems()):
216 # Perform string completions and removals
217 for k,v in pkg_.iteritems():
219 if v is None or not len(v.strip()):
222 # Sort the fields according to FIELDS_ORDER. Unfortunately
223 # silly Deb822* cannot create from list of tuples, so will do
225 pkg__ = deb822.Deb822()
226 for k,v in sorted(pkg_.items(),
228 key_prefix_compare(x, y, order=FIELDS_ORDER)):
231 # Move Pkg-source/name into attributes
232 pkg__.source = pkg__.pop('Pkg-Source')
233 pkg__.name = pkg__.pop('Pkg-name')
235 tasks[task] = tasks.get(task, []) + [pkg__]
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)
247 stats = dict(Added=[], Modified=[])
249 msgs = {'Name': pkg.name.strip(), 'Action': None}
250 # Find either it is known to the task file already
252 # Load entirely so we could simply manipulate
253 entries = open(taskfile).readlines()
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))),
261 for istart, e in enumerate(entries):
263 verbose(4, "Found %s in position %i: %s" %
264 (pkg.name, istart, e.rstrip()))
268 descr = ' ; Added by %s %s. Modified manually: False\n' % \
269 (__prog__, __version__)
270 # Replace existing entry
272 # TODO: Check if previous copy does not have our preceding comment
273 # Find the previous end
276 while entries[istart+icount].strip() != '':
278 except IndexError, e:
279 pass # if we go beyond
281 # Lets not change file without necessity, if entry is identical --
284 old_entry = entries[istart:istart+icount]
286 if u''.join(old_entry) == entry:
288 else: # Rewrite the entry
289 if __prog__ in entries[istart-1]:
292 if not 'Removed' in pkg.keys():
293 entries = entries[:istart] + [descr + entry] + entries[istart+icount:]
294 msgs['Action'] = 'Changed'
296 while entries[istart-1].strip() == '':
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(),))
308 verbose(3, "%(Action)s %(Name)s" % msgs)
311 class DebianMaterials(object):
312 """Extract selected information from an existing debian/
314 _WNPP_RE = re.compile('^ *\* *Initial release.*closes:? #(?P<bug>[0-9]*).*', re.I)
316 def __init__(self, topdir):
317 #self.topdir = topdir
318 self._debiandir = join(topdir, 'debian')
320 self._binaries = None
324 if self._source is None:
325 self._assign_packages()
330 if self._binaries is None:
331 self._assign_packages()
332 return self._binaries
334 def fpath(self, name):
335 return join(self._debiandir, name)
337 def _assign_packages(self):
339 control = deb822.Deb822.iter_paragraphs(
340 open(self.fpath('control')))
343 "Cannot parse %s file necessary for the %s package entry. Error: %s"
344 % (control_file, pkg['Pkg-Name'], str(e)))
348 if v.get('Source', None):
351 self._binaries[v['Package']] = v
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
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_
363 copyright_file = self.fpath('copyright')
366 for p in deb822.Deb822.iter_paragraphs(open(copyright_file)):
367 if not 'Files' in p or p['Files'].strip().startswith('debian/'):
370 # Take only the short version of first line
371 l = re.sub('\n.*', '', l).strip()
374 if not l in licenses:
381 return ', '.join(licenses)
384 """Search for a template changelog entry closing "Initial bug
386 for l in open(self.fpath('changelog')):
387 rr = self._WNPP_RE.match(l)
389 return rr.groupdict()['bug']
392 def get_responsible(self):
393 """Returns responsible, atm -- maintainer
395 return self.source['Maintainer']
397 def get_vcsfields(self):
398 vcs = deb822.Deb822()
399 for f,v in self._source.iteritems():
400 if f.lower().startswith('vcs-'):
408 usage="%s [OPTIONS] [blends_files]\n\n" % __prog__ + __doc__,
409 version="%prog " + __version__)
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."))
419 Option("-c", "--config-file", action="store",
420 dest="config_file", default=join(expanduser('~'), '.%s.cfg' % __prog__),
421 help="Noise level."))
424 Option("-v", "--verbosity", action="store", type="int",
425 dest="verbosity", default=1, help="Noise level."))
427 (options, infiles) = p.parse_args()
428 global verbosity; verbosity = options.verbosity
431 infiles = [join(options.topdir or './', 'debian/blends')] # default one
434 config = ConfigParser()
435 config.read(options.config_file)
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))
447 topdir = '.' # and hope for the best
449 topdir = options.topdir
450 expand_pkgs(pkgs, topdir=topdir)
451 tasks = group_packages_into_tasks(pkgs)
452 inject_tasks(tasks, config)
454 #for t,v in tasks.iteritems():
455 # print "-------TASK: ", t
456 # print ''.join(str(t_) for t_ in v)
458 #print tasks['debian-med/documentation'][0]
460 if __name__ == '__main__':