Write data (floppy)
[helenos.git] / tools / config.py
blob487f8a79167f25298d4f7d0f4152e286f94c4eb3
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 ARGPOS_RULES = 1
45 ARGPOS_PRESETS_DIR = 2
46 ARGPOS_CHOICE = 3
47 ARGPOS_PRESET = 4
48 ARGPOS_MASK_PLATFORM = 3
50 RULES_FILE = sys.argv[ARGPOS_RULES]
51 MAKEFILE = 'Makefile.config'
52 MACROS = 'config.h'
53 PRESETS_DIR = sys.argv[ARGPOS_PRESETS_DIR]
55 class BinaryOp:
56 def __init__(self, operator, left, right):
57 assert operator in ('&', '|', '=', '!=')
59 self._operator = operator
60 self._left = left
61 self._right = right
63 def evaluate(self, config):
64 if self._operator == '&':
65 return self._left.evaluate(config) and \
66 self._right.evaluate(config)
67 if self._operator == '|':
68 return self._left.evaluate(config) or \
69 self._right.evaluate(config)
71 # '=' or '!='
72 if not self._left in config:
73 config_val = ''
74 else:
75 config_val = config[self._left]
76 if config_val == '*':
77 config_val = 'y'
79 if self._operator == '=':
80 return self._right == config_val
81 return self._right != config_val
83 # Expression parser
84 class CondParser:
85 TOKEN_EOE = 0
86 TOKEN_SPECIAL = 1
87 TOKEN_STRING = 2
89 def __init__(self, text):
90 self._text = text
92 def parse(self):
93 self._position = -1
94 self._next_char()
95 self._next_token()
97 res = self._parse_expr()
98 if self._token_type != self.TOKEN_EOE:
99 self._error("Expected end of expression")
100 return res
102 def _next_char(self):
103 self._position += 1
104 if self._position >= len(self._text):
105 self._char = None
106 else:
107 self._char = self._text[self._position]
108 self._is_special_char = self._char in \
109 ('&', '|', '=', '!', '(', ')')
111 def _error(self, msg):
112 raise RuntimeError("Error parsing expression: %s:\n%s\n%s^" %
113 (msg, self._text, " " * self._token_position))
115 def _next_token(self):
116 self._token_position = self._position
118 # End of expression
119 if self._char == None:
120 self._token = None
121 self._token_type = self.TOKEN_EOE
122 return
124 # '&', '|', '=', '!=', '(', ')'
125 if self._is_special_char:
126 self._token = self._char
127 self._next_char()
128 if self._token == '!':
129 if self._char != '=':
130 self._error("Expected '='")
131 self._token += self._char
132 self._next_char()
133 self._token_type = self.TOKEN_SPECIAL
134 return
136 # <var> or <val>
137 self._token = ''
138 self._token_type = self.TOKEN_STRING
139 while True:
140 self._token += self._char
141 self._next_char()
142 if self._is_special_char or self._char == None:
143 break
145 def _parse_expr(self):
146 """ <expr> ::= <or_expr> ('&' <or_expr>)* """
148 left = self._parse_or_expr()
149 while self._token == '&':
150 self._next_token()
151 left = BinaryOp('&', left, self._parse_or_expr())
152 return left
154 def _parse_or_expr(self):
155 """ <or_expr> ::= <factor> ('|' <factor>)* """
157 left = self._parse_factor()
158 while self._token == '|':
159 self._next_token()
160 left = BinaryOp('|', left, self._parse_factor())
161 return left
163 def _parse_factor(self):
164 """ <factor> ::= <var> <cond> | '(' <expr> ')' """
166 if self._token == '(':
167 self._next_token()
168 res = self._parse_expr()
169 if self._token != ')':
170 self._error("Expected ')'")
171 self._next_token()
172 return res
174 if self._token_type == self.TOKEN_STRING:
175 var = self._token
176 self._next_token()
177 return self._parse_cond(var)
179 self._error("Expected '(' or <var>")
181 def _parse_cond(self, var):
182 """ <cond> ::= '=' <val> | '!=' <val> """
184 if self._token not in ('=', '!='):
185 self._error("Expected '=' or '!='")
187 oper = self._token
188 self._next_token()
190 if self._token_type != self.TOKEN_STRING:
191 self._error("Expected <val>")
193 val = self._token
194 self._next_token()
196 return BinaryOp(oper, var, val)
198 def read_config(fname, config):
199 "Read saved values from last configuration run or a preset file"
201 inf = open(fname, 'r')
203 for line in inf:
204 res = re.match(r'^(?:#!# )?([^#]\w*)\s*=\s*(.*?)\s*$', line)
205 if res:
206 config[res.group(1)] = res.group(2)
208 inf.close()
210 def parse_rules(fname, rules):
211 "Parse rules file"
213 inf = open(fname, 'r')
215 name = ''
216 choices = []
218 for line in inf:
220 if line.startswith('!'):
221 # Ask a question
222 res = re.search(r'!\s*(?:\[(.*?)\])?\s*([^\s]+)\s*\((.*)\)\s*$', line)
224 if not res:
225 raise RuntimeError("Weird line: %s" % line)
227 cond = res.group(1)
228 if cond:
229 cond = CondParser(cond).parse()
230 varname = res.group(2)
231 vartype = res.group(3)
233 rules.append((varname, vartype, name, choices, cond))
234 name = ''
235 choices = []
236 continue
238 if line.startswith('@'):
239 # Add new line into the 'choices' array
240 res = re.match(r'@\s*(?:\[(.*?)\])?\s*"(.*?)"\s*(.*)$', line)
242 if not res:
243 raise RuntimeError("Bad line: %s" % line)
245 choices.append((res.group(2), res.group(3)))
246 continue
248 if line.startswith('%'):
249 # Name of the option
250 name = line[1:].strip()
251 continue
253 if line.startswith('#') or (line == '\n'):
254 # Comment or empty line
255 continue
258 raise RuntimeError("Unknown syntax: %s" % line)
260 inf.close()
262 def yes_no(default):
263 "Return '*' if yes, ' ' if no"
265 if default == 'y':
266 return '*'
268 return ' '
270 def subchoice(screen, name, choices, default):
271 "Return choice of choices"
273 maxkey = 0
274 for key, val in choices:
275 length = len(key)
276 if (length > maxkey):
277 maxkey = length
279 options = []
280 position = None
281 cnt = 0
282 for key, val in choices:
283 if (default) and (key == default):
284 position = cnt
286 options.append(" %-*s %s " % (maxkey, key, val))
287 cnt += 1
289 (button, value) = xtui.choice_window(screen, name, 'Choose value', options, position)
291 if button == 'cancel':
292 return None
294 return choices[value][0]
296 ## Infer and verify configuration values.
298 # Augment @a config with values that can be inferred, purge invalid ones
299 # and verify that all variables have a value (previously specified or inferred).
301 # @param config Configuration to work on
302 # @param rules Rules
304 # @return True if configuration is complete and valid, False
305 # otherwise.
307 def infer_verify_choices(config, rules):
308 "Infer and verify configuration values."
310 for rule in rules:
311 varname, vartype, name, choices, cond = rule
313 if cond and not cond.evaluate(config):
314 continue
316 if not varname in config:
317 value = None
318 else:
319 value = config[varname]
321 if not validate_rule_value(rule, value):
322 value = None
324 default = get_default_rule(rule)
327 # If we don't have a value but we do have
328 # a default, use it.
330 if value == None and default != None:
331 value = default
332 config[varname] = default
334 if not varname in config:
335 return False
337 return True
339 ## Fill the configuration with random (but valid) values.
341 # The random selection takes next rule and if the condition does
342 # not violate existing configuration, random value of the variable
343 # is selected.
344 # This happens recursively as long as there are more rules.
345 # If a conflict is found, we backtrack and try other settings of the
346 # variable or ignoring the variable altogether.
348 # @param config Configuration to work on
349 # @param rules Rules
350 # @param start_index With which rule to start (initial call must specify 0 here).
351 # @return True if able to find a valid configuration
352 def random_choices(config, rules, start_index):
353 "Fill the configuration with random (but valid) values."
354 if start_index >= len(rules):
355 return True
357 varname, vartype, name, choices, cond = rules[start_index]
359 # First check that this rule would make sense
360 if cond and not cond.evaluate(config):
361 return random_choices(config, rules, start_index + 1)
363 # Remember previous choices for backtracking
364 yes_no = 0
365 choices_indexes = range(0, len(choices))
366 random.shuffle(choices_indexes)
368 # Remember current configuration value
369 old_value = None
370 try:
371 old_value = config[varname]
372 except KeyError:
373 old_value = None
375 # For yes/no choices, we ran the loop at most 2 times, for select
376 # choices as many times as there are options.
377 try_counter = 0
378 while True:
379 if vartype == 'choice':
380 if try_counter >= len(choices_indexes):
381 break
382 value = choices[choices_indexes[try_counter]][0]
383 elif vartype == 'y' or vartype == 'n':
384 if try_counter > 0:
385 break
386 value = vartype
387 elif vartype == 'y/n' or vartype == 'n/y':
388 if try_counter == 0:
389 yes_no = random.randint(0, 1)
390 elif try_counter == 1:
391 yes_no = 1 - yes_no
392 else:
393 break
394 if yes_no == 0:
395 value = 'n'
396 else:
397 value = 'y'
398 else:
399 raise RuntimeError("Unknown variable type: %s" % vartype)
401 config[varname] = value
403 ok = random_choices(config, rules, start_index + 1)
404 if ok:
405 return True
407 try_counter = try_counter + 1
409 # Restore the old value and backtrack
410 # (need to delete to prevent "ghost" variables that do not exist under
411 # certain configurations)
412 config[varname] = old_value
413 if old_value is None:
414 del config[varname]
416 return random_choices(config, rules, start_index + 1)
419 ## Get default value from a rule.
420 def get_default_rule(rule):
421 varname, vartype, name, choices, cond = rule
423 default = None
425 if vartype == 'choice':
426 # If there is just one option, use it
427 if len(choices) == 1:
428 default = choices[0][0]
429 elif vartype == 'y':
430 default = '*'
431 elif vartype == 'n':
432 default = 'n'
433 elif vartype == 'y/n':
434 default = 'y'
435 elif vartype == 'n/y':
436 default = 'n'
437 else:
438 raise RuntimeError("Unknown variable type: %s" % vartype)
440 return default
442 ## Get option from a rule.
444 # @param rule Rule for a variable
445 # @param value Current value of the variable
447 # @return Option (string) to ask or None which means not to ask.
449 def get_rule_option(rule, value):
450 varname, vartype, name, choices, cond = rule
452 option = None
454 if vartype == 'choice':
455 # If there is just one option, don't ask
456 if len(choices) != 1:
457 if (value == None):
458 option = "? %s --> " % name
459 else:
460 option = " %s [%s] --> " % (name, value)
461 elif vartype == 'y':
462 pass
463 elif vartype == 'n':
464 pass
465 elif vartype == 'y/n':
466 option = " <%s> %s " % (yes_no(value), name)
467 elif vartype == 'n/y':
468 option =" <%s> %s " % (yes_no(value), name)
469 else:
470 raise RuntimeError("Unknown variable type: %s" % vartype)
472 return option
474 ## Check if variable value is valid.
476 # @param rule Rule for the variable
477 # @param value Value of the variable
479 # @return True if valid, False if not valid.
481 def validate_rule_value(rule, value):
482 varname, vartype, name, choices, cond = rule
484 if value == None:
485 return True
487 if vartype == 'choice':
488 if not value in [choice[0] for choice in choices]:
489 return False
490 elif vartype == 'y':
491 if value != 'y':
492 return False
493 elif vartype == 'n':
494 if value != 'n':
495 return False
496 elif vartype == 'y/n':
497 if not value in ['y', 'n']:
498 return False
499 elif vartype == 'n/y':
500 if not value in ['y', 'n']:
501 return False
502 else:
503 raise RuntimeError("Unknown variable type: %s" % vartype)
505 return True
507 def preprocess_config(config, rules):
508 "Preprocess configuration"
510 varname_mode = 'CONFIG_BFB_MODE'
511 varname_width = 'CONFIG_BFB_WIDTH'
512 varname_height = 'CONFIG_BFB_HEIGHT'
514 if varname_mode in config:
515 mode = config[varname_mode].partition('x')
517 config[varname_width] = mode[0]
518 rules.append((varname_width, 'choice', 'Default framebuffer width', None, None))
520 config[varname_height] = mode[2]
521 rules.append((varname_height, 'choice', 'Default framebuffer height', None, None))
523 def create_output(mkname, mcname, config, rules):
524 "Create output configuration"
526 varname_strip = 'CONFIG_STRIP_REVISION_INFO'
527 strip_rev_info = (varname_strip in config) and (config[varname_strip] == 'y')
529 if strip_rev_info:
530 timestamp_unix = int(0)
531 else:
532 # TODO: Use commit timestamp instead of build time.
533 timestamp_unix = int(time.time())
535 timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp_unix))
537 sys.stderr.write("Fetching current revision identifier ... ")
539 try:
540 version = subprocess.Popen(['git', '-C', os.path.dirname(RULES_FILE), 'log', '-1', '--pretty=%h'], stdout = subprocess.PIPE).communicate()[0].decode().strip()
541 sys.stderr.write("ok\n")
542 except:
543 version = None
544 sys.stderr.write("failed\n")
546 if (not strip_rev_info) and (version is not None):
547 revision = version
548 else:
549 revision = None
551 outmk = open(mkname, 'w')
552 outmc = open(mcname, 'w')
554 outmk.write('#########################################\n')
555 outmk.write('## AUTO-GENERATED FILE, DO NOT EDIT!!! ##\n')
556 outmk.write('## Generated by: tools/config.py ##\n')
557 outmk.write('#########################################\n\n')
559 outmc.write('/***************************************\n')
560 outmc.write(' * AUTO-GENERATED FILE, DO NOT EDIT!!! *\n')
561 outmc.write(' * Generated by: tools/config.py *\n')
562 outmc.write(' ***************************************/\n\n')
564 defs = 'CONFIG_DEFS ='
566 for varname, vartype, name, choices, cond in rules:
567 if cond and not cond.evaluate(config):
568 continue
570 if not varname in config:
571 value = ''
572 else:
573 value = config[varname]
574 if (value == '*'):
575 value = 'y'
577 outmk.write('# %s\n%s = %s\n\n' % (name, varname, value))
579 if vartype in ["y", "n", "y/n", "n/y"]:
580 if value == "y":
581 outmc.write('/* %s */\n#define %s\n\n' % (name, varname))
582 defs += ' -D%s' % varname
583 else:
584 outmc.write('/* %s */\n#define %s %s\n#define %s_%s\n\n' % (name, varname, value, varname, value))
585 defs += ' -D%s=%s -D%s_%s' % (varname, value, varname, value)
587 if revision is not None:
588 outmk.write('REVISION = %s\n' % revision)
589 outmc.write('#define REVISION %s\n' % revision)
590 defs += ' "-DREVISION=%s"' % revision
592 outmk.write('TIMESTAMP_UNIX = %d\n' % timestamp_unix)
593 outmc.write('#define TIMESTAMP_UNIX %d\n' % timestamp_unix)
594 defs += ' "-DTIMESTAMP_UNIX=%d"' % timestamp_unix
596 outmk.write('TIMESTAMP = %s\n' % timestamp)
597 outmc.write('#define TIMESTAMP %s\n' % timestamp)
598 defs += ' "-DTIMESTAMP=%s"' % timestamp
600 outmk.write('%s\n' % defs)
602 outmk.close()
603 outmc.close()
605 def sorted_dir(root):
606 list = os.listdir(root)
607 list.sort()
608 return list
610 ## Ask user to choose a configuration profile.
612 def choose_profile(root, fname, screen, config):
613 options = []
614 opt2path = {}
615 cnt = 0
617 # Look for profiles
618 for name in sorted_dir(root):
619 path = os.path.join(root, name)
620 canon = os.path.join(path, fname)
622 if os.path.isdir(path) and os.path.exists(canon) and os.path.isfile(canon):
623 subprofile = False
625 # Look for subprofiles
626 for subname in sorted_dir(path):
627 subpath = os.path.join(path, subname)
628 subcanon = os.path.join(subpath, fname)
630 if os.path.isdir(subpath) and os.path.exists(subcanon) and os.path.isfile(subcanon):
631 subprofile = True
632 options.append("%s (%s)" % (name, subname))
633 opt2path[cnt] = [name, subname]
634 cnt += 1
636 if not subprofile:
637 options.append(name)
638 opt2path[cnt] = [name]
639 cnt += 1
641 (button, value) = xtui.choice_window(screen, 'Load preconfigured defaults', 'Choose configuration profile', options, None)
643 if button == 'cancel':
644 return None
646 return opt2path[value]
648 ## Read presets from a configuration profile.
650 # @param profile Profile to load from (a list of string components)
651 # @param config Output configuration
653 def read_presets(profile, config):
654 path = os.path.join(PRESETS_DIR, profile[0], MAKEFILE)
655 read_config(path, config)
657 if len(profile) > 1:
658 path = os.path.join(PRESETS_DIR, profile[0], profile[1], MAKEFILE)
659 read_config(path, config)
661 ## Parse profile name (relative OS path) into a list of components.
663 # @param profile_name Relative path (using OS separator)
664 # @return List of components
666 def parse_profile_name(profile_name):
667 profile = []
669 head, tail = os.path.split(profile_name)
670 if head != '':
671 profile.append(head)
673 profile.append(tail)
674 return profile
676 def main():
677 profile = None
678 config = {}
679 rules = []
681 # Parse rules file
682 parse_rules(RULES_FILE, rules)
684 if len(sys.argv) > ARGPOS_CHOICE:
685 choice = sys.argv[ARGPOS_CHOICE]
686 else:
687 choice = None
689 if len(sys.argv) > ARGPOS_PRESET:
690 preset = sys.argv[ARGPOS_PRESET]
691 else:
692 preset = None
694 mask_platform = (len(sys.argv) > ARGPOS_MASK_PLATFORM and sys.argv[ARGPOS_MASK_PLATFORM] == "--mask-platform")
696 # Input configuration file can be specified on command line
697 # otherwise configuration from previous run is used.
698 if preset is not None:
699 profile = parse_profile_name(preset)
700 read_presets(profile, config)
701 elif os.path.exists(MAKEFILE):
702 read_config(MAKEFILE, config)
704 # Default mode: check values and regenerate configuration files
705 if choice == 'default':
706 if (infer_verify_choices(config, rules)):
707 preprocess_config(config, rules)
708 create_output(MAKEFILE, MACROS, config, rules)
709 return 0
711 # Hands-off mode: check values and regenerate configuration files,
712 # but no interactive fallback
713 if choice == 'hands-off':
714 # We deliberately test this because we do not want
715 # to read implicitly any possible previous run configuration
716 if preset is None:
717 sys.stderr.write("Configuration error: No presets specified\n")
718 return 2
720 if (infer_verify_choices(config, rules)):
721 preprocess_config(config, rules)
722 create_output(MAKEFILE, MACROS, config, rules)
723 return 0
725 sys.stderr.write("Configuration error: The presets are ambiguous\n")
726 return 1
728 # Check mode: only check configuration
729 if choice == 'check':
730 if infer_verify_choices(config, rules):
731 return 0
732 return 1
734 # Random mode
735 if choice == 'random':
736 ok = random_choices(config, rules, 0)
737 if not ok:
738 sys.stderr.write("Internal error: unable to generate random config.\n")
739 return 2
740 if not infer_verify_choices(config, rules):
741 sys.stderr.write("Internal error: random configuration not consistent.\n")
742 return 2
743 preprocess_config(config, rules)
744 create_output(MAKEFILE, MACROS, config, rules)
746 return 0
748 screen = xtui.screen_init()
749 try:
750 selname = None
751 position = None
752 while True:
754 # Cancel out all values which have to be deduced
755 for varname, vartype, name, choices, cond in rules:
756 if (vartype == 'y') and (varname in config) and (config[varname] == '*'):
757 config[varname] = None
759 options = []
760 opt2row = {}
761 cnt = 0
763 if not mask_platform:
764 cnt += 1
765 options.append(" --- Load preconfigured defaults ... ")
767 for rule in rules:
768 varname, vartype, name, choices, cond = rule
770 if cond and not cond.evaluate(config):
771 continue
773 if mask_platform and (varname == "PLATFORM" or varname == "MACHINE" or varname == "COMPILER"):
774 rule = varname, vartype, "(locked) " + name, choices, cond
776 if varname == selname:
777 position = cnt
779 if not varname in config:
780 value = None
781 else:
782 value = config[varname]
784 if not validate_rule_value(rule, value):
785 value = None
787 default = get_default_rule(rule)
790 # If we don't have a value but we do have
791 # a default, use it.
793 if value == None and default != None:
794 value = default
795 config[varname] = default
797 option = get_rule_option(rule, value)
798 if option != None:
799 options.append(option)
800 else:
801 continue
803 opt2row[cnt] = (varname, vartype, name, choices)
805 cnt += 1
807 if (position != None) and (position >= len(options)):
808 position = None
810 (button, value) = xtui.choice_window(screen, 'HelenOS configuration', 'Choose configuration option', options, position)
812 if button == 'cancel':
813 return 'Configuration canceled'
815 if button == 'done':
816 if (infer_verify_choices(config, rules)):
817 break
818 else:
819 xtui.error_dialog(screen, 'Error', 'Some options have still undefined values. These options are marked with the "?" sign.')
820 continue
822 if value == 0 and not mask_platform:
823 profile = choose_profile(PRESETS_DIR, MAKEFILE, screen, config)
824 if profile != None:
825 read_presets(profile, config)
826 position = 1
827 continue
829 position = None
830 if not value in opt2row:
831 raise RuntimeError("Error selecting value: %s" % value)
833 (selname, seltype, name, choices) = opt2row[value]
835 if not selname in config:
836 value = None
837 else:
838 value = config[selname]
840 if mask_platform and (selname == "PLATFORM" or selname == "MACHINE" or selname == "COMPILER"):
841 continue
843 if seltype == 'choice':
844 config[selname] = subchoice(screen, name, choices, value)
845 elif (seltype == 'y/n') or (seltype == 'n/y'):
846 if config[selname] == 'y':
847 config[selname] = 'n'
848 else:
849 config[selname] = 'y'
850 finally:
851 xtui.screen_done(screen)
853 preprocess_config(config, rules)
854 create_output(MAKEFILE, MACROS, config, rules)
855 return 0
857 if __name__ == '__main__':
858 sys.exit(main())