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 varname_strip
= 'CONFIG_STRIP_REVISION_INFO'
449 strip_rev_info
= (varname_strip
in config
) and (config
[varname_strip
] == 'y')
452 timestamp_unix
= int(0)
454 # TODO: Use commit timestamp instead of build time.
455 timestamp_unix
= int(time
.time())
457 timestamp
= time
.strftime("%Y-%m-%d %H:%M:%S", time
.localtime(timestamp_unix
))
459 sys
.stderr
.write("Fetching current revision identifier ... ")
462 version
= subprocess
.Popen(['git', 'log', '-1', '--pretty=%h'], stdout
= subprocess
.PIPE
).communicate()[0].decode().strip()
463 sys
.stderr
.write("ok\n")
466 sys
.stderr
.write("failed\n")
468 if (not strip_rev_info
) and (version
is not None):
473 outmk
= open(mkname
, 'w')
474 outmc
= open(mcname
, 'w')
476 outmk
.write('#########################################\n')
477 outmk
.write('## AUTO-GENERATED FILE, DO NOT EDIT!!! ##\n')
478 outmk
.write('## Generated by: tools/config.py ##\n')
479 outmk
.write('#########################################\n\n')
481 outmc
.write('/***************************************\n')
482 outmc
.write(' * AUTO-GENERATED FILE, DO NOT EDIT!!! *\n')
483 outmc
.write(' * Generated by: tools/config.py *\n')
484 outmc
.write(' ***************************************/\n\n')
486 defs
= 'CONFIG_DEFS ='
488 for varname
, vartype
, name
, choices
, cond
in rules
:
489 if cond
and (not check_condition(cond
, config
, rules
)):
492 if not varname
in config
:
495 value
= config
[varname
]
499 outmk
.write('# %s\n%s = %s\n\n' % (name
, varname
, value
))
501 if vartype
in ["y", "n", "y/n", "n/y"]:
503 outmc
.write('/* %s */\n#define %s\n\n' % (name
, varname
))
504 defs
+= ' -D%s' % varname
506 outmc
.write('/* %s */\n#define %s %s\n#define %s_%s\n\n' % (name
, varname
, value
, varname
, value
))
507 defs
+= ' -D%s=%s -D%s_%s' % (varname
, value
, varname
, value
)
509 if revision
is not None:
510 outmk
.write('REVISION = %s\n' % revision
)
511 outmc
.write('#define REVISION %s\n' % revision
)
512 defs
+= ' "-DREVISION=%s"' % revision
514 outmk
.write('TIMESTAMP_UNIX = %d\n' % timestamp_unix
)
515 outmc
.write('#define TIMESTAMP_UNIX %d\n' % timestamp_unix
)
516 defs
+= ' "-DTIMESTAMP_UNIX=%d"\n' % timestamp_unix
518 outmk
.write('TIMESTAMP = %s\n' % timestamp
)
519 outmc
.write('#define TIMESTAMP %s\n' % timestamp
)
520 defs
+= ' "-DTIMESTAMP=%s"\n' % timestamp
527 def sorted_dir(root
):
528 list = os
.listdir(root
)
532 ## Ask user to choose a configuration profile.
534 def choose_profile(root
, fname
, screen
, config
):
540 for name
in sorted_dir(root
):
541 path
= os
.path
.join(root
, name
)
542 canon
= os
.path
.join(path
, fname
)
544 if os
.path
.isdir(path
) and os
.path
.exists(canon
) and os
.path
.isfile(canon
):
547 # Look for subprofiles
548 for subname
in sorted_dir(path
):
549 subpath
= os
.path
.join(path
, subname
)
550 subcanon
= os
.path
.join(subpath
, fname
)
552 if os
.path
.isdir(subpath
) and os
.path
.exists(subcanon
) and os
.path
.isfile(subcanon
):
554 options
.append("%s (%s)" % (name
, subname
))
555 opt2path
[cnt
] = [name
, subname
]
560 opt2path
[cnt
] = [name
]
563 (button
, value
) = xtui
.choice_window(screen
, 'Load preconfigured defaults', 'Choose configuration profile', options
, None)
565 if button
== 'cancel':
568 return opt2path
[value
]
570 ## Read presets from a configuration profile.
572 # @param profile Profile to load from (a list of string components)
573 # @param config Output configuration
575 def read_presets(profile
, config
):
576 path
= os
.path
.join(PRESETS_DIR
, profile
[0], MAKEFILE
)
577 read_config(path
, config
)
580 path
= os
.path
.join(PRESETS_DIR
, profile
[0], profile
[1], MAKEFILE
)
581 read_config(path
, config
)
583 ## Parse profile name (relative OS path) into a list of components.
585 # @param profile_name Relative path (using OS separator)
586 # @return List of components
588 def parse_profile_name(profile_name
):
591 head
, tail
= os
.path
.split(profile_name
)
604 parse_rules(RULES_FILE
, rules
)
606 # Input configuration file can be specified on command line
607 # otherwise configuration from previous run is used.
608 if len(sys
.argv
) >= 4:
609 profile
= parse_profile_name(sys
.argv
[3])
610 read_presets(profile
, config
)
611 elif os
.path
.exists(MAKEFILE
):
612 read_config(MAKEFILE
, config
)
614 # Default mode: check values and regenerate configuration files
615 if (len(sys
.argv
) >= 3) and (sys
.argv
[2] == 'default'):
616 if (infer_verify_choices(config
, rules
)):
617 preprocess_config(config
, rules
)
618 create_output(MAKEFILE
, MACROS
, config
, rules
)
621 # Hands-off mode: check values and regenerate configuration files,
622 # but no interactive fallback
623 if (len(sys
.argv
) >= 3) and (sys
.argv
[2] == 'hands-off'):
624 # We deliberately test sys.argv >= 4 because we do not want
625 # to read implicitly any possible previous run configuration
626 if len(sys
.argv
) < 4:
627 sys
.stderr
.write("Configuration error: No presets specified\n")
630 if (infer_verify_choices(config
, rules
)):
631 preprocess_config(config
, rules
)
632 create_output(MAKEFILE
, MACROS
, config
, rules
)
635 sys
.stderr
.write("Configuration error: The presets are ambiguous\n")
638 # Check mode: only check configuration
639 if (len(sys
.argv
) >= 3) and (sys
.argv
[2] == 'check'):
640 if infer_verify_choices(config
, rules
):
645 if (len(sys
.argv
) == 3) and (sys
.argv
[2] == 'random'):
646 ok
= random_choices(config
, rules
, 0)
648 sys
.stderr
.write("Internal error: unable to generate random config.\n")
650 if not infer_verify_choices(config
, rules
):
651 sys
.stderr
.write("Internal error: random configuration not consistent.\n")
653 preprocess_config(config
, rules
)
654 create_output(MAKEFILE
, MACROS
, config
, rules
)
658 screen
= xtui
.screen_init()
664 # Cancel out all values which have to be deduced
665 for varname
, vartype
, name
, choices
, cond
in rules
:
666 if (vartype
== 'y') and (varname
in config
) and (config
[varname
] == '*'):
667 config
[varname
] = None
673 options
.append(" --- Load preconfigured defaults ... ")
676 varname
, vartype
, name
, choices
, cond
= rule
678 if cond
and (not check_condition(cond
, config
, rules
)):
681 if varname
== selname
:
684 if not varname
in config
:
687 value
= config
[varname
]
689 if not validate_rule_value(rule
, value
):
692 default
= get_default_rule(rule
)
695 # If we don't have a value but we do have
698 if value
== None and default
!= None:
700 config
[varname
] = default
702 option
= get_rule_option(rule
, value
)
704 options
.append(option
)
708 opt2row
[cnt
] = (varname
, vartype
, name
, choices
)
712 if (position
!= None) and (position
>= len(options
)):
715 (button
, value
) = xtui
.choice_window(screen
, 'HelenOS configuration', 'Choose configuration option', options
, position
)
717 if button
== 'cancel':
718 return 'Configuration canceled'
721 if (infer_verify_choices(config
, rules
)):
724 xtui
.error_dialog(screen
, 'Error', 'Some options have still undefined values. These options are marked with the "?" sign.')
728 profile
= choose_profile(PRESETS_DIR
, MAKEFILE
, screen
, config
)
730 read_presets(profile
, config
)
735 if not value
in opt2row
:
736 raise RuntimeError("Error selecting value: %s" % value
)
738 (selname
, seltype
, name
, choices
) = opt2row
[value
]
740 if not selname
in config
:
743 value
= config
[selname
]
745 if seltype
== 'choice':
746 config
[selname
] = subchoice(screen
, name
, choices
, value
)
747 elif (seltype
== 'y/n') or (seltype
== 'n/y'):
748 if config
[selname
] == 'y':
749 config
[selname
] = 'n'
751 config
[selname
] = 'y'
753 xtui
.screen_done(screen
)
755 preprocess_config(config
, rules
)
756 create_output(MAKEFILE
, MACROS
, config
, rules
)
759 if __name__
== '__main__':