deb2spectacle 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. #!/usr/bin/python -tt
  2. # vim: ai ts=4 sts=4 et sw=4
  3. # Copyright (c) 2009 Intel Corporation
  4. #
  5. # This program is free software; you can redistribute it and/or modify it
  6. # under the terms of the GNU General Public License as published by the Free
  7. # Software Foundation; version 2 of the License
  8. #
  9. # This program is distributed in the hope that it will be useful, but
  10. # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
  11. # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
  12. # for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License along
  15. # with this program; if not, write to the Free Software Foundation, Inc., 59
  16. # Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  17. """
  18. Overview of deb2spectacle:
  19. Tool to convert debian file to spectacle compatibile YAML/spec
  20. The input files can be:
  21. """
  22. INPUT_SRC="""
  23. 1. "control" file for only package metainfo
  24. 2. "debian/" directory that contains all the deb files
  25. 3. tarball "debian.tar.gz" which contain all the deb files
  26. """
  27. #TODO: 4. gzipped diff file with names *.diff.gz which include all the content of deb files
  28. #TODO: 5. binary .deb support using pure python parser
  29. import os, sys
  30. import re
  31. import string
  32. # spectacle modules
  33. from spectacle.convertor import *
  34. from spectacle.dumper import *
  35. from spectacle import logger
  36. from spectacle import deb822
  37. from spectacle.__version__ import VERSION
  38. # global constants
  39. DEBTARBALL = 'debian.tar.gz'
  40. class DebConvertor(Convertor):
  41. """ Convertor for deb keys to yaml ones """
  42. def __init__(self):
  43. deb_cv_table = {
  44. 'Section': 'Group',
  45. 'Build-Depends': 'PkgBR',
  46. 'Build-Depends-Indep': 'PkgBR',
  47. 'Build-Conflicts': 'BuildConflicts',
  48. 'Build-Conflicts-Indep': 'BuildConflicts',
  49. 'Depends': 'Requires',
  50. 'Pre-Depends': 'RequiresPre',
  51. 'Pre-Depends': 'RequiresPre',
  52. 'Suggests': 'Requires', # FIXME
  53. 'Recommends': 'Requires', # FIXME
  54. 'Replaces': 'Obsoletes',
  55. 'Breaks': 'Conflicts',
  56. 'Homepage': 'URL',
  57. 'Architecture': 'BuildArch',
  58. 'Vcs-Git': 'SCM',
  59. 'Vcs-Svn': 'SCM',
  60. 'Vcs-Cvs': 'SCM',
  61. 'Vcs-Hg': 'SCM',
  62. }
  63. self.re_debvar = re.compile('\$\{([^}]+)\}', re.M)
  64. self.debvars = {
  65. 'shlibs:Depends': '', # spec autodep/autopro can handle it
  66. 'shlibs:Pre-Depends': '',
  67. 'misc:Depends': '',
  68. 'python:Depends': 'python',
  69. 'python:Provides': '',
  70. 'perl:Depends': 'perl',
  71. 'binary:Version': '%{version}-%{release}',
  72. 'source:Version': '%{version}-%{release}',
  73. 'Source-Version': '%{version}-%{release}', #legacy one
  74. # TODO more
  75. }
  76. self.depkeys = (
  77. 'PkgBR',
  78. 'Requires',
  79. 'RequiresPre',
  80. 'Provides',
  81. 'Obsoletes',
  82. 'Conflicts',
  83. 'BuildConflicts',
  84. )
  85. self.re_userfield = re.compile('^X[BCS]+-.*')
  86. self.dropkeys = (
  87. 'Maintainer',
  88. 'Uploaders',
  89. 'Changed-By',
  90. 'Priority',
  91. 'Essential',
  92. 'Standards-Version',
  93. 'Vcs-Browser',
  94. 'Vcs-Bzr'
  95. )
  96. # regex of depends compare
  97. self.re_depcomp = re.compile('\(([<>=]+)\s*([^)]*)\)')
  98. # regex of depends about os and arch
  99. self.re_deposarch = re.compile('(.*)\[[^]]*\](.*)$')
  100. Convertor.__init__(self, deb_cv_table)
  101. def _replace_var(self, val):
  102. if isinstance(val, list):
  103. return val
  104. while self.re_debvar.search(val):
  105. nval = val
  106. for m in self.re_debvar.finditer(val):
  107. var = m.group(1)
  108. if var in self.debvars:
  109. rep = self.debvars[var]
  110. nval = nval.replace('${%s}' % var, rep)
  111. if val == nval:
  112. break
  113. val = nval
  114. return val
  115. def _conv_dep(self, single):
  116. def comp_replace(dep):
  117. if not dep:
  118. return None
  119. dep = dep.strip()
  120. m = self.re_deposarch.search(dep)
  121. if m:
  122. (old, name, rest) = m.group(0, 1, 2)
  123. dep = dep.replace(old,'%s %s' %(name, rest))
  124. m = self.re_depcomp.search(dep)
  125. if m:
  126. (old, op, ver) = m.group(0, 1, 2)
  127. if op == '>>':
  128. op = '>'
  129. elif op == '<<':
  130. op = '<'
  131. dep = dep.replace(old, '%s %s' %(op, ver))
  132. return dep
  133. return filter(lambda x:x, map(comp_replace, str(single).split(',')))
  134. def _translate_keys(self, items):
  135. """ override parent class method for pre-processing
  136. """
  137. #! at this point, all keys name have been translated
  138. # drop debian only keys
  139. for key in items.keys():
  140. if key in self.dropkeys or self.re_userfield.match(key):
  141. del items[key]
  142. # split 'Description' to 'Summary' + 'Description'
  143. lines = items['Description'].splitlines()
  144. items['Summary'] = lines[0]
  145. items['Description'] = '\n'.join(lines[1:])
  146. # expand all debian variables
  147. for key, val in items.iteritems():
  148. items[key] = self._replace_var(val)
  149. # depends keys handling
  150. for key, val in items.iteritems():
  151. if key in self.depkeys:
  152. items[key] = self._conv_dep(val)
  153. # architecture translate
  154. if 'BuildArch' in items:
  155. if items['BuildArch'] == 'any':
  156. items['BuildArch'] = 'noarch'
  157. # otherwise, no BuildArch needed
  158. del items['BuildArch']
  159. Convertor._translate_keys(self, items)
  160. class DebInfo:
  161. """ Container of package information from debian files """
  162. def __init__(self, binver, license):
  163. self.fields = {'Version': binver,
  164. 'License': license,
  165. 'Release': '1',
  166. }
  167. self.pkgname = None
  168. self.sec_section = None
  169. def _get_subpkgname(self, raw):
  170. if raw.startswith(self.pkgname):
  171. left = raw.lstrip(self.pkgname)
  172. try:
  173. int(left)
  174. # should be main pkg
  175. return self.pkgname
  176. except ValueError:
  177. # not pure number string
  178. if left.startswith('-'):
  179. # sub packages with main-name prefix
  180. subname = left.lstrip('-')
  181. if subname == 'dev':
  182. # the rpm tradition
  183. return 'devel'
  184. else:
  185. return subname
  186. # return orignal value without special recog
  187. return raw
  188. def format_filelist(self, filelist):
  189. """ format the debian filelist format
  190. """
  191. ret = []
  192. for file in filelist:
  193. file = string.replace (file, 'debian/tmp', '')
  194. file = file.strip()
  195. if file.find (' ') != -1:
  196. base = file.split (' ')[0].split ('/')[-1]
  197. dir = file.split (' ')[1]
  198. file = '%s/%s' % (dir, base)
  199. ret.append (file)
  200. return ret
  201. def feed(self, ctl, rules=None):
  202. """ feed in the file content of:
  203. ctl: "control"
  204. rules: "rules", optional
  205. """
  206. for para in deb822.Deb822.iter_paragraphs(ctl.splitlines()):
  207. items = dict(para.items())
  208. if 'Source' in items:
  209. # Source paragraph, and must be the 1st para
  210. items['Name'] = items['Source']
  211. del items['Source']
  212. items['Sources'] = '%s-%s.tar.gz' % (items['Name'], self.fields['Version'])
  213. self.fields.update(items)
  214. self.pkgname = items['Name']
  215. self.src_section = items['Section']
  216. else:
  217. # confirm the format of input
  218. if 'Package' not in items or not self.pkgname:
  219. logger.error('input debian control is in wrong format')
  220. bpkg = items['Package']
  221. del items['Package']
  222. subpkg = self._get_subpkgname(bpkg)
  223. if subpkg == self.pkgname:
  224. # main package
  225. if os.path.isdir('debian') and os.path.isfile('debian/%s.install' % (subpkg)):
  226. items['Files'] = self.format_filelist (open('debian/%s.install' % (subpkg)).readlines())
  227. self.fields.update(items)
  228. else:
  229. # sub-packages
  230. if 'SubPackages' not in self.fields:
  231. self.fields['SubPackages'] = []
  232. if os.path.isdir('debian') and os.path.isfile('debian/%s.install' % (subpkg)):
  233. items['Files'] = self.format_filelist (open('debian/%s.install' % (subpkg)).readlines())
  234. items['Name'] = subpkg
  235. if subpkg == bpkg:
  236. items['AsWholeName'] = 'True'
  237. if 'Section' not in items:
  238. items['Section'] = self.src_section
  239. self.fields['SubPackages'].append(items)
  240. def all_info(self):
  241. return self.fields
  242. def convert_deb(ctl, binver, license, outdir):
  243. """ Generate spectacle files based on content of debain/control
  244. argument :ctl contain the content of debian/control
  245. """
  246. convertor = DebConvertor()
  247. # feed in the debian/control file to parser
  248. debi = DebInfo(binver, license)
  249. debi.feed(ctl)
  250. # Dump data to yaml
  251. dumper = SpectacleDumper(opath = '%s/%s.yaml' % (outdir, debi.pkgname))
  252. spec_fpath = dumper.dump(convertor.convert(debi.all_info()))
  253. logger.info('YAML and SPEC file are created with pkg name %s in dir: %s' % (debi.pkgname, outdir))
  254. def get_binver_from_dsc(dsc_fp):
  255. re_ver = re.compile('^Version:\s*(.*)')
  256. try:
  257. with open(dsc_fp) as f:
  258. for line in f:
  259. line = line.strip()
  260. if not line:
  261. continue
  262. m = re_ver.match(line)
  263. if m:
  264. return m.group(1)
  265. logger.error('cannot found version information in dsc file %s, please specify version using -V|--version' % dsc_fp)
  266. except OSError:
  267. logger.error('cannot read dsc file %s' % dsc_fp)
  268. def parse_options(args):
  269. import optparse
  270. usage = """Usage: %prog [options] [debian-path]
  271. The "debian-path" can be:""" + INPUT_SRC
  272. parser = optparse.OptionParser(usage, version=VERSION)
  273. parser.add_option("-o", "--outdir", type="string",
  274. dest="outdir", default=None,
  275. help="Path of output yaml/spec files")
  276. parser.add_option("-V", "--binver", type="string",
  277. help="The version string of binary packages")
  278. parser.add_option("", "--dsc", type="string",
  279. help='The path of "dsc" file for version information')
  280. parser.add_option("-L", "--license", type="string",
  281. help="The License string of this package")
  282. return parser.parse_args()
  283. if __name__ == '__main__':
  284. """ Main Function """
  285. (opts, args) = parse_options(sys.argv[1:])
  286. if not opts.binver and not opts.dsc:
  287. logger.error('Must provide one of the two options "-V|--binver" and "--dsc"')
  288. if not opts.license:
  289. logger.error('Must provide the License string using "-L|--license" options')
  290. ctl_path = None
  291. if not args:
  292. if os.path.isfile('control'):
  293. ctl_path = os.path.abspath('./control')
  294. logger.info('using "control" under current directory')
  295. elif os.path.isdir('debian') and os.path.isfile('debian/control'):
  296. ctl_path = os.path.abspath('./debian/control')
  297. logger.info('using "control" under "debian/" directory')
  298. elif os.path.isfile(DEBTARBALL):
  299. logger.info('using "%s" under current directory' % DEBTARBALL)
  300. else:
  301. # no debian packaging file found in cwd
  302. logger.error('found no debian files in current dir, please specify one')
  303. else:
  304. if not args[0].endswith(DEBTARBALL):
  305. ctl_path = os.path.abspath(os.path.expanduser(args[0]))
  306. if ctl_path:
  307. if not os.path.exists(ctl_path):
  308. # input file does not exist
  309. logger.error("%s: File does not exist" % ctl_path)
  310. ctl_cont = open(ctl_path).read()
  311. else: # means the input is debian.tar.gz
  312. # extract "control" from it
  313. import tarfile
  314. if tarfile.is_tarfile(DEBTARBALL):
  315. tar = tarfile.open(DEBTARBALL)
  316. found = False
  317. for member in tar.getmembers():
  318. if member.name == 'debian/control' and \
  319. member.isfile():
  320. found = True
  321. break
  322. if found:
  323. ctl_cont = tar.extractfile('debian/control').read()
  324. tar.close()
  325. logger.info('using "debian/control" in debian.tar.gz archive')
  326. else:
  327. logger.error('debian.tar.gz contains no "debian/control" file')
  328. else:
  329. logger.error('debian.tar.gz is not a valid tarball')
  330. # prepare the output folder
  331. if opts.outdir:
  332. outdir = os.path.abspath(os.path.expanduser(opts.outdir))
  333. else:
  334. outdir = os.path.abspath('./spectacle')
  335. if opts.binver:
  336. binver = opts.binver
  337. else:
  338. # opts.dsc must be valid
  339. binver = get_binver_from_dsc(opts.dsc) # TODO
  340. binver = binver.split('-',)[0]
  341. if not os.path.exists(outdir):
  342. os.makedirs(outdir)
  343. convert_deb(ctl_cont, binver, opts.license, outdir)