3 # Copyright (c) 2006 Ondrej Palkovsky
4 # Copyright (c) 2009 Martin Decky
5 # Copyright (c) 2010 Jiri Svoboda
8 # Redistribution and use in source and binary forms, with or without
9 # modification, are permitted provided that the following conditions
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.
33 HelenOS configuration system
45 ARGPOS_PRESETS_DIR
= 2
48 ARGPOS_MASK_PLATFORM
= 3
50 RULES_FILE
= sys
.argv
[ARGPOS_RULES
]
51 MAKEFILE
= 'Makefile.config'
53 PRESETS_DIR
= sys
.argv
[ARGPOS_PRESETS_DIR
]
56 def __init__(self
, operator
, left
, right
):
57 assert operator
in ('&', '|', '=', '!=')
59 self
._operator
= operator
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
)
72 if not self
._left
in config
:
75 config_val
= config
[self
._left
]
79 if self
._operator
== '=':
80 return self
._right
== config_val
81 return self
._right
!= config_val
89 def __init__(self
, text
):
97 res
= self
._parse
_expr
()
98 if self
._token
_type
!= self
.TOKEN_EOE
:
99 self
._error
("Expected end of expression")
102 def _next_char(self
):
104 if self
._position
>= len(self
._text
):
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
119 if self
._char
== None:
121 self
._token
_type
= self
.TOKEN_EOE
124 # '&', '|', '=', '!=', '(', ')'
125 if self
._is
_special
_char
:
126 self
._token
= self
._char
128 if self
._token
== '!':
129 if self
._char
!= '=':
130 self
._error
("Expected '='")
131 self
._token
+= self
._char
133 self
._token
_type
= self
.TOKEN_SPECIAL
138 self
._token
_type
= self
.TOKEN_STRING
140 self
._token
+= self
._char
142 if self
._is
_special
_char
or self
._char
== None:
145 def _parse_expr(self
):
146 """ <expr> ::= <or_expr> ('&' <or_expr>)* """
148 left
= self
._parse
_or
_expr
()
149 while self
._token
== '&':
151 left
= BinaryOp('&', left
, self
._parse
_or
_expr
())
154 def _parse_or_expr(self
):
155 """ <or_expr> ::= <factor> ('|' <factor>)* """
157 left
= self
._parse
_factor
()
158 while self
._token
== '|':
160 left
= BinaryOp('|', left
, self
._parse
_factor
())
163 def _parse_factor(self
):
164 """ <factor> ::= <var> <cond> | '(' <expr> ')' """
166 if self
._token
== '(':
168 res
= self
._parse
_expr
()
169 if self
._token
!= ')':
170 self
._error
("Expected ')'")
174 if self
._token
_type
== self
.TOKEN_STRING
:
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 '!='")
190 if self
._token
_type
!= self
.TOKEN_STRING
:
191 self
._error
("Expected <val>")
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')
204 res
= re
.match(r
'^(?:#!# )?([^#]\w*)\s*=\s*(.*?)\s*$', line
)
206 config
[res
.group(1)] = res
.group(2)
210 def parse_rules(fname
, rules
):
213 inf
= open(fname
, 'r')
220 if line
.startswith('!'):
222 res
= re
.search(r
'!\s*(?:\[(.*?)\])?\s*([^\s]+)\s*\((.*)\)\s*$', line
)
225 raise RuntimeError("Weird line: %s" % line
)
229 cond
= CondParser(cond
).parse()
230 varname
= res
.group(2)
231 vartype
= res
.group(3)
233 rules
.append((varname
, vartype
, name
, choices
, cond
))
238 if line
.startswith('@'):
239 # Add new line into the 'choices' array
240 res
= re
.match(r
'@\s*(?:\[(.*?)\])?\s*"(.*?)"\s*(.*)$', line
)
243 raise RuntimeError("Bad line: %s" % line
)
245 choices
.append((res
.group(2), res
.group(3)))
248 if line
.startswith('%'):
250 name
= line
[1:].strip()
253 if line
.startswith('#') or (line
== '\n'):
254 # Comment or empty line
258 raise RuntimeError("Unknown syntax: %s" % line
)
263 "Return '*' if yes, ' ' if no"
270 def subchoice(screen
, name
, choices
, default
):
271 "Return choice of choices"
274 for key
, val
in choices
:
276 if (length
> maxkey
):
282 for key
, val
in choices
:
283 if (default
) and (key
== default
):
286 options
.append(" %-*s %s " % (maxkey
, key
, val
))
289 (button
, value
) = xtui
.choice_window(screen
, name
, 'Choose value', options
, position
)
291 if button
== 'cancel':
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
304 # @return True if configuration is complete and valid, False
307 def infer_verify_choices(config
, rules
):
308 "Infer and verify configuration values."
311 varname
, vartype
, name
, choices
, cond
= rule
313 if cond
and not cond
.evaluate(config
):
316 if not varname
in config
:
319 value
= config
[varname
]
321 if not validate_rule_value(rule
, value
):
324 default
= get_default_rule(rule
)
327 # If we don't have a value but we do have
330 if value
== None and default
!= None:
332 config
[varname
] = default
334 if not varname
in config
:
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
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
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
):
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
365 choices_indexes
= range(0, len(choices
))
366 random
.shuffle(choices_indexes
)
368 # Remember current configuration value
371 old_value
= config
[varname
]
375 # For yes/no choices, we ran the loop at most 2 times, for select
376 # choices as many times as there are options.
379 if vartype
== 'choice':
380 if try_counter
>= len(choices_indexes
):
382 value
= choices
[choices_indexes
[try_counter
]][0]
383 elif vartype
== 'y' or vartype
== 'n':
387 elif vartype
== 'y/n' or vartype
== 'n/y':
389 yes_no
= random
.randint(0, 1)
390 elif try_counter
== 1:
399 raise RuntimeError("Unknown variable type: %s" % vartype
)
401 config
[varname
] = value
403 ok
= random_choices(config
, rules
, start_index
+ 1)
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:
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
425 if vartype
== 'choice':
426 # If there is just one option, use it
427 if len(choices
) == 1:
428 default
= choices
[0][0]
433 elif vartype
== 'y/n':
435 elif vartype
== 'n/y':
438 raise RuntimeError("Unknown variable type: %s" % vartype
)
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
454 if vartype
== 'choice':
455 # If there is just one option, don't ask
456 if len(choices
) != 1:
458 option
= "? %s --> " % name
460 option
= " %s [%s] --> " % (name
, value
)
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
)
470 raise RuntimeError("Unknown variable type: %s" % vartype
)
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
487 if vartype
== 'choice':
488 if not value
in [choice
[0] for choice
in choices
]:
496 elif vartype
== 'y/n':
497 if not value
in ['y', 'n']:
499 elif vartype
== 'n/y':
500 if not value
in ['y', 'n']:
503 raise RuntimeError("Unknown variable type: %s" % vartype
)
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')
530 timestamp_unix
= int(0)
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 ... ")
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")
544 sys
.stderr
.write("failed\n")
546 if (not strip_rev_info
) and (version
is not 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
):
570 if not varname
in config
:
573 value
= config
[varname
]
577 outmk
.write('# %s\n%s = %s\n\n' % (name
, varname
, value
))
579 if vartype
in ["y", "n", "y/n", "n/y"]:
581 outmc
.write('/* %s */\n#define %s\n\n' % (name
, varname
))
582 defs
+= ' -D%s' % varname
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
)
605 def sorted_dir(root
):
606 list = os
.listdir(root
)
610 ## Ask user to choose a configuration profile.
612 def choose_profile(root
, fname
, screen
, config
):
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
):
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
):
632 options
.append("%s (%s)" % (name
, subname
))
633 opt2path
[cnt
] = [name
, subname
]
638 opt2path
[cnt
] = [name
]
641 (button
, value
) = xtui
.choice_window(screen
, 'Load preconfigured defaults', 'Choose configuration profile', options
, None)
643 if button
== 'cancel':
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
)
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
):
669 head
, tail
= os
.path
.split(profile_name
)
682 parse_rules(RULES_FILE
, rules
)
684 if len(sys
.argv
) > ARGPOS_CHOICE
:
685 choice
= sys
.argv
[ARGPOS_CHOICE
]
689 if len(sys
.argv
) > ARGPOS_PRESET
:
690 preset
= sys
.argv
[ARGPOS_PRESET
]
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
)
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
717 sys
.stderr
.write("Configuration error: No presets specified\n")
720 if (infer_verify_choices(config
, rules
)):
721 preprocess_config(config
, rules
)
722 create_output(MAKEFILE
, MACROS
, config
, rules
)
725 sys
.stderr
.write("Configuration error: The presets are ambiguous\n")
728 # Check mode: only check configuration
729 if choice
== 'check':
730 if infer_verify_choices(config
, rules
):
735 if choice
== 'random':
736 ok
= random_choices(config
, rules
, 0)
738 sys
.stderr
.write("Internal error: unable to generate random config.\n")
740 if not infer_verify_choices(config
, rules
):
741 sys
.stderr
.write("Internal error: random configuration not consistent.\n")
743 preprocess_config(config
, rules
)
744 create_output(MAKEFILE
, MACROS
, config
, rules
)
748 screen
= xtui
.screen_init()
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
763 if not mask_platform
:
765 options
.append(" --- Load preconfigured defaults ... ")
768 varname
, vartype
, name
, choices
, cond
= rule
770 if cond
and not cond
.evaluate(config
):
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
:
779 if not varname
in config
:
782 value
= config
[varname
]
784 if not validate_rule_value(rule
, value
):
787 default
= get_default_rule(rule
)
790 # If we don't have a value but we do have
793 if value
== None and default
!= None:
795 config
[varname
] = default
797 option
= get_rule_option(rule
, value
)
799 options
.append(option
)
803 opt2row
[cnt
] = (varname
, vartype
, name
, choices
)
807 if (position
!= None) and (position
>= len(options
)):
810 (button
, value
) = xtui
.choice_window(screen
, 'HelenOS configuration', 'Choose configuration option', options
, position
)
812 if button
== 'cancel':
813 return 'Configuration canceled'
816 if (infer_verify_choices(config
, rules
)):
819 xtui
.error_dialog(screen
, 'Error', 'Some options have still undefined values. These options are marked with the "?" sign.')
822 if value
== 0 and not mask_platform
:
823 profile
= choose_profile(PRESETS_DIR
, MAKEFILE
, screen
, config
)
825 read_presets(profile
, config
)
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
:
838 value
= config
[selname
]
840 if mask_platform
and (selname
== "PLATFORM" or selname
== "MACHINE" or selname
== "COMPILER"):
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'
849 config
[selname
] = 'y'
851 xtui
.screen_done(screen
)
853 preprocess_config(config
, rules
)
854 create_output(MAKEFILE
, MACROS
, config
, rules
)
857 if __name__
== '__main__':