cmake2meson.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. #!/usr/bin/env python3
  2. # Copyright 2014 Jussi Pakkanen
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. # Unless required by applicable law or agreed to in writing, software
  8. # distributed under the License is distributed on an "AS IS" BASIS,
  9. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. # See the License for the specific language governing permissions and
  11. # limitations under the License.
  12. import typing as T
  13. from pathlib import Path
  14. import sys
  15. import re
  16. import argparse
  17. class Token:
  18. def __init__(self, tid: str, value: str):
  19. self.tid = tid
  20. self.value = value
  21. self.lineno = 0
  22. self.colno = 0
  23. class Statement:
  24. def __init__(self, name: str, args: list):
  25. self.name = name.lower()
  26. self.args = args
  27. class Lexer:
  28. def __init__(self) -> None:
  29. self.token_specification = [
  30. # Need to be sorted longest to shortest.
  31. ('ignore', re.compile(r'[ \t]')),
  32. ('string', re.compile(r'"([^\\]|(\\.))*?"', re.M)),
  33. ('varexp', re.compile(r'\${[-_0-9a-z/A-Z.]+}')),
  34. ('id', re.compile('''[,-><${}=+_0-9a-z/A-Z|@.*]+''')),
  35. ('eol', re.compile(r'\n')),
  36. ('comment', re.compile(r'#.*')),
  37. ('lparen', re.compile(r'\(')),
  38. ('rparen', re.compile(r'\)')),
  39. ]
  40. def lex(self, code: str) -> T.Iterator[Token]:
  41. lineno = 1
  42. line_start = 0
  43. loc = 0
  44. col = 0
  45. while loc < len(code):
  46. matched = False
  47. for (tid, reg) in self.token_specification:
  48. mo = reg.match(code, loc)
  49. if mo:
  50. col = mo.start() - line_start
  51. matched = True
  52. loc = mo.end()
  53. match_text = mo.group()
  54. if tid == 'ignore':
  55. continue
  56. if tid == 'comment':
  57. yield(Token('comment', match_text))
  58. elif tid == 'lparen':
  59. yield(Token('lparen', '('))
  60. elif tid == 'rparen':
  61. yield(Token('rparen', ')'))
  62. elif tid == 'string':
  63. yield(Token('string', match_text[1:-1]))
  64. elif tid == 'id':
  65. yield(Token('id', match_text))
  66. elif tid == 'eol':
  67. # yield('eol')
  68. lineno += 1
  69. col = 1
  70. line_start = mo.end()
  71. elif tid == 'varexp':
  72. yield(Token('varexp', match_text[2:-1]))
  73. else:
  74. raise ValueError(f'lex: unknown element {tid}')
  75. break
  76. if not matched:
  77. raise ValueError('Lexer got confused line %d column %d' % (lineno, col))
  78. class Parser:
  79. def __init__(self, code: str) -> None:
  80. self.stream = Lexer().lex(code)
  81. self.getsym()
  82. def getsym(self) -> None:
  83. try:
  84. self.current = next(self.stream)
  85. except StopIteration:
  86. self.current = Token('eof', '')
  87. def accept(self, s: str) -> bool:
  88. if self.current.tid == s:
  89. self.getsym()
  90. return True
  91. return False
  92. def expect(self, s: str) -> bool:
  93. if self.accept(s):
  94. return True
  95. raise ValueError(f'Expecting {s} got {self.current.tid}.', self.current.lineno, self.current.colno)
  96. def statement(self) -> Statement:
  97. cur = self.current
  98. if self.accept('comment'):
  99. return Statement('_', [cur.value])
  100. self.accept('id')
  101. self.expect('lparen')
  102. args = self.arguments()
  103. self.expect('rparen')
  104. return Statement(cur.value, args)
  105. def arguments(self) -> T.List[T.Union[Token, T.Any]]:
  106. args = [] # type: T.List[T.Union[Token, T.Any]]
  107. if self.accept('lparen'):
  108. args.append(self.arguments())
  109. self.expect('rparen')
  110. arg = self.current
  111. if self.accept('comment'):
  112. rest = self.arguments()
  113. args += rest
  114. elif self.accept('string') \
  115. or self.accept('varexp') \
  116. or self.accept('id'):
  117. args.append(arg)
  118. rest = self.arguments()
  119. args += rest
  120. return args
  121. def parse(self) -> T.Iterator[Statement]:
  122. while not self.accept('eof'):
  123. yield(self.statement())
  124. def token_or_group(arg: T.Union[Token, T.List[Token]]) -> str:
  125. if isinstance(arg, Token):
  126. return ' ' + arg.value
  127. elif isinstance(arg, list):
  128. line = ' ('
  129. for a in arg:
  130. line += ' ' + token_or_group(a)
  131. line += ' )'
  132. return line
  133. raise RuntimeError('Conversion error in token_or_group')
  134. class Converter:
  135. ignored_funcs = {'cmake_minimum_required': True,
  136. 'enable_testing': True,
  137. 'include': True}
  138. def __init__(self, cmake_root: str):
  139. self.cmake_root = Path(cmake_root).expanduser()
  140. self.indent_unit = ' '
  141. self.indent_level = 0
  142. self.options = [] # type: T.List[tuple]
  143. def convert_args(self, args: T.List[Token], as_array: bool = True) -> str:
  144. res = []
  145. if as_array:
  146. start = '['
  147. end = ']'
  148. else:
  149. start = ''
  150. end = ''
  151. for i in args:
  152. if i.tid == 'id':
  153. res.append("'%s'" % i.value)
  154. elif i.tid == 'varexp':
  155. res.append('%s' % i.value.lower())
  156. elif i.tid == 'string':
  157. res.append("'%s'" % i.value)
  158. else:
  159. raise ValueError(f'Unknown arg type {i.tid}')
  160. if len(res) > 1:
  161. return start + ', '.join(res) + end
  162. if len(res) == 1:
  163. return res[0]
  164. return ''
  165. def write_entry(self, outfile: T.TextIO, t: Statement) -> None:
  166. if t.name in Converter.ignored_funcs:
  167. return
  168. preincrement = 0
  169. postincrement = 0
  170. if t.name == '_':
  171. line = t.args[0]
  172. elif t.name == 'add_subdirectory':
  173. line = "subdir('" + t.args[0].value + "')"
  174. elif t.name == 'pkg_search_module' or t.name == 'pkg_search_modules':
  175. varname = t.args[0].value.lower()
  176. mods = ["dependency('%s')" % i.value for i in t.args[1:]]
  177. if len(mods) == 1:
  178. line = '{} = {}'.format(varname, mods[0])
  179. else:
  180. line = '{} = [{}]'.format(varname, ', '.join(["'%s'" % i for i in mods]))
  181. elif t.name == 'find_package':
  182. line = "{}_dep = dependency('{}')".format(t.args[0].value, t.args[0].value)
  183. elif t.name == 'find_library':
  184. line = "{} = find_library('{}')".format(t.args[0].value.lower(), t.args[0].value)
  185. elif t.name == 'add_executable':
  186. line = '{}_exe = executable({})'.format(t.args[0].value, self.convert_args(t.args, False))
  187. elif t.name == 'add_library':
  188. if t.args[1].value == 'SHARED':
  189. libcmd = 'shared_library'
  190. args = [t.args[0]] + t.args[2:]
  191. elif t.args[1].value == 'STATIC':
  192. libcmd = 'static_library'
  193. args = [t.args[0]] + t.args[2:]
  194. else:
  195. libcmd = 'library'
  196. args = t.args
  197. line = '{}_lib = {}({})'.format(t.args[0].value, libcmd, self.convert_args(args, False))
  198. elif t.name == 'add_test':
  199. line = 'test(%s)' % self.convert_args(t.args, False)
  200. elif t.name == 'option':
  201. optname = t.args[0].value
  202. description = t.args[1].value
  203. if len(t.args) > 2:
  204. default = t.args[2].value
  205. else:
  206. default = None
  207. self.options.append((optname, description, default))
  208. return
  209. elif t.name == 'project':
  210. pname = t.args[0].value
  211. args = [pname]
  212. for l in t.args[1:]:
  213. l = l.value.lower()
  214. if l == 'cxx':
  215. l = 'cpp'
  216. args.append(l)
  217. args = ["'%s'" % i for i in args]
  218. line = 'project(' + ', '.join(args) + ", default_options : ['default_library=static'])"
  219. elif t.name == 'set':
  220. varname = t.args[0].value.lower()
  221. line = '{} = {}\n'.format(varname, self.convert_args(t.args[1:]))
  222. elif t.name == 'if':
  223. postincrement = 1
  224. try:
  225. line = 'if %s' % self.convert_args(t.args, False)
  226. except AttributeError: # complex if statements
  227. line = t.name
  228. for arg in t.args:
  229. line += token_or_group(arg)
  230. elif t.name == 'elseif':
  231. preincrement = -1
  232. postincrement = 1
  233. try:
  234. line = 'elif %s' % self.convert_args(t.args, False)
  235. except AttributeError: # complex if statements
  236. line = t.name
  237. for arg in t.args:
  238. line += token_or_group(arg)
  239. elif t.name == 'else':
  240. preincrement = -1
  241. postincrement = 1
  242. line = 'else'
  243. elif t.name == 'endif':
  244. preincrement = -1
  245. line = 'endif'
  246. else:
  247. line = '''# {}({})'''.format(t.name, self.convert_args(t.args))
  248. self.indent_level += preincrement
  249. indent = self.indent_level * self.indent_unit
  250. outfile.write(indent)
  251. outfile.write(line)
  252. if not(line.endswith('\n')):
  253. outfile.write('\n')
  254. self.indent_level += postincrement
  255. def convert(self, subdir: Path = None) -> None:
  256. if not subdir:
  257. subdir = self.cmake_root
  258. cfile = Path(subdir).expanduser() / 'CMakeLists.txt'
  259. try:
  260. with cfile.open(encoding='utf-8') as f:
  261. cmakecode = f.read()
  262. except FileNotFoundError:
  263. print('\nWarning: No CMakeLists.txt in', subdir, '\n', file=sys.stderr)
  264. return
  265. p = Parser(cmakecode)
  266. with (subdir / 'meson.build').open('w', encoding='utf-8') as outfile:
  267. for t in p.parse():
  268. if t.name == 'add_subdirectory':
  269. # print('\nRecursing to subdir',
  270. # self.cmake_root / t.args[0].value,
  271. # '\n')
  272. self.convert(subdir / t.args[0].value)
  273. # print('\nReturning to', self.cmake_root, '\n')
  274. self.write_entry(outfile, t)
  275. if subdir == self.cmake_root and len(self.options) > 0:
  276. self.write_options()
  277. def write_options(self) -> None:
  278. filename = self.cmake_root / 'meson_options.txt'
  279. with filename.open('w', encoding='utf-8') as optfile:
  280. for o in self.options:
  281. (optname, description, default) = o
  282. if default is None:
  283. typestr = ''
  284. defaultstr = ''
  285. else:
  286. if default == 'OFF':
  287. typestr = ' type : \'boolean\','
  288. default = 'false'
  289. elif default == 'ON':
  290. default = 'true'
  291. typestr = ' type : \'boolean\','
  292. else:
  293. typestr = ' type : \'string\','
  294. defaultstr = ' value : %s,' % default
  295. line = "option({!r},{}{} description : '{}')\n".format(optname,
  296. typestr,
  297. defaultstr,
  298. description)
  299. optfile.write(line)
  300. if __name__ == '__main__':
  301. p = argparse.ArgumentParser(description='Convert CMakeLists.txt to meson.build and meson_options.txt')
  302. p.add_argument('cmake_root', help='CMake project root (where top-level CMakeLists.txt is)')
  303. P = p.parse_args()
  304. Converter(P.cmake_root).convert()