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
43 RULES_FILE
= sys
.argv
[1]
44 MAKEFILE
= 'Makefile.config'
46 PRESETS_DIR
= 'defaults'
48 def read_config(fname
, config
):
49 "Read saved values from last configuration run or a preset file"
51 inf
= open(fname
, 'r')
54 res
= re
.match(r
'^(?:#!# )?([^#]\w*)\s*=\s*(.*?)\s*$', line
)
56 config
[res
.group(1)] = res
.group(2)
60 def check_condition(text
, config
, rules
):
61 "Check that the condition specified on input line is True (only CNF and DNF is supported)"
65 if (')|' in text
) or ('|(' in text
):
69 conds
= text
.split('&')
71 conds
= text
.split('|')
74 if cond
.startswith('(') and cond
.endswith(')'):
77 inside
= check_inside(cond
, config
, ctype
)
79 if (ctype
== 'cnf') and (not inside
):
82 if (ctype
== 'dnf') and inside
:
90 def check_inside(text
, config
, ctype
):
94 conds
= text
.split('|')
96 conds
= text
.split('&')
99 res
= re
.match(r
'^(.*?)(!?=)(.*)$', cond
)
101 raise RuntimeError("Invalid condition: %s" % cond
)
103 condname
= res
.group(1)
105 condval
= res
.group(3)
107 if not condname
in config
:
110 varval
= config
[condname
]
115 if (oper
== '=') and (condval
== varval
):
118 if (oper
== '!=') and (condval
!= varval
):
121 if (oper
== '=') and (condval
!= varval
):
124 if (oper
== '!=') and (condval
== varval
):
132 def parse_rules(fname
, rules
):
135 inf
= open(fname
, 'r')
142 if line
.startswith('!'):
144 res
= re
.search(r
'!\s*(?:\[(.*?)\])?\s*([^\s]+)\s*\((.*)\)\s*$', line
)
147 raise RuntimeError("Weird line: %s" % line
)
150 varname
= res
.group(2)
151 vartype
= res
.group(3)
153 rules
.append((varname
, vartype
, name
, choices
, cond
))
158 if line
.startswith('@'):
159 # Add new line into the 'choices' array
160 res
= re
.match(r
'@\s*(?:\[(.*?)\])?\s*"(.*?)"\s*(.*)$', line
)
163 raise RuntimeError("Bad line: %s" % line
)
165 choices
.append((res
.group(2), res
.group(3)))
168 if line
.startswith('%'):
170 name
= line
[1:].strip()
173 if line
.startswith('#') or (line
== '\n'):
174 # Comment or empty line
178 raise RuntimeError("Unknown syntax: %s" % line
)
183 "Return '*' if yes, ' ' if no"
190 def subchoice(screen
, name
, choices
, default
):
191 "Return choice of choices"
194 for key
, val
in choices
:
196 if (length
> maxkey
):
202 for key
, val
in choices
:
203 if (default
) and (key
== default
):
206 options
.append(" %-*s %s " % (maxkey
, key
, val
))
209 (button
, value
) = xtui
.choice_window(screen
, name
, 'Choose value', options
, position
)
211 if button
== 'cancel':
214 return choices
[value
][0]
216 ## Infer and verify configuration values.
218 # Augment @a config with values that can be inferred, purge invalid ones
219 # and verify that all variables have a value (previously specified or inferred).
221 # @param config Configuration to work on
224 # @return True if configuration is complete and valid, False
227 def infer_verify_choices(config
, rules
):
228 "Infer and verify configuration values."
231 varname
, vartype
, name
, choices
, cond
= rule
233 if cond
and (not check_condition(cond
, config
, rules
)):
236 if not varname
in config
:
239 value
= config
[varname
]
241 if not validate_rule_value(rule
, value
):
244 default
= get_default_rule(rule
)
247 # If we don't have a value but we do have
250 if value
== None and default
!= None:
252 config
[varname
] = default
254 if not varname
in config
:
259 ## Get default value from a rule.
260 def get_default_rule(rule
):
261 varname
, vartype
, name
, choices
, cond
= rule
265 if vartype
== 'choice':
266 # If there is just one option, use it
267 if len(choices
) == 1:
268 default
= choices
[0][0]
273 elif vartype
== 'y/n':
275 elif vartype
== 'n/y':
278 raise RuntimeError("Unknown variable type: %s" % vartype
)
282 ## Get option from a rule.
284 # @param rule Rule for a variable
285 # @param value Current value of the variable
287 # @return Option (string) to ask or None which means not to ask.
289 def get_rule_option(rule
, value
):
290 varname
, vartype
, name
, choices
, cond
= rule
294 if vartype
== 'choice':
295 # If there is just one option, don't ask
296 if len(choices
) != 1:
298 option
= "? %s --> " % name
300 option
= " %s [%s] --> " % (name
, value
)
305 elif vartype
== 'y/n':
306 option
= " <%s> %s " % (yes_no(value
), name
)
307 elif vartype
== 'n/y':
308 option
=" <%s> %s " % (yes_no(value
), name
)
310 raise RuntimeError("Unknown variable type: %s" % vartype
)
314 ## Check if variable value is valid.
316 # @param rule Rule for the variable
317 # @param value Value of the variable
319 # @return True if valid, False if not valid.
321 def validate_rule_value(rule
, value
):
322 varname
, vartype
, name
, choices
, cond
= rule
327 if vartype
== 'choice':
328 if not value
in [choice
[0] for choice
in choices
]:
336 elif vartype
== 'y/n':
337 if not value
in ['y', 'n']:
339 elif vartype
== 'n/y':
340 if not value
in ['y', 'n']:
343 raise RuntimeError("Unknown variable type: %s" % vartype
)
347 def preprocess_config(config
, rules
):
348 "Preprocess configuration"
350 varname_mode
= 'CONFIG_BFB_MODE'
351 varname_width
= 'CONFIG_BFB_WIDTH'
352 varname_height
= 'CONFIG_BFB_HEIGHT'
354 if varname_mode
in config
:
355 mode
= config
[varname_mode
].partition('x')
357 config
[varname_width
] = mode
[0]
358 rules
.append((varname_width
, 'choice', 'Default framebuffer width', None, None))
360 config
[varname_height
] = mode
[2]
361 rules
.append((varname_height
, 'choice', 'Default framebuffer height', None, None))
363 def create_output(mkname
, mcname
, config
, rules
):
364 "Create output configuration"
366 timestamp_unix
= int(time
.time())
367 timestamp
= time
.strftime("%Y-%m-%d %H:%M:%S", time
.localtime(timestamp_unix
))
369 sys
.stderr
.write("Fetching current revision identifier ... ")
372 version
= subprocess
.Popen(['bzr', 'version-info', '--custom', '--template={clean}:{revno}:{revision_id}'], stdout
= subprocess
.PIPE
).communicate()[0].decode().split(':')
373 sys
.stderr
.write("ok\n")
375 version
= [1, "unknown", "unknown"]
376 sys
.stderr
.write("failed\n")
378 if len(version
) == 3:
379 revision
= version
[1]
382 revision
+= ' (%s)' % version
[2]
386 outmk
= open(mkname
, 'w')
387 outmc
= open(mcname
, 'w')
389 outmk
.write('#########################################\n')
390 outmk
.write('## AUTO-GENERATED FILE, DO NOT EDIT!!! ##\n')
391 outmk
.write('## Generated by: tools/config.py ##\n')
392 outmk
.write('#########################################\n\n')
394 outmc
.write('/***************************************\n')
395 outmc
.write(' * AUTO-GENERATED FILE, DO NOT EDIT!!! *\n')
396 outmc
.write(' * Generated by: tools/config.py *\n')
397 outmc
.write(' ***************************************/\n\n')
399 defs
= 'CONFIG_DEFS ='
401 for varname
, vartype
, name
, choices
, cond
in rules
:
402 if cond
and (not check_condition(cond
, config
, rules
)):
405 if not varname
in config
:
408 value
= config
[varname
]
412 outmk
.write('# %s\n%s = %s\n\n' % (name
, varname
, value
))
414 if vartype
in ["y", "n", "y/n", "n/y"]:
416 outmc
.write('/* %s */\n#define %s\n\n' % (name
, varname
))
417 defs
+= ' -D%s' % varname
419 outmc
.write('/* %s */\n#define %s %s\n#define %s_%s\n\n' % (name
, varname
, value
, varname
, value
))
420 defs
+= ' -D%s=%s -D%s_%s' % (varname
, value
, varname
, value
)
422 if revision
is not None:
423 outmk
.write('REVISION = %s\n' % revision
)
424 outmc
.write('#define REVISION %s\n' % revision
)
425 defs
+= ' "-DREVISION=%s"' % revision
427 outmk
.write('TIMESTAMP_UNIX = %d\n' % timestamp_unix
)
428 outmc
.write('#define TIMESTAMP_UNIX %d\n' % timestamp_unix
)
429 defs
+= ' "-DTIMESTAMP_UNIX=%d"\n' % timestamp_unix
431 outmk
.write('TIMESTAMP = %s\n' % timestamp
)
432 outmc
.write('#define TIMESTAMP %s\n' % timestamp
)
433 defs
+= ' "-DTIMESTAMP=%s"\n' % timestamp
440 def sorted_dir(root
):
441 list = os
.listdir(root
)
445 ## Ask user to choose a configuration profile.
447 def choose_profile(root
, fname
, screen
, config
):
453 for name
in sorted_dir(root
):
454 path
= os
.path
.join(root
, name
)
455 canon
= os
.path
.join(path
, fname
)
457 if os
.path
.isdir(path
) and os
.path
.exists(canon
) and os
.path
.isfile(canon
):
460 # Look for subprofiles
461 for subname
in sorted_dir(path
):
462 subpath
= os
.path
.join(path
, subname
)
463 subcanon
= os
.path
.join(subpath
, fname
)
465 if os
.path
.isdir(subpath
) and os
.path
.exists(subcanon
) and os
.path
.isfile(subcanon
):
467 options
.append("%s (%s)" % (name
, subname
))
468 opt2path
[cnt
] = [name
, subname
]
473 opt2path
[cnt
] = [name
]
476 (button
, value
) = xtui
.choice_window(screen
, 'Load preconfigured defaults', 'Choose configuration profile', options
, None)
478 if button
== 'cancel':
481 return opt2path
[value
]
483 ## Read presets from a configuration profile.
485 # @param profile Profile to load from (a list of string components)
486 # @param config Output configuration
488 def read_presets(profile
, config
):
489 path
= os
.path
.join(PRESETS_DIR
, profile
[0], MAKEFILE
)
490 read_config(path
, config
)
493 path
= os
.path
.join(PRESETS_DIR
, profile
[0], profile
[1], MAKEFILE
)
494 read_config(path
, config
)
496 ## Parse profile name (relative OS path) into a list of components.
498 # @param profile_name Relative path (using OS separator)
499 # @return List of components
501 def parse_profile_name(profile_name
):
504 head
, tail
= os
.path
.split(profile_name
)
517 parse_rules(RULES_FILE
, rules
)
519 # Input configuration file can be specified on command line
520 # otherwise configuration from previous run is used.
521 if len(sys
.argv
) >= 4:
522 profile
= parse_profile_name(sys
.argv
[3])
523 read_presets(profile
, config
)
524 elif os
.path
.exists(MAKEFILE
):
525 read_config(MAKEFILE
, config
)
527 # Default mode: check values and regenerate configuration files
528 if (len(sys
.argv
) >= 3) and (sys
.argv
[2] == 'default'):
529 if (infer_verify_choices(config
, rules
)):
530 preprocess_config(config
, rules
)
531 create_output(MAKEFILE
, MACROS
, config
, rules
)
534 # Hands-off mode: check values and regenerate configuration files,
535 # but no interactive fallback
536 if (len(sys
.argv
) >= 3) and (sys
.argv
[2] == 'hands-off'):
537 # We deliberately test sys.argv >= 4 because we do not want
538 # to read implicitly any possible previous run configuration
539 if len(sys
.argv
) < 4:
540 sys
.stderr
.write("Configuration error: No presets specified\n")
543 if (infer_verify_choices(config
, rules
)):
544 preprocess_config(config
, rules
)
545 create_output(MAKEFILE
, MACROS
, config
, rules
)
548 sys
.stderr
.write("Configuration error: The presets are ambiguous\n")
551 # Check mode: only check configuration
552 if (len(sys
.argv
) >= 3) and (sys
.argv
[2] == 'check'):
553 if infer_verify_choices(config
, rules
):
557 screen
= xtui
.screen_init()
563 # Cancel out all values which have to be deduced
564 for varname
, vartype
, name
, choices
, cond
in rules
:
565 if (vartype
== 'y') and (varname
in config
) and (config
[varname
] == '*'):
566 config
[varname
] = None
572 options
.append(" --- Load preconfigured defaults ... ")
575 varname
, vartype
, name
, choices
, cond
= rule
577 if cond
and (not check_condition(cond
, config
, rules
)):
580 if varname
== selname
:
583 if not varname
in config
:
586 value
= config
[varname
]
588 if not validate_rule_value(rule
, value
):
591 default
= get_default_rule(rule
)
594 # If we don't have a value but we do have
597 if value
== None and default
!= None:
599 config
[varname
] = default
601 option
= get_rule_option(rule
, value
)
603 options
.append(option
)
607 opt2row
[cnt
] = (varname
, vartype
, name
, choices
)
611 if (position
!= None) and (position
>= len(options
)):
614 (button
, value
) = xtui
.choice_window(screen
, 'HelenOS configuration', 'Choose configuration option', options
, position
)
616 if button
== 'cancel':
617 return 'Configuration canceled'
620 if (infer_verify_choices(config
, rules
)):
623 xtui
.error_dialog(screen
, 'Error', 'Some options have still undefined values. These options are marked with the "?" sign.')
627 profile
= choose_profile(PRESETS_DIR
, MAKEFILE
, screen
, config
)
629 read_presets(profile
, config
)
634 if not value
in opt2row
:
635 raise RuntimeError("Error selecting value: %s" % value
)
637 (selname
, seltype
, name
, choices
) = opt2row
[value
]
639 if not selname
in config
:
642 value
= config
[selname
]
644 if seltype
== 'choice':
645 config
[selname
] = subchoice(screen
, name
, choices
, value
)
646 elif (seltype
== 'y/n') or (seltype
== 'n/y'):
647 if config
[selname
] == 'y':
648 config
[selname
] = 'n'
650 config
[selname
] = 'y'
652 xtui
.screen_done(screen
)
654 preprocess_config(config
, rules
)
655 create_output(MAKEFILE
, MACROS
, config
, rules
)
658 if __name__
== '__main__':