Salome HOME
3894212cc40449d4235004d11323c701660cf5a5
[modules/kernel.git] / bin / SalomeOnDemandTK / extension_query.py
1 #!/usr/bin/env python3
2 # -*- coding:utf-8 -*-
3 # Copyright (C) 2007-2023  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_query.py
26 #  Author : Konstantin Leontev, Open Cascade
27 #
28 #  @package SalomeOnDemandTK
29 #  @brief An utility package that will allow you to know the size of an extension
30 # and generate a dependency tree.
31
32 """
33 An utility package that will allow you to know the size of an extension
34 and generate a dependency tree.
35 """
36
37 import os
38 import sys
39 from traceback import format_exc
40
41 from .extension_utilities import logger, \
42     SALOME_EXTDIR, DFILE_EXT, EXTDEPENDSON_KEY, EXTDESCR_KEY, EXTAUTHOR_KEY, EXTCOMPONENT_KEY, \
43     isvalid_dirname, find_salomexc, list_files_ext, read_salomexd
44
45
46 def size_to_str(size, format_in_bytes=False):
47     """
48     Returns a string describing a size.
49
50     Args:
51         size - the size to represent as a string.
52         format_in_bytes - if True, size is expressed in bytes,
53             otherwise human readable units are used.
54
55     Returns:
56         The size as a string with units.
57     """
58
59     if format_in_bytes:
60         return '%s' % size
61
62     __units__ = ["", "Ki", "Mi", "Gi", "Ti"]
63
64     kilo = 1024.0
65     prefix_index = 0
66     prefix_index_max = len(__units__) - 1
67     while (size > kilo and prefix_index < prefix_index_max):
68         prefix_index += 1
69         size /= kilo
70
71     unit = __units__[prefix_index]
72     return '%.2f %sB' % (size, unit)
73
74
75 def dir_size(directory):
76     """
77     Calculate a total size of the given directory.
78
79     Args:
80         directory - the given directory
81
82     Returns:
83         Size of the directory.
84     """
85
86     logger.debug('Get the size of %s', directory)
87
88     total_size = 0
89     for root, _, files in os.walk(directory):
90         for file in files:
91             itempath = os.path.join(root, file)
92             if os.path.islink(itempath):
93                 continue
94
95             total_size += os.path.getsize(itempath)
96
97     logger.debug('The size of %s: %s', directory, total_size)
98     return total_size
99
100
101 def dir_size_str(directory, format_in_bytes=False):
102     """
103     Calculate a total size of the given directory and format as a string.
104
105     Args:
106         directory - the given directory
107         format_in_bytes - if True, size is expressed in bytes,
108             otherwise human readable units are used.
109
110     Returns:
111         Size of the directory as a formatted string.
112     """
113
114     size = dir_size(directory)
115     size_str = size_to_str(size, format_in_bytes)
116
117     logger.debug('The size of %s: %s', directory, size_str)
118     return size_str
119
120
121 def size_bylist(root_dir, salomexc):
122     """
123     Calcualate the total size of files listed in the given salomexc file.
124
125     Args:
126         root_dir - a root dir for listed files
127         salomexc - file that contents a list of files.
128
129     Returns:
130         The total size of listed files.
131     """
132
133     logger.debug('Calcualate the total size of files inside %s listed in %s...',
134         root_dir, salomexc)
135
136     try:
137         with open(salomexc, 'r', encoding='UTF-8') as file:
138             total_size = 0
139             for line in file:
140                 path_to_file = os.path.join(root_dir, line.strip())
141                 #logger.debug('Check the file %s...', path_to_file)
142
143                 if os.path.isfile(path_to_file):
144                     size = os.path.getsize(path_to_file)
145                     total_size += size
146                     logger.debug('%s size: %s', path_to_file, size)
147
148                 elif os.path.islink(path_to_file):
149                     logger.debug('%s is link. Skip.', path_to_file)
150                     continue
151
152                 elif os.path.isdir(path_to_file):
153                     logger.warning('Directories are not expected to be listed in %s file! '
154                         'Skip.',
155                         salomexc)
156                     continue
157
158                 else:
159                     logger.warning('Unexpected path %s '
160                         'is not a file, link or directory. Skip.',
161                         path_to_file)
162
163             return total_size
164
165     except OSError:
166         logger.error(format_exc())
167
168     return None
169
170
171 def ext_size(install_dir, salomex_name):
172     """
173     Calculate a total size of a salome extension from SALOME install root.
174
175     Args:
176         salome_root - path to SALOME install root directory.
177         salomex_name - a name of the given salome extension.
178
179     Returns:
180         Size of the directory in bytes.
181     """
182
183     # Check if provided dirname is valid
184     if not isvalid_dirname(install_dir):
185         return None
186
187     # Check if an extension with this name is installed
188     salomexc = find_salomexc(install_dir, salomex_name)
189     if not salomexc:
190         logger.error('Cannot calculate the size of %s extension!',
191             salomex_name)
192         return None
193
194     # Calculate the size
195     return size_bylist(os.path.join(install_dir, SALOME_EXTDIR), salomexc)
196
197
198 def ext_size_str(install_dir, salomex_name, format_in_bytes=False):
199     """
200     Calculate a total size of the given extension and format as a string.
201
202     Args:
203         install_dir - directory where the given extension is installed.
204         salomex_name - the given extension's name.
205         format_in_bytes - if True, size is expressed in bytes,
206             otherwise human readable units are used.
207
208     Returns:
209         Size of the extension as a formatted string.
210     """
211
212     size = ext_size(install_dir, salomex_name)
213     if size is None:
214         return ''
215
216     size_str = size_to_str(size, format_in_bytes)
217
218     logger.debug('The size of %s: %s', salomex_name, size_str)
219     return size_str
220
221
222 def dependency_tree(directory):
223     r"""
224     Create a dependency tree for all salome extensions
225     installed in the given directory.
226
227     Args:
228         directory - the given directory
229
230     Returns:
231         A dictionary like that for extensions A, B, C, D and E:
232           A
233          /|\
234         / B D
235         \/ \/
236         C   E
237
238         { 'A': ['B', 'C', 'D'], 'B': ['C', 'E'], 'C': [], 'D': ['E'], 'E': [] }.
239     """
240
241     logger.debug('Build dependency tree for extensions in %s', directory)
242
243     tree = {}
244     salomexd_files = list_files_ext(directory, DFILE_EXT)
245     logger.debug('There are %s extensions in %s', len(salomexd_files), directory)
246
247     for salomexd_file in salomexd_files:
248         ext_name, _ = os.path.splitext(os.path.basename(salomexd_file))
249
250         logger.debug('Check dependencies for %s...', salomexd_file)
251         salomexd_content = read_salomexd(salomexd_file)
252
253         if EXTDEPENDSON_KEY in salomexd_content:
254             depends_on = salomexd_content[EXTDEPENDSON_KEY]
255             logger.debug('List of dependencies: %s', depends_on)
256
257             tree[ext_name] = depends_on
258
259     logger.debug('Dependency tree: %s', tree)
260     return tree
261
262
263 def ext_info_dict(directory):
264     r"""
265     Get installed salome extensions info.
266
267     Args:
268         directory - the given ext install directory.
269
270     Returns:
271         A dictionary {name: [descr, author, components, size]}.
272     """
273
274     logger.debug('Build info dictionary for extensions in %s', directory)
275
276     ext_info = {}
277     salomexd_files = list_files_ext(directory, DFILE_EXT)
278     logger.debug('There are %s extensions in %s', len(salomexd_files), directory)
279
280     for salomexd_file in salomexd_files:
281         # Collect size info
282         ext_name, _ = os.path.splitext(os.path.basename(salomexd_file))
283         size = ext_size_str(directory, ext_name)
284
285         # Collect salomexd info
286         salomexd_content = read_salomexd(salomexd_file)
287
288         descr = ''
289         if EXTDESCR_KEY in salomexd_content:
290             descr = salomexd_content[EXTDESCR_KEY]
291             logger.debug('descr: %s', descr)
292
293         author = ''
294         if EXTAUTHOR_KEY in salomexd_content:
295             author = salomexd_content[EXTAUTHOR_KEY]
296             logger.debug('author: %s', author)
297
298         components = ''
299         if EXTCOMPONENT_KEY in salomexd_content:
300             components = ', '.join(salomexd_content[EXTCOMPONENT_KEY])
301             logger.debug('components: %s', components)
302
303         ext_info[ext_name] = [descr, author, components, size]
304
305     logger.debug('Installed extensions info: %s', ext_info)
306     return ext_info
307
308
309 def ext_by_dependants(dep_tree, depends_on=None):
310     r"""
311     Calcualate a list of extensions names sorted by dependencies.
312
313     Args:
314         dep_tree - a dependecy tree of all considered extensions.
315         depends_on - a set of extensions names to check if the current one depends on them.
316
317     Returns:
318         A list of of extensions from dep_tree sorted by dependencies.
319         For example, dictionary like that for extensions A, B, C, D and E:
320           A
321          /|\
322         / B D
323         \/ \/
324         C   E
325
326         represented { 'A': ['B', 'C', 'D'], 'B': ['C', 'E'], 'C': [], 'D': ['E'], 'E': [] }
327         returns ['A', 'B', 'D', 'C', 'E'].
328     """
329
330     logger.debug('dep_tree: %s, depends_on: %s', dep_tree, depends_on)
331
332     if not dep_tree:
333         logger.debug('Dependency tree is empty! Return')
334         return []
335
336     # Check if we've got a default value
337     if not depends_on:
338         depends_on = set()
339
340     # Collect all dependencies for the given dependency level in the tree
341     cur_depends_on = set()
342     for ext, dependendants in dep_tree.items():
343         # Select all components with empty dependendants
344         is_selected = not dependendants
345
346         # Check dependendants intersection if the case
347         if not is_selected:
348             dep_set = set(dependendants)
349             intersection = dep_set & depends_on
350             logger.debug('ext: %s, dep_set: %s, intersection: %s', ext, dep_set, intersection)
351             is_selected = intersection == dep_set
352
353         if is_selected:
354             cur_depends_on.add(ext)
355
356     # Check collected dependencies
357     logger.debug('cur_depends_on: %s', cur_depends_on)
358     if not cur_depends_on:
359         logger.warning(
360             'Extensions in the tree doesnt rely on any other extensions! Return all of them')
361         return [ext for ext, _ in dep_tree.items()]
362
363     # Collect all extension for this level
364     res_extensions = []
365     for ext in cur_depends_on:
366         res_extensions.append(ext)
367         del dep_tree[ext]
368
369     logger.debug('res_extensions: %s', res_extensions)
370
371     # Get extensions from the next dependency level of the tree
372     cur_depends_on |= depends_on
373     return res_extensions + ext_by_dependants(dep_tree, cur_depends_on)
374
375
376 def ext_by_name(directory):
377     """
378     Calcualate a list of extensions installed in the given directory.
379
380     Args:
381         directory - a directory where extensions are installed.
382
383     Returns:
384         A list of extensions names sorted by name.
385     """
386
387     logger.debug('directory: %s', directory)
388
389     # Get ext files
390     salomexd_files = list_files_ext(directory, DFILE_EXT)
391     logger.debug('There are %s extensions in %s', len(salomexd_files), directory)
392
393     # Get ext names
394     res_names = []
395     for salomexd_file in salomexd_files:
396         ext_name, _ = os.path.splitext(os.path.basename(salomexd_file))
397         res_names.append(ext_name)
398
399     # Sort
400     res_names.sort()
401     logger.debug('Installed extensions: %s', res_names)
402
403     return res_names
404
405
406 def ext_canremove_flags(directory):
407     r"""
408     Calcualate a dict of extensions names paired with bool flag if able to be removed.
409
410     Args:
411         directory - the given ext install directory.
412
413     Returns:
414         For dependency tree for extensions A, B, C, D and E:
415           A
416          /|\
417         / B D
418         \/ \/
419         C   E
420
421         represented { 'A': ['B', 'C', 'D'], 'B': ['C', 'E'], 'C': [], 'D': ['E'], 'E': [] }
422         returns ['A': True, 'B': False, 'D': False, 'C': False, 'E': False].
423         We can remove only A here, because we don't have any other modules depend on it.
424     """
425
426     logger.debug('directory: %s', directory)
427
428     dep_tree = dependency_tree(directory)
429
430     res_dict = {}
431     for ext in dep_tree:
432         can_remove = True
433         for dep in dep_tree.values():
434             if ext in dep:
435                 # At least one ext depends on it, so can't remove
436                 can_remove = False
437                 break
438
439         res_dict[ext] = can_remove
440
441     # We shouldn't remove Base in any case
442     if 'Base' in dep_tree:
443         res_dict['Base'] = False
444
445     logger.debug('res_dict: %s', res_dict)
446
447     return res_dict
448
449
450 if __name__ == '__main__':
451     if len(sys.argv) == 2:
452         dir_size_str(sys.argv[1])
453         ext_tree = dependency_tree(sys.argv[1])
454         ext_list = ext_by_dependants(ext_tree)
455         logger.info('ext_list: %s', ext_list)
456         ext_by_name(sys.argv[1])
457         ext_info_dict(sys.argv[1])
458         ext_canremove_flags(sys.argv[1])
459     elif len(sys.argv) == 3:
460         arg_1, arg_2 = sys.argv[1:] # pylint: disable=unbalanced-tuple-unpacking
461         ext_size_str(arg_1, arg_2)
462     else:
463         logger.error('You must provide all the arguments!')
464         logger.info(dir_size_str.__doc__)
465         logger.info(dependency_tree.__doc__)
466         logger.info(ext_size_str.__doc__)