looper/cmake2meson.py

331 lines
12 KiB
Python
Raw Normal View History

2023-04-24 13:45:06 -07:00
#!/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()