Salome HOME
[bos #32522][EDF] SALOME on Demand. Added functions and refactored to support adding...
[modules/kernel.git] / bin / SalomeOnDemandTK / extension_utilities.py
1 #!/usr/bin/env python3
2 # -*- coding:utf-8 -*-
3 # Copyright (C) 2007-2022  CEA/DEN, EDF R&D, 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
43 # Usually logging verbosity is set inside bin/runSalomeCommon.py when salome is starting.
44 # Here we do just the same for a case if we call this package stand alone.
45 FORMAT = '%(levelname)s : %(asctime)s : [%(filename)s:%(funcName)s:%(lineno)s] : %(message)s'
46 logging.basicConfig(format=FORMAT, level=logging.DEBUG, force=False)
47 logger = logging.getLogger()
48
49 # SalomeContext sets the logging verbosity level on its own,
50 # and we put it here, so it doesn't override the local format above.
51 #pylint: disable=wrong-import-position
52 import salomeContext
53 #pylint: enable=wrong-import-position
54
55 SALOME_EXTDIR = '__SALOME_EXT__'
56 ARCFILE_EXT = 'salomex'
57 BFILE_EXT = 'salomexb'
58 CFILE_EXT = 'salomexc'
59 DFILE_EXT = 'salomexd'
60 PYFILE_EXT = 'py'
61 ENVPYFILE_SUF = '_env.py'
62
63 EXTNAME_KEY = 'name'
64 EXTDESCR_KEY = 'descr'
65 EXTDEPENDSON_KEY = 'depends_on'
66 EXTAUTHOR_KEY = 'author'
67 EXTCOMPONENT_KEY = 'components'
68
69
70 def create_salomexd(name, descr='', depends_on=None, author='', components=None):
71     """
72     Create a basic salomexd file from provided args.
73     Current version is a json file with function args as the keys.
74
75     Args:
76         name - the name of the corresponding salome extension.
77         depends_on - list of the modules that current extension depends on.
78         author - an author of the extension.
79         components - the names of the modules those current extension was built from.
80
81     Returns:
82         None
83     """
84
85     logger.debug('Create salomexd file...')
86
87     if depends_on is None:
88         depends_on = []
89
90     if components is None:
91         components = []
92
93     json_string = json.dumps(
94         {
95             EXTNAME_KEY: name,
96             EXTDESCR_KEY: descr,
97             EXTDEPENDSON_KEY: depends_on,
98             EXTAUTHOR_KEY: author,
99             EXTCOMPONENT_KEY: components
100         },
101         indent=4
102     )
103
104     try:
105         with open(name + '.' + DFILE_EXT, "w", encoding="utf-8") as file:
106             file.write(json_string)
107
108     except OSError:
109         logger.error(format_exc())
110
111
112 def read_salomexd(file_path):
113     """
114     Reads a content of a salomexd file. Current version is a json file.
115     There's no check if the file_path is a valid salomexd file name.
116     It's expected that user call isvalid_filename() in advance.
117
118     Args:
119         file_path - the path to the salomexd file.
120
121     Returns:
122         A dictionary that represents the content of the salomexd file.
123     """
124
125     logger.debug('Read salomexd file %s', file_path)
126
127     try:
128         with open(file_path, 'r', encoding='UTF-8') as file:
129             return json.load(file)
130
131     except OSError:
132         logger.error(format_exc())
133         return {}
134
135
136 def value_from_salomexd(file_path, key):
137     """
138     Reads a content of a salomexd file and return a value for the given key.
139
140     Args:
141         file_path - the path to the salomexd file.
142         key - the key to search an assigned value.
143
144     Returns:
145         A value assigned to the given key if exist, otherwise None.
146     """
147
148     file_content = read_salomexd(file_path)
149     if key in file_content and file_content[key]:
150         logger.debug('Key: %s, value: %s', key, file_content[key])
151         return file_content[key]
152
153     logger.warning('Cannot get a value for key %s in salomexd file %s', key, file_path)
154     return None
155
156
157 def ext_info_bykey(salome_root, salomex_name, key):
158     """
159     Search a salomexd file by ext name and return a value for the given key.
160
161     Args:
162         install_dir - directory where the given extension is installed.
163         salomex_name - the given extension's name.
164         key - the key to search an assigned value.
165
166     Returns:
167         A value for key in the given ext salomexd file.
168     """
169
170     salomexd = find_salomexd(salome_root, salomex_name)
171     if salomexd:
172         return value_from_salomexd(salomexd, key)
173
174     return None
175
176
177 def create_salomexb(name, included):
178     """
179     Create a salomexb file from a list of included file names.
180     For example:
181     */*.py
182     doc/*.html
183     doc/*.jp
184
185     Args:
186         name - the name of the corresponding salome extension.
187         included - list of the directories those must be included inside a salomex.
188
189     Returns:
190         None
191     """
192
193     logger.debug('Create salomexb file...')
194
195     try:
196         with open(name + '.' + BFILE_EXT, "w", encoding="utf-8") as file:
197             file.write('\n'.join(included[0:]))
198
199     except OSError:
200         logger.error(format_exc())
201
202
203 def read_salomexb(file_path):
204     """
205     Returns a content af a salomexb file as a list of strings.
206     There's no check if the file_path is a valid salomexb file name.
207     It's expected that user call isvalid_filename() in advance.
208
209     Args:
210         file_path - the path to the salomexb file.
211
212     Returns:
213         List of strings - paths to the directories those must be included in
214         corresponding salomex archive file.
215     """
216
217     logger.debug('Read salomexb file %s', file_path)
218
219     try:
220         with open(file_path, 'r', encoding='UTF-8') as file:
221             return [line.rstrip() for line in file]
222
223     except OSError:
224         logger.error(format_exc())
225         return []
226
227
228 def list_files(dir_path):
229     """
230     Returns the recursive list of relative paths to files as strings
231     in the dir_path root directory and all subdirectories.
232
233     Args:
234         dir_path - the path to the directory where you search for files.
235
236     Raises:
237         Raises OSError exception.
238
239     Returns:
240         A list of relative paths to files inside the given directory.
241     """
242
243     files_list = []
244     for root, _, files in os.walk(dir_path):
245         for file in files:
246             files_list += os.path.relpath(os.path.join(root, file), dir_path)
247
248     return files_list
249
250
251 def list_files_filter(dir_path, filter_patterns):
252     """
253     Returns the recursive list of relative paths to files as strings
254     in the dir_path root directory and all subdirectories.
255
256     Args:
257         dir_path - the path to the directory where you search for files.
258         filter_patterns - list of expressions for matching file names.
259
260     Returns:
261         files_abs - a list of absolute paths to selected files.
262         files_rel - a list of relative paths to selected files.
263     """
264
265     logger.debug('Get list of files to add into archive...')
266
267     files_abs = []
268     files_rel = []
269
270     for root, _, files in os.walk(dir_path):
271         for file in files:
272             for pattern in filter_patterns:
273                 filename_abs = os.path.join(root, file)
274                 filename_rel = os.path.relpath(filename_abs, dir_path)
275
276                 if filename_rel.startswith(pattern):
277                     logger.debug('File name %s matches pattern %s', filename_rel, pattern)
278                     files_abs.append(filename_abs)
279                     files_rel.append(filename_rel)
280
281     return files_abs, files_rel
282
283
284 def list_files_ext(dir_path, ext):
285     """
286     Returns a list of abs paths to files with a given extension
287     in the dir_path directory.
288
289     Args:
290         dir_path - the path to the directory where you search for files.
291         ext - a given extension.
292
293     Returns:
294         A list of absolute paths to selected files.
295     """
296
297     logger.debug('Get list of files with extension %s...', ext)
298
299     dot_ext = '.' + ext
300     return [os.path.join(dir_path, f) for f in os.listdir(dir_path) if f.endswith(dot_ext)]
301
302
303 def list_tonewline_str(str_list):
304     """
305     Converts the given list of strings to a newline separated string.
306
307     Args:
308         dir_path - the path to the directory where you search for files.
309
310     Returns:
311         A newline separated string.
312     """
313     return '\n'.join(file for file in str_list)
314
315
316 def isvalid_filename(filename, extension):
317     """
318     Checks if a given filename is valid in a sense that it exists and have a given extension.
319
320     Args:
321         filename - the name of the file to check.
322         extension - expected file name extension.
323
324     Returns:
325         True if the given filename is valid for given extension.
326     """
327
328     logger.debug('Check if the filename %s exists and has an extension %s', filename, extension)
329
330     # First do we even have something to check here
331     if filename == '' or extension == '':
332         logger.error('A filename and extension cannot be empty! Args: filename=%s, extension=%s',
333             filename, extension)
334         return False
335
336     # Check if the filename matchs the provided extension
337     _, ext = os.path.splitext(filename)
338     ext = ext.lstrip('.')
339     if ext != extension:
340         logger.error('The filename %s doesnt have a valid extension! \
341             The valid extension must be: %s, but get: %s',
342             filename, extension, ext)
343         return False
344
345     # Check if the file base name is not empty
346     base_name = os.path.basename(filename)
347     if base_name == '':
348         logger.error('The file name %s has an empty base name!', filename)
349         return False
350
351     # Check if a file with given filename exists
352     if not os.path.isfile(filename):
353         logger.error('The filename %s is not an existing regular file!', filename)
354         return False
355
356     logger.debug('Filename %s exists and has an extension %s', filename, extension)
357     return True
358
359
360 def isvalid_dirname(dirname):
361     """
362     Checks if a given directory name exists.
363
364     Args:
365         dirname - the name of the directory to check.
366
367     Returns:
368         True if the given dirname is valid.
369     """
370
371     logger.debug('Check if the dirname %s exists', dirname)
372
373     # First do we even have something to check here
374     if dirname == '':
375         logger.error('A dirname argument cannot be empty! dirname=%s', dirname)
376         return False
377
378     # Check if a file with given filename exists
379     if not os.path.isdir(dirname):
380         logger.error('The dirname %s is not an existing regular file!', dirname)
381         return False
382
383     logger.debug('Directory %s exists', dirname)
384     return True
385
386
387 def list_dependants(install_dir, salomex_name):
388     """
389     Checks if we have installed extensions those depend on a given extension.
390
391     Args:
392         install_dir - path to SALOME install root directory.
393         salomex_name - a name of salome extension to check.
394
395     Returns:
396         True if the given extension has dependants.
397     """
398
399     logger.debug('Check if there are other extensions that depends on %s...', salomex_name)
400     dependants = []
401     salomexd_files = list_files_ext(install_dir, DFILE_EXT)
402
403     for salomexd_file in salomexd_files:
404         dependant_name, _ = os.path.splitext(os.path.basename(salomexd_file))
405
406         # Don't process <salomex_name> extension itself
407         if dependant_name == salomex_name:
408             continue
409
410         logger.debug('Check dependencies for %s...', salomexd_file)
411         salomexd_content = read_salomexd(salomexd_file)
412
413         if EXTDEPENDSON_KEY in salomexd_content and salomexd_content[EXTDEPENDSON_KEY]:
414             depends_on = salomexd_content[EXTDEPENDSON_KEY]
415             logger.debug('List of dependencies: %s', depends_on)
416
417             if salomex_name in depends_on:
418                 dependants.append(dependant_name)
419
420     if len(dependants) > 0:
421         logger.debug('Followed extensions %s depend on %s',
422             dependants, salomex_name)
423
424     return dependants
425
426
427 def is_empty_dir(directory):
428     """
429     Checks if the given directory is empty.
430
431     Args:
432         directory - path to directory to check.
433
434     Returns:
435         True if the given directory is empty.
436     """
437
438     return not next(os.scandir(directory), None)
439
440
441 def find_file(directory, file_name):
442     """
443     Finds a file in the given directory.
444
445     Args:
446         directory - path to directory to check.
447         file_name - given base filename with extension
448
449     Returns:
450         Abs path if the file exist, otherwise None.
451     """
452
453     logger.debug('Try to find %s file in %s...', file_name, directory)
454     file = os.path.join(directory, file_name)
455     if os.path.isfile(file):
456         logger.debug('File %s exists.', file)
457         return file
458
459     logger.debug('File %s doesnt\'t exist. Return None.', file)
460     return None
461
462
463 def find_salomexd(install_dir, salomex_name):
464     """
465     Finds a salomexd file for the given extension.
466
467     Args:
468         install_dir - path to directory to check.
469         salomex_name - extension's name.
470
471     Returns:
472         Abs path if the file exist, otherwise None.
473     """
474
475     return find_file(install_dir, salomex_name + '.' + DFILE_EXT)
476
477
478 def find_salomexc(install_dir, salomex_name):
479     """
480     Finds a salomexc file for the given extension.
481
482     Args:
483         install_dir - path to directory to check.
484         salomex_name - extension's name.
485
486     Returns:
487         Abs path if the file exist, otherwise None.
488     """
489
490     return find_file(install_dir, salomex_name + '.' + CFILE_EXT)
491
492
493 def find_envpy(install_dir, salomex_name):
494     """
495     Finds a _env.py file for the given extension.
496
497     Args:
498         install_dir - path to directory to check.
499         salomex_name - extension's name.
500
501     Returns:
502         Abs path if the file exist, otherwise None.
503     """
504
505     return find_file(install_dir, salomex_name + ENVPYFILE_SUF)
506
507
508 def module_from_filename(filename):
509     """
510     Create and execute a module by filename.
511
512     Args:
513         filename - a given python filename.
514
515     Returns:
516         Module.
517     """
518
519     # Get the module from the filename
520     basename = os.path.basename(filename)
521     module_name, _ = os.path.splitext(basename)
522
523     spec = importlib.util.spec_from_file_location(module_name, filename)
524     if not spec:
525         logger.error('Could not get a spec for %s file!')
526         return None
527
528     module = importlib.util.module_from_spec(spec)
529     if not module:
530         logger.error('Could not get a module for %s file!')
531         return None
532
533     sys.modules[module_name] = module
534
535     logger.debug('Execute %s module', module_name)
536     spec.loader.exec_module(module)
537
538     return module
539
540
541 def set_selext_env(install_dir, salomex_name, context=None):
542     """
543     Finds and run _env.py file for the given extension.
544
545     Args:
546         install_dir - path to directory to check.
547         salomex_name - extension's name.
548         context - SalomeContext object.
549
550     Returns:
551         True if an envpy file was found and run its init func.
552     """
553
554     logger.debug('Set an env for salome extension: %s...', salomex_name)
555
556     # Set the root dir as env variable
557     if not context:
558         context = salomeContext.SalomeContext(None)
559         context.setVariable('SALOME_APPLICATION_DIR', install_dir, overwrite=False)
560
561     # Find env file
562     ext_envpy = find_envpy(install_dir, salomex_name)
563     if not ext_envpy:
564         return False
565
566     # Get a module
567     envpy_module = module_from_filename(ext_envpy)
568     if not envpy_module:
569         return False
570
571     # Set env if we have something to set
572     ext_root = os.path.join(install_dir, SALOME_EXTDIR)
573     if hasattr(envpy_module, 'init'):
574         envpy_module.init(context, ext_root)
575         return True
576     else:
577         logger.warning('Env file %s doesnt have init func:!', ext_envpy)
578
579     logger.warning('Setting an env for salome extension %s failed!', salomex_name)
580     return False
581
582
583 def get_app_root(levels_up=5):
584     """
585     Finds an app root by going up on the given steps.
586
587     Args:
588         levels_up - steps up in dir hierarchy relative to the current file.
589
590     Returns:
591         Path to the app root.
592     """
593
594     app_root = str(Path(__file__).resolve().parents[levels_up - 1])
595     logger.debug('App root: %s', app_root)
596
597     return app_root