source: mainline/tools/config.py@ 512579c

lfn serial ticket/834-toolchain-update topic/msim-upgrade topic/simplify-dev-export
Last change on this file since 512579c was aca97582, checked in by Jiří Zárevúcky <zarevucky.jiri@…>, 6 years ago

tools/config.py: Clean up argument handling

  • Property mode set to 100755
File size: 20.9 KB
Line 
1#!/usr/bin/env python
2#
3# Copyright (c) 2006 Ondrej Palkovsky
4# Copyright (c) 2009 Martin Decky
5# Copyright (c) 2010 Jiri Svoboda
6# All rights reserved.
7#
8# Redistribution and use in source and binary forms, with or without
9# modification, are permitted provided that the following conditions
10# are met:
11#
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.
19#
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.
30#
31
32"""
33HelenOS configuration system
34"""
35
36import sys
37import os
38import re
39import time
40import subprocess
41import xtui
42import random
43
44ARGPOS_RULES = 1
45ARGPOS_CHOICE = 2
46ARGPOS_PRESET = 3
47
48RULES_FILE = sys.argv[ARGPOS_RULES]
49MAKEFILE = 'Makefile.config'
50MACROS = 'config.h'
51PRESETS_DIR = 'defaults'
52
53class BinaryOp:
54 def __init__(self, operator, left, right):
55 assert operator in ('&', '|', '=', '!=')
56
57 self._operator = operator
58 self._left = left
59 self._right = right
60
61 def evaluate(self, config):
62 if self._operator == '&':
63 return self._left.evaluate(config) and \
64 self._right.evaluate(config)
65 if self._operator == '|':
66 return self._left.evaluate(config) or \
67 self._right.evaluate(config)
68
69 # '=' or '!='
70 if not self._left in config:
71 config_val = ''
72 else:
73 config_val = config[self._left]
74 if config_val == '*':
75 config_val = 'y'
76
77 if self._operator == '=':
78 return self._right == config_val
79 return self._right != config_val
80
81# Expression parser
82class CondParser:
83 TOKEN_EOE = 0
84 TOKEN_SPECIAL = 1
85 TOKEN_STRING = 2
86
87 def __init__(self, text):
88 self._text = text
89
90 def parse(self):
91 self._position = -1
92 self._next_char()
93 self._next_token()
94
95 res = self._parse_expr()
96 if self._token_type != self.TOKEN_EOE:
97 self._error("Expected end of expression")
98 return res
99
100 def _next_char(self):
101 self._position += 1
102 if self._position >= len(self._text):
103 self._char = None
104 else:
105 self._char = self._text[self._position]
106 self._is_special_char = self._char in \
107 ('&', '|', '=', '!', '(', ')')
108
109 def _error(self, msg):
110 raise RuntimeError("Error parsing expression: %s:\n%s\n%s^" %
111 (msg, self._text, " " * self._token_position))
112
113 def _next_token(self):
114 self._token_position = self._position
115
116 # End of expression
117 if self._char == None:
118 self._token = None
119 self._token_type = self.TOKEN_EOE
120 return
121
122 # '&', '|', '=', '!=', '(', ')'
123 if self._is_special_char:
124 self._token = self._char
125 self._next_char()
126 if self._token == '!':
127 if self._char != '=':
128 self._error("Expected '='")
129 self._token += self._char
130 self._next_char()
131 self._token_type = self.TOKEN_SPECIAL
132 return
133
134 # <var> or <val>
135 self._token = ''
136 self._token_type = self.TOKEN_STRING
137 while True:
138 self._token += self._char
139 self._next_char()
140 if self._is_special_char or self._char == None:
141 break
142
143 def _parse_expr(self):
144 """ <expr> ::= <or_expr> ('&' <or_expr>)* """
145
146 left = self._parse_or_expr()
147 while self._token == '&':
148 self._next_token()
149 left = BinaryOp('&', left, self._parse_or_expr())
150 return left
151
152 def _parse_or_expr(self):
153 """ <or_expr> ::= <factor> ('|' <factor>)* """
154
155 left = self._parse_factor()
156 while self._token == '|':
157 self._next_token()
158 left = BinaryOp('|', left, self._parse_factor())
159 return left
160
161 def _parse_factor(self):
162 """ <factor> ::= <var> <cond> | '(' <expr> ')' """
163
164 if self._token == '(':
165 self._next_token()
166 res = self._parse_expr()
167 if self._token != ')':
168 self._error("Expected ')'")
169 self._next_token()
170 return res
171
172 if self._token_type == self.TOKEN_STRING:
173 var = self._token
174 self._next_token()
175 return self._parse_cond(var)
176
177 self._error("Expected '(' or <var>")
178
179 def _parse_cond(self, var):
180 """ <cond> ::= '=' <val> | '!=' <val> """
181
182 if self._token not in ('=', '!='):
183 self._error("Expected '=' or '!='")
184
185 oper = self._token
186 self._next_token()
187
188 if self._token_type != self.TOKEN_STRING:
189 self._error("Expected <val>")
190
191 val = self._token
192 self._next_token()
193
194 return BinaryOp(oper, var, val)
195
196def read_config(fname, config):
197 "Read saved values from last configuration run or a preset file"
198
199 inf = open(fname, 'r')
200
201 for line in inf:
202 res = re.match(r'^(?:#!# )?([^#]\w*)\s*=\s*(.*?)\s*$', line)
203 if res:
204 config[res.group(1)] = res.group(2)
205
206 inf.close()
207
208def parse_rules(fname, rules):
209 "Parse rules file"
210
211 inf = open(fname, 'r')
212
213 name = ''
214 choices = []
215
216 for line in inf:
217
218 if line.startswith('!'):
219 # Ask a question
220 res = re.search(r'!\s*(?:\[(.*?)\])?\s*([^\s]+)\s*\((.*)\)\s*$', line)
221
222 if not res:
223 raise RuntimeError("Weird line: %s" % line)
224
225 cond = res.group(1)
226 if cond:
227 cond = CondParser(cond).parse()
228 varname = res.group(2)
229 vartype = res.group(3)
230
231 rules.append((varname, vartype, name, choices, cond))
232 name = ''
233 choices = []
234 continue
235
236 if line.startswith('@'):
237 # Add new line into the 'choices' array
238 res = re.match(r'@\s*(?:\[(.*?)\])?\s*"(.*?)"\s*(.*)$', line)
239
240 if not res:
241 raise RuntimeError("Bad line: %s" % line)
242
243 choices.append((res.group(2), res.group(3)))
244 continue
245
246 if line.startswith('%'):
247 # Name of the option
248 name = line[1:].strip()
249 continue
250
251 if line.startswith('#') or (line == '\n'):
252 # Comment or empty line
253 continue
254
255
256 raise RuntimeError("Unknown syntax: %s" % line)
257
258 inf.close()
259
260def yes_no(default):
261 "Return '*' if yes, ' ' if no"
262
263 if default == 'y':
264 return '*'
265
266 return ' '
267
268def subchoice(screen, name, choices, default):
269 "Return choice of choices"
270
271 maxkey = 0
272 for key, val in choices:
273 length = len(key)
274 if (length > maxkey):
275 maxkey = length
276
277 options = []
278 position = None
279 cnt = 0
280 for key, val in choices:
281 if (default) and (key == default):
282 position = cnt
283
284 options.append(" %-*s %s " % (maxkey, key, val))
285 cnt += 1
286
287 (button, value) = xtui.choice_window(screen, name, 'Choose value', options, position)
288
289 if button == 'cancel':
290 return None
291
292 return choices[value][0]
293
294## Infer and verify configuration values.
295#
296# Augment @a config with values that can be inferred, purge invalid ones
297# and verify that all variables have a value (previously specified or inferred).
298#
299# @param config Configuration to work on
300# @param rules Rules
301#
302# @return True if configuration is complete and valid, False
303# otherwise.
304#
305def infer_verify_choices(config, rules):
306 "Infer and verify configuration values."
307
308 for rule in rules:
309 varname, vartype, name, choices, cond = rule
310
311 if cond and not cond.evaluate(config):
312 continue
313
314 if not varname in config:
315 value = None
316 else:
317 value = config[varname]
318
319 if not validate_rule_value(rule, value):
320 value = None
321
322 default = get_default_rule(rule)
323
324 #
325 # If we don't have a value but we do have
326 # a default, use it.
327 #
328 if value == None and default != None:
329 value = default
330 config[varname] = default
331
332 if not varname in config:
333 return False
334
335 return True
336
337## Fill the configuration with random (but valid) values.
338#
339# The random selection takes next rule and if the condition does
340# not violate existing configuration, random value of the variable
341# is selected.
342# This happens recursively as long as there are more rules.
343# If a conflict is found, we backtrack and try other settings of the
344# variable or ignoring the variable altogether.
345#
346# @param config Configuration to work on
347# @param rules Rules
348# @param start_index With which rule to start (initial call must specify 0 here).
349# @return True if able to find a valid configuration
350def random_choices(config, rules, start_index):
351 "Fill the configuration with random (but valid) values."
352 if start_index >= len(rules):
353 return True
354
355 varname, vartype, name, choices, cond = rules[start_index]
356
357 # First check that this rule would make sense
358 if cond and not cond.evaluate(config):
359 return random_choices(config, rules, start_index + 1)
360
361 # Remember previous choices for backtracking
362 yes_no = 0
363 choices_indexes = range(0, len(choices))
364 random.shuffle(choices_indexes)
365
366 # Remember current configuration value
367 old_value = None
368 try:
369 old_value = config[varname]
370 except KeyError:
371 old_value = None
372
373 # For yes/no choices, we ran the loop at most 2 times, for select
374 # choices as many times as there are options.
375 try_counter = 0
376 while True:
377 if vartype == 'choice':
378 if try_counter >= len(choices_indexes):
379 break
380 value = choices[choices_indexes[try_counter]][0]
381 elif vartype == 'y' or vartype == 'n':
382 if try_counter > 0:
383 break
384 value = vartype
385 elif vartype == 'y/n' or vartype == 'n/y':
386 if try_counter == 0:
387 yes_no = random.randint(0, 1)
388 elif try_counter == 1:
389 yes_no = 1 - yes_no
390 else:
391 break
392 if yes_no == 0:
393 value = 'n'
394 else:
395 value = 'y'
396 else:
397 raise RuntimeError("Unknown variable type: %s" % vartype)
398
399 config[varname] = value
400
401 ok = random_choices(config, rules, start_index + 1)
402 if ok:
403 return True
404
405 try_counter = try_counter + 1
406
407 # Restore the old value and backtrack
408 # (need to delete to prevent "ghost" variables that do not exist under
409 # certain configurations)
410 config[varname] = old_value
411 if old_value is None:
412 del config[varname]
413
414 return random_choices(config, rules, start_index + 1)
415
416
417## Get default value from a rule.
418def get_default_rule(rule):
419 varname, vartype, name, choices, cond = rule
420
421 default = None
422
423 if vartype == 'choice':
424 # If there is just one option, use it
425 if len(choices) == 1:
426 default = choices[0][0]
427 elif vartype == 'y':
428 default = '*'
429 elif vartype == 'n':
430 default = 'n'
431 elif vartype == 'y/n':
432 default = 'y'
433 elif vartype == 'n/y':
434 default = 'n'
435 else:
436 raise RuntimeError("Unknown variable type: %s" % vartype)
437
438 return default
439
440## Get option from a rule.
441#
442# @param rule Rule for a variable
443# @param value Current value of the variable
444#
445# @return Option (string) to ask or None which means not to ask.
446#
447def get_rule_option(rule, value):
448 varname, vartype, name, choices, cond = rule
449
450 option = None
451
452 if vartype == 'choice':
453 # If there is just one option, don't ask
454 if len(choices) != 1:
455 if (value == None):
456 option = "? %s --> " % name
457 else:
458 option = " %s [%s] --> " % (name, value)
459 elif vartype == 'y':
460 pass
461 elif vartype == 'n':
462 pass
463 elif vartype == 'y/n':
464 option = " <%s> %s " % (yes_no(value), name)
465 elif vartype == 'n/y':
466 option =" <%s> %s " % (yes_no(value), name)
467 else:
468 raise RuntimeError("Unknown variable type: %s" % vartype)
469
470 return option
471
472## Check if variable value is valid.
473#
474# @param rule Rule for the variable
475# @param value Value of the variable
476#
477# @return True if valid, False if not valid.
478#
479def validate_rule_value(rule, value):
480 varname, vartype, name, choices, cond = rule
481
482 if value == None:
483 return True
484
485 if vartype == 'choice':
486 if not value in [choice[0] for choice in choices]:
487 return False
488 elif vartype == 'y':
489 if value != 'y':
490 return False
491 elif vartype == 'n':
492 if value != 'n':
493 return False
494 elif vartype == 'y/n':
495 if not value in ['y', 'n']:
496 return False
497 elif vartype == 'n/y':
498 if not value in ['y', 'n']:
499 return False
500 else:
501 raise RuntimeError("Unknown variable type: %s" % vartype)
502
503 return True
504
505def preprocess_config(config, rules):
506 "Preprocess configuration"
507
508 varname_mode = 'CONFIG_BFB_MODE'
509 varname_width = 'CONFIG_BFB_WIDTH'
510 varname_height = 'CONFIG_BFB_HEIGHT'
511
512 if varname_mode in config:
513 mode = config[varname_mode].partition('x')
514
515 config[varname_width] = mode[0]
516 rules.append((varname_width, 'choice', 'Default framebuffer width', None, None))
517
518 config[varname_height] = mode[2]
519 rules.append((varname_height, 'choice', 'Default framebuffer height', None, None))
520
521def create_output(mkname, mcname, config, rules):
522 "Create output configuration"
523
524 varname_strip = 'CONFIG_STRIP_REVISION_INFO'
525 strip_rev_info = (varname_strip in config) and (config[varname_strip] == 'y')
526
527 if strip_rev_info:
528 timestamp_unix = int(0)
529 else:
530 # TODO: Use commit timestamp instead of build time.
531 timestamp_unix = int(time.time())
532
533 timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp_unix))
534
535 sys.stderr.write("Fetching current revision identifier ... ")
536
537 try:
538 version = subprocess.Popen(['git', 'log', '-1', '--pretty=%h'], stdout = subprocess.PIPE).communicate()[0].decode().strip()
539 sys.stderr.write("ok\n")
540 except:
541 version = None
542 sys.stderr.write("failed\n")
543
544 if (not strip_rev_info) and (version is not None):
545 revision = version
546 else:
547 revision = None
548
549 outmk = open(mkname, 'w')
550 outmc = open(mcname, 'w')
551
552 outmk.write('#########################################\n')
553 outmk.write('## AUTO-GENERATED FILE, DO NOT EDIT!!! ##\n')
554 outmk.write('## Generated by: tools/config.py ##\n')
555 outmk.write('#########################################\n\n')
556
557 outmc.write('/***************************************\n')
558 outmc.write(' * AUTO-GENERATED FILE, DO NOT EDIT!!! *\n')
559 outmc.write(' * Generated by: tools/config.py *\n')
560 outmc.write(' ***************************************/\n\n')
561
562 defs = 'CONFIG_DEFS ='
563
564 for varname, vartype, name, choices, cond in rules:
565 if cond and not cond.evaluate(config):
566 continue
567
568 if not varname in config:
569 value = ''
570 else:
571 value = config[varname]
572 if (value == '*'):
573 value = 'y'
574
575 outmk.write('# %s\n%s = %s\n\n' % (name, varname, value))
576
577 if vartype in ["y", "n", "y/n", "n/y"]:
578 if value == "y":
579 outmc.write('/* %s */\n#define %s\n\n' % (name, varname))
580 defs += ' -D%s' % varname
581 else:
582 outmc.write('/* %s */\n#define %s %s\n#define %s_%s\n\n' % (name, varname, value, varname, value))
583 defs += ' -D%s=%s -D%s_%s' % (varname, value, varname, value)
584
585 if revision is not None:
586 outmk.write('REVISION = %s\n' % revision)
587 outmc.write('#define REVISION %s\n' % revision)
588 defs += ' "-DREVISION=%s"' % revision
589
590 outmk.write('TIMESTAMP_UNIX = %d\n' % timestamp_unix)
591 outmc.write('#define TIMESTAMP_UNIX %d\n' % timestamp_unix)
592 defs += ' "-DTIMESTAMP_UNIX=%d"' % timestamp_unix
593
594 outmk.write('TIMESTAMP = %s\n' % timestamp)
595 outmc.write('#define TIMESTAMP %s\n' % timestamp)
596 defs += ' "-DTIMESTAMP=%s"' % timestamp
597
598 outmk.write('%s\n' % defs)
599
600 outmk.close()
601 outmc.close()
602
603def sorted_dir(root):
604 list = os.listdir(root)
605 list.sort()
606 return list
607
608## Ask user to choose a configuration profile.
609#
610def choose_profile(root, fname, screen, config):
611 options = []
612 opt2path = {}
613 cnt = 0
614
615 # Look for profiles
616 for name in sorted_dir(root):
617 path = os.path.join(root, name)
618 canon = os.path.join(path, fname)
619
620 if os.path.isdir(path) and os.path.exists(canon) and os.path.isfile(canon):
621 subprofile = False
622
623 # Look for subprofiles
624 for subname in sorted_dir(path):
625 subpath = os.path.join(path, subname)
626 subcanon = os.path.join(subpath, fname)
627
628 if os.path.isdir(subpath) and os.path.exists(subcanon) and os.path.isfile(subcanon):
629 subprofile = True
630 options.append("%s (%s)" % (name, subname))
631 opt2path[cnt] = [name, subname]
632 cnt += 1
633
634 if not subprofile:
635 options.append(name)
636 opt2path[cnt] = [name]
637 cnt += 1
638
639 (button, value) = xtui.choice_window(screen, 'Load preconfigured defaults', 'Choose configuration profile', options, None)
640
641 if button == 'cancel':
642 return None
643
644 return opt2path[value]
645
646## Read presets from a configuration profile.
647#
648# @param profile Profile to load from (a list of string components)
649# @param config Output configuration
650#
651def read_presets(profile, config):
652 path = os.path.join(PRESETS_DIR, profile[0], MAKEFILE)
653 read_config(path, config)
654
655 if len(profile) > 1:
656 path = os.path.join(PRESETS_DIR, profile[0], profile[1], MAKEFILE)
657 read_config(path, config)
658
659## Parse profile name (relative OS path) into a list of components.
660#
661# @param profile_name Relative path (using OS separator)
662# @return List of components
663#
664def parse_profile_name(profile_name):
665 profile = []
666
667 head, tail = os.path.split(profile_name)
668 if head != '':
669 profile.append(head)
670
671 profile.append(tail)
672 return profile
673
674def main():
675 profile = None
676 config = {}
677 rules = []
678
679 # Parse rules file
680 parse_rules(RULES_FILE, rules)
681
682 if len(sys.argv) > ARGPOS_CHOICE:
683 choice = sys.argv[ARGPOS_CHOICE]
684 else:
685 choice = None
686
687 if len(sys.argv) > ARGPOS_PRESET:
688 preset = sys.argv[ARGPOS_PRESET]
689 else:
690 preset = None
691
692 # Input configuration file can be specified on command line
693 # otherwise configuration from previous run is used.
694 if preset is not None:
695 profile = parse_profile_name(preset)
696 read_presets(profile, config)
697 elif os.path.exists(MAKEFILE):
698 read_config(MAKEFILE, config)
699
700 # Default mode: check values and regenerate configuration files
701 if choice == 'default':
702 if (infer_verify_choices(config, rules)):
703 preprocess_config(config, rules)
704 create_output(MAKEFILE, MACROS, config, rules)
705 return 0
706
707 # Hands-off mode: check values and regenerate configuration files,
708 # but no interactive fallback
709 if choice == 'hands-off':
710 # We deliberately test this because we do not want
711 # to read implicitly any possible previous run configuration
712 if preset is None:
713 sys.stderr.write("Configuration error: No presets specified\n")
714 return 2
715
716 if (infer_verify_choices(config, rules)):
717 preprocess_config(config, rules)
718 create_output(MAKEFILE, MACROS, config, rules)
719 return 0
720
721 sys.stderr.write("Configuration error: The presets are ambiguous\n")
722 return 1
723
724 # Check mode: only check configuration
725 if choice == 'check':
726 if infer_verify_choices(config, rules):
727 return 0
728 return 1
729
730 # Random mode
731 if choice == 'random':
732 ok = random_choices(config, rules, 0)
733 if not ok:
734 sys.stderr.write("Internal error: unable to generate random config.\n")
735 return 2
736 if not infer_verify_choices(config, rules):
737 sys.stderr.write("Internal error: random configuration not consistent.\n")
738 return 2
739 preprocess_config(config, rules)
740 create_output(MAKEFILE, MACROS, config, rules)
741
742 return 0
743
744 screen = xtui.screen_init()
745 try:
746 selname = None
747 position = None
748 while True:
749
750 # Cancel out all values which have to be deduced
751 for varname, vartype, name, choices, cond in rules:
752 if (vartype == 'y') and (varname in config) and (config[varname] == '*'):
753 config[varname] = None
754
755 options = []
756 opt2row = {}
757 cnt = 1
758
759 options.append(" --- Load preconfigured defaults ... ")
760
761 for rule in rules:
762 varname, vartype, name, choices, cond = rule
763
764 if cond and not cond.evaluate(config):
765 continue
766
767 if varname == selname:
768 position = cnt
769
770 if not varname in config:
771 value = None
772 else:
773 value = config[varname]
774
775 if not validate_rule_value(rule, value):
776 value = None
777
778 default = get_default_rule(rule)
779
780 #
781 # If we don't have a value but we do have
782 # a default, use it.
783 #
784 if value == None and default != None:
785 value = default
786 config[varname] = default
787
788 option = get_rule_option(rule, value)
789 if option != None:
790 options.append(option)
791 else:
792 continue
793
794 opt2row[cnt] = (varname, vartype, name, choices)
795
796 cnt += 1
797
798 if (position != None) and (position >= len(options)):
799 position = None
800
801 (button, value) = xtui.choice_window(screen, 'HelenOS configuration', 'Choose configuration option', options, position)
802
803 if button == 'cancel':
804 return 'Configuration canceled'
805
806 if button == 'done':
807 if (infer_verify_choices(config, rules)):
808 break
809 else:
810 xtui.error_dialog(screen, 'Error', 'Some options have still undefined values. These options are marked with the "?" sign.')
811 continue
812
813 if value == 0:
814 profile = choose_profile(PRESETS_DIR, MAKEFILE, screen, config)
815 if profile != None:
816 read_presets(profile, config)
817 position = 1
818 continue
819
820 position = None
821 if not value in opt2row:
822 raise RuntimeError("Error selecting value: %s" % value)
823
824 (selname, seltype, name, choices) = opt2row[value]
825
826 if not selname in config:
827 value = None
828 else:
829 value = config[selname]
830
831 if seltype == 'choice':
832 config[selname] = subchoice(screen, name, choices, value)
833 elif (seltype == 'y/n') or (seltype == 'n/y'):
834 if config[selname] == 'y':
835 config[selname] = 'n'
836 else:
837 config[selname] = 'y'
838 finally:
839 xtui.screen_done(screen)
840
841 preprocess_config(config, rules)
842 create_output(MAKEFILE, MACROS, config, rules)
843 return 0
844
845if __name__ == '__main__':
846 sys.exit(main())
Note: See TracBrowser for help on using the repository browser.