summaryrefslogtreecommitdiffstats
path: root/tools/cmake2meson.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rwxr-xr-xtools/cmake2meson.py330
1 files changed, 330 insertions, 0 deletions
diff --git a/tools/cmake2meson.py b/tools/cmake2meson.py
new file mode 100755
index 0000000..a12d9cf
--- /dev/null
+++ b/tools/cmake2meson.py
@@ -0,0 +1,330 @@
+#!/usr/bin/env python3
+
+# Copyright 2014 Jussi Pakkanen
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import typing as T
+from pathlib import Path
+import sys
+import re
+import argparse
+
+
+class Token:
+ def __init__(self, tid: str, value: str):
+ self.tid = tid
+ self.value = value
+ self.lineno = 0
+ self.colno = 0
+
+class Statement:
+ def __init__(self, name: str, args: list):
+ self.name = name.lower()
+ self.args = args
+
+class Lexer:
+ def __init__(self) -> None:
+ self.token_specification = [
+ # Need to be sorted longest to shortest.
+ ('ignore', re.compile(r'[ \t]')),
+ ('string', re.compile(r'"([^\\]|(\\.))*?"', re.M)),
+ ('varexp', re.compile(r'\${[-_0-9a-z/A-Z.]+}')),
+ ('id', re.compile('''[,-><${}=+_0-9a-z/A-Z|@.*]+''')),
+ ('eol', re.compile(r'\n')),
+ ('comment', re.compile(r'#.*')),
+ ('lparen', re.compile(r'\(')),
+ ('rparen', re.compile(r'\)')),
+ ]
+
+ def lex(self, code: str) -> T.Iterator[Token]:
+ lineno = 1
+ line_start = 0
+ loc = 0
+ col = 0
+ while loc < len(code):
+ matched = False
+ for (tid, reg) in self.token_specification:
+ mo = reg.match(code, loc)
+ if mo:
+ col = mo.start() - line_start
+ matched = True
+ loc = mo.end()
+ match_text = mo.group()
+ if tid == 'ignore':
+ continue
+ if tid == 'comment':
+ yield(Token('comment', match_text))
+ elif tid == 'lparen':
+ yield(Token('lparen', '('))
+ elif tid == 'rparen':
+ yield(Token('rparen', ')'))
+ elif tid == 'string':
+ yield(Token('string', match_text[1:-1]))
+ elif tid == 'id':
+ yield(Token('id', match_text))
+ elif tid == 'eol':
+ # yield('eol')
+ lineno += 1
+ col = 1
+ line_start = mo.end()
+ elif tid == 'varexp':
+ yield(Token('varexp', match_text[2:-1]))
+ else:
+ raise ValueError(f'lex: unknown element {tid}')
+ break
+ if not matched:
+ raise ValueError('Lexer got confused line %d column %d' % (lineno, col))
+
+class Parser:
+ def __init__(self, code: str) -> None:
+ self.stream = Lexer().lex(code)
+ self.getsym()
+
+ def getsym(self) -> None:
+ try:
+ self.current = next(self.stream)
+ except StopIteration:
+ self.current = Token('eof', '')
+
+ def accept(self, s: str) -> bool:
+ if self.current.tid == s:
+ self.getsym()
+ return True
+ return False
+
+ def expect(self, s: str) -> bool:
+ if self.accept(s):
+ return True
+ raise ValueError(f'Expecting {s} got {self.current.tid}.', self.current.lineno, self.current.colno)
+
+ def statement(self) -> Statement:
+ cur = self.current
+ if self.accept('comment'):
+ return Statement('_', [cur.value])
+ self.accept('id')
+ self.expect('lparen')
+ args = self.arguments()
+ self.expect('rparen')
+ return Statement(cur.value, args)
+
+ def arguments(self) -> T.List[T.Union[Token, T.Any]]:
+ args = [] # type: T.List[T.Union[Token, T.Any]]
+ if self.accept('lparen'):
+ args.append(self.arguments())
+ self.expect('rparen')
+ arg = self.current
+ if self.accept('comment'):
+ rest = self.arguments()
+ args += rest
+ elif self.accept('string') \
+ or self.accept('varexp') \
+ or self.accept('id'):
+ args.append(arg)
+ rest = self.arguments()
+ args += rest
+ return args
+
+ def parse(self) -> T.Iterator[Statement]:
+ while not self.accept('eof'):
+ yield(self.statement())
+
+def token_or_group(arg: T.Union[Token, T.List[Token]]) -> str:
+ if isinstance(arg, Token):
+ return ' ' + arg.value
+ elif isinstance(arg, list):
+ line = ' ('
+ for a in arg:
+ line += ' ' + token_or_group(a)
+ line += ' )'
+ return line
+ raise RuntimeError('Conversion error in token_or_group')
+
+class Converter:
+ ignored_funcs = {'cmake_minimum_required': True,
+ 'enable_testing': True,
+ 'include': True}
+
+ def __init__(self, cmake_root: str):
+ self.cmake_root = Path(cmake_root).expanduser()
+ self.indent_unit = ' '
+ self.indent_level = 0
+ self.options = [] # type: T.List[tuple]
+
+ def convert_args(self, args: T.List[Token], as_array: bool = True) -> str:
+ res = []
+ if as_array:
+ start = '['
+ end = ']'
+ else:
+ start = ''
+ end = ''
+ for i in args:
+ if i.tid == 'id':
+ res.append("'%s'" % i.value)
+ elif i.tid == 'varexp':
+ res.append('%s' % i.value.lower())
+ elif i.tid == 'string':
+ res.append("'%s'" % i.value)
+ else:
+ raise ValueError(f'Unknown arg type {i.tid}')
+ if len(res) > 1:
+ return start + ', '.join(res) + end
+ if len(res) == 1:
+ return res[0]
+ return ''
+
+ def write_entry(self, outfile: T.TextIO, t: Statement) -> None:
+ if t.name in Converter.ignored_funcs:
+ return
+ preincrement = 0
+ postincrement = 0
+ if t.name == '_':
+ line = t.args[0]
+ elif t.name == 'add_subdirectory':
+ line = "subdir('" + t.args[0].value + "')"
+ elif t.name == 'pkg_search_module' or t.name == 'pkg_search_modules':
+ varname = t.args[0].value.lower()
+ mods = ["dependency('%s')" % i.value for i in t.args[1:]]
+ if len(mods) == 1:
+ line = '{} = {}'.format(varname, mods[0])
+ else:
+ line = '{} = [{}]'.format(varname, ', '.join(["'%s'" % i for i in mods]))
+ elif t.name == 'find_package':
+ line = "{}_dep = dependency('{}')".format(t.args[0].value, t.args[0].value)
+ elif t.name == 'find_library':
+ line = "{} = find_library('{}')".format(t.args[0].value.lower(), t.args[0].value)
+ elif t.name == 'add_executable':
+ line = '{}_exe = executable({})'.format(t.args[0].value, self.convert_args(t.args, False))
+ elif t.name == 'add_library':
+ if t.args[1].value == 'SHARED':
+ libcmd = 'shared_library'
+ args = [t.args[0]] + t.args[2:]
+ elif t.args[1].value == 'STATIC':
+ libcmd = 'static_library'
+ args = [t.args[0]] + t.args[2:]
+ else:
+ libcmd = 'library'
+ args = t.args
+ line = '{}_lib = {}({})'.format(t.args[0].value, libcmd, self.convert_args(args, False))
+ elif t.name == 'add_test':
+ line = 'test(%s)' % self.convert_args(t.args, False)
+ elif t.name == 'option':
+ optname = t.args[0].value
+ description = t.args[1].value
+ if len(t.args) > 2:
+ default = t.args[2].value
+ else:
+ default = None
+ self.options.append((optname, description, default))
+ return
+ elif t.name == 'project':
+ pname = t.args[0].value
+ args = [pname]
+ for l in t.args[1:]:
+ l = l.value.lower()
+ if l == 'cxx':
+ l = 'cpp'
+ args.append(l)
+ args = ["'%s'" % i for i in args]
+ line = 'project(' + ', '.join(args) + ", default_options : ['default_library=static'])"
+ elif t.name == 'set':
+ varname = t.args[0].value.lower()
+ line = '{} = {}\n'.format(varname, self.convert_args(t.args[1:]))
+ elif t.name == 'if':
+ postincrement = 1
+ try:
+ line = 'if %s' % self.convert_args(t.args, False)
+ except AttributeError: # complex if statements
+ line = t.name
+ for arg in t.args:
+ line += token_or_group(arg)
+ elif t.name == 'elseif':
+ preincrement = -1
+ postincrement = 1
+ try:
+ line = 'elif %s' % self.convert_args(t.args, False)
+ except AttributeError: # complex if statements
+ line = t.name
+ for arg in t.args:
+ line += token_or_group(arg)
+ elif t.name == 'else':
+ preincrement = -1
+ postincrement = 1
+ line = 'else'
+ elif t.name == 'endif':
+ preincrement = -1
+ line = 'endif'
+ else:
+ line = '''# {}({})'''.format(t.name, self.convert_args(t.args))
+ self.indent_level += preincrement
+ indent = self.indent_level * self.indent_unit
+ outfile.write(indent)
+ outfile.write(line)
+ if not(line.endswith('\n')):
+ outfile.write('\n')
+ self.indent_level += postincrement
+
+ def convert(self, subdir: Path = None) -> None:
+ if not subdir:
+ subdir = self.cmake_root
+ cfile = Path(subdir).expanduser() / 'CMakeLists.txt'
+ try:
+ with cfile.open(encoding='utf-8') as f:
+ cmakecode = f.read()
+ except FileNotFoundError:
+ print('\nWarning: No CMakeLists.txt in', subdir, '\n', file=sys.stderr)
+ return
+ p = Parser(cmakecode)
+ with (subdir / 'meson.build').open('w', encoding='utf-8') as outfile:
+ for t in p.parse():
+ if t.name == 'add_subdirectory':
+ # print('\nRecursing to subdir',
+ # self.cmake_root / t.args[0].value,
+ # '\n')
+ self.convert(subdir / t.args[0].value)
+ # print('\nReturning to', self.cmake_root, '\n')
+ self.write_entry(outfile, t)
+ if subdir == self.cmake_root and len(self.options) > 0:
+ self.write_options()
+
+ def write_options(self) -> None:
+ filename = self.cmake_root / 'meson_options.txt'
+ with filename.open('w', encoding='utf-8') as optfile:
+ for o in self.options:
+ (optname, description, default) = o
+ if default is None:
+ typestr = ''
+ defaultstr = ''
+ else:
+ if default == 'OFF':
+ typestr = ' type : \'boolean\','
+ default = 'false'
+ elif default == 'ON':
+ default = 'true'
+ typestr = ' type : \'boolean\','
+ else:
+ typestr = ' type : \'string\','
+ defaultstr = ' value : %s,' % default
+ line = "option({!r},{}{} description : '{}')\n".format(optname,
+ typestr,
+ defaultstr,
+ description)
+ optfile.write(line)
+
+if __name__ == '__main__':
+ p = argparse.ArgumentParser(description='Convert CMakeLists.txt to meson.build and meson_options.txt')
+ p.add_argument('cmake_root', help='CMake project root (where top-level CMakeLists.txt is)')
+ P = p.parse_args()
+
+ Converter(P.cmake_root).convert()