#!/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: 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: T.List[T.Tuple[str, str, T.Optional[str]]] = [] 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()