Salome HOME
a74f1db2bfe70edc24d9e0d6e1fa4dcf4300cca0
[modules/kernel.git] / bin / SalomeOnDemandTK / extension_utilities.py
1 #!/usr/bin/env python3
2 # -*- coding:utf-8 -*-
3 # Copyright (C) 2007-2023  CEA, EDF, OPEN CASCADE
4 #
5 # Copyright (C) 2003-2007  OPEN CASCADE, EADS/CCR, LIP6, CEA/DEN,
6 # CEDRAT, EDF R&D, LEG, PRINCIPIA R&D, BUREAU VERITAS
7 #
8 # This library is free software; you can redistribute it and/or
9 # modify it under the terms of the GNU Lesser General Public
10 # License as published by the Free Software Foundation; either
11 # version 2.1 of the License, or (at your option) any later version.
12 #
13 # This library is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16 # Lesser General Public License for more details.
17 #
18 # You should have received a copy of the GNU Lesser General Public
19 # License along with this library; if not, write to the Free Software
20 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
21 #
22 # See https://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
23 #
24
25 #  File   : extension_utilities.py
26 #  Author : Konstantin Leontev, Open Cascade
27 #
28 #  @package SalomeOnDemandTK
29 #  @brief Utilities and constants those help to deal with salome extension files.
30
31 """
32 Utilities and constants those help to deal with salome extension files.
33 """
34
35 import os
36 import sys
37 import logging
38 import json
39 from traceback import format_exc
40 from pathlib import Path
41 import importlib.util
42 import fnmatch
43 import re
44
45 # Usually logging verbosity is set inside bin/runSalomeCommon.py when salome is starting.
46 # Here we do just the same for a case if we call this package stand alone.
47 FORMAT = '%(levelname)s : %(asctime)s : [%(filename)s:%(funcName)s:%(lineno)s] : %(message)s'
48 logging.basicConfig(format=FORMAT, level=logging.DEBUG)
49 logger = logging.getLogger()
50
51 # SalomeContext sets the logging verbosity level on its own,
52 # and we put it here, so it doesn't override the local format above.
53 #pylint: disable=wrong-import-position
54 import salomeContext
55 #pylint: enable=wrong-import-position
56
57 SALOME_EXTDIR = '__SALOME_EXT__'
58 ARCFILE_EXT = 'salomex'
59 BFILE_EXT = 'salomexb'
60 CFILE_EXT = 'salomexc'
61 DFILE_EXT = 'salomexd'
62 PYFILE_EXT = 'py'
63 ENVPYFILE_SUF = '_env.py'
64
65 EXTNAME_KEY = 'name'
66 EXTDESCR_KEY = 'descr'
67 EXTDEPENDSON_KEY = 'depends_on'
68 EXTAUTHOR_KEY = 'author'
69 EXTCOMPONENT_KEY = 'components'
70
71
72 def create_salomexd(name, descr='', depends_on=None, author='', components=None):
73     """
74     Create a basic salomexd file from provided args.
75     Current version is a json file with function args as the keys.
76
77     Args:
78         name - the name of the corresponding salome extension.
79         depends_on - list of the modules that current extension depends on.
80         author - an author of the extension.
81         components - the names of the modules those current extension was built from.
82
83     Returns:
84         None
85     """
86
87     logger.debug('Create salomexd file...')
88
89     if depends_on is None:
90         depends_on = []
91
92     if components is None:
93         components = []
94
95     json_string = json.dumps(
96         {
97             EXTNAME_KEY: name,
98             EXTDESCR_KEY: descr,
99             EXTDEPENDSON_KEY: depends_on,
100             EXTAUTHOR_KEY: author,
101             EXTCOMPONENT_KEY: components
102         },
103         indent=4
104     )
105
106     try:
107         with open(name + '.' + DFILE_EXT, "w", encoding="utf-8") as file:
108             file.write(json_string)
109
110     except OSError:
111         logger.error(format_exc())
112
113
114 def read_salomexd(file_path):
115     """
116     Reads a content of a salomexd file. Current version is a json file.
117     There's no check if the file_path is a valid salomexd file name.
118     It's expected that user call isvalid_filename() in advance.
119
120     Args:
121         file_path - the path to the salomexd file.
122
123     Returns:
124         A dictionary that represents the content of the salomexd file.
125     """
126
127     logger.debug('Read salomexd file %s', file_path)
128
129     try:
130         with open(file_path, 'r', encoding='UTF-8') as file:
131             return json.load(file)
132
133     except OSError:
134         logger.error(format_exc())
135         return {}
136
137
138 def value_from_salomexd(file_path, key):
139     """
140     Reads a content of a salomexd file and return a value for the given key.
141
142     Args:
143         file_path - the path to the salomexd file.
144         key - the key to search an assigned value.
145
146     Returns:
147         A value assigned to the given key if exist, otherwise None.
148     """
149
150     file_content = read_salomexd(file_path)
151     if key in file_content and file_content[key]:
152         logger.debug('Key: %s, value: %s', key, file_content[key])
153         return file_content[key]
154
155     logger.warning('Cannot get a value for key %s in salomexd file %s', key, file_path)
156     return None
157
158
159 def ext_info_bykey(salome_root, salomex_name, key):
160     """
161     Search a salomexd file by ext name and return a value for the given key.
162
163     Args:
164         install_dir - directory where the given extension is installed.
165         salomex_name - the given extension's name.
166         key - the key to search an assigned value.
167
168     Returns:
169         A value for key in the given ext salomexd file.
170     """
171
172     salomexd = find_salomexd(salome_root, salomex_name)
173     if salomexd:
174         return value_from_salomexd(salomexd, key)
175
176     return None
177
178
179 def create_salomexb(name, included):
180     """
181     Create a salomexb file from a list of included file names.
182     For example:
183     */*.py
184     doc/*.html
185     doc/*.jp
186
187     Args:
188         name - the name of the corresponding salome extension.
189         included - list of the directories those must be included inside a salomex.
190
191     Returns:
192         None
193     """
194
195     logger.debug('Create salomexb file...')
196
197     try:
198         with open(name + '.' + BFILE_EXT, "w", encoding="utf-8") as file:
199             file.write('\n'.join(included[0:]))
200
201     except OSError:
202         logger.error(format_exc())
203
204
205 def read_salomexb(file_path):
206     """
207     Returns a content af a salomexb file as a list of strings.
208     There's no check if the file_path is a valid salomexb file name.
209     It's expected that user call isvalid_filename() in advance.
210
211     Args:
212         file_path - the path to the salomexb file.
213
214     Returns:
215         List of strings - paths to the directories those must be included in
216         corresponding salomex archive file.
217     """
218
219     logger.debug('Read salomexb file %s', file_path)
220
221     try:
222         with open(file_path, 'r', encoding='UTF-8') as file:
223             return [line.rstrip() for line in file]
224
225     except OSError:
226         logger.error(format_exc())
227         return []
228
229
230 def list_files(dir_path):
231     """
232     Returns the recursive list of relative paths to files as strings
233     in the dir_path root directory and all subdirectories.
234
235     Args:
236         dir_path - the path to the directory where you search for files.
237
238     Raises:
239         Raises OSError exception.
240
241     Returns:
242         A list of relative paths to files inside the given directory.
243     """
244
245     files_list = []
246     for root, _, files in os.walk(dir_path):
247         for file in files:
248             files_list += os.path.relpath(os.path.join(root, file), dir_path)
249
250     return files_list
251
252
253 def filter_to_regex(dir_path, filter_patterns):
254     r"""
255     Makes a regex pattern from a given filter.
256
257     Args:
258         dir_path - the path to the directory where you search for files.
259         filter_patterns - list of expressions for matching file names.
260
261     Returns:
262         A regex string translated from the filter.
263         For example:
264         Filter:  ['SMESH/**.cmake', 'SMESH/share/s*.med']
265         Regex:   (?s:SMESH/.*\.cmake)\Z|(?s:SMESH/share/s.*\.med)\Z
266         Matches: SMESH/adm_local/cmake_files/SalomeSMESHConfig.cmake
267                  SMESH/share/salome/resources/smesh/padderdata/ferraill.med
268     """
269
270     logger.debug('Convert given filter to regex...')
271
272     # On Windows, it converts forward slashes to backward slashes.
273     norm_filter = [os.path.normpath(pat) for pat in filter_patterns]
274
275     # Make a regex pattern
276     # Adding '*' at the end of the folders names to match all the files inside.
277     regex_pattern = r'|'.join(
278         [fnmatch.translate(pat + '*' if os.path.isdir(os.path.join(dir_path, pat)) else pat)
279         for pat
280         in norm_filter])
281
282     logger.debug('Regex pattern: %s', regex_pattern)
283
284     return regex_pattern
285
286
287 def list_files_filter(dir_path, filter_patterns):
288     """
289     Returns the recursive list of relative paths to files as strings
290     in the dir_path root directory and all subdirectories.
291
292     Args:
293         dir_path - the path to the directory where you search for files.
294         filter_patterns - list of expressions for matching file names.
295
296     Returns:
297         files_abs - a list of absolute paths to selected files.
298         files_rel - a list of relative paths to selected files.
299     """
300
301     logger.debug('Get list of files to add into archive...')
302
303     regex_pattern = filter_to_regex(dir_path, filter_patterns)
304
305     files_abs = []
306     files_rel = []
307
308     for root, _, files in os.walk(dir_path):
309         for file in files:
310             filename_abs = os.path.join(root, file)
311             filename_rel = os.path.relpath(filename_abs, dir_path)
312
313             if re.match(regex_pattern, filename_rel):
314                 logger.debug('File name %s matches pattern', filename_rel)
315                 files_abs.append(filename_abs)
316                 files_rel.append(filename_rel)
317
318     return files_abs, files_rel
319
320
321 def list_files_ext(dir_path, ext):
322     """
323     Returns a list of abs paths to files with a given extension
324     in the dir_path directory.
325
326     Args:
327         dir_path - the path to the directory where you search for files.
328         ext - a given extension.
329
330     Returns:
331         A list of absolute paths to selected files.
332     """
333
334     logger.debug('Get list of files with extension %s...', ext)
335
336     dot_ext = '.' + ext
337     return [os.path.join(dir_path, f) for f in os.listdir(dir_path) if f.endswith(dot_ext)]
338
339
340 def list_tonewline_str(str_list):
341     """
342     Converts the given list of strings to a newline separated string.
343
344     Args:
345         dir_path - the path to the directory where you search for files.
346
347     Returns:
348         A newline separated string.
349     """
350     return '\n'.join(file for file in str_list)
351
352
353 def isvalid_filename(filename, extension):
354     """
355     Checks if a given filename is valid in a sense that it exists and have a given extension.
356
357     Args:
358         filename - the name of the file to check.
359         extension - expected file name extension.
360
361     Returns:
362         True if the given filename is valid for given extension.
363     """
364
365     logger.debug('Check if the filename %s exists and has an extension %s', filename, extension)
366
367     # First do we even have something to check here
368     if filename == '' or extension == '':
369         logger.error('A filename and extension cannot be empty! Args: filename=%s, extension=%s',
370             filename, extension)
371         return False
372
373     # Check if the filename matchs the provided extension
374     _, ext = os.path.splitext(filename)
375     ext = ext.lstrip('.')
376     if ext != extension:
377         logger.error('The filename %s doesnt have a valid extension! \
378             The valid extension must be: %s, but get: %s',
379             filename, extension, ext)
380         return False
381
382     # Check if the file base name is not empty
383     base_name = os.path.basename(filename)
384     if base_name == '':
385         logger.error('The file name %s has an empty base name!', filename)
386         return False
387
388     # Check if a file with given filename exists
389     if not os.path.isfile(filename):
390         logger.error('The filename %s is not an existing regular file!', filename)
391         return False
392
393     logger.debug('Filename %s exists and has an extension %s', filename, extension)
394     return True
395
396
397 def isvalid_dirname(dirname):
398     """
399     Checks if a given directory name exists.
400
401     Args:
402         dirname - the name of the directory to check.
403
404     Returns:
405         True if the given dirname is valid.
406     """
407
408     logger.debug('Check if the dirname %s exists', dirname)
409
410     # First do we even have something to check here
411     if dirname == '':
412         logger.error('A dirname argument cannot be empty! dirname=%s', dirname)
413         return False
414
415     # Check if a file with given filename exists
416     if not os.path.isdir(dirname):
417         logger.error('The dirname %s is not an existing regular file!', dirname)
418         return False
419
420     logger.debug('Directory %s exists', dirname)
421     return True
422
423
424 def list_dependants(install_dir, salomex_name):
425     """
426     Checks if we have installed extensions those depend on a given extension.
427
428     Args:
429         install_dir - path to SALOME install root directory.
430         salomex_name - a name of salome extension to check.
431
432     Returns:
433         True if the given extension has dependants.
434     """
435
436     logger.debug('Check if there are other extensions that depends on %s...', salomex_name)
437     dependants = []
438     salomexd_files = list_files_ext(install_dir, DFILE_EXT)
439
440     for salomexd_file in salomexd_files:
441         dependant_name, _ = os.path.splitext(os.path.basename(salomexd_file))
442
443         # Don't process <salomex_name> extension itself
444         if dependant_name == salomex_name:
445             continue
446
447         logger.debug('Check dependencies for %s...', salomexd_file)
448         salomexd_content = read_salomexd(salomexd_file)
449
450         if EXTDEPENDSON_KEY in salomexd_content and salomexd_content[EXTDEPENDSON_KEY]:
451             depends_on = salomexd_content[EXTDEPENDSON_KEY]
452             logger.debug('List of dependencies: %s', depends_on)
453
454             if salomex_name in depends_on:
455                 dependants.append(dependant_name)
456
457     if len(dependants) > 0:
458         logger.debug('Followed extensions %s depend on %s',
459             dependants, salomex_name)
460
461     return dependants
462
463
464 def is_empty_dir(directory):
465     """
466     Checks if the given directory is empty.
467
468     Args:
469         directory - path to directory to check.
470
471     Returns:
472         True if the given directory is empty.
473     """
474
475     return not next(os.scandir(directory), None)
476
477
478 def find_file(directory, file_name):
479     """
480     Finds a file in the given directory.
481
482     Args:
483         directory - path to directory to check.
484         file_name - given base filename with extension
485
486     Returns:
487         Abs path if the file exist, otherwise None.
488     """
489
490     logger.debug('Try to find %s file in %s...', file_name, directory)
491     file = os.path.join(directory, file_name)
492     if os.path.isfile(file):
493         logger.debug('File %s exists.', file)
494         return file
495
496     logger.debug('File %s doesnt\'t exist. Return None.', file)
497     return None
498
499
500 def find_salomexd(install_dir, salomex_name):
501     """
502     Finds a salomexd file for the given extension.
503
504     Args:
505         install_dir - path to directory to check.
506         salomex_name - extension's name.
507
508     Returns:
509         Abs path if the file exist, otherwise None.
510     """
511
512     return find_file(install_dir, salomex_name + '.' + DFILE_EXT)
513
514
515 def find_salomexc(install_dir, salomex_name):
516     """
517     Finds a salomexc file for the given extension.
518
519     Args:
520         install_dir - path to directory to check.
521         salomex_name - extension's name.
522
523     Returns:
524         Abs path if the file exist, otherwise None.
525     """
526
527     return find_file(install_dir, salomex_name + '.' + CFILE_EXT)
528
529
530 def find_envpy(install_dir, salomex_name):
531     """
532     Finds a _env.py file for the given extension.
533
534     Args:
535         install_dir - path to directory to check.
536         salomex_name - extension's name.
537
538     Returns:
539         Abs path if the file exist, otherwise None.
540     """
541
542     return find_file(install_dir, salomex_name + ENVPYFILE_SUF)
543
544
545 def module_from_filename(filename):
546     """
547     Create and execute a module by filename.
548
549     Args:
550         filename - a given python filename.
551
552     Returns:
553         Module.
554     """
555
556     # Get the module from the filename
557     basename = os.path.basename(filename)
558     module_name, _ = os.path.splitext(basename)
559
560     spec = importlib.util.spec_from_file_location(module_name, filename)
561     if not spec:
562         logger.error('Could not get a spec for %s file!')
563         return None
564
565     module = importlib.util.module_from_spec(spec)
566     if not module:
567         logger.error('Could not get a module for %s file!')
568         return None
569
570     sys.modules[module_name] = module
571
572     logger.debug('Execute %s module', module_name)
573     if not spec.loader:
574         logger.error('spec.loader is None for %s file!')
575         return None
576
577     spec.loader.exec_module(module)
578
579     return module
580
581
582 def set_selext_env(install_dir, salomex_name, context=None):
583     """
584     Finds and run _env.py file for the given extension.
585
586     Args:
587         install_dir - path to directory to check.
588         salomex_name - extension's name.
589         context - SalomeContext object.
590
591     Returns:
592         True if an envpy file was found and run its init func.
593     """
594
595     logger.debug('Set an env for salome extension: %s...', salomex_name)
596
597     # Set the root dir as env variable
598     if not context:
599         context = salomeContext.SalomeContext(None)
600         context.setVariable('SALOME_APPLICATION_DIR', install_dir, overwrite=False)
601
602     # Find env file
603     ext_envpy = find_envpy(install_dir, salomex_name)
604     if not ext_envpy:
605         return False
606
607     # Get a module
608     envpy_module = module_from_filename(ext_envpy)
609     if not envpy_module:
610         return False
611
612     # Set env if we have something to set
613     ext_dir = os.path.join(install_dir, SALOME_EXTDIR)
614     if hasattr(envpy_module, 'init'):
615         envpy_module.init(context, ext_dir)
616         return True
617     else:
618         logger.warning('Env file %s doesnt have init func:!', ext_envpy)
619
620     logger.warning('Setting an env for salome extension %s failed!', salomex_name)
621     return False
622
623
624 def get_app_root(levels_up=5):
625     """
626     Finds an app root by going up on the given steps.
627
628     Args:
629         levels_up - steps up in dir hierarchy relative to the current file.
630
631     Returns:
632         Path to the app root.
633     """
634
635     app_root = str(Path(__file__).resolve().parents[levels_up - 1])
636     logger.debug('App root: %s', app_root)
637
638     return app_root
639
640
641 def check_if_installed(install_dir, salomex_name):
642     """
643     Check if a given salome extension is installed in install_dir.
644     Now for install|remove process we consider an ext is installed
645     if we have at least salomexc file with list of files to remove
646     if we need to clean up.
647
648     Args:
649         install_dir - path to SALOME install root directory.
650         salomex_name - a given ext name.
651
652     Returns:
653         salomexd, salomexc file names.
654     """
655
656     logger.debug('Check if %s extension is installed in %s...', salomex_name, install_dir)
657
658     salomexd = find_salomexd(install_dir, salomex_name)
659     if not salomexd:
660         logger.debug('Extension has been already removed or %s file was deleted by mistake. '
661             'In the former case we can use %s file to clean up.', DFILE_EXT, CFILE_EXT)
662
663     salomexc = find_salomexc(install_dir, salomex_name)
664     if salomexc:
665         logger.debug('An extension %s IS installed.', salomex_name)
666     else:
667         logger.debug('An extension %s IS NOT installed.', salomex_name)
668
669     return salomexd, salomexc