]>
Commit | Line | Data |
---|---|---|
20effc67 TL |
1 | #!/usr/bin/env python3 |
2 | ||
3 | import yaml | |
4 | import argparse | |
5 | import math | |
6 | import os | |
7 | import sys | |
8 | ||
9 | # flake8: noqa: E127 | |
10 | ||
11 | def type_to_cxx(t): | |
12 | return f'Option::TYPE_{t.upper()}' | |
13 | ||
14 | ||
15 | def level_to_cxx(lv): | |
16 | return f'Option::LEVEL_{lv.upper()}' | |
17 | ||
18 | ||
19 | def eval_str(v): | |
20 | if v == "": | |
21 | return v | |
22 | v = v.strip('"').replace('"', '\\"') | |
23 | return f'"{v}"' | |
24 | ||
25 | ||
26 | def eval_value(v, typ): | |
27 | try: | |
28 | if typ == 'str': | |
29 | return eval_str(v) | |
30 | if typ == 'float': | |
31 | return float(v) | |
32 | if typ in ('uint', 'int', 'size', 'secs', 'millisecs'): | |
33 | return int(v) | |
34 | if typ == 'bool': | |
35 | return 'true' if v else 'false' | |
36 | else: | |
37 | return f'"{v}"' | |
38 | except ValueError: | |
39 | times = dict(_min=60, | |
40 | _hr=60*60, | |
41 | _day=24*60*60, | |
42 | _K=1 << 10, | |
43 | _M=1 << 20, | |
44 | _G=1 << 30, | |
45 | _T=1 << 40) | |
46 | for unit, m in times.items(): | |
47 | if v.endswith(unit): | |
48 | int(v[:-len(unit)]) | |
49 | # user defined literals | |
50 | return v | |
51 | raise ValueError(f'unknown value: {v}') | |
52 | ||
53 | ||
54 | def set_default(default, typ): | |
55 | if default is None: | |
56 | return '' | |
57 | v = eval_value(default, typ) | |
58 | return f'.set_default({v})\n' | |
59 | ||
60 | ||
61 | def set_daemon_default(default, typ): | |
62 | if default is None: | |
63 | return '' | |
64 | v = eval_value(default, typ) | |
65 | return f'.set_daemon_default({v})\n' | |
66 | ||
67 | ||
68 | def add_tags(tags): | |
69 | if tags is None: | |
70 | return '' | |
71 | cxx = '' | |
72 | for tag in tags: | |
73 | v = eval_str(tag) | |
74 | cxx += f'.add_tag({v})\n' | |
75 | return cxx | |
76 | ||
77 | ||
78 | def add_services(services): | |
79 | if services is None: | |
80 | return '' | |
81 | if len(services) == 1: | |
82 | return f'.add_service("{services[0]}")\n' | |
83 | else: | |
84 | param = ', '.join(f'"{s}"' for s in services) | |
85 | return f'.add_service({{{param}}})\n' | |
86 | ||
87 | ||
88 | def add_see_also(see_also): | |
89 | if see_also is None: | |
90 | return '' | |
91 | param = ', '.join(f'"{v}"' for v in see_also) | |
92 | return f'.add_see_also({{{param}}})\n' | |
93 | ||
94 | ||
95 | def set_desc(desc): | |
96 | if desc is None: | |
97 | return '' | |
98 | v = eval_str(desc) | |
99 | return f'.set_description({v})\n' | |
100 | ||
101 | ||
102 | def set_long_desc(desc): | |
103 | if desc is None: | |
104 | return '' | |
105 | v = eval_str(desc) | |
106 | return f'.set_long_description({v})\n' | |
107 | ||
108 | ||
109 | def set_min_max(mi, ma, typ): | |
110 | if mi is None and ma is None: | |
111 | return '' | |
112 | if mi is not None and ma is not None: | |
113 | min_v = eval_value(mi, typ) | |
114 | max_v = eval_value(ma, typ) | |
115 | if isinstance(min_v, str) and isinstance(max_v, int): | |
116 | return f'.set_min_max({min_v}, {max_v}ULL)\n' | |
117 | elif isinstance(min_v, int) and isinstance(max_v, str): | |
118 | return f'.set_min_max({min_v}ULL, {max_v})\n' | |
119 | else: | |
120 | return f'.set_min_max({min_v}, {max_v})\n' | |
121 | if mi is not None: | |
122 | min_v = eval_value(mi, typ) | |
123 | return f'.set_min({min_v})\n' | |
124 | raise ValueError('set_max() is not implemented') | |
125 | ||
126 | ||
127 | def set_enum_allowed(values): | |
128 | if values is None: | |
129 | return '' | |
130 | param = ', '.join(f'"{v}"' for v in values) | |
131 | return f'.set_enum_allowed({{{param}}})\n' | |
132 | ||
133 | ||
134 | def add_flags(flags): | |
135 | if flags is None: | |
136 | return '' | |
137 | cxx = '' | |
138 | for flag in flags: | |
139 | cxx += f'.set_flag(Option::FLAG_{flag.upper()})\n' | |
140 | return cxx | |
141 | ||
142 | ||
143 | def set_validator(validator): | |
144 | if validator is None: | |
145 | return '' | |
146 | validator = validator.rstrip() | |
147 | return f'.set_validator({validator})\n' | |
148 | ||
149 | ||
150 | def add_verbatim(verbatim): | |
151 | if verbatim is None: | |
152 | return '' | |
153 | return verbatim + '\n' | |
154 | ||
155 | ||
156 | def yaml_to_cxx(opt, indent): | |
157 | name = opt['name'] | |
158 | typ = opt['type'] | |
159 | ctyp = type_to_cxx(typ) | |
160 | level = level_to_cxx(opt['level']) | |
161 | cxx = f'Option("{name}", {ctyp}, {level})\n' | |
162 | cxx += set_desc(opt.get('desc')) | |
163 | cxx += set_long_desc(opt.get('long_desc')) | |
164 | cxx += set_default(opt.get('default'), typ) | |
165 | cxx += set_daemon_default(opt.get('daemon_default'), typ) | |
166 | cxx += set_min_max(opt.get('min'), opt.get('max'), typ) | |
167 | cxx += set_enum_allowed(opt.get('enum_values')) | |
168 | cxx += set_validator(opt.get('validator')) | |
169 | cxx += add_flags(opt.get('flags')) | |
170 | cxx += add_services(opt.get('services')) | |
171 | cxx += add_tags(opt.get('tags')) | |
172 | cxx += add_see_also(opt.get('see_also')) | |
173 | verbatim = add_verbatim(opt.get('verbatim')) | |
174 | cxx += verbatim | |
175 | if verbatim: | |
176 | cxx += '\n' | |
177 | else: | |
178 | cxx = cxx.rstrip() | |
179 | cxx += ',\n' | |
180 | if indent > 0: | |
181 | indented = [] | |
182 | for line in cxx.split('\n'): | |
183 | if line: | |
184 | indented.append(' ' * indent + line + '\n') | |
185 | cxx = ''.join(indented) | |
186 | return cxx | |
187 | ||
188 | ||
189 | def type_to_h(t): | |
190 | if t == 'uint': | |
191 | return 'OPT_U32' | |
192 | return f'OPT_{t.upper()}' | |
193 | ||
194 | ||
195 | def yaml_to_h(opt): | |
196 | if opt.get('with_legacy', False): | |
197 | name = opt['name'] | |
198 | typ = opt['type'] | |
199 | htyp = type_to_h(typ) | |
200 | return f'OPTION({name}, {htyp})' | |
201 | else: | |
202 | return '' | |
203 | ||
204 | ||
205 | TEMPLATE_CC = '''#include "common/options.h" | |
206 | {headers} | |
207 | ||
208 | std::vector<Option> get_{name}_options() {{ | |
209 | return std::vector<Option>({{ | |
210 | @body@ | |
211 | }}); | |
212 | }} | |
213 | ''' | |
214 | ||
215 | ||
216 | # PyYAML doesn't check for duplicates even though the YAML spec says | |
217 | # that mapping keys must be unique and that duplicates must be treated | |
218 | # as an error. See https://github.com/yaml/pyyaml/issues/165. | |
219 | # | |
220 | # This workaround breaks merge keys -- in "<<: *xyz", duplicate keys | |
221 | # from xyz mapping raise an error instead of being discarded. | |
222 | class UniqueKeySafeLoader(yaml.SafeLoader): | |
223 | def construct_mapping(self, node, deep=False): | |
224 | mapping = super().construct_mapping(node, deep) | |
225 | keys = set() | |
226 | for key_node, _ in node.value: | |
227 | key = self.construct_object(key_node, deep=deep) | |
228 | if key in keys: | |
229 | raise yaml.constructor.ConstructorError(None, None, | |
230 | "found duplicate key", | |
231 | key_node.start_mark) | |
232 | keys.add(key) | |
233 | return mapping | |
234 | ||
235 | ||
236 | def translate(opts): | |
237 | if opts.raw: | |
238 | prelude, epilogue = '', '' | |
239 | else: | |
240 | prelude, epilogue = TEMPLATE_CC.split('@body@') | |
241 | ||
242 | if opts.name: | |
243 | name = opts.name | |
244 | else: | |
245 | name = os.path.split(opts.input)[-1] | |
246 | name = name.rsplit('.', 1)[0] | |
247 | name = name.replace('-', '_') | |
248 | # noqa: E127 | |
249 | with open(opts.input) as infile, \ | |
250 | open(opts.output, 'w') as cc_file, \ | |
251 | open(opts.legacy, 'w') as h_file: | |
252 | yml = yaml.load(infile, Loader=UniqueKeySafeLoader) | |
253 | headers = yml.get('headers', '') | |
254 | cc_file.write(prelude.format(name=name, headers=headers)) | |
255 | options = yml['options'] | |
256 | for option in options: | |
257 | try: | |
258 | cc_file.write(yaml_to_cxx(option, opts.indent) + '\n') | |
259 | if option.get('with_legacy', False): | |
260 | h_file.write(yaml_to_h(option) + '\n') | |
261 | except ValueError as e: | |
262 | print(f'failed to translate option "{name}": {e}', | |
263 | file=sys.stderr) | |
264 | return 1 | |
265 | cc_file.write(epilogue.replace("}}", "}")) | |
266 | ||
267 | ||
268 | def readable_size(value, typ): | |
269 | times = dict(T=1 << 40, | |
270 | G=1 << 30, | |
271 | M=1 << 20, | |
272 | K=1 << 10) | |
273 | if isinstance(value, str): | |
274 | value = value.strip('"') | |
275 | try: | |
276 | v = int(value) | |
277 | if v == 0: | |
278 | return 0 | |
279 | for unit, m in times.items(): | |
280 | if v % m == 0: | |
281 | v = int(v / m) | |
282 | return f'{v}_{unit}' | |
283 | return v | |
284 | except ValueError: | |
285 | return value | |
286 | ||
287 | ||
288 | def readable_duration(value, typ): | |
289 | times = dict(day=24*60*60, | |
290 | hr=60*60, | |
291 | min=60) | |
292 | if isinstance(value, str): | |
293 | value = value.strip('"') | |
294 | try: | |
295 | v = float(value) | |
296 | if math.floor(v) != v: | |
297 | return v | |
298 | v = int(v) | |
299 | if v == 0: | |
300 | return 0 | |
301 | for unit, m in times.items(): | |
302 | if v % m == 0: | |
303 | v = int(v / m) | |
304 | return f'{v}_{unit}' | |
305 | return v | |
306 | except ValueError: | |
307 | return value | |
308 | ||
309 | ||
310 | def readable_millisecs(value, typ): | |
311 | return int(value) | |
312 | ||
313 | ||
314 | def readable(opts): | |
315 | with open(opts.input) as infile, open(opts.output, 'w') as outfile: | |
316 | yml = yaml.load(infile, Loader=UniqueKeySafeLoader) | |
317 | options = yml['options'] | |
318 | for option in options: | |
319 | typ = option['type'] | |
320 | if typ in ('size', 'uint'): | |
321 | do_readable = readable_size | |
322 | elif typ in ('float', 'int', 'secs'): | |
323 | do_readable = readable_duration | |
324 | elif typ == 'millisecs': | |
325 | do_readable = readable_millisecs | |
326 | else: | |
327 | continue | |
328 | for field in ['default', 'min', 'max', 'daemon_default']: | |
329 | v = option.get(field) | |
330 | if v is not None: | |
331 | option[field] = do_readable(v, typ) | |
332 | yml['options'] = options | |
333 | yaml.dump(yml, outfile, sort_keys=False, indent=2) | |
334 | ||
335 | ||
336 | def main(): | |
337 | parser = argparse.ArgumentParser( | |
338 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) | |
339 | parser.add_argument('-i', '--input', dest='input', | |
340 | default='options.yaml', | |
341 | help='the YAML file to be processed') | |
342 | parser.add_argument('-o', '--output', dest='output', | |
343 | default='options', | |
344 | help='the path to the generated .cc file') | |
345 | parser.add_argument('--legacy', dest='legacy', | |
346 | default='legacy_options', | |
347 | help='the path to the generated legacy .h file') | |
348 | parser.add_argument('--indent', type=int, | |
349 | default=4, | |
350 | help='the number of spaces added before each line') | |
351 | parser.add_argument('--name', | |
352 | help='the name of the option group') | |
353 | parser.add_argument('--raw', action='store_true', | |
354 | help='output the array without the full function') | |
355 | parser.add_argument('--op', choices=('readable', 'translate'), | |
356 | default='translate', | |
357 | help='operation to perform.') | |
358 | opts = parser.parse_args(sys.argv[1:]) | |
359 | if opts.op == 'translate': | |
360 | translate(opts) | |
361 | else: | |
362 | readable(opts) | |
363 | ||
364 | ||
365 | if __name__ == '__main__': | |
366 | main() |