The Meson Build System
http://mesonbuild.com/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
330 lines
12 KiB
330 lines
12 KiB
#!/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()
|
|
|