merge libext4 fixes
[helenos.git] / tools / config.py
blobc9877de36f7c48d8f78e7bd086aa352ebe5b95ef
1 #!/usr/bin/env python
3 # Copyright (c) 2006 Ondrej Palkovsky
4 # Copyright (c) 2009 Martin Decky
5 # Copyright (c) 2010 Jiri Svoboda
6 # All rights reserved.
8 # Redistribution and use in source and binary forms, with or without
9 # modification, are permitted provided that the following conditions
10 # are met:
12 # - Redistributions of source code must retain the above copyright
13 # notice, this list of conditions and the following disclaimer.
14 # - Redistributions in binary form must reproduce the above copyright
15 # notice, this list of conditions and the following disclaimer in the
16 # documentation and/or other materials provided with the distribution.
17 # - The name of the author may not be used to endorse or promote products
18 # derived from this software without specific prior written permission.
20 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
21 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
22 # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
23 # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
24 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
25 # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
29 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 """
33 HelenOS configuration system
34 """
36 import sys
37 import os
38 import re
39 import time
40 import subprocess
41 import xtui
43 RULES_FILE = sys.argv[1]
44 MAKEFILE = 'Makefile.config'
45 MACROS = 'config.h'
46 PRESETS_DIR = 'defaults'
48 def read_config(fname, config):
49 "Read saved values from last configuration run or a preset file"
51 inf = open(fname, 'r')
53 for line in inf:
54 res = re.match(r'^(?:#!# )?([^#]\w*)\s*=\s*(.*?)\s*$', line)
55 if res:
56 config[res.group(1)] = res.group(2)
58 inf.close()
60 def check_condition(text, config, rules):
61 "Check that the condition specified on input line is True (only CNF and DNF is supported)"
63 ctype = 'cnf'
65 if (')|' in text) or ('|(' in text):
66 ctype = 'dnf'
68 if ctype == 'cnf':
69 conds = text.split('&')
70 else:
71 conds = text.split('|')
73 for cond in conds:
74 if cond.startswith('(') and cond.endswith(')'):
75 cond = cond[1:-1]
77 inside = check_inside(cond, config, ctype)
79 if (ctype == 'cnf') and (not inside):
80 return False
82 if (ctype == 'dnf') and inside:
83 return True
85 if ctype == 'cnf':
86 return True
88 return False
90 def check_inside(text, config, ctype):
91 "Check for condition"
93 if ctype == 'cnf':
94 conds = text.split('|')
95 else:
96 conds = text.split('&')
98 for cond in conds:
99 res = re.match(r'^(.*?)(!?=)(.*)$', cond)
100 if not res:
101 raise RuntimeError("Invalid condition: %s" % cond)
103 condname = res.group(1)
104 oper = res.group(2)
105 condval = res.group(3)
107 if not condname in config:
108 varval = ''
109 else:
110 varval = config[condname]
111 if (varval == '*'):
112 varval = 'y'
114 if ctype == 'cnf':
115 if (oper == '=') and (condval == varval):
116 return True
118 if (oper == '!=') and (condval != varval):
119 return True
120 else:
121 if (oper == '=') and (condval != varval):
122 return False
124 if (oper == '!=') and (condval == varval):
125 return False
127 if ctype == 'cnf':
128 return False
130 return True
132 def parse_rules(fname, rules):
133 "Parse rules file"
135 inf = open(fname, 'r')
137 name = ''
138 choices = []
140 for line in inf:
142 if line.startswith('!'):
143 # Ask a question
144 res = re.search(r'!\s*(?:\[(.*?)\])?\s*([^\s]+)\s*\((.*)\)\s*$', line)
146 if not res:
147 raise RuntimeError("Weird line: %s" % line)
149 cond = res.group(1)
150 varname = res.group(2)
151 vartype = res.group(3)
153 rules.append((varname, vartype, name, choices, cond))
154 name = ''
155 choices = []
156 continue
158 if line.startswith('@'):
159 # Add new line into the 'choices' array
160 res = re.match(r'@\s*(?:\[(.*?)\])?\s*"(.*?)"\s*(.*)$', line)
162 if not res:
163 raise RuntimeError("Bad line: %s" % line)
165 choices.append((res.group(2), res.group(3)))
166 continue
168 if line.startswith('%'):
169 # Name of the option
170 name = line[1:].strip()
171 continue
173 if line.startswith('#') or (line == '\n'):
174 # Comment or empty line
175 continue
178 raise RuntimeError("Unknown syntax: %s" % line)
180 inf.close()
182 def yes_no(default):
183 "Return '*' if yes, ' ' if no"
185 if default == 'y':
186 return '*'
188 return ' '
190 def subchoice(screen, name, choices, default):
191 "Return choice of choices"
193 maxkey = 0
194 for key, val in choices:
195 length = len(key)
196 if (length > maxkey):
197 maxkey = length
199 options = []
200 position = None
201 cnt = 0
202 for key, val in choices:
203 if (default) and (key == default):
204 position = cnt
206 options.append(" %-*s %s " % (maxkey, key, val))
207 cnt += 1
209 (button, value) = xtui.choice_window(screen, name, 'Choose value', options, position)
211 if button == 'cancel':
212 return None
214 return choices[value][0]
216 ## Infer and verify configuration values.
218 # Augment @a config with values that can be inferred, purge invalid ones
219 # and verify that all variables have a value (previously specified or inferred).
221 # @param config Configuration to work on
222 # @param rules Rules
224 # @return True if configuration is complete and valid, False
225 # otherwise.
227 def infer_verify_choices(config, rules):
228 "Infer and verify configuration values."
230 for rule in rules:
231 varname, vartype, name, choices, cond = rule
233 if cond and (not check_condition(cond, config, rules)):
234 continue
236 if not varname in config:
237 value = None
238 else:
239 value = config[varname]
241 if not validate_rule_value(rule, value):
242 value = None
244 default = get_default_rule(rule)
247 # If we don't have a value but we do have
248 # a default, use it.
250 if value == None and default != None:
251 value = default
252 config[varname] = default
254 if not varname in config:
255 return False
257 return True
259 ## Get default value from a rule.
260 def get_default_rule(rule):
261 varname, vartype, name, choices, cond = rule
263 default = None
265 if vartype == 'choice':
266 # If there is just one option, use it
267 if len(choices) == 1:
268 default = choices[0][0]
269 elif vartype == 'y':
270 default = '*'
271 elif vartype == 'n':
272 default = 'n'
273 elif vartype == 'y/n':
274 default = 'y'
275 elif vartype == 'n/y':
276 default = 'n'
277 else:
278 raise RuntimeError("Unknown variable type: %s" % vartype)
280 return default
282 ## Get option from a rule.
284 # @param rule Rule for a variable
285 # @param value Current value of the variable
287 # @return Option (string) to ask or None which means not to ask.
289 def get_rule_option(rule, value):
290 varname, vartype, name, choices, cond = rule
292 option = None
294 if vartype == 'choice':
295 # If there is just one option, don't ask
296 if len(choices) != 1:
297 if (value == None):
298 option = "? %s --> " % name
299 else:
300 option = " %s [%s] --> " % (name, value)
301 elif vartype == 'y':
302 pass
303 elif vartype == 'n':
304 pass
305 elif vartype == 'y/n':
306 option = " <%s> %s " % (yes_no(value), name)
307 elif vartype == 'n/y':
308 option =" <%s> %s " % (yes_no(value), name)
309 else:
310 raise RuntimeError("Unknown variable type: %s" % vartype)
312 return option
314 ## Check if variable value is valid.
316 # @param rule Rule for the variable
317 # @param value Value of the variable
319 # @return True if valid, False if not valid.
321 def validate_rule_value(rule, value):
322 varname, vartype, name, choices, cond = rule
324 if value == None:
325 return True
327 if vartype == 'choice':
328 if not value in [choice[0] for choice in choices]:
329 return False
330 elif vartype == 'y':
331 if value != 'y':
332 return False
333 elif vartype == 'n':
334 if value != 'n':
335 return False
336 elif vartype == 'y/n':
337 if not value in ['y', 'n']:
338 return False
339 elif vartype == 'n/y':
340 if not value in ['y', 'n']:
341 return False
342 else:
343 raise RuntimeError("Unknown variable type: %s" % vartype)
345 return True
347 def preprocess_config(config, rules):
348 "Preprocess configuration"
350 varname_mode = 'CONFIG_BFB_MODE'
351 varname_width = 'CONFIG_BFB_WIDTH'
352 varname_height = 'CONFIG_BFB_HEIGHT'
354 if varname_mode in config:
355 mode = config[varname_mode].partition('x')
357 config[varname_width] = mode[0]
358 rules.append((varname_width, 'choice', 'Default framebuffer width', None, None))
360 config[varname_height] = mode[2]
361 rules.append((varname_height, 'choice', 'Default framebuffer height', None, None))
363 def create_output(mkname, mcname, config, rules):
364 "Create output configuration"
366 timestamp_unix = int(time.time())
367 timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp_unix))
369 sys.stderr.write("Fetching current revision identifier ... ")
371 try:
372 version = subprocess.Popen(['bzr', 'version-info', '--custom', '--template={clean}:{revno}:{revision_id}'], stdout = subprocess.PIPE).communicate()[0].decode().split(':')
373 sys.stderr.write("ok\n")
374 except:
375 version = [1, "unknown", "unknown"]
376 sys.stderr.write("failed\n")
378 if len(version) == 3:
379 revision = version[1]
380 if version[0] != 1:
381 revision += 'M'
382 revision += ' (%s)' % version[2]
383 else:
384 revision = None
386 outmk = open(mkname, 'w')
387 outmc = open(mcname, 'w')
389 outmk.write('#########################################\n')
390 outmk.write('## AUTO-GENERATED FILE, DO NOT EDIT!!! ##\n')
391 outmk.write('## Generated by: tools/config.py ##\n')
392 outmk.write('#########################################\n\n')
394 outmc.write('/***************************************\n')
395 outmc.write(' * AUTO-GENERATED FILE, DO NOT EDIT!!! *\n')
396 outmc.write(' * Generated by: tools/config.py *\n')
397 outmc.write(' ***************************************/\n\n')
399 defs = 'CONFIG_DEFS ='
401 for varname, vartype, name, choices, cond in rules:
402 if cond and (not check_condition(cond, config, rules)):
403 continue
405 if not varname in config:
406 value = ''
407 else:
408 value = config[varname]
409 if (value == '*'):
410 value = 'y'
412 outmk.write('# %s\n%s = %s\n\n' % (name, varname, value))
414 if vartype in ["y", "n", "y/n", "n/y"]:
415 if value == "y":
416 outmc.write('/* %s */\n#define %s\n\n' % (name, varname))
417 defs += ' -D%s' % varname
418 else:
419 outmc.write('/* %s */\n#define %s %s\n#define %s_%s\n\n' % (name, varname, value, varname, value))
420 defs += ' -D%s=%s -D%s_%s' % (varname, value, varname, value)
422 if revision is not None:
423 outmk.write('REVISION = %s\n' % revision)
424 outmc.write('#define REVISION %s\n' % revision)
425 defs += ' "-DREVISION=%s"' % revision
427 outmk.write('TIMESTAMP_UNIX = %d\n' % timestamp_unix)
428 outmc.write('#define TIMESTAMP_UNIX %d\n' % timestamp_unix)
429 defs += ' "-DTIMESTAMP_UNIX=%d"\n' % timestamp_unix
431 outmk.write('TIMESTAMP = %s\n' % timestamp)
432 outmc.write('#define TIMESTAMP %s\n' % timestamp)
433 defs += ' "-DTIMESTAMP=%s"\n' % timestamp
435 outmk.write(defs)
437 outmk.close()
438 outmc.close()
440 def sorted_dir(root):
441 list = os.listdir(root)
442 list.sort()
443 return list
445 ## Ask user to choose a configuration profile.
447 def choose_profile(root, fname, screen, config):
448 options = []
449 opt2path = {}
450 cnt = 0
452 # Look for profiles
453 for name in sorted_dir(root):
454 path = os.path.join(root, name)
455 canon = os.path.join(path, fname)
457 if os.path.isdir(path) and os.path.exists(canon) and os.path.isfile(canon):
458 subprofile = False
460 # Look for subprofiles
461 for subname in sorted_dir(path):
462 subpath = os.path.join(path, subname)
463 subcanon = os.path.join(subpath, fname)
465 if os.path.isdir(subpath) and os.path.exists(subcanon) and os.path.isfile(subcanon):
466 subprofile = True
467 options.append("%s (%s)" % (name, subname))
468 opt2path[cnt] = [name, subname]
469 cnt += 1
471 if not subprofile:
472 options.append(name)
473 opt2path[cnt] = [name]
474 cnt += 1
476 (button, value) = xtui.choice_window(screen, 'Load preconfigured defaults', 'Choose configuration profile', options, None)
478 if button == 'cancel':
479 return None
481 return opt2path[value]
483 ## Read presets from a configuration profile.
485 # @param profile Profile to load from (a list of string components)
486 # @param config Output configuration
488 def read_presets(profile, config):
489 path = os.path.join(PRESETS_DIR, profile[0], MAKEFILE)
490 read_config(path, config)
492 if len(profile) > 1:
493 path = os.path.join(PRESETS_DIR, profile[0], profile[1], MAKEFILE)
494 read_config(path, config)
496 ## Parse profile name (relative OS path) into a list of components.
498 # @param profile_name Relative path (using OS separator)
499 # @return List of components
501 def parse_profile_name(profile_name):
502 profile = []
504 head, tail = os.path.split(profile_name)
505 if head != '':
506 profile.append(head)
508 profile.append(tail)
509 return profile
511 def main():
512 profile = None
513 config = {}
514 rules = []
516 # Parse rules file
517 parse_rules(RULES_FILE, rules)
519 # Input configuration file can be specified on command line
520 # otherwise configuration from previous run is used.
521 if len(sys.argv) >= 4:
522 profile = parse_profile_name(sys.argv[3])
523 read_presets(profile, config)
524 elif os.path.exists(MAKEFILE):
525 read_config(MAKEFILE, config)
527 # Default mode: check values and regenerate configuration files
528 if (len(sys.argv) >= 3) and (sys.argv[2] == 'default'):
529 if (infer_verify_choices(config, rules)):
530 preprocess_config(config, rules)
531 create_output(MAKEFILE, MACROS, config, rules)
532 return 0
534 # Hands-off mode: check values and regenerate configuration files,
535 # but no interactive fallback
536 if (len(sys.argv) >= 3) and (sys.argv[2] == 'hands-off'):
537 # We deliberately test sys.argv >= 4 because we do not want
538 # to read implicitly any possible previous run configuration
539 if len(sys.argv) < 4:
540 sys.stderr.write("Configuration error: No presets specified\n")
541 return 2
543 if (infer_verify_choices(config, rules)):
544 preprocess_config(config, rules)
545 create_output(MAKEFILE, MACROS, config, rules)
546 return 0
548 sys.stderr.write("Configuration error: The presets are ambiguous\n")
549 return 1
551 # Check mode: only check configuration
552 if (len(sys.argv) >= 3) and (sys.argv[2] == 'check'):
553 if infer_verify_choices(config, rules):
554 return 0
555 return 1
557 screen = xtui.screen_init()
558 try:
559 selname = None
560 position = None
561 while True:
563 # Cancel out all values which have to be deduced
564 for varname, vartype, name, choices, cond in rules:
565 if (vartype == 'y') and (varname in config) and (config[varname] == '*'):
566 config[varname] = None
568 options = []
569 opt2row = {}
570 cnt = 1
572 options.append(" --- Load preconfigured defaults ... ")
574 for rule in rules:
575 varname, vartype, name, choices, cond = rule
577 if cond and (not check_condition(cond, config, rules)):
578 continue
580 if varname == selname:
581 position = cnt
583 if not varname in config:
584 value = None
585 else:
586 value = config[varname]
588 if not validate_rule_value(rule, value):
589 value = None
591 default = get_default_rule(rule)
594 # If we don't have a value but we do have
595 # a default, use it.
597 if value == None and default != None:
598 value = default
599 config[varname] = default
601 option = get_rule_option(rule, value)
602 if option != None:
603 options.append(option)
604 else:
605 continue
607 opt2row[cnt] = (varname, vartype, name, choices)
609 cnt += 1
611 if (position != None) and (position >= len(options)):
612 position = None
614 (button, value) = xtui.choice_window(screen, 'HelenOS configuration', 'Choose configuration option', options, position)
616 if button == 'cancel':
617 return 'Configuration canceled'
619 if button == 'done':
620 if (infer_verify_choices(config, rules)):
621 break
622 else:
623 xtui.error_dialog(screen, 'Error', 'Some options have still undefined values. These options are marked with the "?" sign.')
624 continue
626 if value == 0:
627 profile = choose_profile(PRESETS_DIR, MAKEFILE, screen, config)
628 if profile != None:
629 read_presets(profile, config)
630 position = 1
631 continue
633 position = None
634 if not value in opt2row:
635 raise RuntimeError("Error selecting value: %s" % value)
637 (selname, seltype, name, choices) = opt2row[value]
639 if not selname in config:
640 value = None
641 else:
642 value = config[selname]
644 if seltype == 'choice':
645 config[selname] = subchoice(screen, name, choices, value)
646 elif (seltype == 'y/n') or (seltype == 'n/y'):
647 if config[selname] == 'y':
648 config[selname] = 'n'
649 else:
650 config[selname] = 'y'
651 finally:
652 xtui.screen_done(screen)
654 preprocess_config(config, rules)
655 create_output(MAKEFILE, MACROS, config, rules)
656 return 0
658 if __name__ == '__main__':
659 sys.exit(main())