isa: read() is not guaranteed to read all the bytes requested.
[helenos.git] / tools / config.py
blobdea4e7fe3774d8e8c6a48ac73dc4be6cf47e9419
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
42 import random
44 RULES_FILE = sys.argv[1]
45 MAKEFILE = 'Makefile.config'
46 MACROS = 'config.h'
47 PRESETS_DIR = 'defaults'
49 def read_config(fname, config):
50 "Read saved values from last configuration run or a preset file"
52 inf = open(fname, 'r')
54 for line in inf:
55 res = re.match(r'^(?:#!# )?([^#]\w*)\s*=\s*(.*?)\s*$', line)
56 if res:
57 config[res.group(1)] = res.group(2)
59 inf.close()
61 def check_condition(text, config, rules):
62 "Check that the condition specified on input line is True (only CNF and DNF is supported)"
64 ctype = 'cnf'
66 if (')|' in text) or ('|(' in text):
67 ctype = 'dnf'
69 if ctype == 'cnf':
70 conds = text.split('&')
71 else:
72 conds = text.split('|')
74 for cond in conds:
75 if cond.startswith('(') and cond.endswith(')'):
76 cond = cond[1:-1]
78 inside = check_inside(cond, config, ctype)
80 if (ctype == 'cnf') and (not inside):
81 return False
83 if (ctype == 'dnf') and inside:
84 return True
86 if ctype == 'cnf':
87 return True
89 return False
91 def check_inside(text, config, ctype):
92 "Check for condition"
94 if ctype == 'cnf':
95 conds = text.split('|')
96 else:
97 conds = text.split('&')
99 for cond in conds:
100 res = re.match(r'^(.*?)(!?=)(.*)$', cond)
101 if not res:
102 raise RuntimeError("Invalid condition: %s" % cond)
104 condname = res.group(1)
105 oper = res.group(2)
106 condval = res.group(3)
108 if not condname in config:
109 varval = ''
110 else:
111 varval = config[condname]
112 if (varval == '*'):
113 varval = 'y'
115 if ctype == 'cnf':
116 if (oper == '=') and (condval == varval):
117 return True
119 if (oper == '!=') and (condval != varval):
120 return True
121 else:
122 if (oper == '=') and (condval != varval):
123 return False
125 if (oper == '!=') and (condval == varval):
126 return False
128 if ctype == 'cnf':
129 return False
131 return True
133 def parse_rules(fname, rules):
134 "Parse rules file"
136 inf = open(fname, 'r')
138 name = ''
139 choices = []
141 for line in inf:
143 if line.startswith('!'):
144 # Ask a question
145 res = re.search(r'!\s*(?:\[(.*?)\])?\s*([^\s]+)\s*\((.*)\)\s*$', line)
147 if not res:
148 raise RuntimeError("Weird line: %s" % line)
150 cond = res.group(1)
151 varname = res.group(2)
152 vartype = res.group(3)
154 rules.append((varname, vartype, name, choices, cond))
155 name = ''
156 choices = []
157 continue
159 if line.startswith('@'):
160 # Add new line into the 'choices' array
161 res = re.match(r'@\s*(?:\[(.*?)\])?\s*"(.*?)"\s*(.*)$', line)
163 if not res:
164 raise RuntimeError("Bad line: %s" % line)
166 choices.append((res.group(2), res.group(3)))
167 continue
169 if line.startswith('%'):
170 # Name of the option
171 name = line[1:].strip()
172 continue
174 if line.startswith('#') or (line == '\n'):
175 # Comment or empty line
176 continue
179 raise RuntimeError("Unknown syntax: %s" % line)
181 inf.close()
183 def yes_no(default):
184 "Return '*' if yes, ' ' if no"
186 if default == 'y':
187 return '*'
189 return ' '
191 def subchoice(screen, name, choices, default):
192 "Return choice of choices"
194 maxkey = 0
195 for key, val in choices:
196 length = len(key)
197 if (length > maxkey):
198 maxkey = length
200 options = []
201 position = None
202 cnt = 0
203 for key, val in choices:
204 if (default) and (key == default):
205 position = cnt
207 options.append(" %-*s %s " % (maxkey, key, val))
208 cnt += 1
210 (button, value) = xtui.choice_window(screen, name, 'Choose value', options, position)
212 if button == 'cancel':
213 return None
215 return choices[value][0]
217 ## Infer and verify configuration values.
219 # Augment @a config with values that can be inferred, purge invalid ones
220 # and verify that all variables have a value (previously specified or inferred).
222 # @param config Configuration to work on
223 # @param rules Rules
225 # @return True if configuration is complete and valid, False
226 # otherwise.
228 def infer_verify_choices(config, rules):
229 "Infer and verify configuration values."
231 for rule in rules:
232 varname, vartype, name, choices, cond = rule
234 if cond and (not check_condition(cond, config, rules)):
235 continue
237 if not varname in config:
238 value = None
239 else:
240 value = config[varname]
242 if not validate_rule_value(rule, value):
243 value = None
245 default = get_default_rule(rule)
248 # If we don't have a value but we do have
249 # a default, use it.
251 if value == None and default != None:
252 value = default
253 config[varname] = default
255 if not varname in config:
256 return False
258 return True
260 ## Fill the configuration with random (but valid) values.
262 # The random selection takes next rule and if the condition does
263 # not violate existing configuration, random value of the variable
264 # is selected.
265 # This happens recursively as long as there are more rules.
266 # If a conflict is found, we backtrack and try other settings of the
267 # variable or ignoring the variable altogether.
269 # @param config Configuration to work on
270 # @param rules Rules
271 # @param start_index With which rule to start (initial call must specify 0 here).
272 # @return True if able to find a valid configuration
273 def random_choices(config, rules, start_index):
274 "Fill the configuration with random (but valid) values."
275 if start_index >= len(rules):
276 return True
278 varname, vartype, name, choices, cond = rules[start_index]
280 # First check that this rule would make sense
281 if cond:
282 if not check_condition(cond, config, rules):
283 return random_choices(config, rules, start_index + 1)
285 # Remember previous choices for backtracking
286 yes_no = 0
287 choices_indexes = range(0, len(choices))
288 random.shuffle(choices_indexes)
290 # Remember current configuration value
291 old_value = None
292 try:
293 old_value = config[varname]
294 except KeyError:
295 old_value = None
297 # For yes/no choices, we ran the loop at most 2 times, for select
298 # choices as many times as there are options.
299 try_counter = 0
300 while True:
301 if vartype == 'choice':
302 if try_counter >= len(choices_indexes):
303 break
304 value = choices[choices_indexes[try_counter]][0]
305 elif vartype == 'y' or vartype == 'n':
306 if try_counter > 0:
307 break
308 value = vartype
309 elif vartype == 'y/n' or vartype == 'n/y':
310 if try_counter == 0:
311 yes_no = random.randint(0, 1)
312 elif try_counter == 1:
313 yes_no = 1 - yes_no
314 else:
315 break
316 if yes_no == 0:
317 value = 'n'
318 else:
319 value = 'y'
320 else:
321 raise RuntimeError("Unknown variable type: %s" % vartype)
323 config[varname] = value
325 ok = random_choices(config, rules, start_index + 1)
326 if ok:
327 return True
329 try_counter = try_counter + 1
331 # Restore the old value and backtrack
332 # (need to delete to prevent "ghost" variables that do not exist under
333 # certain configurations)
334 config[varname] = old_value
335 if old_value is None:
336 del config[varname]
338 return random_choices(config, rules, start_index + 1)
341 ## Get default value from a rule.
342 def get_default_rule(rule):
343 varname, vartype, name, choices, cond = rule
345 default = None
347 if vartype == 'choice':
348 # If there is just one option, use it
349 if len(choices) == 1:
350 default = choices[0][0]
351 elif vartype == 'y':
352 default = '*'
353 elif vartype == 'n':
354 default = 'n'
355 elif vartype == 'y/n':
356 default = 'y'
357 elif vartype == 'n/y':
358 default = 'n'
359 else:
360 raise RuntimeError("Unknown variable type: %s" % vartype)
362 return default
364 ## Get option from a rule.
366 # @param rule Rule for a variable
367 # @param value Current value of the variable
369 # @return Option (string) to ask or None which means not to ask.
371 def get_rule_option(rule, value):
372 varname, vartype, name, choices, cond = rule
374 option = None
376 if vartype == 'choice':
377 # If there is just one option, don't ask
378 if len(choices) != 1:
379 if (value == None):
380 option = "? %s --> " % name
381 else:
382 option = " %s [%s] --> " % (name, value)
383 elif vartype == 'y':
384 pass
385 elif vartype == 'n':
386 pass
387 elif vartype == 'y/n':
388 option = " <%s> %s " % (yes_no(value), name)
389 elif vartype == 'n/y':
390 option =" <%s> %s " % (yes_no(value), name)
391 else:
392 raise RuntimeError("Unknown variable type: %s" % vartype)
394 return option
396 ## Check if variable value is valid.
398 # @param rule Rule for the variable
399 # @param value Value of the variable
401 # @return True if valid, False if not valid.
403 def validate_rule_value(rule, value):
404 varname, vartype, name, choices, cond = rule
406 if value == None:
407 return True
409 if vartype == 'choice':
410 if not value in [choice[0] for choice in choices]:
411 return False
412 elif vartype == 'y':
413 if value != 'y':
414 return False
415 elif vartype == 'n':
416 if value != 'n':
417 return False
418 elif vartype == 'y/n':
419 if not value in ['y', 'n']:
420 return False
421 elif vartype == 'n/y':
422 if not value in ['y', 'n']:
423 return False
424 else:
425 raise RuntimeError("Unknown variable type: %s" % vartype)
427 return True
429 def preprocess_config(config, rules):
430 "Preprocess configuration"
432 varname_mode = 'CONFIG_BFB_MODE'
433 varname_width = 'CONFIG_BFB_WIDTH'
434 varname_height = 'CONFIG_BFB_HEIGHT'
436 if varname_mode in config:
437 mode = config[varname_mode].partition('x')
439 config[varname_width] = mode[0]
440 rules.append((varname_width, 'choice', 'Default framebuffer width', None, None))
442 config[varname_height] = mode[2]
443 rules.append((varname_height, 'choice', 'Default framebuffer height', None, None))
445 def create_output(mkname, mcname, config, rules):
446 "Create output configuration"
448 timestamp_unix = int(time.time())
449 timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp_unix))
451 sys.stderr.write("Fetching current revision identifier ... ")
453 try:
454 version = subprocess.Popen(['bzr', 'version-info', '--custom', '--template={clean}:{revno}:{revision_id}'], stdout = subprocess.PIPE).communicate()[0].decode().split(':')
455 sys.stderr.write("ok\n")
456 except:
457 version = [1, "unknown", "unknown"]
458 sys.stderr.write("failed\n")
460 if len(version) == 3:
461 revision = version[1]
462 if version[0] != 1:
463 revision += 'M'
464 revision += ' (%s)' % version[2]
465 else:
466 revision = None
468 outmk = open(mkname, 'w')
469 outmc = open(mcname, 'w')
471 outmk.write('#########################################\n')
472 outmk.write('## AUTO-GENERATED FILE, DO NOT EDIT!!! ##\n')
473 outmk.write('## Generated by: tools/config.py ##\n')
474 outmk.write('#########################################\n\n')
476 outmc.write('/***************************************\n')
477 outmc.write(' * AUTO-GENERATED FILE, DO NOT EDIT!!! *\n')
478 outmc.write(' * Generated by: tools/config.py *\n')
479 outmc.write(' ***************************************/\n\n')
481 defs = 'CONFIG_DEFS ='
483 for varname, vartype, name, choices, cond in rules:
484 if cond and (not check_condition(cond, config, rules)):
485 continue
487 if not varname in config:
488 value = ''
489 else:
490 value = config[varname]
491 if (value == '*'):
492 value = 'y'
494 outmk.write('# %s\n%s = %s\n\n' % (name, varname, value))
496 if vartype in ["y", "n", "y/n", "n/y"]:
497 if value == "y":
498 outmc.write('/* %s */\n#define %s\n\n' % (name, varname))
499 defs += ' -D%s' % varname
500 else:
501 outmc.write('/* %s */\n#define %s %s\n#define %s_%s\n\n' % (name, varname, value, varname, value))
502 defs += ' -D%s=%s -D%s_%s' % (varname, value, varname, value)
504 if revision is not None:
505 outmk.write('REVISION = %s\n' % revision)
506 outmc.write('#define REVISION %s\n' % revision)
507 defs += ' "-DREVISION=%s"' % revision
509 outmk.write('TIMESTAMP_UNIX = %d\n' % timestamp_unix)
510 outmc.write('#define TIMESTAMP_UNIX %d\n' % timestamp_unix)
511 defs += ' "-DTIMESTAMP_UNIX=%d"\n' % timestamp_unix
513 outmk.write('TIMESTAMP = %s\n' % timestamp)
514 outmc.write('#define TIMESTAMP %s\n' % timestamp)
515 defs += ' "-DTIMESTAMP=%s"\n' % timestamp
517 outmk.write(defs)
519 outmk.close()
520 outmc.close()
522 def sorted_dir(root):
523 list = os.listdir(root)
524 list.sort()
525 return list
527 ## Ask user to choose a configuration profile.
529 def choose_profile(root, fname, screen, config):
530 options = []
531 opt2path = {}
532 cnt = 0
534 # Look for profiles
535 for name in sorted_dir(root):
536 path = os.path.join(root, name)
537 canon = os.path.join(path, fname)
539 if os.path.isdir(path) and os.path.exists(canon) and os.path.isfile(canon):
540 subprofile = False
542 # Look for subprofiles
543 for subname in sorted_dir(path):
544 subpath = os.path.join(path, subname)
545 subcanon = os.path.join(subpath, fname)
547 if os.path.isdir(subpath) and os.path.exists(subcanon) and os.path.isfile(subcanon):
548 subprofile = True
549 options.append("%s (%s)" % (name, subname))
550 opt2path[cnt] = [name, subname]
551 cnt += 1
553 if not subprofile:
554 options.append(name)
555 opt2path[cnt] = [name]
556 cnt += 1
558 (button, value) = xtui.choice_window(screen, 'Load preconfigured defaults', 'Choose configuration profile', options, None)
560 if button == 'cancel':
561 return None
563 return opt2path[value]
565 ## Read presets from a configuration profile.
567 # @param profile Profile to load from (a list of string components)
568 # @param config Output configuration
570 def read_presets(profile, config):
571 path = os.path.join(PRESETS_DIR, profile[0], MAKEFILE)
572 read_config(path, config)
574 if len(profile) > 1:
575 path = os.path.join(PRESETS_DIR, profile[0], profile[1], MAKEFILE)
576 read_config(path, config)
578 ## Parse profile name (relative OS path) into a list of components.
580 # @param profile_name Relative path (using OS separator)
581 # @return List of components
583 def parse_profile_name(profile_name):
584 profile = []
586 head, tail = os.path.split(profile_name)
587 if head != '':
588 profile.append(head)
590 profile.append(tail)
591 return profile
593 def main():
594 profile = None
595 config = {}
596 rules = []
598 # Parse rules file
599 parse_rules(RULES_FILE, rules)
601 # Input configuration file can be specified on command line
602 # otherwise configuration from previous run is used.
603 if len(sys.argv) >= 4:
604 profile = parse_profile_name(sys.argv[3])
605 read_presets(profile, config)
606 elif os.path.exists(MAKEFILE):
607 read_config(MAKEFILE, config)
609 # Default mode: check values and regenerate configuration files
610 if (len(sys.argv) >= 3) and (sys.argv[2] == 'default'):
611 if (infer_verify_choices(config, rules)):
612 preprocess_config(config, rules)
613 create_output(MAKEFILE, MACROS, config, rules)
614 return 0
616 # Hands-off mode: check values and regenerate configuration files,
617 # but no interactive fallback
618 if (len(sys.argv) >= 3) and (sys.argv[2] == 'hands-off'):
619 # We deliberately test sys.argv >= 4 because we do not want
620 # to read implicitly any possible previous run configuration
621 if len(sys.argv) < 4:
622 sys.stderr.write("Configuration error: No presets specified\n")
623 return 2
625 if (infer_verify_choices(config, rules)):
626 preprocess_config(config, rules)
627 create_output(MAKEFILE, MACROS, config, rules)
628 return 0
630 sys.stderr.write("Configuration error: The presets are ambiguous\n")
631 return 1
633 # Check mode: only check configuration
634 if (len(sys.argv) >= 3) and (sys.argv[2] == 'check'):
635 if infer_verify_choices(config, rules):
636 return 0
637 return 1
639 # Random mode
640 if (len(sys.argv) == 3) and (sys.argv[2] == 'random'):
641 ok = random_choices(config, rules, 0)
642 if not ok:
643 sys.stderr.write("Internal error: unable to generate random config.\n")
644 return 2
645 if not infer_verify_choices(config, rules):
646 sys.stderr.write("Internal error: random configuration not consistent.\n")
647 return 2
648 preprocess_config(config, rules)
649 create_output(MAKEFILE, MACROS, config, rules)
651 return 0
653 screen = xtui.screen_init()
654 try:
655 selname = None
656 position = None
657 while True:
659 # Cancel out all values which have to be deduced
660 for varname, vartype, name, choices, cond in rules:
661 if (vartype == 'y') and (varname in config) and (config[varname] == '*'):
662 config[varname] = None
664 options = []
665 opt2row = {}
666 cnt = 1
668 options.append(" --- Load preconfigured defaults ... ")
670 for rule in rules:
671 varname, vartype, name, choices, cond = rule
673 if cond and (not check_condition(cond, config, rules)):
674 continue
676 if varname == selname:
677 position = cnt
679 if not varname in config:
680 value = None
681 else:
682 value = config[varname]
684 if not validate_rule_value(rule, value):
685 value = None
687 default = get_default_rule(rule)
690 # If we don't have a value but we do have
691 # a default, use it.
693 if value == None and default != None:
694 value = default
695 config[varname] = default
697 option = get_rule_option(rule, value)
698 if option != None:
699 options.append(option)
700 else:
701 continue
703 opt2row[cnt] = (varname, vartype, name, choices)
705 cnt += 1
707 if (position != None) and (position >= len(options)):
708 position = None
710 (button, value) = xtui.choice_window(screen, 'HelenOS configuration', 'Choose configuration option', options, position)
712 if button == 'cancel':
713 return 'Configuration canceled'
715 if button == 'done':
716 if (infer_verify_choices(config, rules)):
717 break
718 else:
719 xtui.error_dialog(screen, 'Error', 'Some options have still undefined values. These options are marked with the "?" sign.')
720 continue
722 if value == 0:
723 profile = choose_profile(PRESETS_DIR, MAKEFILE, screen, config)
724 if profile != None:
725 read_presets(profile, config)
726 position = 1
727 continue
729 position = None
730 if not value in opt2row:
731 raise RuntimeError("Error selecting value: %s" % value)
733 (selname, seltype, name, choices) = opt2row[value]
735 if not selname in config:
736 value = None
737 else:
738 value = config[selname]
740 if seltype == 'choice':
741 config[selname] = subchoice(screen, name, choices, value)
742 elif (seltype == 'y/n') or (seltype == 'n/y'):
743 if config[selname] == 'y':
744 config[selname] = 'n'
745 else:
746 config[selname] = 'y'
747 finally:
748 xtui.screen_done(screen)
750 preprocess_config(config, rules)
751 create_output(MAKEFILE, MACROS, config, rules)
752 return 0
754 if __name__ == '__main__':
755 sys.exit(main())