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