Salome HOME
Copyright update 2020
[tools/configuration.git] / config / sconfig / salome_config.py
1 #  -*- coding: iso-8859-1 -*-
2 # Copyright (C) 2016-2020  OPEN CASCADE
3 #
4 # This library is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU Lesser General Public
6 # License as published by the Free Software Foundation; either
7 # version 2.1 of the License, or (at your option) any later version.
8 #
9 # This library is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12 # Lesser General Public License for more details.
13 #
14 # You should have received a copy of the GNU Lesser General Public
15 # License along with this library; if not, write to the Free Software
16 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
17 #
18 # See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
19 #
20
21 """
22 Manage SALOME configuration.
23
24 Typical usage:
25
26 tool = CfgTool()
27 # Create configuration and set its parameters
28 tool.set("cfg", "name", "V7_6_0", "comment", "SALOME version 7.6.0")
29 # Add product
30 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")
31 # Add patches to the product (note: patch file should  be manually put to the patches directory)
32 tool.set("boost.patches.boost_patch_1.patch", "comment", "Fixes compilation problems on some platforms")
33 tool.set("boost.patches.boost_patch_2.patch", "comment", "gcc 5 compatibility")
34 # Inspect configuration: give all products
35 tool.get("cfg", "products")
36 # Inspect configuration: give parameters of the configuration
37 tool.get("cfg", "name")
38 tool.get("cfg", "comment")
39 # Inspect configuration: give parameters of the product
40 tool.get("boost", "version")
41 tool.get("boost", "url")
42 # Inspect configuration: give patches for the product
43 tool.get("boost", "patches")
44 # Verify configuration
45 conf_ok = tool.verify()
46 # Verify product
47 boost_ok = tool.verify("boost")
48 # Dump configuration
49 tool.dump()
50 # Dump product
51 tool.dump("boost")
52 # Remove parameters of configuration
53 tool.remove("cfg", "comment")
54 # Remove parameters of product
55 tool.remove("boost", "url")
56 # Remove patch from product
57 tool.remove("boost.patches.boost_patch_2.patch")
58 # Remove product
59 tool.remove("boost")
60 # Clean configuration
61 tool.clean()
62 """
63
64 import os
65 import xml.etree.ElementTree as ET
66 try:
67     exceptionClass = ET.ParseError
68 except:
69     import xml.parsers.expat
70     exceptionClass = xml.parsers.expat.ExpatError
71
72 __all__ = [
73     "defaultConfFile",
74     "configTag",
75     "softwareTag",
76     "patchesTag",
77     "patchTag",
78     "nameAttr",
79     "commentAttr",
80     "versionAttr",
81     "urlAttr",
82     "supportedTags",
83     "supportedAttributes",
84     "tagAttributes",
85     "tagChildren",
86     "softwareAlias",
87     "patchesAlias",
88     "childAlias",
89     "pathSeparator",
90     "CfgTool",
91  ]
92
93 def defaultConfFile():
94     """
95     Return path to the default SALOME configuration file (string).
96     """
97     return os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "salome.xml"))
98
99 def configTag():
100     """
101     Return XML tag for configuration (string).
102     """
103     return "config"
104
105 def softwareTag():
106     """
107     Return XML tag for software (string).
108     """
109     return "product"
110
111 def patchesTag():
112     """
113     Return XML tag for patches set (string).
114     """
115     return "patches"
116     
117 def patchTag():
118     """
119     Return XML tag for patch (string).
120     """
121     return "patch"
122
123 def nameAttr():
124     """
125     Return XML attribute for name parameter (string).
126     """
127     return "name"
128
129 def commentAttr():
130     """
131     Return XML attribute for comment parameter (string).
132     """
133     return "comment"
134
135 def versionAttr():
136     """
137     Return XML attribute for version parameter (string).
138     """
139     return "version"
140
141 def urlAttr():
142     """
143     Return XML attribute for url parameter (string).
144     """
145     return "url"
146
147 def supportedTags():
148     """
149     Return list of all supported XML tags (list of strings).
150     """
151     return [configTag(), softwareTag(), patchesTag(), patchTag()]
152     
153 def supportedAttributes():
154     """
155     Return list of all supported XML attributes (list of strings).
156     """
157     return [nameAttr(), commentAttr(), versionAttr(), urlAttr()]
158     
159 def tagAttributes(tag, force = False):
160     """
161     Return list of attributes supported for the specified XML tag.
162     
163     Parameters:
164       tag: XML tag.
165       force: if True, all supported attributes are returned, including "special" ones.
166
167     Return value is list of strings.
168     """
169     attrs = {}
170     if tag == configTag():
171         # config
172         attrs[nameAttr()]     = False  # optional
173         attrs[commentAttr()]  = False  # optional
174         pass
175     elif tag == softwareTag():
176         # software
177         # note: name for software is specified implicitly via the target path
178         if force:
179             attrs[nameAttr()] = True   # mandatory
180             pass
181         attrs[versionAttr()]  = True   # mandatory
182         attrs[urlAttr()]      = False  # optional
183         attrs[commentAttr()]  = False  # optional
184         pass
185     elif tag == patchTag():
186         # patch
187         # note: name for patch is specified implicitly via the target path
188         if force:
189             attrs[nameAttr()] = True   # mandatory
190             pass
191         pass
192         attrs[urlAttr()]      = False
193         attrs[commentAttr()]  = False
194     return attrs
195
196 def tagChildren(tag):
197     """
198     Return supported child nodes' tags for given XML element.
199     Note: None means root 'config' XML element.
200     
201     Parameters:
202       tag: XML tag.
203
204     Return value is list of strings.
205     """
206     ctags = []
207     if tag == configTag():     ctags += [softwareTag()]
208     elif tag == softwareTag(): ctags += [patchesTag()]
209     elif tag == patchesTag():  ctags += [patchTag()]
210     elif tag is None:          ctags += [configTag()]
211     return ctags
212
213 def softwareAlias():
214     """
215     Return parameter's alias for list of software are to be used with 'get' command (string).
216     """
217     return softwareTag()+"s"
218
219 def patchesAlias():
220     """
221     Return parameter's alias for list patches to be used with 'get' command (string).
222     """
223     return patchesTag()
224
225 def childAlias(tag, param):
226     """
227     Return children node tag for children list alias.
228     
229     Parameters:
230       tag: XML tag.
231       param: children list alias.
232
233     Return child node tag name or None if alias is unsupported.
234     """
235     ctag = None
236     if tag == configTag():
237         if param == softwareAlias(): ctag = softwareTag()
238         pass
239     elif tag == softwareTag():
240         if param == patchesAlias(): ctag = patchTag()
241         pass
242     return ctag
243
244 def pathSeparator():
245     """
246     Return string used as a separator of path's component (string).
247     """
248     return "."
249
250 class CfgTool(object):
251     """
252     A tool to manage SALOME configuration files.
253     """
254     def __init__(self, cfgFile=None):
255         """
256         Constructor.
257         
258         Parameters:
259           cfgFile: a path to the configuration file (string);
260                    if not specified, default one is used.
261         """
262         self.enc = "utf-8"
263         self.cfgFile = cfgFile if cfgFile else defaultConfFile()
264         try:
265             self.tree = ET.parse(self.cfgFile).getroot()
266             self._checkConfig()
267             pass
268         except IOError as e:
269             self.tree = self._new()
270             pass
271         except exceptionClass as e:
272             if e.code == 3: # no element found, it's OK
273                 self.tree = self._new()
274             else:
275                 raise Exception("bad XML file %s: %s" % (self.cfgFile, str(e)))
276             pass
277         except Exception as e:
278             raise Exception("unkwnown error: %s" % str(e))
279         pass
280     
281     def encoding(self):
282         """
283         Return current encoding of the configuration file (string).
284         Default is "utf-8".
285         """
286         return self.enc
287     
288     def setEncoding(self, value):
289         """
290         Set encoding for configuration file..
291         Parameters:
292           value: new encoding to be used when writing configuration file (string).
293         """
294         self.enc = value
295         self._write()
296         pass
297
298     def get(self, target, param):
299         """
300         Get value of specified object's parameter.
301         Parameter can be a specific keyword that refers to the list of
302         child nodes. In this case the function returns list that
303         contains names of all child nodes.
304
305         Parameters:
306           target: object being checked (string).
307           param: parameter which value is being inspected.
308          
309         Return value is string or list of strings.
310         """
311         path = self._processTarget(target)
312         tag = path[-1][0]
313         elem = self._findPath(path)
314         if elem is None:
315             raise Exception("no such target %s" % target)
316         result = None
317         if childAlias(tag, param):
318             result = self._children(elem, childAlias(tag, param))
319             pass
320         elif param in tagAttributes(tag):
321             result = elem.get(param) if elem is not None and elem.get(param) else ""
322             pass
323         else:
324             raise Exception("unsupported parameter %s for target %s" % (param, target))
325         return result
326
327     def set(self, target = None, *args, **kwargs):
328         """
329         Create or modify an object in the SALOME configuration.
330
331         Parameters:
332           target: object being created or modified (string); if not specified,
333                   parameters of config itself will be modified.
334           args:   positional arguments that describe parameters to be set (couple);
335                   each couple of arguments specifies a parameter and its value
336                   (strings).
337           kwargs: keyword arguments - same as 'args' but specified in form of
338                   dictionary.
339         """
340         path = self._processTarget(target)
341         tag = path[-1][0]
342         params = {}
343         # process keyword arguments
344         for param, value in list(kwargs.items()):
345             if param not in tagAttributes(tag):
346                 raise Exception("unsupported parameter %s for target %s" % (param, target))
347             params[param] = value
348             pass
349         # process positional arguments
350         i = 0
351         while i < len(args):
352             param = args[i]
353             if param not in tagAttributes(tag):
354                 raise Exception("unsupported parameter %s for target %s" % (param, target))
355             value = ""
356             if i+1 < len(args) and args[i+1] not in tagAttributes(tag):
357                 value = args[i+1]
358                 i += 1
359                 pass
360             params[param] = value
361             i += 1
362             pass
363         # create / modify target
364         elem = self._findPath(path, True)
365         for param, value in list(params.items()):
366             elem.set(param, value)
367             pass
368         self._write()
369         pass
370
371     def remove(self, target, *args):
372         """
373         Remove object or its parameter(s).
374
375         Parameters:
376           target: object (string).
377           args: list of parameters which have to be removed (strings).
378          
379         Return value is string.
380         """
381         path = self._processTarget(target)
382         tag = path[-1][0]
383         elem = self._findPath(path)
384         if elem is None:
385             raise Exception("no such target %s" % target)
386         if args:
387             # remove attributes of the target
388             # first check that all attributes are valid
389             for param in args:
390                 if param not in tagAttributes(tag):
391                     raise Exception("unsupported parameter %s for target %s" % (param, target))
392                 elif param not in elem.attrib:
393                     raise Exception("parameter %s is not set for target %s" % (param, target))
394                 pass
395             # now remove all attributes
396             for param in args:
397                 elem.attrib.pop(param)
398                 pass
399             pass
400         else:
401             # remove target
402             if elem == self.tree:
403                 self.tree = self._new()
404                 pass
405             else:
406                 path = path[:-1]
407                 parent = self._findPath(path)
408                 if parent is not None: parent.remove(elem)
409                 pass
410             pass
411         self._write()
412         pass
413     
414     def dump(self, target = None):
415         """
416         Dump the configuration.
417         
418         Parameters:
419           target: object (string); if not specified, all configuration is dumped.
420         """
421         if target is not None:
422             path = self._processTarget(target)
423             elem = self._findPath(path)
424             if elem is None:
425                 raise Exception("no such target %s" % target)
426             pass
427         else:
428             elem = self.tree
429             pass
430         self._dump(elem)
431         pass
432
433     def verify(self, target = None, errors = None):
434         """
435         Verify configuration
436         
437         Parameters:
438           target: object (string); if not specified, all configuration is verified.
439
440         Returns True if object is valid or False otherwise.
441         """
442         if errors is None: errors = []
443         if target is not None:
444             path = self._processTarget(target)
445             elem = self._findPath(path)
446             if elem is None:
447                 raise Exception("no such target %s" % target)
448             pass
449         else:
450             elem = self.tree
451             pass
452         return self._verifyTag(elem, errors)
453
454     def clean(self):
455         """
456         Clean the configuration.
457         """
458         self.tree = self._new()
459         self._write()
460         pass
461     
462     def patchesDir(self):
463         """
464         Return path to the patches directory (string).
465         """
466         return os.path.join(os.path.dirname(self.cfgFile), "patches")
467
468     def _new(self):
469         """
470         (internal)
471         Create and return new empty root element.
472         
473         Return values is an XML element (xml.etree.ElementTree.Element).
474         """
475         return ET.Element(configTag())
476
477     def _makeChild(self, elem, tag):
478         """
479         (internal)
480         Create child element for given parent element.
481
482         Parameters:
483           elem: XML element (xml.etree.ElementTree.Element).
484           tag: tag of the child element
485
486         Return value is new XML element (xml.etree.ElementTree.Element).
487         """
488         child = ET.SubElement(elem, tag)
489         child._parent = elem # set parent!!!
490         return child
491
492     def _processTarget(self, target):
493         """
494         (internal)
495         Check target and return XML path for it.
496
497         Parameters:
498           target: target path.
499           
500         Return value is a list of tuples; each tuple is a couple
501         of path component and optional component's name.
502         """
503         if target is None: target = ""
504         comps = [i.strip() for i in target.split(pathSeparator())]
505         path = []
506         # add root to the path
507         path.append((configTag(), None))
508         if comps[0] in ["", "cfg", configTag()]: comps = comps[1:]
509         if comps:
510             # second component of path can be only "software"
511             if not comps[0] or comps[0] in supportedTags() + supportedAttributes() + ["cfg"]:
512                 raise Exception("badly specified target '%s'" % target)
513             path.append((softwareTag(), comps[0]))
514             comps = comps[1:]
515             pass
516         if comps:
517             # third component of path can be only "patches" or patch
518             if comps[0] not in [patchesTag(), patchTag()]:
519                 raise Exception("badly specified target '%s'" % target)
520             path.append((patchesTag(), None))
521             comps = comps[1:]
522             pass
523         if comps:
524             # fourth component of path can be only a patch name
525             path.append((patchTag(), pathSeparator().join(comps)))
526             pass
527         return path
528     
529     def _findPath(self, path, create=False):
530         """
531         (internal)
532         Find and return XML element specified by its path.
533         If path does not exist and 'create' is True, XML element will be created.
534
535         Parameters:
536           path: XML element's path data (see _processTarget()).
537           create: flag that forces creating XML element if it does not exist
538                   (default is False).
539
540         Return value is an XML element (xml.etree.ElementTree.Element).
541         """
542         if len(path) == 1:
543             if path[0][0] != configTag():
544                 raise Exception("error parsing target path")
545             return self.tree
546         elem = self.tree
547         for tag, name in path[1:]:
548             if name:
549                 children = [i for i in elem.getchildren() if i.tag == tag and i.get(nameAttr()) == name]
550                 if len(children) > 1:
551                     raise Exception("error parsing target path: more than one child element found")
552                 elif len(children) == 1:
553                     elem = children[0]
554                     pass
555                 elif create:
556                     elem = self._makeChild(elem, tag)
557                     elem.set(nameAttr(), name)
558                     pass
559                 else:
560                     return None
561                 pass
562             else:
563                 children = [i for i in elem.getchildren() if i.tag == tag]
564                 if len(children) > 1:
565                     raise Exception("error parsing target path: more than one child element found")
566                 elif len(children) == 1:
567                     elem = children[0]
568                     pass
569                 elif create:
570                     elem = self._makeChild(elem, tag)
571                     pass
572                 else:
573                     return None
574                 pass
575             pass
576         return elem
577
578     def _path(self, elem):
579         """
580         (internal)
581         Construct path to the XML element.
582
583         Parameters:
584           elem: XML element (xml.etree.ElementTree.Element).
585
586         Return value is string.
587         """
588         def _mkname(_obj):
589             _name = _obj.tag
590             attrs = tagAttributes(_obj.tag, True)
591             if nameAttr() in attrs and attrs[nameAttr()]:
592                 if nameAttr() not in list(_obj.keys()): _name += " [unnamed]"
593                 else: _name += " [%s]" % _obj.get(nameAttr())
594                 pass
595             return _name
596         path = []
597         while elem is not None:
598             path.append(_mkname(elem))
599             elem = elem._parent if hasattr(elem, "_parent") else None
600             pass
601         path.reverse()
602         return pathSeparator().join(path)
603
604     def _children(self, elem, param):
605         """
606         (internal)
607         Get names of children nodes for element.
608
609         Parameters:
610           elem: XML element (xml.etree.ElementTree.Element).
611           param: name of the children' tag.
612
613         Return value is a list of names of child elements (strings).
614         """
615         result = []
616         result += [i.get(nameAttr()) for i in \
617                        [i for i in elem.getchildren() if i.tag == param and i.get(nameAttr())]]
618         for c in elem.getchildren():
619             result += self._children(c, param)
620             pass
621         return result
622         
623     def _write(self):
624         """
625         (internal)
626         Write data tree content to the associated XML file.
627         """
628         try:
629             with open(self.cfgFile, 'w') as f:
630                 # write header
631                 f.write('<?xml version="1.0" encoding="%s" ?>\n' % self.encoding() )
632                 f.write('<!DOCTYPE config>\n')
633                 # prettify content
634                 self._prettify(self.tree)
635                 # write content
636                 et = ET.ElementTree(self.tree)
637                 et.write(f, self.encoding())
638                 pass
639             pass
640         except IOError as e:
641             raise Exception("can't write to %s: %s" % (self.cfgFile, e.strerror))
642         pass
643
644     def _prettify(self, elem, level=0, hasSiblings=False):
645         """
646         (internal)
647         Prettify XML file content.
648
649         Parameters:
650           elem: XML element (xml.etree.ElementTree.Element).
651           level: indentation level.
652           hasSiblings: True when item has siblings (i.e. this is not the last item
653              in the parent's children list).
654         """
655         indent = "  "
656         children = elem.getchildren()
657         tail = "\n"
658         if hasSiblings: tail += indent * level
659         elif level > 0: tail += indent * (level-1)
660         text = None
661         if children: text = "\n" + indent * (level+1)
662         elem.tail = tail
663         elem.text = text
664         for i in range(len(children)):
665             self._prettify(children[i], level+1, len(children)>1 and i+1<len(children))
666             pass
667         pass
668
669     def _dump(self, elem, level=0):
670         """
671         (internal)
672         Dump XML element.
673
674         Parameters:
675           elem: XML element (xml.etree.ElementTree.Element).
676           level: indentation level.
677         """
678         if elem is None:
679             return
680         indent = "  "
681         # dump element
682         print("%s%s" % (indent * level, elem.tag))
683         attrs = tagAttributes(elem.tag, True)
684         format = "%" + "-%ds" % max([len(i) for i in supportedAttributes()]) + " : %s"
685         for a in attrs:
686             if a in list(elem.attrib.keys()):
687                 print(indent*(level+1) + format % (a, elem.get(a)))
688                 pass
689             pass
690         print()
691         # dump all childrens recursively
692         for c in elem.getchildren():
693             self._dump(c, level+1)
694             pass
695         pass
696
697     def _checkConfig(self):
698         """
699         (internal)
700         Verify configuration (used to check validity of associated XML file).
701         """
702         errors = []
703         self._checkTag(self.tree, None, errors)
704         if errors:
705             errors = ["Bad XML format:"] + ["- %s" % i for i in errors]
706             raise Exception("\n".join(errors))
707         pass
708
709     def _checkTag(self, elem, tag, errors):
710         """
711         (internal)
712         Check if format of given XML element is valid.
713         
714         Parameters:
715           elem: XML element (xml.etree.ElementTree.Element).
716           tag: expected XML element's tag (string).
717           errors: output list to collect error messages (strings).
718         """
719         if elem.tag not in tagChildren(tag):
720             errors.append("bad XML element: %s" % elem.tag)
721         else:
722             # check attributes
723             attrs = list(elem.keys())
724             for attr in attrs:
725                 if attr not in tagAttributes(elem.tag, True):
726                     errors.append("unsupported attribute '%s' for XML element '%s'" % (attr, elem.tag))
727                     pass
728                 pass
729             # check all childrens recursively
730             children = elem.getchildren()
731             for child in children:
732                 child._parent = elem # set parent!!!
733                 self._checkTag(child, elem.tag, errors)
734                 pass
735             pass
736         pass
737
738     def _verifyTag(self, elem, errors):
739         """
740         (internal)
741         Verify given XML element is valid in terms of SALOME configuration.
742         
743         Parameters:
744           elem: XML element (xml.etree.ElementTree.Element).
745           errors: output list to collect error messages (strings).
746         """
747         attrs = tagAttributes(elem.tag, True)
748         # check mandatory attributes
749         for attr in attrs:
750             if attrs[attr] and (attr not in list(elem.keys()) or not elem.get(attr).strip()):
751                 errors.append("mandatory parameter '%s' of object '%s' is not set" % (attr, self._path(elem)))
752                 pass
753             pass
754         # specific check for particular XML element
755         try:
756             self._checkObject(elem)
757         except Exception as e:
758             errors.append("%s : %s" % (self._path(elem), str(e)))
759         # check all childrens recursively
760         for c in elem.getchildren():
761             self._verifyTag(c, errors)
762             pass
763         return len(errors) == 0
764
765     def _checkObject(self, elem):
766         """
767         (internal)
768         Perform specific check for given XML element.
769         
770         Raises an exception that if object is invalid.
771         
772         Parameters:
773           elem: XML element (xml.etree.ElementTree.Element).
774         """
775         if elem.tag == patchTag():
776             filename = elem.get(nameAttr())
777             url = elem.get(urlAttr())
778             if filename and not url:
779                 # if url is not given, we should check that file is present locally
780                 filepath = os.path.join(self.patchesDir(), filename)
781                 if not os.path.exists(filepath):
782                     raise Exception("patch file %s is not found" % filepath)
783                 pass
784             else:
785                 # TODO: we might check validity of URL here (see urlparse)!
786                 pass
787             pass
788         pass
789
790     pass # class CfgTool