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
44 RULES_FILE
= sys
.argv
[1]
45 MAKEFILE
= 'Makefile.config'
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')
55 res
= re
.match(r
'^(?:#!# )?([^#]\w*)\s*=\s*(.*?)\s*$', line
)
57 config
[res
.group(1)] = res
.group(2)
61 def check_condition(text
, config
, rules
):
62 "Check that the condition specified on input line is True (only CNF and DNF is supported)"
66 if (')|' in text
) or ('|(' in text
):
70 conds
= text
.split('&')
72 conds
= text
.split('|')
75 if cond
.startswith('(') and cond
.endswith(')'):
78 inside
= check_inside(cond
, config
, ctype
)
80 if (ctype
== 'cnf') and (not inside
):
83 if (ctype
== 'dnf') and inside
:
91 def check_inside(text
, config
, ctype
):
95 conds
= text
.split('|')
97 conds
= text
.split('&')
100 res
= re
.match(r
'^(.*?)(!?=)(.*)$', cond
)
102 raise RuntimeError("Invalid condition: %s" % cond
)
104 condname
= res
.group(1)
106 condval
= res
.group(3)
108 if not condname
in config
:
111 varval
= config
[condname
]
116 if (oper
== '=') and (condval
== varval
):
119 if (oper
== '!=') and (condval
!= varval
):
122 if (oper
== '=') and (condval
!= varval
):
125 if (oper
== '!=') and (condval
== varval
):
133 def parse_rules(fname
, rules
):
136 inf
= open(fname
, 'r')
143 if line
.startswith('!'):
145 res
= re
.search(r
'!\s*(?:\[(.*?)\])?\s*([^\s]+)\s*\((.*)\)\s*$', line
)
148 raise RuntimeError("Weird line: %s" % line
)
151 varname
= res
.group(2)
152 vartype
= res
.group(3)
154 rules
.append((varname
, vartype
, name
, choices
, cond
))
159 if line
.startswith('@'):
160 # Add new line into the 'choices' array
161 res
= re
.match(r
'@\s*(?:\[(.*?)\])?\s*"(.*?)"\s*(.*)$', line
)
164 raise RuntimeError("Bad line: %s" % line
)
166 choices
.append((res
.group(2), res
.group(3)))
169 if line
.startswith('%'):
171 name
= line
[1:].strip()
174 if line
.startswith('#') or (line
== '\n'):
175 # Comment or empty line
179 raise RuntimeError("Unknown syntax: %s" % line
)
184 "Return '*' if yes, ' ' if no"
191 def subchoice(screen
, name
, choices
, default
):
192 "Return choice of choices"
195 for key
, val
in choices
:
197 if (length
> maxkey
):
203 for key
, val
in choices
:
204 if (default
) and (key
== default
):
207 options
.append(" %-*s %s " % (maxkey
, key
, val
))
210 (button
, value
) = xtui
.choice_window(screen
, name
, 'Choose value', options
, position
)
212 if button
== 'cancel':
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
225 # @return True if configuration is complete and valid, False
228 def infer_verify_choices(config
, rules
):
229 "Infer and verify configuration values."
232 varname
, vartype
, name
, choices
, cond
= rule
234 if cond
and (not check_condition(cond
, config
, rules
)):
237 if not varname
in config
:
240 value
= config
[varname
]
242 if not validate_rule_value(rule
, value
):
245 default
= get_default_rule(rule
)
248 # If we don't have a value but we do have
251 if value
== None and default
!= None:
253 config
[varname
] = default
255 if not varname
in config
:
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
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
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
):
278 varname
, vartype
, name
, choices
, cond
= rules
[start_index
]
280 # First check that this rule would make sense
282 if not check_condition(cond
, config
, rules
):
283 return random_choices(config
, rules
, start_index
+ 1)
285 # Remember previous choices for backtracking
287 choices_indexes
= range(0, len(choices
))
288 random
.shuffle(choices_indexes
)
290 # Remember current configuration value
293 old_value
= config
[varname
]
297 # For yes/no choices, we ran the loop at most 2 times, for select
298 # choices as many times as there are options.
301 if vartype
== 'choice':
302 if try_counter
>= len(choices_indexes
):
304 value
= choices
[choices_indexes
[try_counter
]][0]
305 elif vartype
== 'y' or vartype
== 'n':
309 elif vartype
== 'y/n' or vartype
== 'n/y':
311 yes_no
= random
.randint(0, 1)
312 elif try_counter
== 1:
321 raise RuntimeError("Unknown variable type: %s" % vartype
)
323 config
[varname
] = value
325 ok
= random_choices(config
, rules
, start_index
+ 1)
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:
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
347 if vartype
== 'choice':
348 # If there is just one option, use it
349 if len(choices
) == 1:
350 default
= choices
[0][0]
355 elif vartype
== 'y/n':
357 elif vartype
== 'n/y':
360 raise RuntimeError("Unknown variable type: %s" % vartype
)
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
376 if vartype
== 'choice':
377 # If there is just one option, don't ask
378 if len(choices
) != 1:
380 option
= "? %s --> " % name
382 option
= " %s [%s] --> " % (name
, value
)
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
)
392 raise RuntimeError("Unknown variable type: %s" % vartype
)
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
409 if vartype
== 'choice':
410 if not value
in [choice
[0] for choice
in choices
]:
418 elif vartype
== 'y/n':
419 if not value
in ['y', 'n']:
421 elif vartype
== 'n/y':
422 if not value
in ['y', 'n']:
425 raise RuntimeError("Unknown variable type: %s" % vartype
)
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 ... ")
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")
457 version
= [1, "unknown", "unknown"]
458 sys
.stderr
.write("failed\n")
460 if len(version
) == 3:
461 revision
= version
[1]
464 revision
+= ' (%s)' % version
[2]
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
)):
487 if not varname
in config
:
490 value
= config
[varname
]
494 outmk
.write('# %s\n%s = %s\n\n' % (name
, varname
, value
))
496 if vartype
in ["y", "n", "y/n", "n/y"]:
498 outmc
.write('/* %s */\n#define %s\n\n' % (name
, varname
))
499 defs
+= ' -D%s' % varname
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
522 def sorted_dir(root
):
523 list = os
.listdir(root
)
527 ## Ask user to choose a configuration profile.
529 def choose_profile(root
, fname
, screen
, config
):
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
):
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
):
549 options
.append("%s (%s)" % (name
, subname
))
550 opt2path
[cnt
] = [name
, subname
]
555 opt2path
[cnt
] = [name
]
558 (button
, value
) = xtui
.choice_window(screen
, 'Load preconfigured defaults', 'Choose configuration profile', options
, None)
560 if button
== 'cancel':
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
)
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
):
586 head
, tail
= os
.path
.split(profile_name
)
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
)
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")
625 if (infer_verify_choices(config
, rules
)):
626 preprocess_config(config
, rules
)
627 create_output(MAKEFILE
, MACROS
, config
, rules
)
630 sys
.stderr
.write("Configuration error: The presets are ambiguous\n")
633 # Check mode: only check configuration
634 if (len(sys
.argv
) >= 3) and (sys
.argv
[2] == 'check'):
635 if infer_verify_choices(config
, rules
):
640 if (len(sys
.argv
) == 3) and (sys
.argv
[2] == 'random'):
641 ok
= random_choices(config
, rules
, 0)
643 sys
.stderr
.write("Internal error: unable to generate random config.\n")
645 if not infer_verify_choices(config
, rules
):
646 sys
.stderr
.write("Internal error: random configuration not consistent.\n")
648 preprocess_config(config
, rules
)
649 create_output(MAKEFILE
, MACROS
, config
, rules
)
653 screen
= xtui
.screen_init()
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
668 options
.append(" --- Load preconfigured defaults ... ")
671 varname
, vartype
, name
, choices
, cond
= rule
673 if cond
and (not check_condition(cond
, config
, rules
)):
676 if varname
== selname
:
679 if not varname
in config
:
682 value
= config
[varname
]
684 if not validate_rule_value(rule
, value
):
687 default
= get_default_rule(rule
)
690 # If we don't have a value but we do have
693 if value
== None and default
!= None:
695 config
[varname
] = default
697 option
= get_rule_option(rule
, value
)
699 options
.append(option
)
703 opt2row
[cnt
] = (varname
, vartype
, name
, choices
)
707 if (position
!= None) and (position
>= len(options
)):
710 (button
, value
) = xtui
.choice_window(screen
, 'HelenOS configuration', 'Choose configuration option', options
, position
)
712 if button
== 'cancel':
713 return 'Configuration canceled'
716 if (infer_verify_choices(config
, rules
)):
719 xtui
.error_dialog(screen
, 'Error', 'Some options have still undefined values. These options are marked with the "?" sign.')
723 profile
= choose_profile(PRESETS_DIR
, MAKEFILE
, screen
, config
)
725 read_presets(profile
, config
)
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
:
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'
746 config
[selname
] = 'y'
748 xtui
.screen_done(screen
)
750 preprocess_config(config
, rules
)
751 create_output(MAKEFILE
, MACROS
, config
, rules
)
754 if __name__
== '__main__':