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