| #!/usr/bin/env python3 |
| # |
| # Mini-Kconfig parser |
| # |
| # Copyright (c) 2015 Red Hat Inc. |
| # |
| # Authors: |
| # Paolo Bonzini <pbonzini@redhat.com> |
| # |
| # This work is licensed under the terms of the GNU GPL, version 2 |
| # or, at your option, any later version. See the COPYING file in |
| # the top-level directory. |
| |
| import os |
| import sys |
| import re |
| import random |
| |
| __all__ = [ 'KconfigDataError', 'KconfigParserError', |
| 'KconfigData', 'KconfigParser' , |
| 'defconfig', 'allyesconfig', 'allnoconfig', 'randconfig' ] |
| |
| def debug_print(*args): |
| #print('# ' + (' '.join(str(x) for x in args))) |
| pass |
| |
| # ------------------------------------------- |
| # KconfigData implements the Kconfig semantics. For now it can only |
| # detect undefined symbols, i.e. symbols that were referenced in |
| # assignments or dependencies but were not declared with "config FOO". |
| # |
| # Semantic actions are represented by methods called do_*. The do_var |
| # method return the semantic value of a variable (which right now is |
| # just its name). |
| # ------------------------------------------- |
| |
| class KconfigDataError(Exception): |
| def __init__(self, msg): |
| self.msg = msg |
| |
| def __str__(self): |
| return self.msg |
| |
| allyesconfig = lambda x: True |
| allnoconfig = lambda x: False |
| defconfig = lambda x: x |
| randconfig = lambda x: random.randint(0, 1) == 1 |
| |
| class KconfigData: |
| class Expr: |
| def __and__(self, rhs): |
| return KconfigData.AND(self, rhs) |
| def __or__(self, rhs): |
| return KconfigData.OR(self, rhs) |
| def __invert__(self): |
| return KconfigData.NOT(self) |
| |
| # Abstract methods |
| def add_edges_to(self, var): |
| pass |
| def evaluate(self): |
| assert False |
| |
| class AND(Expr): |
| def __init__(self, lhs, rhs): |
| self.lhs = lhs |
| self.rhs = rhs |
| def __str__(self): |
| return "(%s && %s)" % (self.lhs, self.rhs) |
| |
| def add_edges_to(self, var): |
| self.lhs.add_edges_to(var) |
| self.rhs.add_edges_to(var) |
| def evaluate(self): |
| return self.lhs.evaluate() and self.rhs.evaluate() |
| |
| class OR(Expr): |
| def __init__(self, lhs, rhs): |
| self.lhs = lhs |
| self.rhs = rhs |
| def __str__(self): |
| return "(%s || %s)" % (self.lhs, self.rhs) |
| |
| def add_edges_to(self, var): |
| self.lhs.add_edges_to(var) |
| self.rhs.add_edges_to(var) |
| def evaluate(self): |
| return self.lhs.evaluate() or self.rhs.evaluate() |
| |
| class NOT(Expr): |
| def __init__(self, lhs): |
| self.lhs = lhs |
| def __str__(self): |
| return "!%s" % (self.lhs) |
| |
| def add_edges_to(self, var): |
| self.lhs.add_edges_to(var) |
| def evaluate(self): |
| return not self.lhs.evaluate() |
| |
| class Var(Expr): |
| def __init__(self, name): |
| self.name = name |
| self.value = None |
| self.outgoing = set() |
| self.clauses_for_var = list() |
| def __str__(self): |
| return self.name |
| |
| def has_value(self): |
| return not (self.value is None) |
| def set_value(self, val, clause): |
| self.clauses_for_var.append(clause) |
| if self.has_value() and self.value != val: |
| print("The following clauses were found for " + self.name, file=sys.stderr) |
| for i in self.clauses_for_var: |
| print(" " + str(i), file=sys.stderr) |
| raise KconfigDataError('contradiction between clauses when setting %s' % self) |
| debug_print("=> %s is now %s" % (self.name, val)) |
| self.value = val |
| |
| # depth first search of the dependency graph |
| def dfs(self, visited, f): |
| if self in visited: |
| return |
| visited.add(self) |
| for v in self.outgoing: |
| v.dfs(visited, f) |
| f(self) |
| |
| def add_edges_to(self, var): |
| self.outgoing.add(var) |
| def evaluate(self): |
| if not self.has_value(): |
| raise KconfigDataError('cycle found including %s' % self) |
| return self.value |
| |
| class Clause: |
| def __init__(self, dest): |
| self.dest = dest |
| def priority(self): |
| return 0 |
| def process(self): |
| pass |
| |
| class AssignmentClause(Clause): |
| def __init__(self, dest, value): |
| KconfigData.Clause.__init__(self, dest) |
| self.value = value |
| def __str__(self): |
| return "CONFIG_%s=%s" % (self.dest, 'y' if self.value else 'n') |
| |
| def process(self): |
| self.dest.set_value(self.value, self) |
| |
| class DefaultClause(Clause): |
| def __init__(self, dest, value, cond=None): |
| KconfigData.Clause.__init__(self, dest) |
| self.value = value |
| self.cond = cond |
| if not (self.cond is None): |
| self.cond.add_edges_to(self.dest) |
| def __str__(self): |
| value = 'y' if self.value else 'n' |
| if self.cond is None: |
| return "config %s default %s" % (self.dest, value) |
| else: |
| return "config %s default %s if %s" % (self.dest, value, self.cond) |
| |
| def priority(self): |
| # Defaults are processed just before leaving the variable |
| return -1 |
| def process(self): |
| if not self.dest.has_value() and \ |
| (self.cond is None or self.cond.evaluate()): |
| self.dest.set_value(self.value, self) |
| |
| class DependsOnClause(Clause): |
| def __init__(self, dest, expr): |
| KconfigData.Clause.__init__(self, dest) |
| self.expr = expr |
| self.expr.add_edges_to(self.dest) |
| def __str__(self): |
| return "config %s depends on %s" % (self.dest, self.expr) |
| |
| def process(self): |
| if not self.expr.evaluate(): |
| self.dest.set_value(False, self) |
| |
| class SelectClause(Clause): |
| def __init__(self, dest, cond): |
| KconfigData.Clause.__init__(self, dest) |
| self.cond = cond |
| self.cond.add_edges_to(self.dest) |
| def __str__(self): |
| return "select %s if %s" % (self.dest, self.cond) |
| |
| def process(self): |
| if self.cond.evaluate(): |
| self.dest.set_value(True, self) |
| |
| def __init__(self, value_mangler=defconfig): |
| self.value_mangler = value_mangler |
| self.previously_included = [] |
| self.incl_info = None |
| self.defined_vars = set() |
| self.referenced_vars = dict() |
| self.clauses = list() |
| |
| # semantic analysis ------------- |
| |
| def check_undefined(self): |
| undef = False |
| for i in self.referenced_vars: |
| if not (i in self.defined_vars): |
| print("undefined symbol %s" % (i), file=sys.stderr) |
| undef = True |
| return undef |
| |
| def compute_config(self): |
| if self.check_undefined(): |
| raise KconfigDataError("there were undefined symbols") |
| return None |
| |
| debug_print("Input:") |
| for clause in self.clauses: |
| debug_print(clause) |
| |
| debug_print("\nDependency graph:") |
| for i in self.referenced_vars: |
| debug_print(i, "->", [str(x) for x in self.referenced_vars[i].outgoing]) |
| |
| # The reverse of the depth-first order is the topological sort |
| dfo = dict() |
| visited = set() |
| debug_print("\n") |
| def visit_fn(var): |
| debug_print(var, "has DFS number", len(dfo)) |
| dfo[var] = len(dfo) |
| |
| for name, v in self.referenced_vars.items(): |
| self.do_default(v, False) |
| v.dfs(visited, visit_fn) |
| |
| # Put higher DFS numbers and higher priorities first. This |
| # places the clauses in topological order and places defaults |
| # after assignments and dependencies. |
| self.clauses.sort(key=lambda x: (-dfo[x.dest], -x.priority())) |
| |
| debug_print("\nSorted clauses:") |
| for clause in self.clauses: |
| debug_print(clause) |
| clause.process() |
| |
| debug_print("") |
| values = dict() |
| for name, v in self.referenced_vars.items(): |
| debug_print("Evaluating", name) |
| values[name] = v.evaluate() |
| |
| return values |
| |
| # semantic actions ------------- |
| |
| def do_declaration(self, var): |
| if (var in self.defined_vars): |
| raise KconfigDataError('variable "' + var + '" defined twice') |
| |
| self.defined_vars.add(var.name) |
| |
| # var is a string with the variable's name. |
| def do_var(self, var): |
| if (var in self.referenced_vars): |
| return self.referenced_vars[var] |
| |
| var_obj = self.referenced_vars[var] = KconfigData.Var(var) |
| return var_obj |
| |
| def do_assignment(self, var, val): |
| self.clauses.append(KconfigData.AssignmentClause(var, val)) |
| |
| def do_default(self, var, val, cond=None): |
| val = self.value_mangler(val) |
| self.clauses.append(KconfigData.DefaultClause(var, val, cond)) |
| |
| def do_depends_on(self, var, expr): |
| self.clauses.append(KconfigData.DependsOnClause(var, expr)) |
| |
| def do_select(self, var, symbol, cond=None): |
| cond = (cond & var) if cond is not None else var |
| self.clauses.append(KconfigData.SelectClause(symbol, cond)) |
| |
| def do_imply(self, var, symbol, cond=None): |
| # "config X imply Y [if COND]" is the same as |
| # "config Y default y if X [&& COND]" |
| cond = (cond & var) if cond is not None else var |
| self.do_default(symbol, True, cond) |
| |
| # ------------------------------------------- |
| # KconfigParser implements a recursive descent parser for (simplified) |
| # Kconfig syntax. |
| # ------------------------------------------- |
| |
| # tokens table |
| TOKENS = {} |
| TOK_NONE = -1 |
| TOK_LPAREN = 0; TOKENS[TOK_LPAREN] = '"("'; |
| TOK_RPAREN = 1; TOKENS[TOK_RPAREN] = '")"'; |
| TOK_EQUAL = 2; TOKENS[TOK_EQUAL] = '"="'; |
| TOK_AND = 3; TOKENS[TOK_AND] = '"&&"'; |
| TOK_OR = 4; TOKENS[TOK_OR] = '"||"'; |
| TOK_NOT = 5; TOKENS[TOK_NOT] = '"!"'; |
| TOK_DEPENDS = 6; TOKENS[TOK_DEPENDS] = '"depends"'; |
| TOK_ON = 7; TOKENS[TOK_ON] = '"on"'; |
| TOK_SELECT = 8; TOKENS[TOK_SELECT] = '"select"'; |
| TOK_IMPLY = 9; TOKENS[TOK_IMPLY] = '"imply"'; |
| TOK_CONFIG = 10; TOKENS[TOK_CONFIG] = '"config"'; |
| TOK_DEFAULT = 11; TOKENS[TOK_DEFAULT] = '"default"'; |
| TOK_Y = 12; TOKENS[TOK_Y] = '"y"'; |
| TOK_N = 13; TOKENS[TOK_N] = '"n"'; |
| TOK_SOURCE = 14; TOKENS[TOK_SOURCE] = '"source"'; |
| TOK_BOOL = 15; TOKENS[TOK_BOOL] = '"bool"'; |
| TOK_IF = 16; TOKENS[TOK_IF] = '"if"'; |
| TOK_ID = 17; TOKENS[TOK_ID] = 'identifier'; |
| TOK_EOF = 18; TOKENS[TOK_EOF] = 'end of file'; |
| |
| class KconfigParserError(Exception): |
| def __init__(self, parser, msg, tok=None): |
| self.loc = parser.location() |
| tok = tok or parser.tok |
| if tok != TOK_NONE: |
| location = TOKENS.get(tok, None) or ('"%s"' % tok) |
| msg = '%s before %s' % (msg, location) |
| self.msg = msg |
| |
| def __str__(self): |
| return "%s: %s" % (self.loc, self.msg) |
| |
| class KconfigParser: |
| |
| @classmethod |
| def parse(self, fp, mode=None): |
| data = KconfigData(mode or KconfigParser.defconfig) |
| parser = KconfigParser(data) |
| parser.parse_file(fp) |
| return data |
| |
| def __init__(self, data): |
| self.data = data |
| |
| def parse_file(self, fp): |
| self.abs_fname = os.path.abspath(fp.name) |
| self.fname = fp.name |
| self.data.previously_included.append(self.abs_fname) |
| self.src = fp.read() |
| if self.src == '' or self.src[-1] != '\n': |
| self.src += '\n' |
| self.cursor = 0 |
| self.line = 1 |
| self.line_pos = 0 |
| self.get_token() |
| self.parse_config() |
| |
| def do_assignment(self, var, val): |
| if not var.startswith("CONFIG_"): |
| raise Error('assigned variable should start with CONFIG_') |
| var = self.data.do_var(var[7:]) |
| self.data.do_assignment(var, val) |
| |
| # file management ----- |
| |
| def error_path(self): |
| inf = self.data.incl_info |
| res = "" |
| while inf: |
| res = ("In file included from %s:%d:\n" % (inf['file'], |
| inf['line'])) + res |
| inf = inf['parent'] |
| return res |
| |
| def location(self): |
| col = 1 |
| for ch in self.src[self.line_pos:self.pos]: |
| if ch == '\t': |
| col += 8 - ((col - 1) % 8) |
| else: |
| col += 1 |
| return '%s%s:%d:%d' %(self.error_path(), self.fname, self.line, col) |
| |
| def do_include(self, include): |
| incl_abs_fname = os.path.join(os.path.dirname(self.abs_fname), |
| include) |
| # catch inclusion cycle |
| inf = self.data.incl_info |
| while inf: |
| if incl_abs_fname == os.path.abspath(inf['file']): |
| raise KconfigParserError(self, "Inclusion loop for %s" |
| % include) |
| inf = inf['parent'] |
| |
| # skip multiple include of the same file |
| if incl_abs_fname in self.data.previously_included: |
| return |
| try: |
| fp = open(incl_abs_fname, 'rt', encoding='utf-8') |
| except IOError as e: |
| raise KconfigParserError(self, |
| '%s: %s' % (e.strerror, include)) |
| |
| inf = self.data.incl_info |
| self.data.incl_info = { 'file': self.fname, 'line': self.line, |
| 'parent': inf } |
| KconfigParser(self.data).parse_file(fp) |
| self.data.incl_info = inf |
| |
| # recursive descent parser ----- |
| |
| # y_or_n: Y | N |
| def parse_y_or_n(self): |
| if self.tok == TOK_Y: |
| self.get_token() |
| return True |
| if self.tok == TOK_N: |
| self.get_token() |
| return False |
| raise KconfigParserError(self, 'Expected "y" or "n"') |
| |
| # var: ID |
| def parse_var(self): |
| if self.tok == TOK_ID: |
| val = self.val |
| self.get_token() |
| return self.data.do_var(val) |
| else: |
| raise KconfigParserError(self, 'Expected identifier') |
| |
| # assignment_var: ID (starting with "CONFIG_") |
| def parse_assignment_var(self): |
| if self.tok == TOK_ID: |
| val = self.val |
| if not val.startswith("CONFIG_"): |
| raise KconfigParserError(self, |
| 'Expected identifier starting with "CONFIG_"', TOK_NONE) |
| self.get_token() |
| return self.data.do_var(val[7:]) |
| else: |
| raise KconfigParserError(self, 'Expected identifier') |
| |
| # assignment: var EQUAL y_or_n |
| def parse_assignment(self): |
| var = self.parse_assignment_var() |
| if self.tok != TOK_EQUAL: |
| raise KconfigParserError(self, 'Expected "="') |
| self.get_token() |
| self.data.do_assignment(var, self.parse_y_or_n()) |
| |
| # primary: NOT primary |
| # | LPAREN expr RPAREN |
| # | var |
| def parse_primary(self): |
| if self.tok == TOK_NOT: |
| self.get_token() |
| val = ~self.parse_primary() |
| elif self.tok == TOK_LPAREN: |
| self.get_token() |
| val = self.parse_expr() |
| if self.tok != TOK_RPAREN: |
| raise KconfigParserError(self, 'Expected ")"') |
| self.get_token() |
| elif self.tok == TOK_ID: |
| val = self.parse_var() |
| else: |
| raise KconfigParserError(self, 'Expected "!" or "(" or identifier') |
| return val |
| |
| # disj: primary (OR primary)* |
| def parse_disj(self): |
| lhs = self.parse_primary() |
| while self.tok == TOK_OR: |
| self.get_token() |
| lhs = lhs | self.parse_primary() |
| return lhs |
| |
| # expr: disj (AND disj)* |
| def parse_expr(self): |
| lhs = self.parse_disj() |
| while self.tok == TOK_AND: |
| self.get_token() |
| lhs = lhs & self.parse_disj() |
| return lhs |
| |
| # condition: IF expr |
| # | empty |
| def parse_condition(self): |
| if self.tok == TOK_IF: |
| self.get_token() |
| return self.parse_expr() |
| else: |
| return None |
| |
| # property: DEFAULT y_or_n condition |
| # | DEPENDS ON expr |
| # | SELECT var condition |
| # | BOOL |
| def parse_property(self, var): |
| if self.tok == TOK_DEFAULT: |
| self.get_token() |
| val = self.parse_y_or_n() |
| cond = self.parse_condition() |
| self.data.do_default(var, val, cond) |
| elif self.tok == TOK_DEPENDS: |
| self.get_token() |
| if self.tok != TOK_ON: |
| raise KconfigParserError(self, 'Expected "on"') |
| self.get_token() |
| self.data.do_depends_on(var, self.parse_expr()) |
| elif self.tok == TOK_SELECT: |
| self.get_token() |
| symbol = self.parse_var() |
| cond = self.parse_condition() |
| self.data.do_select(var, symbol, cond) |
| elif self.tok == TOK_IMPLY: |
| self.get_token() |
| symbol = self.parse_var() |
| cond = self.parse_condition() |
| self.data.do_imply(var, symbol, cond) |
| elif self.tok == TOK_BOOL: |
| self.get_token() |
| else: |
| raise KconfigParserError(self, 'Error in recursive descent?') |
| |
| # properties: properties property |
| # | /* empty */ |
| def parse_properties(self, var): |
| had_default = False |
| while self.tok == TOK_DEFAULT or self.tok == TOK_DEPENDS or \ |
| self.tok == TOK_SELECT or self.tok == TOK_BOOL or \ |
| self.tok == TOK_IMPLY: |
| self.parse_property(var) |
| |
| # for nicer error message |
| if self.tok != TOK_SOURCE and self.tok != TOK_CONFIG and \ |
| self.tok != TOK_ID and self.tok != TOK_EOF: |
| raise KconfigParserError(self, 'expected "source", "config", identifier, ' |
| + '"default", "depends on", "imply" or "select"') |
| |
| # declaration: config var properties |
| def parse_declaration(self): |
| if self.tok == TOK_CONFIG: |
| self.get_token() |
| var = self.parse_var() |
| self.data.do_declaration(var) |
| self.parse_properties(var) |
| else: |
| raise KconfigParserError(self, 'Error in recursive descent?') |
| |
| # clause: SOURCE |
| # | declaration |
| # | assignment |
| def parse_clause(self): |
| if self.tok == TOK_SOURCE: |
| val = self.val |
| self.get_token() |
| self.do_include(val) |
| elif self.tok == TOK_CONFIG: |
| self.parse_declaration() |
| elif self.tok == TOK_ID: |
| self.parse_assignment() |
| else: |
| raise KconfigParserError(self, 'expected "source", "config" or identifier') |
| |
| # config: clause+ EOF |
| def parse_config(self): |
| while self.tok != TOK_EOF: |
| self.parse_clause() |
| return self.data |
| |
| # scanner ----- |
| |
| def get_token(self): |
| while True: |
| self.tok = self.src[self.cursor] |
| self.pos = self.cursor |
| self.cursor += 1 |
| |
| self.val = None |
| self.tok = self.scan_token() |
| if self.tok is not None: |
| return |
| |
| def check_keyword(self, rest): |
| if not self.src.startswith(rest, self.cursor): |
| return False |
| length = len(rest) |
| if self.src[self.cursor + length].isalnum() or self.src[self.cursor + length] == '_': |
| return False |
| self.cursor += length |
| return True |
| |
| def scan_token(self): |
| if self.tok == '#': |
| self.cursor = self.src.find('\n', self.cursor) |
| return None |
| elif self.tok == '=': |
| return TOK_EQUAL |
| elif self.tok == '(': |
| return TOK_LPAREN |
| elif self.tok == ')': |
| return TOK_RPAREN |
| elif self.tok == '&' and self.src[self.pos+1] == '&': |
| self.cursor += 1 |
| return TOK_AND |
| elif self.tok == '|' and self.src[self.pos+1] == '|': |
| self.cursor += 1 |
| return TOK_OR |
| elif self.tok == '!': |
| return TOK_NOT |
| elif self.tok == 'd' and self.check_keyword("epends"): |
| return TOK_DEPENDS |
| elif self.tok == 'o' and self.check_keyword("n"): |
| return TOK_ON |
| elif self.tok == 's' and self.check_keyword("elect"): |
| return TOK_SELECT |
| elif self.tok == 'i' and self.check_keyword("mply"): |
| return TOK_IMPLY |
| elif self.tok == 'c' and self.check_keyword("onfig"): |
| return TOK_CONFIG |
| elif self.tok == 'd' and self.check_keyword("efault"): |
| return TOK_DEFAULT |
| elif self.tok == 'b' and self.check_keyword("ool"): |
| return TOK_BOOL |
| elif self.tok == 'i' and self.check_keyword("f"): |
| return TOK_IF |
| elif self.tok == 'y' and self.check_keyword(""): |
| return TOK_Y |
| elif self.tok == 'n' and self.check_keyword(""): |
| return TOK_N |
| elif (self.tok == 's' and self.check_keyword("ource")) or \ |
| self.tok == 'i' and self.check_keyword("nclude"): |
| # source FILENAME |
| # include FILENAME |
| while self.src[self.cursor].isspace(): |
| self.cursor += 1 |
| start = self.cursor |
| self.cursor = self.src.find('\n', self.cursor) |
| self.val = self.src[start:self.cursor] |
| return TOK_SOURCE |
| elif self.tok.isalnum(): |
| # identifier |
| while self.src[self.cursor].isalnum() or self.src[self.cursor] == '_': |
| self.cursor += 1 |
| self.val = self.src[self.pos:self.cursor] |
| return TOK_ID |
| elif self.tok == '\n': |
| if self.cursor == len(self.src): |
| return TOK_EOF |
| self.line += 1 |
| self.line_pos = self.cursor |
| elif not self.tok.isspace(): |
| raise KconfigParserError(self, 'invalid input') |
| |
| return None |
| |
| if __name__ == '__main__': |
| argv = sys.argv |
| mode = defconfig |
| if len(sys.argv) > 1: |
| if argv[1] == '--defconfig': |
| del argv[1] |
| elif argv[1] == '--randconfig': |
| random.seed() |
| mode = randconfig |
| del argv[1] |
| elif argv[1] == '--allyesconfig': |
| mode = allyesconfig |
| del argv[1] |
| elif argv[1] == '--allnoconfig': |
| mode = allnoconfig |
| del argv[1] |
| |
| if len(argv) == 1: |
| print ("%s: at least one argument is required" % argv[0], file=sys.stderr) |
| sys.exit(1) |
| |
| if argv[1].startswith('-'): |
| print ("%s: invalid option %s" % (argv[0], argv[1]), file=sys.stderr) |
| sys.exit(1) |
| |
| data = KconfigData(mode) |
| parser = KconfigParser(data) |
| external_vars = set() |
| for arg in argv[3:]: |
| m = re.match(r'^(CONFIG_[A-Z0-9_]+)=([yn]?)$', arg) |
| if m is not None: |
| name, value = m.groups() |
| parser.do_assignment(name, value == 'y') |
| external_vars.add(name[7:]) |
| else: |
| fp = open(arg, 'rt', encoding='utf-8') |
| parser.parse_file(fp) |
| fp.close() |
| |
| config = data.compute_config() |
| for key in sorted(config.keys()): |
| if key not in external_vars and config[key]: |
| print ('CONFIG_%s=y' % key) |
| |
| deps = open(argv[2], 'wt', encoding='utf-8') |
| for fname in data.previously_included: |
| print ('%s: %s' % (argv[1], fname), file=deps) |
| deps.close() |