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