Salome HOME
git: ignore VSCode cache files, sat cache file and add a pre-commit config
[tools/sat.git] / src / __init__.py
1 #!/usr/bin/env python
2 #-*- coding:utf-8 -*-
3 #  Copyright (C) 2010-2013  CEA/DEN
4 #
5 #  This library is free software; you can redistribute it and/or
6 #  modify it under the terms of the GNU Lesser General Public
7 #  License as published by the Free Software Foundation; either
8 #  version 2.1 of the License.
9 #
10 #  This library is distributed in the hope that it will be useful,
11 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 #  Lesser General Public License for more details.
14 #
15 #  You should have received a copy of the GNU Lesser General Public
16 #  License along with this library; if not, write to the Free Software
17 #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
18
19
20 """\
21 initial imports and utilities methods for salomeTools
22 """
23
24 import os
25 import shutil
26 import errno
27 import stat
28 import fnmatch
29 import pprint as PP
30 from ftplib import FTP
31
32 from . import pyconf
33 from . import architecture
34 from . import printcolors
35 from . import options
36 from . import system
37 from . import ElementTree
38 from . import logger
39 from . import product
40 from . import environment
41 from . import fileEnviron
42 from . import compilation
43 from . import test_module
44 from . import template
45
46 import platform
47 if platform.system() == "Windows" :
48     import colorama
49     colorama.init()
50
51 OK_STATUS = "OK"
52 KO_STATUS = "KO"
53 NA_STATUS = "NA"
54 KNOWNFAILURE_STATUS = "KF"
55 TIMEOUT_STATUS = "TIMEOUT"
56
57 class SatException(Exception):
58     """sat exception class"""
59     def message(self, arg):
60         if sys.version_info[0] >= 3:
61             # message method is not available for python 3.8+
62             return super().msg(arg)
63         else:
64             return super(SatException,self).message(arg)
65
66 def ensure_path_exists(p):
67     """Create a path if not existing
68
69     :param p str: The path.
70     """
71     if not os.path.exists(p):
72         os.makedirs(p)
73
74 def check_config_has_application( config, details = None ):
75     """check that the config has the key APPLICATION. Else raise an exception.
76
77     :param config class 'common.pyconf.Config': The config.
78     """
79     if 'APPLICATION' not in config:
80         message = _("An APPLICATION is required. Use 'config --list' to get the list of available applications.\n")
81         if details :
82             details.append(message)
83         raise SatException( message )
84
85 def check_platform_is_supported( config, logger ):
86     """check that the platform is supported, write warning if not.
87
88     :param config class 'common.pyconf.Config': The config.
89     """
90     if 'platform' in config.APPLICATION and config.VARS.dist not in config.APPLICATION.platform:
91         msg = "WARNING: Your application configuration is not supported on this platform (%s)\n"\
92               "         Please consider using the native application!" % config.VARS.dist
93         logger.write("\n%s\n\n" % printcolors.printcWarning(msg), 1)
94     return
95
96 def check_config_has_profile( config, details = None ):
97     """\
98     check that the config has the key APPLICATION.profile.
99     else, raise an exception.
100
101     :param config class 'common.pyconf.Config': The config.
102     """
103     check_config_has_application(config)
104     if 'profile' not in config.APPLICATION:
105         message = _("A profile section is required in your application.\n")
106         if details :
107             details.append(message)
108         raise SatException( message )
109
110 def appli_test_property(config,property_name, property_value):
111     """Generic function to test if an application has a property set to a value
112     :param config class 'common.pyconf.Config': The config.
113     :param property_name : The name of the property to check
114     :param property_value : The value of the property to test
115     :return: True if the application has the property set to property_value
116     :rtype: boolean
117     """
118     # first check if application has property_value
119     if not ("APPLICATION"  in config and
120             "properties"   in config.APPLICATION and
121             property_name  in config.APPLICATION.properties):
122         return False
123
124     # then check to the property is set to property_value
125     eval_expression = 'config.APPLICATION.properties.%s == "%s"' %\
126                       (property_name,property_value)
127     result = eval(eval_expression)
128     return result
129
130
131 def config_has_application( config ):
132     return 'APPLICATION' in config
133
134 def get_cfg_param(config, param_name, default):
135     """\
136     eearch for param_name value in config.
137     if param_name is not in config
138     then return default,
139     else return the found value
140
141     :param config class 'common.pyconf.Config': The config.
142     :param param_name str: the name of the parameter to get the value
143     :param default str: The value to return if param_name is not in config
144     :return: see initial description of the function
145     :rtype: str
146     """
147     if param_name in config:
148         return config[param_name]
149     return default
150
151 def strSplitN(aList, nb, skip="\n     "):
152     """
153     example
154     aStr = 'this-is-a-string'
155     splitN(aStr, 2, '-')
156     split it by every 2nd '-' rather than every '-'
157     """
158     strValue = ""
159     i = 0
160     for v in aList:
161       strValue += "%15s, " % str(v)
162       i += 1
163       if i >= nb:
164         strValue += skip
165         i = 0
166     if len(aList) > nb:
167         strValue = skip + strValue
168     return strValue
169
170 def getProductNames(cfg, wildcards, logger):
171     """get products names using * or ? as wildcards like shell Linux"""
172     res = []
173     if type(wildcards) is list:
174       wilds = wildcards
175     else:
176       wilds = [wildcards]
177     notFound = {}
178     products = cfg.APPLICATION.products.keys()
179     for wild in wildcards:
180       ok = False
181       for prod in products:
182         filtered = fnmatch.filter([prod], wild)
183         # print("filtered", prod, wild, filtered)
184         if len(filtered) > 0:
185           res.append(prod)
186           ok = True
187           continue
188       if not ok:
189         notFound[wild] = None
190     if len(res) == 0:
191       logger.warning("Empty list of products, from %s" % wilds)
192     if len(notFound.keys()) > 0:
193       strProd = strSplitN( sorted(products), 5)
194       logger.warning("products not found: %s\n  availables products are:\n%s" % \
195                      (sorted(notFound.keys()), strProd) )
196     return res
197
198
199 def print_info(logger, info):
200     """\
201     Prints the tuples that are in info variable in a formatted way.
202
203     :param logger Logger: The logging instance to use for the prints.
204     :param info list: The list of tuples to display
205     """
206     # find the maximum length of the first value of the tuples in info
207     smax = max(map(lambda l: len(l[0]), info))
208     # Print each item of info with good indentation
209     for i in info:
210         sp = " " * (smax - len(i[0]))
211         printcolors.print_value(logger, sp + i[0], i[1], 2)
212     logger.write("\n", 2)
213
214 def get_base_path(config):
215     """\
216     Returns the path of the products base.
217
218     :param config Config: The global Config instance.
219     :return: The path of the products base.
220     :rtype: str
221     """
222     if "base" not in config.LOCAL:
223         local_file_path = os.path.join(config.VARS.salometoolsway,
224                                       "data",
225                                       "local.pyconf")
226         msg = _("Please define a base path in the file %s" % local_file_path)
227         raise SatException(msg)
228
229     base_path = os.path.abspath(config.LOCAL.base)
230
231     return base_path
232
233 def get_launcher_name(config):
234     """\
235     Returns the name of salome launcher.
236
237     :param config Config: The global Config instance.
238     :return: The name of salome launcher.
239     :rtype: str
240     """
241     check_config_has_application(config)
242     if 'profile' in config.APPLICATION and 'launcher_name' in config.APPLICATION.profile:
243         launcher_name = config.APPLICATION.profile.launcher_name
244     else:
245         launcher_name = 'salome'
246
247     return launcher_name
248
249 def get_launcher_exe(config):
250     """\
251     Returns the name of exe defined in profile section.
252
253     :param config Config: The global Config instance.
254     :return: The name of the exe to use in launcher.
255     :rtype: str
256     """
257     check_config_has_application(config)
258     if 'profile' in config.APPLICATION and 'exe' in config.APPLICATION.profile:
259         exe_name = config.APPLICATION.profile.exe
260     else:
261         exe_name = None
262
263     return exe_name
264
265
266 def get_log_path(config):
267     """\
268     Returns the path of the logs.
269
270     :param config Config: The global Config instance.
271     :return: The path of the logs.
272     :rtype: str
273     """
274     if "log_dir" not in config.LOCAL:
275         local_file_path = os.path.join(config.VARS.salometoolsway,
276                                       "data",
277                                       "local.pyconf")
278         msg = _("Please define a log_dir in the file %s" % local_file_path)
279         raise SatException(msg)
280
281     log_dir_path = os.path.abspath(config.LOCAL.log_dir)
282
283     return log_dir_path
284
285 def get_salometool_version(config):
286    """Return the salomeTool version.
287
288    :param config Config: The global Config instance.
289    :return: the description of this version of sat in terms of tag and commit
290    """
291    return config.LOCAL.tag
292
293
294 def get_salome_version(config):
295     import versionMinorMajorPatch as VMMP
296
297     if hasattr(config.APPLICATION, 'version_salome'):
298         version = VMMP.MinorMajorPatch(config.APPLICATION.version_salome)
299     else:
300         kernel_info = product.get_product_config(config, "KERNEL")
301         aFile = os.path.join(
302                             kernel_info.install_dir,
303                             "bin",
304                             "salome",
305                             "VERSION")
306         if not os.path.isfile(aFile):
307             return None
308         with open(aFile) as f:
309           line = f.readline()  # example: '[SALOME KERNEL] : 8.4.0'
310         version = VMMP.MinorMajorPatch(line.split(":")[1])
311
312     # from nov. 2023 and SALOME 9.10.0 forbid test(s) on integer, use MajorMinorPatch class tests
313     return version
314
315 def read_config_from_a_file(filePath):
316         try:
317             cfg_file = pyconf.Config(filePath)
318         except pyconf.ConfigError as e:
319             raise SatException(_("Error in configuration file: %(file)s\n  %(error)s") % \
320                 { 'file': filePath, 'error': str(e) })
321         return cfg_file
322
323 def get_tmp_filename(cfg, name):
324     if not os.path.exists(cfg.VARS.tmp_root):
325         os.makedirs(cfg.VARS.tmp_root)
326
327     return os.path.join(cfg.VARS.tmp_root, name)
328
329 ##
330 # Utils class to simplify path manipulations.
331 class Path:
332     def __init__(self, path):
333         self.path = str(path)
334
335     def __add__(self, other):
336         return Path(os.path.join(self.path, str(other)))
337
338     def __abs__(self):
339         return Path(os.path.abspath(self.path))
340
341     def __str__(self):
342         return self.path
343
344     def __eq__(self, other):
345         return self.path == other.path
346
347     def exists(self):
348         return self.islink() or os.path.exists(self.path)
349
350     def islink(self):
351         return os.path.islink(self.path)
352
353     def isdir(self):
354         return os.path.isdir(self.path)
355
356     def isfile(self):
357         return os.path.isfile(self.path)
358
359     def list(self):
360         return [Path(p) for p in os.listdir(self.path)]
361
362     def dir(self):
363         return Path(os.path.dirname(self.path))
364
365     def base(self):
366         return Path(os.path.basename(self.path))
367
368     def make(self, mode=None):
369         os.makedirs(self.path)
370         if mode:
371             os.chmod(self.path, mode)
372
373     def chmod(self, mode):
374         os.chmod(self.path, mode)
375
376     def rm(self):
377         if self.islink():
378             os.remove(self.path)
379         else:
380             shutil.rmtree( self.path, onerror = handleRemoveReadonly )
381
382     def copy(self, path, smart=False):
383         if not isinstance(path, Path):
384             path = Path(path)
385
386         if os.path.islink(self.path):
387             return self.copylink(path)
388         elif os.path.isdir(self.path):
389             return self.copydir(path, smart)
390         else:
391             return self.copyfile(path)
392
393     def smartcopy(self, path):
394         return self.copy(path, True)
395
396     def readlink(self):
397         if self.islink():
398             return os.readlink(self.path)
399         else:
400             return False
401
402     def symlink(self, path):
403         try:
404             os.symlink(str(path), self.path)
405             return True
406         except:
407             return False
408
409     def copylink(self, path):
410         try:
411             os.symlink(os.readlink(self.path), str(path))
412             return True
413         except:
414             return False
415
416     def copydir(self, dst, smart=False):
417         try:
418             names = self.list()
419
420             if not dst.exists():
421                 dst.make()
422
423             for name in names:
424                 if name == dst:
425                     continue
426                 if smart and (str(name) in [".git", "CVS", ".svn"]):
427                     continue
428                 srcname = self + name
429                 dstname = dst + name
430                 srcname.copy(dstname, smart)
431             return True
432         except:
433             return False
434
435     def copyfile(self, path):
436         try:
437             shutil.copy2(self.path, str(path))
438             return True
439         except:
440             return False
441
442 def find_file_in_lpath(file_name, lpath, additional_dir = ""):
443     """\
444     Find in all the directories in lpath list the file that has the same name
445     as file_name.
446     If it is found
447     then return the full path of the file
448     else return False.
449
450     The additional_dir (optional) is the name of the directory to add to all
451     paths in lpath.
452
453     :param file_name str: The file name to search
454     :param lpath List: The list of directories where to search
455     :param additional_dir str: The name of the additional directory
456     :return: the full path of the file or False if not found
457     :rtype: str
458     """
459     for directory in lpath:
460         dir_complete = os.path.join(directory, additional_dir)
461         if not os.path.isdir(directory) or not os.path.isdir(dir_complete):
462             continue
463         l_files = os.listdir(dir_complete)
464         for file_n in l_files:
465             if file_n == file_name:
466                 return os.path.join(dir_complete, file_name)
467     return False
468
469 def find_file_in_ftppath(file_name, ftppath, installation_dir, logger, additional_dir = ""):
470     """\
471     Find in all ftp servers in ftppath the file called file_name
472     If it is found then return the destination path of the file
473     (the place where the file was downloaded"
474     else return False.
475
476     :param file_name str: The file name to search
477     :param ftppath, List: The list of ftp servers where to search
478     :param installation_dir str: The name of the installation directory
479     :return: the full path of the file or False if not found
480     :param logger Logger: The logging instance to use for the prints.
481     :rtype: str
482     """
483
484     # make sure installation_dir exists
485     if not os.path.exists(installation_dir):
486         os.makedirs(installation_dir)
487
488     destination=os.path.join(installation_dir, file_name)
489
490     # paths in ftppath may contain several paths separated by ":"
491     # we plit them, and push all paths in bigftppath
492     bigftppath=[]
493     for ipath in ftppath:
494         splpath=ipath.split(":")
495         bigftppath+=splpath
496
497     for ftp_archive in bigftppath:
498        try:
499            # ftp_archive has the form ftp.xxx.yyy/dir1/dir2/...
500            ftp_archive_split=ftp_archive.split("/")
501            ftp_server=ftp_archive_split[0]
502            ftp = FTP(ftp_server)
503            logger.write("   Connect to ftp server %s\n" % ftp_server, 3)
504            ftp.login()
505            for directory in ftp_archive_split[1:]:
506                logger.write("   Change directory to %s\n" % directory, 3)
507                ftp.cwd(directory)
508            if additional_dir:
509                ftp.cwd(additional_dir)
510        except:
511            logger.error("while connecting to ftp server %s\n" % ftp_server)
512            continue
513
514        try:  # get md5 file if it exists
515            file_name_md5=file_name + ".md5"
516            destination_md5=destination + ".md5"
517            if ftp.size(file_name_md5) > 0:
518                with open(destination_md5,'wb') as dest_file_md5:
519                    ftp.retrbinary("RETR "+file_name_md5, dest_file_md5.write)
520        except:
521            pass
522
523        try:
524            if ftp.size(file_name) > 0:
525                # if file exists and is non empty
526                with open(destination,'wb') as dest_file:
527                    ftp.retrbinary("RETR "+file_name, dest_file.write)
528                logger.write("   Archive %s was retrieved and stored in %s\n" % (file_name, destination), 3)
529                return destination
530        except:
531            logger.error("File not found in ftp_archive %s\n" % ftp_server)
532
533     return False
534
535 def handleRemoveReadonly(func, path, exc):
536     excvalue = exc[1]
537     if func in (os.rmdir, os.remove) and excvalue.errno == errno.EACCES:
538         os.chmod(path, stat.S_IRWXU| stat.S_IRWXG| stat.S_IRWXO) # 0777
539         func(path)
540     else:
541         raise
542
543 def deepcopy_list(input_list):
544     """\
545     Do a deep copy of a list
546
547     :param input_list List: The list to copy
548     :return: The copy of the list
549     :rtype: List
550     """
551     res = []
552     for elem in input_list:
553         res.append(elem)
554     return res
555
556 def remove_item_from_list(input_list, item):
557     """\
558     Remove all occurences of item from input_list
559
560     :param input_list List: The list to modify
561     :return: The without any item
562     :rtype: List
563     """
564     res = []
565     for elem in input_list:
566         if elem == item:
567             continue
568         res.append(elem)
569     return res
570
571 def parse_date(date):
572     """\
573     Transform YYYYMMDD_hhmmss into YYYY-MM-DD hh:mm:ss.
574
575     :param date str: The date to transform
576     :return: The date in the new format
577     :rtype: str
578     """
579     if len(date) != 15:
580         return date
581     res = "%s-%s-%s %s:%s:%s" % (date[0:4],
582                                  date[4:6],
583                                  date[6:8],
584                                  date[9:11],
585                                  date[11:13],
586                                  date[13:])
587     return res
588
589 def merge_dicts(*dict_args):
590     """\
591     Given any number of dicts, shallow copy and merge into a new dict,
592     precedence goes to key value pairs in latter dicts.
593     """
594     result = {}
595     for dictionary in dict_args:
596         result.update(dictionary)
597     return result
598
599 def replace_in_file(filein, strin, strout):
600     """Replace <strin> by <strout> in file <filein>"""
601     shutil.move(filein, filein + "_old")
602     fileout= filein
603     filein = filein + "_old"
604     fin = open(filein, "r")
605     fout = open(fileout, "w")
606     for line in fin:
607         fout.write(line.replace(strin, strout))
608
609 def get_property_in_product_cfg(product_cfg, pprty):
610     if not "properties" in product_cfg:
611         return None
612     if not pprty in product_cfg.properties:
613         return None
614     return product_cfg.properties[pprty]
615
616 def activate_mesa_property(config):
617     """Add mesa property into application properties
618
619     :param config Config: The global configuration. It must have an application!
620     """
621     # Verify the existence of the file
622     if not 'properties' in config.APPLICATION:
623         config.APPLICATION.addMapping( 'properties', pyconf.Mapping(), None )
624     config.APPLICATION.properties.use_mesa="yes"