Rewrite the lunabuild toolchain with enhanced feature (#60)
[lunaix-os.git] / lunaix-os / scripts / build-tools / lib / utils.py
1 import os, ast
2 from pathlib import Path
3 from typing import Any, List
4
5 def join_path(stem, path):
6     if os.path.isabs(path):
7         return path
8     return os.path.join(stem, path)
9
10
11 class Schema:
12     class Any:
13         def __init__(self):
14             pass
15
16         def __str__(self):
17             return "Any"
18         
19         def __repr__(self):
20             return self.__str__()
21
22     class Union:
23         def __init__(self, *args):
24             self.union = [*args]
25
26         def __str__(self):
27             strs = [Schema.get_type_str(t) for t in self.union]
28             return f"{' | '.join(strs)}"
29         
30         def __repr__(self):
31             return self.__str__()
32         
33     class List:
34         def __init__(self, el_type):
35             self.el_type = el_type
36         
37         def __str__(self):
38             strs = Schema.get_type_str(self.el_type)
39             return f"*{strs}"
40         
41         def __repr__(self):
42             return self.__str__()
43
44     def __init__(self, type, **kwargs):
45         self.__type = type
46         self.__fields = kwargs
47
48     def __match_list(self, actual, expect):
49         if len(actual) != len(expect):
50             return False
51         
52         for a, b in zip(actual, expect):
53             if not self.__match(a, b):
54                 return False
55             
56         return True
57     
58     def __match_list_member(self, actual, expect):
59         if not isinstance(actual, List):
60             return False
61         
62         for a in actual:
63             if not self.__match(a, expect.el_type):
64                 return False
65             
66         return True
67     
68     def __match_union(self, actual, union):
69         for s in union.union:
70             if self.__match(actual, s):
71                 return True
72         return False
73
74     def __match(self, val, scheme):
75         if scheme is Any:
76             return True
77         
78         if isinstance(scheme, Schema):
79             return scheme.match(val)
80         
81         if isinstance(scheme, Schema.List):
82             return self.__match_list_member(val, scheme)
83         
84         if isinstance(scheme, list) and isinstance(val, list):
85             return self.__match_list(val, scheme)
86         
87         if isinstance(scheme, Schema.Union):
88             return self.__match_union(val, scheme)
89         
90         if not isinstance(scheme, type):
91             return scheme == val
92         
93         return isinstance(val, scheme)
94     
95     def match(self, instance):
96         if not self.__match(instance, self.__type):
97             return False
98
99         for field, t in self.__fields.items():
100             if not hasattr(instance, field):
101                 return False
102             
103             field_val = getattr(instance, field)
104
105             if not self.__match(field_val, t):
106                 return False
107
108         return True
109
110     def __eq__(self, value):
111         if isinstance(value, Schema):
112             return super().__eq__(value)
113         return self.match(value)
114     
115     def __ne__(self, value):
116         return not self.__eq__(value)
117     
118     def __str__(self):
119         fields = ""
120         if len(self.__fields) > 0:
121             fields = ", ".join([
122                 f"{name} :: {Schema.get_type_str(t)}" 
123                 for name, t in self.__fields.items()])
124             fields = "{%s}"%(fields)
125         
126         return f"{Schema.get_type_str(self.__type)} {fields}"
127     
128     def __repr__(self):
129         return self.__str__()
130     
131     @staticmethod
132     def get_type_str(maybe_type):
133         if isinstance(maybe_type, type):
134             return maybe_type.__name__
135         if isinstance(maybe_type, str):
136             return f'"{maybe_type}"'
137         return str(maybe_type)
138
139 class SourceLogger:
140     def __init__(self, visitor):
141         self.__visitor = visitor
142
143     def warn(self, node, fmt, *args):
144         fname = self.__visitor.current_file()
145         line  = node.lineno
146         coln  = node.col_offset
147
148         print(SourceLogger.fmt_warning(fname, line, coln, fmt%args))
149
150     @staticmethod
151     def fmt_message(fname, line, col, level, msg):
152         return "%s:%d:%d: %s: %s"%(fname, line, col, level, msg)
153
154     @staticmethod
155     def fmt_warning(fname, line, col, msg):
156         return SourceLogger.fmt_message(fname, line, col, "warning", msg)
157     
158     @staticmethod
159     def fmt_info(fname, line, col, msg):
160         return SourceLogger.fmt_message(fname, line, col, "info", msg)
161     
162     @staticmethod
163     def fmt_error(fname, line, col, msg):
164         return SourceLogger.fmt_message(fname, line, col, "error", msg)
165     
166     @staticmethod
167     def log(level, node, token, msg):
168         fname = node._filename
169         line  = token.lineno
170         col   = token.col_offset
171         
172         print( SourceLogger.fmt_message(fname, line, col, level, msg))
173         print(f"        at... {ast.unparse(token)}")
174     
175     @staticmethod
176     def warn(node, token, msg):
177         return SourceLogger.log("warning", node, token, msg)
178     
179 class ConfigAST:
180     ConfigImport = Schema(ast.ImportFrom, level=1)
181
182     class EnterFileMarker:
183         def __init__(self, filename = None):
184             self.name = filename
185
186     class LeaveFileMarker:
187         def __init__(self):
188             pass
189
190     def __init__(self, root_file, cfg_name = "LConfig"):
191         self.__tree = ast.Module([])
192         self.__cfg_name = cfg_name
193         self.__rootfile = Path(root_file)
194
195         self.__load(self.__rootfile)
196
197     def __load(self, file: Path):
198         parent = file.parent
199         
200         with file.open('r') as f:
201             nodes = ast.parse(f.read()).body
202         
203         relfile = str(file.relative_to(self.__rootfile.parent))
204         marker  = ConfigAST.EnterFileMarker(relfile)
205         self.__append_tree(marker)
206         
207         for node in nodes:
208             if ConfigAST.ConfigImport != node:
209                 self.__append_tree(node)
210                 continue
211             
212             module = node.module
213             module = "" if not module else module
214             path = parent.joinpath(*module.split('.'))
215             for alia in node.names:
216                 p = path / alia.name / self.__cfg_name
217                 self.__load(p)
218         
219         self.__append_tree(ConfigAST.LeaveFileMarker())
220     
221     def __append_tree(self, node):
222         self.__tree.body.append(node)    
223
224     def visit(self, visitor):
225         visitor.visit(self.__tree)
226
227 class ConfigASTVisitor:
228     def __init__(self):
229         self.__markers = []
230
231     def _visit_fndef(self, node : ast.FunctionDef):
232         self._visit_subtree(node)
233
234     def _visit_leaf(self, node):
235         pass
236
237     def _visit_subtree(self, node):
238         for n in node.body:
239             self.visit(n)
240
241     def _visit_expr(self, node : ast.Expr):
242         pass
243
244     def current_file(self):
245         if len(self.__markers) == 0:
246             return "<root>"
247         return self.__markers[-1].name
248
249     def visit(self, node):
250         if isinstance(node, ConfigAST.EnterFileMarker):
251             self.__markers.append(node)
252             return
253         
254         if isinstance(node, ConfigAST.LeaveFileMarker):
255             self.__markers.pop()
256             return
257
258         if isinstance(node, ast.FunctionDef):
259             self._visit_fndef(node)
260         
261         elif isinstance(node, ast.Expr):
262             self._visit_expr(node)
263         
264         elif hasattr(node, "body"):
265             self._visit_subtree(node)
266         
267         else:
268             self._visit_leaf(node)