Salome HOME
Introduce SALOME configuration tool
authorvsr <vsr@opencascade.com>
Mon, 20 Jun 2016 13:31:03 +0000 (16:31 +0300)
committervsr <vsr@opencascade.com>
Mon, 20 Jun 2016 13:31:03 +0000 (16:31 +0300)
config/README [new file with mode: 0644]
config/patches/README [new file with mode: 0644]
config/salome.xml [new file with mode: 0644]
config/scfg [new file with mode: 0755]
config/scfgcmp [new file with mode: 0755]
config/sconfig/__init__.py [new file with mode: 0644]
config/sconfig/salome_config.py [new file with mode: 0644]

diff --git a/config/README b/config/README
new file mode 100644 (file)
index 0000000..dd61b7c
--- /dev/null
@@ -0,0 +1,21 @@
+The contents of this package:
+
+salome.xml        SALOME configuration file.
+
+patches           Directory with patches needed to build SALOME configuration.
+
+sconfig           Python module to manage SALOME configuration files.
+                  To learn about usage, run Python and type:
+
+                  from sconfig import salome_config
+                  help(salome_config)
+
+scfg              Command line tool to manage SALOME configuration files.
+                  To learn about usage, type:
+
+                  scfg help
+
+scfgcmp           Command line tool to compare two SALOME configuration files.
+                  To learn about usage, type:
+
+                  scfgcmp -h
diff --git a/config/patches/README b/config/patches/README
new file mode 100644 (file)
index 0000000..279a153
--- /dev/null
@@ -0,0 +1 @@
+This directory containes patches needed to build SALOME pre-requisites.
diff --git a/config/salome.xml b/config/salome.xml
new file mode 100644 (file)
index 0000000..8e8c7ff
--- /dev/null
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!DOCTYPE config>
+<config />
diff --git a/config/scfg b/config/scfg
new file mode 100755 (executable)
index 0000000..870e045
--- /dev/null
@@ -0,0 +1,550 @@
+#!/usr/bin/env python
+#  -*- coding: iso-8859-1 -*-
+
+###
+# Command line tool to manage SALOME configuration.
+# Usage: type "scfg help" to learn how to use tool.
+###
+
+import sys
+import os
+from sconfig.salome_config import CfgTool, defaultConfFile
+
+###
+# TODO
+# 1. Improve parsing of command line: each command should be defined as:
+#    - command:    name of the command
+#    - help:       help string as appears in the short description of a command in a list
+#                  of commands and as a first line of command's help
+#    - options:    (dict) see below
+#    - parameters: (list or dict) see below
+#    Each option should be defined with:
+#    - title:      one letter signature
+#    - help:       help string
+#    - parameter:  name of parameter if option requires a parameter
+#    Each parameter should be defined with:
+#    - title:      as appears in help information
+#    - optional:   flag (boolean) showing if this parameter is optional
+#    - help:       help description
+#    
+#    Parser of command line should then automatically check validity of the command,
+#    its parameters and options.
+#  
+# 2. Usage and help information should be automaticall generated and formatted
+#    from command description as described above.
+# 
+# 3. Protect from adding same named option to a command.
+# 
+# 4. Improve formatting help for command's parameters (see 1) - namely, wrapping
+#    into a block.
+###
+
+##
+# function: get_tool_name
+# 
+# gets a name of this command line tool (string)
+##
+
+def get_tool_name():
+    return os.path.basename(sys.argv[0])
+
+##
+# function: verbose
+# 
+# returns True when in debug mode (env var VERBOSE is set to 1)
+##
+
+def verbose():
+    return bool(os.getenv("VERBOSE", False))
+
+##
+# function: debug
+# 
+# prints debug information (for debug purposes)
+#
+# Parameters:
+#   args: output data to be printed to the screen (any printable objects)
+##
+
+def debug(*args):
+    if verbose():
+        print "[DEBUG]",
+        for a in args: print a,
+        print
+    pass
+
+##
+# function: error_exit
+# 
+# prints error information and exits with non-zero status
+# 
+# Parameters:
+#   msg: error message (string)
+##
+
+def error_exit(msg):
+    print "%s: %s" % (get_tool_name(), msg)
+    sys.exit(1)
+    pass
+
+##
+# function: get_commands
+# 
+# get supported commands
+# 
+# return value is a dictionary:
+# { cmd : ( help, opt_map ) }
+# here:
+# - cmd is a command (string)
+# - help is a command's general help information on a command (string)
+# - opt_map is a disctionary of allowed command's options:
+#   { opt: opt_help }, where 
+#   - opt is option ("a:" if option requires a parameter or just "a" elsewhere, where
+#                    "a" is any letter)
+#   - opt_help is a help information on the option (string).
+# 
+# See also: add_command
+##
+
+def get_commands():
+    if not globals().get("__commands__"): globals()["__commands__"] = {}
+    return globals().get("__commands__")
+
+##
+# function: add_command
+# 
+# adds command to the tool
+#
+# Parameters:
+#   cmd: command (string)
+#   help_string: help information on the command (string)
+#   options: options supported by the command (map {opt: opt_help}, see get_commands)
+##
+
+def add_command(cmd, help_string, options):
+    if cmd in get_commands():
+        debug("command %s already exists" % cmd)
+    else:
+        get_commands()[ cmd ] = ( help_string, options )
+    pass
+
+##
+# function: get_opts
+# 
+# gets supported options for the command
+# 
+# Parameters:
+#   cmd: command (string)
+# 
+# return value is map {opt: opt_help} (see get_commands)
+##
+
+def get_opts(cmd):
+    if cmd is None: cmd = ""
+    opts = {}
+    try:
+        opts = get_commands()[cmd][1]
+    except:
+        pass
+    return opts
+
+##
+# function: get_help
+# 
+# gets help string for the command
+# 
+# Parameters:
+#   cmd: command (string)
+# 
+# return value is unformatted string
+##
+
+def get_help(cmd):
+    if cmd is None: cmd = ""
+    help_string = ""
+    try:
+        help_string = get_commands()[cmd][0]
+    except:
+        pass
+    return help_string
+
+##
+# function: format_string
+# 
+# formats string into block according to the given block width
+# 
+# Parameters:
+#   s: string data
+#   width: requested width of resulting string block
+# 
+# return formatted string block
+##
+
+def format_string(s, width):
+    blocks = s.split("\n")
+    result = []
+    for block in blocks:
+        block = block.replace("\t", " [[TAB]] ")
+        words = block.split()
+        current_string = ""
+        for word in words:
+            word = word if word != "[[TAB]]" else " "*2
+            if not current_string:
+                current_string = word
+            else:
+                if len(current_string + " " + word) <= width:
+                    current_string += " " + word
+                else:
+                    result.append(current_string)
+                    current_string = word
+                    pass
+                pass
+            pass
+        result.append(current_string)
+        pass
+    return "\n".join(result)
+
+##
+# function: format_commands
+# 
+# formats short help on all supported commands
+# 
+# return string that contains formatted help on commands
+##
+
+def format_commands():
+    commands = get_commands()
+    # filter out tool itself
+    commands = filter(lambda a: a != "", commands)
+    # sort commands alphabetically
+    commands.sort()
+    # get max command's length
+    max_length = max([ len(i) for i in commands])
+    # generate formatting string
+    prefix = "  "
+    separator = "    "
+    fmt_string = prefix + "%" + "-%ds" % max_length + separator
+    def _format(_c):
+        _h = get_help(_c).split("\n")[0]
+        _h = format_string(_h, 80-(max_length+len(prefix)+len(separator)))
+        _h = _h.split("\n")
+        _t = prefix + " " * max_length + separator
+        _r = []
+        _r.append(_h[0])
+        for _i in _h[1:]: _r.append(_t + _i)
+        return "\n".join(_r)
+    return "\n".join([ fmt_string % i + _format(i) for i in commands])
+
+##
+# function: format_list_options
+# 
+# formats short help on commands's options to be inserted into short help info on a command
+# 
+# Parameters:
+#   cmd: command (string)
+# 
+# return string that contains formatted help
+##
+
+def format_list_options(cmd):
+    opts = get_opts(cmd)
+    return "".join([" [%s]" % i for i in opts.keys()])
+
+##
+# function: format_options
+# 
+# formats help on commands's options to be inserted after short help info on a command
+# 
+# Parameters:
+#   cmd: command (string)
+# 
+# return string that contains formatted help
+##
+
+def format_options(cmd):
+    opts = get_opts(cmd)
+    opts_names = opts.keys()
+    if not opts: return ""
+    # get max command's length
+    max_length = max([ len(i) for i in opts_names])
+    # generate formatting string
+    prefix = "  "
+    separator = "    "
+    fmt_string = prefix + "%" + "-%ds" % max_length + separator
+    def _format(_o):
+        _h = opts[_o]
+        _h = format_string(_h, 80-(max_length+len(prefix)+len(separator)))
+        _h = _h.split("\n")
+        _t = prefix + " " * max_length + separator
+        _r = []
+        _r.append(_h[0])
+        for _i in _h[1:]: _r.append(_t + _i)
+        return "\n".join(_r)
+    return "\n" + "\n".join([ fmt_string % i + _format(i) for i in opts_names]) + "\n"
+
+##
+# function: usage
+# 
+# prints help and exits with zero status
+#
+# Parameters:
+#   cmd: command (string)
+##
+
+def usage(cmd=None):
+    if cmd is None: cmd = ""
+    if cmd not in get_commands(): error_exit("unknown command: %s" % cmd)
+    fmt = {}
+    fmt[ "prg" ] = get_tool_name()
+    fmt[ "cmd" ] = get_tool_name() if not cmd else cmd
+    fmt[ "cmd_list" ] = format_commands()
+    fmt[ "opts" ] = format_list_options(cmd)
+    fmt[ "opt_list" ] = format_options(cmd)
+    help_string = get_help(cmd)
+    help_string = help_string.replace("\t", "  ")
+    print help_string.format(**fmt)
+    sys.exit(0)
+    pass
+
+##
+# function: parse_cmd_line
+# 
+# parses command line; prints error and exits if unsupported command
+# or option is specified
+# 
+# Parameters:
+#   args: arguments being parsed (list of strings)
+##
+
+def parse_cmd_line(args):
+    tool_opts, cmd, cmd_opts, tgt, params = {}, None, {}, None, []
+    args.reverse()
+    commands = get_commands()
+    while args:
+        a = args.pop()
+        if a == "-":
+            # empty option
+            error_exit("empty option is not allowed")
+        if a.startswith("-"):
+            allowed_opts = get_opts(cmd).keys()
+            allowed_opts = dict([(i[1], len(i)>2) for i in allowed_opts])
+            processed_opts = tool_opts if cmd is None else cmd_opts
+            cmd_descr = "" if cmd is None else " for command '%s'" % cmd
+            opts = a[1:]
+            while opts:
+                o = opts[0]
+                opts = opts[1:]
+                if o == "-":
+                    # "--" format is not supported
+                    error_exit("invalid format of option")
+                elif o in allowed_opts.keys():
+                    if allowed_opts[o]:
+                        # supported option; requires parameter
+                        if opts:
+                            processed_opts[ o ] = opts
+                            opts = []
+                        elif args and not args[-1].startswith("-") and not args[-1] in commands:
+                            processed_opts[ o ] = args.pop()
+                        else:
+                            error_exit("option %s%s requires argument" % (o, cmd_descr))
+                    else:
+                        # supported option; does not require parameter
+                        processed_opts[ o ] = None
+                        pass
+                    pass
+                else:
+                    # unsupported option
+                    error_exit("unknown option: -%s%s" % (o, cmd_descr))
+                pass # while opts
+            pass
+        elif not cmd:
+            cmd = a
+            if cmd not in commands:
+                # unsupported command
+                error_exit("unknown command: %s" % cmd)
+            pass
+        elif not tgt:
+            tgt = a
+            pass
+        else:
+            params += [ a ]
+    return tool_opts, cmd, cmd_opts, tgt, params
+
+##
+# function: main
+# 
+# main entry point of the tool
+##
+
+def main():
+    # set-up commands
+
+    # - tool itself
+    help_string  = "Command line tool to manage SALOME configuration.\n\n"
+    help_string += "Usage: {prg}{opts} COMMAND [ARGS]\n"
+    help_string += "{opt_list}\n"
+    help_string += "Supported commands:\n"
+    help_string += "{cmd_list}\n\n"
+    help_string += "See '{prg} help COMMAND' for more information on a specific command."
+    options = {}
+    options [ "-s FILE" ] = "Path to the configuration file; default: %s." % defaultConfFile()
+    add_command("",
+                help_string,
+                options)
+
+    # - help command
+    help_string  = "Display help information about this tool.\n\n"
+    help_string += "Usage: {prg} {cmd}{opts}\n"
+    help_string += "{opt_list}"
+    add_command("help",
+                help_string,
+                {})
+
+    # - set command
+    help_string  = "Create or modify configuration object.\n\n"
+    help_string += "Usage: {prg} {cmd}{opts} TARGET [PARAM VALUE ...]\n\n"
+    help_string += "\tTARGET: configuration object being created / modified\n"
+    help_string += "\tPARAM: parameter of the target object\n"
+    help_string += "\tVALUE: value to be assigned to the attribute of the target object\n"
+    help_string += "{opt_list}"
+    add_command("set",
+                help_string,
+                {})
+
+    # - get command
+    help_string  = "Get parameter of configuration object.\n\n"
+    help_string += "Usage: {prg} {cmd}{opts} TARGET PARAM\n\n"
+    help_string += "\tTARGET: configuration object being inspected\n"
+    help_string += "\tPARAM: parameter of the target object\n"
+    help_string += "{opt_list}"
+    add_command("get",
+                help_string,
+                {})
+
+    # - remove command
+    help_string  = "Remove configuration object or its attribute(s).\n\n"
+    help_string += "Usage: {prg} {cmd}{opts} TARGET [PARAM ...]\n\n"
+    help_string += "\tTARGET: configuration object\n"
+    help_string += "\tPARAM: attribute of the target object\n"
+    help_string += "{opt_list}"
+    add_command("remove",
+                help_string,
+                {})
+
+    # - dump command
+    help_string  = "Dump configuration.\n\n"
+    help_string += "Usage: {prg} {cmd}{opts} [TARGET]\n\n"
+    help_string += "\tTARGET: optional configuration object (if not specified,\n"
+    help_string += "\t        all configuration is dumped).\n"
+    help_string += "{opt_list}"
+    add_command("dump",
+                help_string,
+                {})
+
+    # - verify command
+    help_string  = "Verify configuration.\n\n"
+    help_string += "Usage: {prg} {cmd}{opts} [TARGET]\n\n"
+    help_string += "\tTARGET: optional configuration object (if not specified,\n"
+    help_string += "\t        all configuration is verified).\n"
+    help_string += "{opt_list}"
+    add_command("verify",
+                help_string,
+                {})
+
+    # - clean command
+    help_string  = "Clean configuration.\n\n"
+    help_string += "Usage: {prg} {cmd}{opts}\n"
+    help_string += "{opt_list}"
+    add_command("clean",
+                help_string,
+                {})
+
+    # parse command line
+
+    opts, cmd, cmd_opts, tgt, params = parse_cmd_line(sys.argv[1:])
+    debug("parse command line: options =", opts, "; command =", cmd, "; command options =", cmd_opts, "; target =", tgt, "; arguments =", params)
+
+    # process command
+
+    if not cmd or cmd == "help":
+        # show help and exit
+        usage(tgt)
+        pass
+
+    try:
+        # check if custom source file is specified
+        
+        src_file = opts["s"] if opts.has_key("s") else None
+        debug("source file:", src_file)
+        
+        # create config tool
+
+        cfg_tool = CfgTool(src_file)
+        debug("cfg tool:", cfg_tool)
+
+        # TODO: we should not check commands in a switch-like block below;
+        # instead processing should be done automatically basing on the
+        # predefined set of commands (TODO)
+
+        if cmd == "get":
+            if not tgt:
+                error_exit("%s: target is not specified!" % cmd)
+            if len(params) < 1:
+                error_exit("%s: parameter of target %s is not specified!" % (cmd, tgt))
+            # only one parameter can be read!
+            if len(params) > 1:
+                error_exit("%s: can't read more than one parameter at once!" % cmd)
+            def _toString(v):
+                if v is None: return ""
+                elif type(v) is list: return " ".join(v)
+                else: return v
+            print _toString(cfg_tool.get(tgt, params[0]))
+            pass
+        elif cmd == "set":
+            if not tgt:
+                error_exit("%s: target is not specified!" % cmd)
+            # empty parameters list is allowed!
+            cfg_tool.set(tgt, *params)
+            pass
+        elif cmd == "remove":
+            if not tgt:
+                error_exit("%s: target is not specified!" % cmd)
+            # empty parameters list is allowed!
+            cfg_tool.remove(tgt, *params)
+            pass
+        elif cmd == "dump":
+            if len(params) > 0:
+                error_exit("%s: command does not support parameters!" % cmd)
+            # empty target is allowed!
+            cfg_tool.dump(tgt)
+            pass
+        elif cmd == "verify":
+            if len(params) > 0:
+                error_exit("%s: command does not support parameters!" % cmd)
+            # empty target is allowed!
+            errors = []
+            if cfg_tool.verify(tgt, errors):
+                msg = " (no products found)" if not cfg_tool.get("cfg", "products") else ""
+                print "Configuration is valid%s." % msg
+            else:
+                raise Exception("Configuration is invalid:\n"+ "\n".join(errors))
+            pass
+        elif cmd == "clean":
+            if tgt:
+                error_exit("%s: command does not support target!" % cmd)
+            if len(params) > 0:
+                error_exit("%s: command does not support parameters!" % cmd)
+            # empty target is allowed!
+            cfg_tool.clean()
+            pass
+        else:
+            # unknown command: normally we should not go here
+            error_exit("unknown command: %s" % cmd)
+            pass
+        pass
+    except Exception, e:
+        error_exit(e)
+    pass
+    
+if __name__ == "__main__":
+    main()
diff --git a/config/scfgcmp b/config/scfgcmp
new file mode 100755 (executable)
index 0000000..43f97a7
--- /dev/null
@@ -0,0 +1,158 @@
+#!/usr/bin/env python
+#  -*- coding: iso-8859-1 -*-
+
+###
+# Command line tool to compare two SALOME configurations.
+# Usage: type "scfgcmp help" to learn how to use tool.
+###
+
+import sys
+import os
+from sconfig.salome_config import *
+import getopt
+
+def get_tool_name():
+    return os.path.basename(sys.argv[0])
+
+def error_exit(msg):
+    print "%s: %s" % (get_tool_name(), msg)
+    print "Try '%s --help' for more information." % get_tool_name()
+    sys.exit(2)
+    pass
+
+def usage():
+    print "Command line tool to compare two SALOME configurations."
+    print
+    print "Usage: %s [options] CFGFILE1 [CFGFILE2]" % get_tool_name()
+    print
+    print "  CFGFILE1       First configuration file to compare."
+    print "  CFGFILE2       Second configuration file to compare; if not specified,"
+    print "                 default one is used: %s." % defaultConfFile()
+    print
+    print "Options:"
+    print "  --help (-h)    Prints this help information."
+    print "  --force (-f)   Perform full comparation, including optional parameters."
+    print "  --verbose (-v) Print results of comparison to the screen."
+    print
+
+def main():
+    # parse command line
+
+    try:
+        opts, args = getopt.getopt(sys.argv[1:], "hfv", ["help", "force", "verbose"])
+    except getopt.GetoptError as err:
+        error_exit(str(err))
+        pass
+    opt_keys = [i[0] for i in opts]
+    isusage   = "-h" in opt_keys or "--help" in opt_keys
+    isverbose = "-v" in opt_keys or "--verbose" in opt_keys
+    isforce   = "-f" in opt_keys or "--force" in opt_keys
+
+    if isusage:
+        usage()
+        pass
+
+    cfgfile1 = args[0] if len(args) > 0 else defaultConfFile()
+    cfgfile2 = args[1] if len(args) > 1 else defaultConfFile()
+
+    if not cfgfile1:
+        error_exit("missing configuration file")
+        pass
+
+    if cfgfile1 and not os.path.isfile(cfgfile1):
+        error_exit("configuration file %s is not found" % cfgfile1)
+        pass
+
+    if cfgfile2 and not os.path.isfile(cfgfile2):
+        error_exit("configuration file %s is not found" % cfgfile2)
+        pass
+
+    if cfgfile1 and cfgfile2 and os.path.samefile(cfgfile1, cfgfile2):
+        error_exit("cannot compare the file with itself")
+        pass
+
+    # create config tools
+
+    try:
+        tool1 = CfgTool(cfgfile1)
+        tool2 = CfgTool(cfgfile2)
+    except Exception, e:
+        error_exit(str(e))
+        pass
+    
+    # diff config files
+
+    errors = []
+
+    def _cmpAttrs(_what, _where):
+        _attrs = tagAttributes(_what)
+        for _attr in _attrs:
+            try:
+                _v1 = tool1.get(_where, _attr)
+            except:
+                _v1 = None
+                pass
+            try:
+                _v2 = tool2.get(_where, _attr)
+            except:
+                _v2 = None
+                pass
+            if (isforce or _attrs[_attr]) and _v1 != _v2:
+                errors.append("attribute '%s' of '%s' differs: '%s' in %s vs '%s' in %s" % (_attr, _where, str(_v1), tool1.cfgFile, str(_v2), tool2.cfgFile))
+                pass
+            pass
+        pass
+    
+    # - compare parameters of configuration
+    _cmpAttrs(configTag(), configTag())
+
+    # - compare products
+
+    products1 = tool1.get(configTag(), softwareAlias())
+    products2 = tool2.get(configTag(), softwareAlias())
+    all_products = list(set(products1+products2))
+
+    for product in all_products:
+        if product in products1 and product not in products2:
+            errors.append("%s '%s' is only in '%s'" % (softwareTag(), product, tool1.cfgFile))
+            pass
+        elif product in products2 and product not in products1:
+            errors.append("%s '%s' is only in '%s'" % (softwareTag(), product, tool2.cfgFile))
+            pass
+        else:
+            # -- compare parameters of products
+            _cmpAttrs(softwareTag(), product)
+            # -- compare patches
+            patches1 = tool1.get(product, patchesAlias())
+            patches2 = tool2.get(product, patchesAlias())
+            all_patches = list(set(patches1+patches2))
+            for patch in all_patches:
+                if patch in patches1 and patch not in patches2:
+                    errors.append("%s '%s' for '%s' is only in '%s'" % (patchTag(), patch, product, tool1.cfgFile))
+                    pass
+                if patch in patches2 and patch not in patches1:
+                    errors.append("%s '%s' for '%s' is only in '%s'" % (patchTag(), patch, product, tool2.cfgFile))
+                    pass
+                else:
+                    # -- compare parameters of products
+                    _cmpAttrs(patchTag(), "%s.%s.%s" % (product, patchesTag(), patch))
+                    pass
+                pass
+            pass
+        pass
+    
+    # print results
+
+    if errors:
+        if isverbose:
+            print "Files %s and %s differ:" % (tool1.cfgFile, tool2.cfgFile)
+            print "\n".join(["  " + i for i in errors])
+            pass
+        return 1
+
+    return 0
+
+# -----------------------------------------------------------------------------
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/config/sconfig/__init__.py b/config/sconfig/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/config/sconfig/salome_config.py b/config/sconfig/salome_config.py
new file mode 100644 (file)
index 0000000..6105406
--- /dev/null
@@ -0,0 +1,772 @@
+#  -*- coding: iso-8859-1 -*-
+
+"""
+Manage SALOME configuration.
+
+Typical usage:
+
+tool = CfgTool()
+# Create configuration and set its parameters
+tool.set("cfg", "name", "V7_6_0", "comment", "SALOME version 7.6.0")
+# Add product
+tool.set("boost", "version", "1.52.2", "url", "https://sourceforge.net/projects/boost/files/boost/1.52.0/boost_1_52_0.tar.gz", "comment", "Set of libraries for the C++ programming language")
+# Add patches to the product (note: patch file should  be manually put to the patches directory)
+tool.set("boost.patches.boost_patch_1.patch", "comment", "Fixes compilation problems on some platforms")
+tool.set("boost.patches.boost_patch_2.patch", "comment", "gcc 5 compatibility")
+# Inspect configuration: give all products
+tool.get("cfg", "products")
+# Inspect configuration: give parameters of the configuration
+tool.get("cfg", "name")
+tool.get("cfg", "comment")
+# Inspect configuration: give parameters of the product
+tool.get("boost", "version")
+tool.get("boost", "url")
+# Inspect configuration: give patches for the product
+tool.get("boost", "patches")
+# Verify configuration
+conf_ok = tool.verify()
+# Verify product
+boost_ok = tool.verify("boost")
+# Dump configuration
+tool.dump()
+# Dump product
+tool.dump("boost")
+# Remove parameters of configuration
+tool.remove("cfg", "comment")
+# Remove parameters of product
+tool.remove("boost", "url")
+# Remove patch from product
+tool.remove("boost.patches.boost_patch_2.patch")
+# Remove product
+tool.remove("boost")
+# Clean configuration
+tool.clean()
+"""
+
+import os
+import xml.etree.ElementTree as ET
+try:
+    exceptionClass = ET.ParseError
+except:
+    import xml.parsers.expat
+    exceptionClass = xml.parsers.expat.ExpatError
+
+__all__ = [
+    "defaultConfFile",
+    "configTag",
+    "softwareTag",
+    "patchesTag",
+    "patchTag",
+    "nameAttr",
+    "commentAttr",
+    "versionAttr",
+    "urlAttr",
+    "supportedTags",
+    "supportedAttributes",
+    "tagAttributes",
+    "tagChildren",
+    "softwareAlias",
+    "patchesAlias",
+    "childAlias",
+    "pathSeparator",
+    "CfgTool",
+ ]
+
+def defaultConfFile():
+    """
+    Return path to the default SALOME configuration file (string).
+    """
+    return os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "salome.xml"))
+
+def configTag():
+    """
+    Return XML tag for configuration (string).
+    """
+    return "config"
+
+def softwareTag():
+    """
+    Return XML tag for software (string).
+    """
+    return "product"
+
+def patchesTag():
+    """
+    Return XML tag for patches set (string).
+    """
+    return "patches"
+    
+def patchTag():
+    """
+    Return XML tag for patch (string).
+    """
+    return "patch"
+
+def nameAttr():
+    """
+    Return XML attribute for name parameter (string).
+    """
+    return "name"
+
+def commentAttr():
+    """
+    Return XML attribute for comment parameter (string).
+    """
+    return "comment"
+
+def versionAttr():
+    """
+    Return XML attribute for version parameter (string).
+    """
+    return "version"
+
+def urlAttr():
+    """
+    Return XML attribute for url parameter (string).
+    """
+    return "url"
+
+def supportedTags():
+    """
+    Return list of all supported XML tags (list of strings).
+    """
+    return [configTag(), softwareTag(), patchesTag(), patchTag()]
+    
+def supportedAttributes():
+    """
+    Return list of all supported XML attributes (list of strings).
+    """
+    return [nameAttr(), commentAttr(), versionAttr(), urlAttr()]
+    
+def tagAttributes(tag, force = False):
+    """
+    Return list of attributes supported for the specified XML tag.
+    
+    Parameters:
+      tag: XML tag.
+      force: if True, all supported attributes are returned, including "special" ones.
+
+    Return value is list of strings.
+    """
+    attrs = {}
+    if tag == configTag():
+        # config
+        attrs[nameAttr()]     = False  # optional
+        attrs[commentAttr()]  = False  # optional
+        pass
+    elif tag == softwareTag():
+        # software
+        # note: name for software is specified implicitly via the target path
+        if force:
+            attrs[nameAttr()] = True   # mandatory
+            pass
+        attrs[versionAttr()]  = True   # mandatory
+        attrs[urlAttr()]      = False  # optional
+        attrs[commentAttr()]  = False  # optional
+        pass
+    elif tag == patchTag():
+        # patch
+        # note: name for patch is specified implicitly via the target path
+        if force:
+            attrs[nameAttr()] = True   # mandatory
+            pass
+        pass
+        attrs[urlAttr()]      = False
+        attrs[commentAttr()]  = False
+    return attrs
+
+def tagChildren(tag):
+    """
+    Return supported child nodes' tags for given XML element.
+    Note: None means root 'config' XML element.
+    
+    Parameters:
+      tag: XML tag.
+
+    Return value is list of strings.
+    """
+    ctags = []
+    if tag == configTag():     ctags += [softwareTag()]
+    elif tag == softwareTag(): ctags += [patchesTag()]
+    elif tag == patchesTag():  ctags += [patchTag()]
+    elif tag is None:          ctags += [configTag()]
+    return ctags
+
+def softwareAlias():
+    """
+    Return parameter's alias for list of software are to be used with 'get' command (string).
+    """
+    return softwareTag()+"s"
+
+def patchesAlias():
+    """
+    Return parameter's alias for list patches to be used with 'get' command (string).
+    """
+    return patchesTag()
+
+def childAlias(tag, param):
+    """
+    Return children node tag for children list alias.
+    
+    Parameters:
+      tag: XML tag.
+      param: children list alias.
+
+    Return child node tag name or None if alias is unsupported.
+    """
+    ctag = None
+    if tag == configTag():
+        if param == softwareAlias(): ctag = softwareTag()
+        pass
+    elif tag == softwareTag():
+        if param == patchesAlias(): ctag = patchTag()
+        pass
+    return ctag
+
+def pathSeparator():
+    """
+    Return string used as a separator of path's component (string).
+    """
+    return "."
+
+class CfgTool(object):
+    """
+    A tool to manage SALOME configuration files.
+    """
+    def __init__(self, cfgFile=None):
+        """
+        Constructor.
+        
+        Parameters:
+          cfgFile: a path to the configuration file (string);
+                   if not specified, default one is used.
+        """
+        self.enc = "utf-8"
+        self.cfgFile = cfgFile if cfgFile else defaultConfFile()
+        try:
+            self.tree = ET.parse(self.cfgFile).getroot()
+            self._checkConfig()
+            pass
+        except IOError, e:
+            self.tree = self._new()
+            pass
+        except exceptionClass, e:
+            if e.code == 3: # no element found, it's OK
+                self.tree = self._new()
+            else:
+                raise Exception("bad XML file %s: %s" % (self.cfgFile, str(e)))
+            pass
+        except Exception, e:
+            raise Exception("unkwnown error: %s" % str(e))
+        pass
+    
+    def encoding(self):
+        """
+        Return current encoding of the configuration file (string).
+        Default is "utf-8".
+        """
+        return self.enc
+    
+    def setEncoding(self, value):
+        """
+        Set encoding for configuration file..
+        Parameters:
+          value: new encoding to be used when writing configuration file (string).
+        """
+        self.enc = value
+        self._write()
+        pass
+
+    def get(self, target, param):
+        """
+        Get value of specified object's parameter.
+        Parameter can be a specific keyword that refers to the list of
+        child nodes. In this case the function returns list that
+        contains names of all child nodes.
+
+        Parameters:
+          target: object being checked (string).
+          param: parameter which value is being inspected.
+         
+        Return value is string or list of strings.
+        """
+        path = self._processTarget(target)
+        tag = path[-1][0]
+        elem = self._findPath(path)
+        if elem is None:
+            raise Exception("no such target %s" % target)
+        result = None
+        if childAlias(tag, param):
+            result = self._children(elem, childAlias(tag, param))
+            pass
+        elif param in tagAttributes(tag):
+            result = elem.get(param) if elem is not None and elem.get(param) else ""
+            pass
+        else:
+            raise Exception("unsupported parameter %s for target %s" % (param, target))
+        return result
+
+    def set(self, target = None, *args, **kwargs):
+        """
+        Create or modify an object in the SALOME configuration.
+
+        Parameters:
+          target: object being created or modified (string); if not specified,
+                  parameters of config itself will be modified.
+          args:   positional arguments that describe parameters to be set (couple);
+                  each couple of arguments specifies a parameter and its value
+                  (strings).
+          kwargs: keyword arguments - same as 'args' but specified in form of
+                  dictionary.
+        """
+        path = self._processTarget(target)
+        tag = path[-1][0]
+        params = {}
+        # process keyword arguments
+        for param, value in kwargs.items():
+            if param not in tagAttributes(tag):
+                raise Exception("unsupported parameter %s for target %s" % (param, target))
+            params[param] = value
+            pass
+        # process positional arguments
+        i = 0
+        while i < len(args):
+            param = args[i]
+            if param not in tagAttributes(tag):
+                raise Exception("unsupported parameter %s for target %s" % (param, target))
+            value = ""
+            if i+1 < len(args) and args[i+1] not in tagAttributes(tag):
+                value = args[i+1]
+                i += 1
+                pass
+            params[param] = value
+            i += 1
+            pass
+        # create / modify target
+        elem = self._findPath(path, True)
+        for param, value in params.items():
+            elem.set(param, value)
+            pass
+        self._write()
+        pass
+
+    def remove(self, target, *args):
+        """
+        Remove object or its parameter(s).
+
+        Parameters:
+          target: object (string).
+          args: list of parameters which have to be removed (strings).
+         
+        Return value is string.
+        """
+        path = self._processTarget(target)
+        tag = path[-1][0]
+        elem = self._findPath(path)
+        if elem is None:
+            raise Exception("no such target %s" % target)
+        if args:
+            # remove attributes of the target
+            # first check that all attributes are valid
+            for param in args:
+                if param not in tagAttributes(tag):
+                    raise Exception("unsupported parameter %s for target %s" % (param, target))
+                elif param not in elem.attrib:
+                    raise Exception("parameter %s is not set for target %s" % (param, target))
+                pass
+            # now remove all attributes
+            for param in args:
+                elem.attrib.pop(param)
+                pass
+            pass
+        else:
+            # remove target
+            if elem == self.tree:
+                self.tree = self._new()
+                pass
+            else:
+                path = path[:-1]
+                parent = self._findPath(path)
+                if parent is not None: parent.remove(elem)
+                pass
+            pass
+        self._write()
+        pass
+    
+    def dump(self, target = None):
+        """
+        Dump the configuration.
+        
+        Parameters:
+          target: object (string); if not specified, all configuration is dumped.
+        """
+        if target is not None:
+            path = self._processTarget(target)
+            elem = self._findPath(path)
+            if elem is None:
+                raise Exception("no such target %s" % target)
+            pass
+        else:
+            elem = self.tree
+            pass
+        self._dump(elem)
+        pass
+
+    def verify(self, target = None, errors = None):
+        """
+        Verify configuration
+        
+        Parameters:
+          target: object (string); if not specified, all configuration is verified.
+
+        Returns True if object is valid or False otherwise.
+        """
+        if errors is None: errors = []
+        if target is not None:
+            path = self._processTarget(target)
+            elem = self._findPath(path)
+            if elem is None:
+                raise Exception("no such target %s" % target)
+            pass
+        else:
+            elem = self.tree
+            pass
+        return self._verifyTag(elem, errors)
+
+    def clean(self):
+        """
+        Clean the configuration.
+        """
+        self.tree = self._new()
+        self._write()
+        pass
+    
+    def patchesDir(self):
+        """
+        Return path to the patches directory (string).
+        """
+        return os.path.join(os.path.dirname(self.cfgFile), "patches")
+
+    def _new(self):
+        """
+        (internal)
+        Create and return new empty root element.
+        
+        Return values is an XML element (xml.etree.ElementTree.Element).
+        """
+        return ET.Element(configTag())
+
+    def _makeChild(self, elem, tag):
+        """
+        (internal)
+        Create child element for given parent element.
+
+        Parameters:
+          elem: XML element (xml.etree.ElementTree.Element).
+          tag: tag of the child element
+
+        Return value is new XML element (xml.etree.ElementTree.Element).
+        """
+        child = ET.SubElement(elem, tag)
+        child._parent = elem # set parent!!!
+        return child
+
+    def _processTarget(self, target):
+        """
+        (internal)
+        Check target and return XML path for it.
+
+        Parameters:
+          target: target path.
+          
+        Return value is a list of tuples; each tuple is a couple
+        of path component and optional component's name.
+        """
+        if target is None: target = ""
+        comps = [i.strip() for i in target.split(pathSeparator())]
+        path = []
+        # add root to the path
+        path.append((configTag(), None))
+        if comps[0] in ["", "cfg", configTag()]: comps = comps[1:]
+        if comps:
+            # second component of path can be only "software"
+            if not comps[0] or comps[0] in supportedTags() + supportedAttributes() + ["cfg"]:
+                raise Exception("badly specified target '%s'" % target)
+            path.append((softwareTag(), comps[0]))
+            comps = comps[1:]
+            pass
+        if comps:
+            # third component of path can be only "patches" or patch
+            if comps[0] not in [patchesTag(), patchTag()]:
+                raise Exception("badly specified target '%s'" % target)
+            path.append((patchesTag(), None))
+            comps = comps[1:]
+            pass
+        if comps:
+            # fourth component of path can be only a patch name
+            path.append((patchTag(), pathSeparator().join(comps)))
+            pass
+        return path
+    
+    def _findPath(self, path, create=False):
+        """
+        (internal)
+        Find and return XML element specified by its path.
+        If path does not exist and 'create' is True, XML element will be created.
+
+        Parameters:
+          path: XML element's path data (see _processTarget()).
+          create: flag that forces creating XML element if it does not exist
+                  (default is False).
+
+        Return value is an XML element (xml.etree.ElementTree.Element).
+        """
+        if len(path) == 1:
+            if path[0][0] != configTag():
+                raise Exception("error parsing target path")
+            return self.tree
+        elem = self.tree
+        for tag, name in path[1:]:
+            if name:
+                children = filter(lambda i: i.tag == tag and i.get(nameAttr()) == name, elem.getchildren())
+                if len(children) > 1:
+                    raise Exception("error parsing target path: more than one child element found")
+                elif len(children) == 1:
+                    elem = children[0]
+                    pass
+                elif create:
+                    elem = self._makeChild(elem, tag)
+                    elem.set(nameAttr(), name)
+                    pass
+                else:
+                    return None
+                pass
+            else:
+                children = filter(lambda i: i.tag == tag, elem.getchildren())
+                if len(children) > 1:
+                    raise Exception("error parsing target path: more than one child element found")
+                elif len(children) == 1:
+                    elem = children[0]
+                    pass
+                elif create:
+                    elem = self._makeChild(elem, tag)
+                    pass
+                else:
+                    return None
+                pass
+            pass
+        return elem
+
+    def _path(self, elem):
+        """
+        (internal)
+        Construct path to the XML element.
+
+        Parameters:
+          elem: XML element (xml.etree.ElementTree.Element).
+
+        Return value is string.
+        """
+        def _mkname(_obj):
+            _name = _obj.tag
+            attrs = tagAttributes(_obj.tag, True)
+            if nameAttr() in attrs and attrs[nameAttr()]:
+                if nameAttr() not in _obj.keys(): _name += " [unnamed]"
+                else: _name += " [%s]" % _obj.get(nameAttr())
+                pass
+            return _name
+        path = []
+        while elem is not None:
+            path.append(_mkname(elem))
+            elem = elem._parent if hasattr(elem, "_parent") else None
+            pass
+        path.reverse()
+        return pathSeparator().join(path)
+
+    def _children(self, elem, param):
+        """
+        (internal)
+        Get names of children nodes for element.
+
+        Parameters:
+          elem: XML element (xml.etree.ElementTree.Element).
+          param: name of the children' tag.
+
+        Return value is a list of names of child elements (strings).
+        """
+        result = []
+        result += [i.get(nameAttr()) for i in \
+                       filter(lambda i: i.tag == param and i.get(nameAttr()), elem.getchildren())]
+        for c in elem.getchildren():
+            result += self._children(c, param)
+            pass
+        return result
+        
+    def _write(self):
+        """
+        (internal)
+        Write data tree content to the associated XML file.
+        """
+        try:
+            with open(self.cfgFile, 'w') as f:
+                # write header
+                f.write('<?xml version="1.0" encoding="%s" ?>\n' % self.encoding() )
+                f.write('<!DOCTYPE config>\n')
+                # prettify content
+                self._prettify(self.tree)
+                # write content
+                et = ET.ElementTree(self.tree)
+                et.write(f, self.encoding())
+                pass
+            pass
+        except IOError, e:
+            raise Exception("can't write to %s: %s" % (self.cfgFile, e.strerror))
+        pass
+
+    def _prettify(self, elem, level=0, hasSiblings=False):
+        """
+        (internal)
+        Prettify XML file content.
+
+        Parameters:
+          elem: XML element (xml.etree.ElementTree.Element).
+          level: indentation level.
+          hasSiblings: True when item has siblings (i.e. this is not the last item
+             in the parent's children list).
+        """
+        indent = "  "
+        children = elem.getchildren()
+        tail = "\n"
+        if hasSiblings: tail += indent * level
+        elif level > 0: tail += indent * (level-1)
+        text = None
+        if children: text = "\n" + indent * (level+1)
+        elem.tail = tail
+        elem.text = text
+        for i in range(len(children)):
+            self._prettify(children[i], level+1, len(children)>1 and i+1<len(children))
+            pass
+        pass
+
+    def _dump(self, elem, level=0):
+        """
+        (internal)
+        Dump XML element.
+
+        Parameters:
+          elem: XML element (xml.etree.ElementTree.Element).
+          level: indentation level.
+        """
+        if elem is None:
+            return
+        indent = "  "
+        # dump element
+        print "%s%s" % (indent * level, elem.tag)
+        attrs = tagAttributes(elem.tag, True)
+        format = "%" + "-%ds" % max([len(i) for i in supportedAttributes()]) + " : %s"
+        for a in attrs:
+            if a in elem.attrib.keys():
+                print indent*(level+1) + format % (a, elem.get(a))
+                pass
+            pass
+        print
+        # dump all childrens recursively
+        for c in elem.getchildren():
+            self._dump(c, level+1)
+            pass
+        pass
+
+    def _checkConfig(self):
+        """
+        (internal)
+        Verify configuration (used to check validity of associated XML file).
+        """
+        errors = []
+        self._checkTag(self.tree, None, errors)
+        if errors:
+            errors = ["Bad XML format:"] + ["- %s" % i for i in errors]
+            raise Exception("\n".join(errors))
+        pass
+
+    def _checkTag(self, elem, tag, errors):
+        """
+        (internal)
+        Check if format of given XML element is valid.
+        
+        Parameters:
+          elem: XML element (xml.etree.ElementTree.Element).
+          tag: expected XML element's tag (string).
+          errors: output list to collect error messages (strings).
+        """
+        if elem.tag not in tagChildren(tag):
+            errors.append("bad XML element: %s" % elem.tag)
+        else:
+            # check attributes
+            attrs = elem.keys()
+            for attr in attrs:
+                if attr not in tagAttributes(elem.tag, True):
+                    errors.append("unsupported attribute '%s' for XML element '%s'" % (attr, elem.tag))
+                    pass
+                pass
+            # check all childrens recursively
+            children = elem.getchildren()
+            for child in children:
+                child._parent = elem # set parent!!!
+                self._checkTag(child, elem.tag, errors)
+                pass
+            pass
+        pass
+
+    def _verifyTag(self, elem, errors):
+        """
+        (internal)
+        Verify given XML element is valid in terms of SALOME configuration.
+        
+        Parameters:
+          elem: XML element (xml.etree.ElementTree.Element).
+          errors: output list to collect error messages (strings).
+        """
+        attrs = tagAttributes(elem.tag, True)
+        # check mandatory attributes
+        for attr in attrs:
+            if attrs[attr] and (attr not in elem.keys() or not elem.get(attr).strip()):
+                errors.append("mandatory parameter '%s' of object '%s' is not set" % (attr, self._path(elem)))
+                pass
+            pass
+        # specific check for particular XML element
+        try:
+            self._checkObject(elem)
+        except Exception, e:
+            errors.append("%s : %s" % (self._path(elem), str(e)))
+        # check all childrens recursively
+        for c in elem.getchildren():
+            self._verifyTag(c, errors)
+            pass
+        return len(errors) == 0
+
+    def _checkObject(self, elem):
+        """
+        (internal)
+        Perform specific check for given XML element.
+        
+        Raises an exception that if object is invalid.
+        
+        Parameters:
+          elem: XML element (xml.etree.ElementTree.Element).
+        """
+        if elem.tag == patchTag():
+            filename = elem.get(nameAttr())
+            url = elem.get(urlAttr())
+            if filename and not url:
+                # if url is not given, we should check that file is present locally
+                filepath = os.path.join(self.patchesDir(), filename)
+                if not os.path.exists(filepath):
+                    raise Exception("patch file %s is not found" % filepath)
+                pass
+            else:
+                # TODO: we might check validity of URL here (see urlparse)!
+                pass
+            pass
+        pass
+
+    pass # class CfgTool